diff --git a/v0.6.9/404.html b/v0.6.9/404.html new file mode 100644 index 000000000..ee7dde854 --- /dev/null +++ b/v0.6.9/404.html @@ -0,0 +1,128 @@ + + + + + + + +Page not found (404) • rtables + + + + + + + + + + + + + + + + + Skip to contents + + +
+
+
+ +Content not found. Please use links in the navbar. + +
+
+ + + +
+ + + + + + + diff --git a/v0.6.9/CODE_OF_CONDUCT.html b/v0.6.9/CODE_OF_CONDUCT.html new file mode 100644 index 000000000..268c6d43b --- /dev/null +++ b/v0.6.9/CODE_OF_CONDUCT.html @@ -0,0 +1,140 @@ + +Contributor Covenant Code of Conduct • rtables + Skip to contents + + +
+
+
+ +
+ +
+

Our Pledge

+

In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.

+
+
+

Our Standards

+

Examples of behavior that contributes to creating a positive environment include:

+
  • Using welcoming and inclusive language
  • +
  • Being respectful of differing viewpoints and experiences
  • +
  • Gracefully accepting constructive criticism
  • +
  • Focusing on what is best for the community
  • +
  • Showing empathy towards other community members
  • +

Examples of unacceptable behavior by participants include:

+
  • The use of sexualized language or imagery and unwelcome sexual attention or advances
  • +
  • Trolling, insulting/derogatory comments, and personal or political attacks
  • +
  • Public or private harassment
  • +
  • Publishing others’ private information, such as a physical or electronic address, without explicit permission
  • +
  • Other conduct which could reasonably be considered inappropriate in a professional setting
  • +
+
+

Our Responsibilities

+

Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.

+

Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.

+
+
+

Scope

+

This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.

+
+
+

Enforcement

+

Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at support@github.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.

+

Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project’s leadership.

+
+
+

Attribution

+

This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html

+

For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq

+
+
+ +
+ + +
+ + + + + + + diff --git a/v0.6.9/CONTRIBUTING.html b/v0.6.9/CONTRIBUTING.html new file mode 100644 index 000000000..1333bb473 --- /dev/null +++ b/v0.6.9/CONTRIBUTING.html @@ -0,0 +1,161 @@ + +Contributing to {rtables} • rtables + Skip to contents + + +
+
+
+ +
+ +

We welcome contributions big and small to the ongoing development of the {rtables} package. For most, the best way to contribute to the package is by filing issues for feature requests or bugs that you have encountered. For those who are interested in contributing code to the package, contributions can be made by working on current issues and opening pull requests with code changes. Any help that you are able to provide is greatly appreciated!

+

Contributions to this project are released to the public under the project’s open source license.

+
+

Filing Issues

+

Issues are used to establish a prioritized timeline and track development progress within the package. If there is a new feature that you feel would be enhance the experience of package users, please open a Feature Request issue. If you notice a bug in the existing code, please file a Bug Fix issue with a description of the bug and a reprex (reproducible example). Other types of issues (questions, typos you’ve noticed, improvements to documentation, etc.) can be filed as well. Click here to file a new issue, and here to see the list of current issues. Please utilize labels wherever possible when creating issues for organization purposes and to narrow down the scope of the work required.

+
+
+

Creating Pull Requests

+

Development of the {rtables} package relies on an Issue → Branch → PR → Code Review → Merge pipeline facilitated through GitHub. If you are a more experienced programmer interested in contributing to the package code, please begin by filing an issue describing the changes you would like to make. It may be the case that your idea has already been implemented in some way, and the package maintainers can help to determine whether the feature is necessary before you begin development. Whether you are opening an issue or a pull request, the more detailed your description, the easier it will be for package maintainers to help you! To make code changes in the package, please follow the following process.

+
+

Pull Request Process

+

The {rtables} package is part of the NEST project and utilizes staged.dependencies to ensure to simplify the development process and track upstream and downstream package dependencies. We highly recommend installing and using this package when developing within {rtables}.

+
+

1. Create a branch

+

In order to work on a new pull request, please first create a branch off of main upon which you can work and commit changes. To comply with staged.dependencies standards, {rtables} uses the following branch naming convention:

+

issue#_description_of_issue@target_merge_branch

+

For example, 443_refactor_splits@main. In most cases, the target merge branch is the base (main) branch.

+

In some cases, a change in {rtables} may first require upstream changes in the {formatters} package. Suppose we have branch 100_update_fmts@main in {formatters} containing the required upstream changes. Then the branch created in {rtables} would be named as follows for this example: 443_refactor_splits@100_update_fmts@main. This ensures that the correct branches are checked out when running tests, etc.

+

For more details on staged.dependencies branch naming conventions, click here.

+
+
+

2. Code

+

Work within the {rtables} package to apply your code changes. Avoid combining issues on a single branch - ideally, each branch should be associated with a single issue and be prefixed by the issue number.

+

For information on the basics of the {rtables} package, please read the package vignettes, which are available here.

+

For advanced development work within {rtables}, consider reading through the {rtables} Developer Guide. The Developer Guide can be accessed from the {rtables} site navigation bar, and is listed here for your convenience:

+
+
Code style
+

The {rtables} package follows the tidyverse style guide so please adhere to these guidelines in your submitted code. After making changes to a file within the package, you can apply the package styler automatically and check for lint by running the following two lines of code while within the file:

+
styler:::style_active_file()
+lintr:::addin_lint()
+
+
+
Documentation
+

Package documentation uses roxygen2. If your contribution requires updates to documentation, ensure that the roxygen comments are updated within the source code file. After updating roxygen documentation, run devtools::document() to update the accompanying .Rd files (do not update these files by hand!).

+
+
+
Tests
+

To ensure high code coverage, we create tests using the testthat package. In most cases, changes to package code necessitate the addition of one or more tests to ensure that any added features are working as expected and no existing features were broken.

+
+
+
NEWS
+

After making updates to the package, please add a descriptive entry to the NEWS file that reflects your changes. See the tidyverse style guide for guidelines on creating a NEWS entry.

+
+
+
+

3. Make a Pull Request

+

Once the previous two steps are complete, you can create a pull request. Indicate in the description which issue is addressed in the pull request, and again utilize labels to help reviewers identify the category of the changes contained within the pull request.

+

Once your pull request has been created, a series of checks will be automatically triggered, including R CMD check, tests/code coverage, auto-documentation, and more. All checks must be passing in order to eventually merge your pull request, and further changes may be required in order to resolve the status of these checks. All pull requests must also be reviewed and approved by at least one of the package maintainers before they can be merged. A review will be automatically requested from several {rtables} maintainers upon creating your pull request. When a maintainer reviews your pull request, please try to address the comments in short order - the {rtables} package is updated on a regular basis and leaving a pull request open too long is likely to result in merge conflicts which create more work for the developer.

+
+
+
+
+

Code of Conduct

+

Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms.

+
+
+ +
+ + +
+ + + + + + + diff --git a/v0.6.9/ISSUE_TEMPLATE.html b/v0.6.9/ISSUE_TEMPLATE.html new file mode 100644 index 000000000..501f998d2 --- /dev/null +++ b/v0.6.9/ISSUE_TEMPLATE.html @@ -0,0 +1,109 @@ + +Reporting an Issue with rtables • rtables + Skip to contents + + +
+
+
+ +
+ +

Please briefly describe your problem and, when relevant, the output you expect. Please also provide the output of utils::sessionInfo() or devtools::session_info() at the end of your post.

+

If at all possible, please include a minimal, reproducible example. The rtables team will be much more likely to resolve your issue if they are able to reproduce it themselves locally.

+

Please delete this preamble after you have read it.

+

your brief description of the problem

+
+library(rtables)
+
+# your reproducible example here
+
+ +
+ + +
+ + + + + + + diff --git a/v0.6.9/LICENSE-text.html b/v0.6.9/LICENSE-text.html new file mode 100644 index 000000000..9b5cc81d5 --- /dev/null +++ b/v0.6.9/LICENSE-text.html @@ -0,0 +1,340 @@ + +License • rtables + Skip to contents + + +
+
+
+ +
The rtables package as a whole is distributed under Apache Liscence 
+Version 2 (see below).
+
+The rtables package also includes the following open source software
+components:
+
+- Bootstrap, https://github.com/twbs/bootstrap
+
+
+
+boostrap liscence:
+---------------------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2011-2017 Twitter, Inc.
+Copyright (c) 2011-2017 The Bootstrap Authors
+
+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.
+
+
+
+
+rtables liscence:
+---------------------------------------------------------------------------
+                                 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 [2022] [F. Hoffman-La Roche Ltd]
+
+   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.
+
+ +
+ + +
+ + + + + + + diff --git a/v0.6.9/analytics.js b/v0.6.9/analytics.js new file mode 100644 index 000000000..9d4ec4d0a --- /dev/null +++ b/v0.6.9/analytics.js @@ -0,0 +1 @@ +$(document).cookieWall({id:'UA-125641273-1'}); diff --git a/v0.6.9/apple-touch-icon-120x120.png b/v0.6.9/apple-touch-icon-120x120.png new file mode 100644 index 000000000..8fa0e3769 Binary files /dev/null and b/v0.6.9/apple-touch-icon-120x120.png differ diff --git a/v0.6.9/apple-touch-icon-152x152.png b/v0.6.9/apple-touch-icon-152x152.png new file mode 100644 index 000000000..34b2c45d2 Binary files /dev/null and b/v0.6.9/apple-touch-icon-152x152.png differ diff --git a/v0.6.9/apple-touch-icon-180x180.png b/v0.6.9/apple-touch-icon-180x180.png new file mode 100644 index 000000000..0875e89b3 Binary files /dev/null and b/v0.6.9/apple-touch-icon-180x180.png differ diff --git a/v0.6.9/apple-touch-icon-60x60.png b/v0.6.9/apple-touch-icon-60x60.png new file mode 100644 index 000000000..ad38a7a4c Binary files /dev/null and b/v0.6.9/apple-touch-icon-60x60.png differ diff --git a/v0.6.9/apple-touch-icon-76x76.png b/v0.6.9/apple-touch-icon-76x76.png new file mode 100644 index 000000000..7a9832619 Binary files /dev/null and b/v0.6.9/apple-touch-icon-76x76.png differ diff --git a/v0.6.9/apple-touch-icon.png b/v0.6.9/apple-touch-icon.png new file mode 100644 index 000000000..a49e4b047 Binary files /dev/null and b/v0.6.9/apple-touch-icon.png differ diff --git a/v0.6.9/articles/advanced_usage.html b/v0.6.9/articles/advanced_usage.html new file mode 100644 index 000000000..5901e698b --- /dev/null +++ b/v0.6.9/articles/advanced_usage.html @@ -0,0 +1,436 @@ + + + + + + + + +{rtables} Advanced Usage • rtables + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

NOTE +

+

This vignette is currently under development. Any code or prose which +appears in a version of this vignette on the main branch of +the repository will work/be correct, but they likely are not in their +final form.

+

Initialization

+ +
+
+

Control splitting with provided function (limited +customization) +

+

rtables provides an array of functions to control the splitting logic +without creating an entirely new split functions. By default +split_*_by facets data based on categorical variable.

+
+d1 <- subset(ex_adsl, AGE < 25)
+d1$AGE <- as.factor(d1$AGE)
+lyt1 <- basic_table() %>%
+  split_cols_by("AGE") %>%
+  analyze("SEX")
+
+build_table(lyt1, d1)
+
##                    20   21   23   24
+## ————————————————————————————————————
+## F                  0    2    4    5 
+## M                  1    1    2    3 
+## U                  0    0    0    0 
+## UNDIFFERENTIATED   0    0    0    0
+

For continuous variables, the split_*_by_cutfun can be +leveraged to create categories and the corresponding faceting, when the +break points are dependent from the data.

+
+sd_cutfun <- function(x) {
+  cutpoints <- c(
+    min(x),
+    mean(x) - sd(x),
+    mean(x) + sd(x),
+    max(x)
+  )
+
+  names(cutpoints) <- c("", "Low", "Medium", "High")
+  cutpoints
+}
+
+lyt1 <- basic_table() %>%
+  split_cols_by_cutfun("AGE", cutfun = sd_cutfun) %>%
+  analyze("SEX")
+
+build_table(lyt1, ex_adsl)
+
##                    Low   Medium   High
+## ——————————————————————————————————————
+## F                  36     165      21 
+## M                  21     115      30 
+## U                   1      8       0  
+## UNDIFFERENTIATED    0      1       2
+

Alternatively, split_*_by_cuts can be used when +breakpoints are predefined and split_*_by_quartiles when +the data should be faceted by quantile.

+
+lyt1 <- basic_table() %>%
+  split_cols_by_cuts(
+    "AGE",
+    cuts = c(0, 30, 60, 100),
+    cutlabels = c("0-30 y.o.", "30-60 y.o.", "60-100 y.o.")
+  ) %>%
+  analyze("SEX")
+
+build_table(lyt1, ex_adsl)
+
##                    0-30 y.o.   30-60 y.o.   60-100 y.o.
+## ———————————————————————————————————————————————————————
+## F                     71          150            1     
+## M                     48          116            2     
+## U                      2           7             0     
+## UNDIFFERENTIATED       1           2             0
+
+
+

Custom Split Functions +

+
+

Adding an Overall Column Only When The Split Would Already Define 2+ +Facets +

+

Our custom split functions can do anything, including conditionally +applying one or more other existing custom split functions.

+

Here we define a function constructor which accepts the variable name +we want to check, and then return a custom split function that has the +behavior you want using functions provided by rtables for both +cases:

+
+picky_splitter <- function(var) {
+  function(df, spl, vals, labels, trim) {
+    orig_vals <- vals
+    if (is.null(vals)) {
+      vec <- df[[var]]
+      vals <- if (is.factor(vec)) levels(vec) else unique(vec)
+    }
+    if (length(vals) == 1) {
+      do_base_split(spl = spl, df = df, vals = vals, labels = labels, trim = trim)
+    } else {
+      add_overall_level(
+        "Overall",
+        label = "All Obs", first = FALSE
+      )(df = df, spl = spl, vals = orig_vals, trim = trim)
+    }
+  }
+}
+
+
+d1 <- subset(ex_adsl, ARM == "A: Drug X")
+d1$ARM <- factor(d1$ARM)
+
+lyt1 <- basic_table() %>%
+  split_cols_by("ARM", split_fun = picky_splitter("ARM")) %>%
+  analyze("AGE")
+

This gives us the desired behavior in both the one column corner +case:

+
+build_table(lyt1, d1)
+
##        A: Drug X
+## ————————————————
+## Mean     33.77
+

and the standard multi-column case:

+
+build_table(lyt1, ex_adsl)
+
##        A: Drug X   B: Placebo   C: Combination   All Obs
+## ————————————————————————————————————————————————————————
+## Mean     33.77       35.43          35.43         34.88
+

Notice we use add_overall_level which is itself a function +constructor, and then immediately call the constructed function in the +more-than-one-columns case.

+
+
+
+

Leveraging .spl_context +

+
+

What Is .spl_context? +

+

.spl_context (see ?spl_context) is a +mechanism by which the rtables tabulation machinery gives +custom split, analysis or content (row-group summary) functions +information about the overarching facet-structure the splits or cells +they generate will reside in.

+

In particular .spl_context ensures that your functions +know (and thus do computations based on) the following types of +information:

+
    +
  • +
+
+
+

Different Formats For Different Values Within A Row-Split +

+
+dta_test <- data.frame(
+  USUBJID = rep(1:6, each = 3),
+  PARAMCD = rep("lab", 6 * 3),
+  AVISIT = rep(paste0("V", 1:3), 6),
+  ARM = rep(LETTERS[1:3], rep(6, 3)),
+  AVAL = c(9:1, rep(NA, 9)),
+  CHG = c(1:9, rep(NA, 9))
+)
+
+my_afun <- function(x, .spl_context) {
+  n <- sum(!is.na(x))
+  meanval <- mean(x, na.rm = TRUE)
+  sdval <- sd(x, na.rm = TRUE)
+
+  ## get the split value of the most recent parent
+  ## (row) split above this analyze
+  val <- .spl_context[nrow(.spl_context), "value"]
+  ## do a silly thing to decide the different format precisiosn
+  ## your real logic would go here
+  valnum <- min(2L, as.integer(gsub("[^[:digit:]]*", "", val)))
+  fstringpt <- paste0("xx.", strrep("x", valnum))
+  fmt_mnsd <- sprintf("%s (%s)", fstringpt, fstringpt)
+  in_rows(
+    n = n,
+    "Mean, SD" = c(meanval, sdval),
+    .formats = c(n = "xx", "Mean, SD" = fmt_mnsd)
+  )
+}
+
+lyt <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  split_rows_by("AVISIT") %>%
+  split_cols_by_multivar(vars = c("AVAL", "CHG")) %>%
+  analyze_colvars(my_afun)
+
+build_table(lyt, dta_test)
+
##                          A                         B                 C     
+##                 AVAL           CHG         AVAL         CHG      AVAL   CHG
+## ———————————————————————————————————————————————————————————————————————————
+## V1                                                                         
+##   n               2             2            1           1        0      0 
+##   Mean, SD    7.5 (2.1)     2.5 (2.1)    3.0 (NA)    7.0 (NA)     NA    NA 
+## V2                                                                         
+##   n               2             2            1           1        0      0 
+##   Mean, SD   6.50 (2.12)   3.50 (2.12)   2.00 (NA)   8.00 (NA)    NA    NA 
+## V3                                                                         
+##   n               2             2            1           1        0      0 
+##   Mean, SD   5.50 (2.12)   4.50 (2.12)   1.00 (NA)   9.00 (NA)    NA    NA
+
+
+

Simulating ‘Baseline Comparison’ In Row Space +

+
+my_afun <- function(x, .var, .spl_context) {
+  n <- sum(!is.na(x))
+  meanval <- mean(x, na.rm = TRUE)
+  sdval <- sd(x, na.rm = TRUE)
+
+  ## get the split value of the most recent parent
+  ## (row) split above this analyze
+  val <- .spl_context[nrow(.spl_context), "value"]
+  ## we show it if its not a CHG within V1
+  show_it <- val != "V1" || .var != "CHG"
+  ## do a silly thing to decide the different format precisiosn
+  ## your real logic would go here
+  valnum <- min(2L, as.integer(gsub("[^[:digit:]]*", "", val)))
+  fstringpt <- paste0("xx.", strrep("x", valnum))
+  fmt_mnsd <- if (show_it) sprintf("%s (%s)", fstringpt, fstringpt) else "xx"
+  in_rows(
+    n = if (show_it) n, ## NULL otherwise
+    "Mean, SD" = if (show_it) c(meanval, sdval), ## NULL otherwise
+    .formats = c(n = "xx", "Mean, SD" = fmt_mnsd)
+  )
+}
+
+lyt <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  split_rows_by("AVISIT") %>%
+  split_cols_by_multivar(vars = c("AVAL", "CHG")) %>%
+  analyze_colvars(my_afun)
+
+build_table(lyt, dta_test)
+
##                          A                         B                 C     
+##                 AVAL           CHG         AVAL         CHG      AVAL   CHG
+## ———————————————————————————————————————————————————————————————————————————
+## V1                                                                         
+##   n               2                          1                    0        
+##   Mean, SD    7.5 (2.1)                  3.0 (NA)                 NA       
+## V2                                                                         
+##   n               2             2            1           1        0      0 
+##   Mean, SD   6.50 (2.12)   3.50 (2.12)   2.00 (NA)   8.00 (NA)    NA    NA 
+## V3                                                                         
+##   n               2             2            1           1        0      0 
+##   Mean, SD   5.50 (2.12)   4.50 (2.12)   1.00 (NA)   9.00 (NA)    NA    NA
+

We can further simulate the formal modeling of reference row(s) using +the extra_args machinery

+
+my_afun <- function(x, .var, ref_rowgroup, .spl_context) {
+  n <- sum(!is.na(x))
+  meanval <- mean(x, na.rm = TRUE)
+  sdval <- sd(x, na.rm = TRUE)
+
+  ## get the split value of the most recent parent
+  ## (row) split above this analyze
+  val <- .spl_context[nrow(.spl_context), "value"]
+  ## we show it if its not a CHG within V1
+  show_it <- val != ref_rowgroup || .var != "CHG"
+  fmt_mnsd <- if (show_it) "xx.x (xx.x)" else "xx"
+  in_rows(
+    n = if (show_it) n, ## NULL otherwise
+    "Mean, SD" = if (show_it) c(meanval, sdval), ## NULL otherwise
+    .formats = c(n = "xx", "Mean, SD" = fmt_mnsd)
+  )
+}
+
+lyt2 <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  split_rows_by("AVISIT") %>%
+  split_cols_by_multivar(vars = c("AVAL", "CHG")) %>%
+  analyze_colvars(my_afun, extra_args = list(ref_rowgroup = "V1"))
+
+build_table(lyt2, dta_test)
+
##                        A                      B                C     
+##                AVAL         CHG        AVAL       CHG      AVAL   CHG
+## —————————————————————————————————————————————————————————————————————
+## V1                                                                   
+##   n              2                      1                   0        
+##   Mean, SD   7.5 (2.1)               3.0 (NA)               NA       
+## V2                                                                   
+##   n              2           2          1          1        0      0 
+##   Mean, SD   6.5 (2.1)   3.5 (2.1)   2.0 (NA)   8.0 (NA)    NA    NA 
+## V3                                                                   
+##   n              2           2          1          1        0      0 
+##   Mean, SD   5.5 (2.1)   4.5 (2.1)   1.0 (NA)   9.0 (NA)    NA    NA
+
+
+
+
+ + + + +
+ + + + + + + diff --git a/v0.6.9/articles/baseline.html b/v0.6.9/articles/baseline.html new file mode 100644 index 000000000..7be336959 --- /dev/null +++ b/v0.6.9/articles/baseline.html @@ -0,0 +1,267 @@ + + + + + + + + +Comparing Against Baselines or Control • rtables + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + + +
+

Introduction +

+

Often the data from one column is considered the +reference/baseline/comparison group and is compared to the data from the +other columns.

+

For example, lets calculate the average age:

+
+library(rtables)
+
+lyt <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  analyze("AGE")
+
+tbl <- build_table(lyt, DM)
+tbl
+
#        A: Drug X   B: Placebo   C: Combination
+# ——————————————————————————————————————————————
+# Mean     34.91       33.02          34.57
+

and then the difference of the average AGE between the +placebo arm and the other arms:

+
+lyt2 <- basic_table() %>%
+  split_cols_by("ARM", ref_group = "B: Placebo") %>%
+  analyze("AGE", afun = function(x, .ref_group) {
+    in_rows(
+      "Difference of Averages" = rcell(mean(x) - mean(.ref_group), format = "xx.xx")
+    )
+  })
+
+tbl2 <- build_table(lyt2, DM)
+tbl2
+
#                          A: Drug X   B: Placebo   C: Combination
+# ————————————————————————————————————————————————————————————————
+# Difference of Averages     1.89         0.00           1.55
+

Note that the column order has changed and the reference group is +displayed in the first column.

+

In cases where we want cells to be blank in the reference column, +(e.g., “B: Placebo”) we use non_ref_rcell() instead of +rcell(), and pass .in_ref_col as the second +argument:

+
+lyt3 <- basic_table() %>%
+  split_cols_by("ARM", ref_group = "B: Placebo") %>%
+  analyze(
+    "AGE",
+    afun = function(x, .ref_group, .in_ref_col) {
+      in_rows(
+        "Difference of Averages" = non_ref_rcell(mean(x) - mean(.ref_group), is_ref = .in_ref_col, format = "xx.xx")
+      )
+    }
+  )
+
+tbl3 <- build_table(lyt3, DM)
+tbl3
+
#                          A: Drug X   B: Placebo   C: Combination
+# ————————————————————————————————————————————————————————————————
+# Difference of Averages     1.89                        1.55
+
+lyt4 <- basic_table() %>%
+  split_cols_by("ARM", ref_group = "B: Placebo") %>%
+  analyze(
+    "AGE",
+    afun = function(x, .ref_group, .in_ref_col) {
+      in_rows(
+        "Difference of Averages" = non_ref_rcell(mean(x) - mean(.ref_group), is_ref = .in_ref_col, format = "xx.xx"),
+        "another row" = non_ref_rcell("aaa", .in_ref_col)
+      )
+    }
+  )
+
+tbl4 <- build_table(lyt4, DM)
+tbl4
+
#                          A: Drug X   B: Placebo   C: Combination
+# ————————————————————————————————————————————————————————————————
+# Difference of Averages     1.89                        1.55     
+# another row                 aaa                        aaa
+

You can see which arguments are available for afun in +the manual for analyze().

+
+
+

Row Splitting +

+

When adding row-splitting the reference data may be represented by +the column with or without row splitting. For example:

+
+lyt5 <- basic_table(show_colcounts = TRUE) %>%
+  split_cols_by("ARM", ref_group = "B: Placebo") %>%
+  split_rows_by("SEX", split_fun = drop_split_levels) %>%
+  analyze("AGE", afun = function(x, .ref_group, .ref_full, .in_ref_col) {
+    in_rows(
+      "is reference (.in_ref_col)" = rcell(.in_ref_col),
+      "ref cell N (.ref_group)" = rcell(length(.ref_group)),
+      "ref column N (.ref_full)" = rcell(length(.ref_full))
+    )
+  })
+
+tbl5 <- build_table(lyt5, subset(DM, SEX %in% c("M", "F")))
+tbl5
+
#                                A: Drug X   B: Placebo   C: Combination
+#                                 (N=121)     (N=106)        (N=129)    
+# ——————————————————————————————————————————————————————————————————————
+# F                                                                     
+#   is reference (.in_ref_col)     FALSE        TRUE          FALSE     
+#   ref cell N (.ref_group)         56           56             56      
+#   ref column N (.ref_full)        106         106            106      
+# M                                                                     
+#   is reference (.in_ref_col)     FALSE        TRUE          FALSE     
+#   ref cell N (.ref_group)         50           50             50      
+#   ref column N (.ref_full)        106         106            106
+

The data assigned to .ref_full is the full data of the +reference column whereas the data assigned to .ref_group +respects the subsetting defined by row-splitting and hence is from the +same subset as the argument x or df to +afun.

+
+
+
+ + + + +
+ + + + + + + diff --git a/v0.6.9/articles/clinical_trials.html b/v0.6.9/articles/clinical_trials.html new file mode 100644 index 000000000..a3d821f59 --- /dev/null +++ b/v0.6.9/articles/clinical_trials.html @@ -0,0 +1,2196 @@ + + + + + + + + +Example Clinical Trials Tables • rtables + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + + +
+

Introduction +

+

In this vignette we create a

+
    +
  • demographic table
  • +
  • adverse event table
  • +
  • response table
  • +
  • time-to-event analysis table
  • +
+

using the rtables layout facility. That is, we +demonstrate how the layout based tabulation framework can specify the +structure and relations that are commonly found when analyzing clinical +trials data.

+

Note that all the data is created using random number generators. All +ex_* data which is currently attached to the +rtables package is provided by the formatters +package and was created using the publicly available random.cdisc.data +R package.

+

The packages used in this vignette are:

+ +
+
+

Demographic Table +

+

Demographic tables summarize the variables content for different +population subsets (encoded in the columns).

+

One feature of analyze() that we have not introduced in +the previous vignette is that the analysis function afun +can specify multiple rows with the in_rows() function:

+
+ADSL <- ex_adsl # Example ADSL dataset
+
+lyt <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  analyze(vars = "AGE", afun = function(x) {
+    in_rows(
+      "Mean (sd)" = rcell(c(mean(x), sd(x)), format = "xx.xx (xx.xx)"),
+      "Range" = rcell(range(x), format = "xx.xx - xx.xx")
+    )
+  })
+
+tbl <- build_table(lyt, ADSL)
+tbl
+
#               A: Drug X      B: Placebo     C: Combination
+# ——————————————————————————————————————————————————————————
+# Mean (sd)   33.77 (6.55)    35.43 (7.90)     35.43 (7.72) 
+# Range       21.00 - 50.00   21.00 - 62.00   20.00 - 69.00
+

Multiple variables can be analyzed in one analyze() +call:

+
+lyt2 <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  analyze(vars = c("AGE", "BMRKR1"), afun = function(x) {
+    in_rows(
+      "Mean (sd)" = rcell(c(mean(x), sd(x)), format = "xx.xx (xx.xx)"),
+      "Range" = rcell(range(x), format = "xx.xx - xx.xx")
+    )
+  })
+
+tbl2 <- build_table(lyt2, ADSL)
+tbl2
+
#                 A: Drug X      B: Placebo     C: Combination
+# ————————————————————————————————————————————————————————————
+# AGE                                                         
+#   Mean (sd)   33.77 (6.55)    35.43 (7.90)     35.43 (7.72) 
+#   Range       21.00 - 50.00   21.00 - 62.00   20.00 - 69.00 
+# BMRKR1                                                      
+#   Mean (sd)    5.97 (3.55)     5.70 (3.31)     5.62 (3.49)  
+#   Range       0.41 - 17.67    0.65 - 14.24     0.17 - 21.39
+

Hence, if afun can process different data vector types +(i.e. variables selected from the data) then we are fairly close to a +standard demographic table. Here is a function that either creates a +count table or some number summary if the argument x is a +factor or numeric, respectively:

+
+s_summary <- function(x) {
+  if (is.numeric(x)) {
+    in_rows(
+      "n" = rcell(sum(!is.na(x)), format = "xx"),
+      "Mean (sd)" = rcell(c(mean(x, na.rm = TRUE), sd(x, na.rm = TRUE)), format = "xx.xx (xx.xx)"),
+      "IQR" = rcell(IQR(x, na.rm = TRUE), format = "xx.xx"),
+      "min - max" = rcell(range(x, na.rm = TRUE), format = "xx.xx - xx.xx")
+    )
+  } else if (is.factor(x)) {
+    vs <- as.list(table(x))
+    do.call(in_rows, lapply(vs, rcell, format = "xx"))
+  } else {
+    stop("type not supported")
+  }
+}
+

Note we use rcell to wrap the results in order to add +formatting instructions for rtables. We can use +s_summary outside the context of tabulation:

+
+s_summary(ADSL$AGE)
+
# RowsVerticalSection (in_rows) object print method:
+# ----------------------------
+#    row_name formatted_cell indent_mod row_label
+# 1         n            400          0         n
+# 2 Mean (sd)   34.88 (7.44)          0 Mean (sd)
+# 3       IQR          10.00          0       IQR
+# 4 min - max  20.00 - 69.00          0 min - max
+

and

+
+s_summary(ADSL$SEX)
+
# RowsVerticalSection (in_rows) object print method:
+# ----------------------------
+#           row_name formatted_cell indent_mod        row_label
+# 1                F            222          0                F
+# 2                M            166          0                M
+# 3                U              9          0                U
+# 4 UNDIFFERENTIATED              3          0 UNDIFFERENTIATED
+

We can now create a commonly used variant of the demographic +table:

+
+summary_lyt <- basic_table() %>%
+  split_cols_by(var = "ARM") %>%
+  analyze(c("AGE", "SEX"), afun = s_summary)
+
+summary_tbl <- build_table(summary_lyt, ADSL)
+summary_tbl
+
#                        A: Drug X      B: Placebo     C: Combination
+# ———————————————————————————————————————————————————————————————————
+# AGE                                                                
+#   n                       134             134             132      
+#   Mean (sd)          33.77 (6.55)    35.43 (7.90)     35.43 (7.72) 
+#   IQR                    11.00           10.00           10.00     
+#   min - max          21.00 - 50.00   21.00 - 62.00   20.00 - 69.00 
+# SEX                                                                
+#   F                       79              77               66      
+#   M                       51              55               60      
+#   U                        3               2               4       
+#   UNDIFFERENTIATED         1               0               2
+

Note that analyze() can also be called multiple times in +sequence:

+
+summary_lyt2 <- basic_table() %>%
+  split_cols_by(var = "ARM") %>%
+  analyze("AGE", s_summary) %>%
+  analyze("SEX", s_summary)
+
+summary_tbl2 <- build_table(summary_lyt2, ADSL)
+summary_tbl2
+
#                        A: Drug X      B: Placebo     C: Combination
+# ———————————————————————————————————————————————————————————————————
+# AGE                                                                
+#   n                       134             134             132      
+#   Mean (sd)          33.77 (6.55)    35.43 (7.90)     35.43 (7.72) 
+#   IQR                    11.00           10.00           10.00     
+#   min - max          21.00 - 50.00   21.00 - 62.00   20.00 - 69.00 
+# SEX                                                                
+#   F                       79              77               66      
+#   M                       51              55               60      
+#   U                        3               2               4       
+#   UNDIFFERENTIATED         1               0               2
+

which leads to the table identical to summary_tbl:

+
+identical(summary_tbl, summary_tbl2)
+
# [1] TRUE
+

In clinical trials analyses the number of patients per column is +often referred to as N (rather than the overall population +which outside of clinical trials is commonly referred to as +N). Column Ns are added by setting the +show_colcounts argument in basic_table() to +TRUE:

+
+summary_lyt3 <- basic_table(show_colcounts = TRUE) %>%
+  split_cols_by(var = "ARMCD") %>%
+  analyze(c("AGE", "SEX"), s_summary)
+
+summary_tbl3 <- build_table(summary_lyt3, ADSL)
+summary_tbl3
+
#                          ARM A           ARM B           ARM C    
+#                         (N=134)         (N=134)         (N=132)   
+# ——————————————————————————————————————————————————————————————————
+# AGE                                                               
+#   n                       134             134             132     
+#   Mean (sd)          33.77 (6.55)    35.43 (7.90)    35.43 (7.72) 
+#   IQR                    11.00           10.00           10.00    
+#   min - max          21.00 - 50.00   21.00 - 62.00   20.00 - 69.00
+# SEX                                                               
+#   F                       79              77              66      
+#   M                       51              55              60      
+#   U                        3               2               4      
+#   UNDIFFERENTIATED         1               0               2
+
+

Variations on the Demographic Table +

+

We will now show a couple of variations of the demographic table that +we developed above. These variations are in structure and not in +analysis, hence they don’t require a modification to the +s_summary function.

+

We will start with a standard table analyzing the variables +AGE and BMRKR2 variables:

+
+lyt <- basic_table(show_colcounts = TRUE) %>%
+  split_cols_by(var = "ARM") %>%
+  analyze(c("AGE", "BMRKR2"), s_summary)
+
+tbl <- build_table(lyt, ADSL)
+tbl
+
#                 A: Drug X      B: Placebo     C: Combination
+#                  (N=134)         (N=134)         (N=132)    
+# ————————————————————————————————————————————————————————————
+# AGE                                                         
+#   n                134             134             132      
+#   Mean (sd)   33.77 (6.55)    35.43 (7.90)     35.43 (7.72) 
+#   IQR             11.00           10.00           10.00     
+#   min - max   21.00 - 50.00   21.00 - 62.00   20.00 - 69.00 
+# BMRKR2                                                      
+#   LOW              50              45               40      
+#   MEDIUM           37              56               42      
+#   HIGH             47              33               50
+

Assume we would like to have this analysis carried out per gender +encoded in the row space:

+
+lyt <- basic_table(show_colcounts = TRUE) %>%
+  split_cols_by(var = "ARM") %>%
+  split_rows_by("SEX") %>%
+  analyze(c("AGE", "BMRKR2"), s_summary)
+
+tbl <- build_table(lyt, ADSL)
+tbl
+
#                      A: Drug X      B: Placebo     C: Combination
+#                       (N=134)         (N=134)         (N=132)    
+# —————————————————————————————————————————————————————————————————
+# F                                                                
+#   AGE                                                            
+#     n                   79              77               66      
+#     Mean (sd)      32.76 (6.09)    34.12 (7.06)     35.20 (7.43) 
+#     IQR                9.00            8.00             6.75     
+#     min - max      21.00 - 47.00   23.00 - 58.00   21.00 - 64.00 
+#   BMRKR2                                                         
+#     LOW                 26              21               26      
+#     MEDIUM              21              38               17      
+#     HIGH                32              18               23      
+# M                                                                
+#   AGE                                                            
+#     n                   51              55               60      
+#     Mean (sd)      35.57 (7.08)    37.44 (8.69)     35.38 (8.24) 
+#     IQR                11.00           9.00            11.00     
+#     min - max      23.00 - 50.00   21.00 - 62.00   20.00 - 69.00 
+#   BMRKR2                                                         
+#     LOW                 21              23               11      
+#     MEDIUM              15              18               23      
+#     HIGH                15              14               26      
+# U                                                                
+#   AGE                                                            
+#     n                    3               2               4       
+#     Mean (sd)      31.67 (3.21)    31.00 (5.66)     35.25 (3.10) 
+#     IQR                3.00            4.00             3.25     
+#     min - max      28.00 - 34.00   27.00 - 35.00   31.00 - 38.00 
+#   BMRKR2                                                         
+#     LOW                  2               1               1       
+#     MEDIUM               1               0               2       
+#     HIGH                 0               1               1       
+# UNDIFFERENTIATED                                                 
+#   AGE                                                            
+#     n                    1               0               2       
+#     Mean (sd)       28.00 (NA)          NA          45.00 (1.41) 
+#     IQR                0.00             NA              1.00     
+#     min - max      28.00 - 28.00    Inf - -Inf     44.00 - 46.00 
+#   BMRKR2                                                         
+#     LOW                  1               0               2       
+#     MEDIUM               0               0               0       
+#     HIGH                 0               0               0
+

We will now subset ADSL to include only males and +females in the analysis in order to reduce the number of rows in the +table:

+
+ADSL_M_F <- filter(ADSL, SEX %in% c("M", "F"))
+
+lyt2 <- basic_table(show_colcounts = TRUE) %>%
+  split_cols_by(var = "ARM") %>%
+  split_rows_by("SEX") %>%
+  analyze(c("AGE", "BMRKR2"), s_summary)
+
+tbl2 <- build_table(lyt2, ADSL_M_F)
+tbl2
+
#                      A: Drug X      B: Placebo     C: Combination
+#                       (N=130)         (N=132)         (N=126)    
+# —————————————————————————————————————————————————————————————————
+# F                                                                
+#   AGE                                                            
+#     n                   79              77               66      
+#     Mean (sd)      32.76 (6.09)    34.12 (7.06)     35.20 (7.43) 
+#     IQR                9.00            8.00             6.75     
+#     min - max      21.00 - 47.00   23.00 - 58.00   21.00 - 64.00 
+#   BMRKR2                                                         
+#     LOW                 26              21               26      
+#     MEDIUM              21              38               17      
+#     HIGH                32              18               23      
+# M                                                                
+#   AGE                                                            
+#     n                   51              55               60      
+#     Mean (sd)      35.57 (7.08)    37.44 (8.69)     35.38 (8.24) 
+#     IQR                11.00           9.00            11.00     
+#     min - max      23.00 - 50.00   21.00 - 62.00   20.00 - 69.00 
+#   BMRKR2                                                         
+#     LOW                 21              23               11      
+#     MEDIUM              15              18               23      
+#     HIGH                15              14               26      
+# U                                                                
+#   AGE                                                            
+#     n                    0               0               0       
+#     Mean (sd)           NA              NA               NA      
+#     IQR                 NA              NA               NA      
+#     min - max       Inf - -Inf      Inf - -Inf       Inf - -Inf  
+#   BMRKR2                                                         
+#     LOW                  0               0               0       
+#     MEDIUM               0               0               0       
+#     HIGH                 0               0               0       
+# UNDIFFERENTIATED                                                 
+#   AGE                                                            
+#     n                    0               0               0       
+#     Mean (sd)           NA              NA               NA      
+#     IQR                 NA              NA               NA      
+#     min - max       Inf - -Inf      Inf - -Inf       Inf - -Inf  
+#   BMRKR2                                                         
+#     LOW                  0               0               0       
+#     MEDIUM               0               0               0       
+#     HIGH                 0               0               0
+

Note that the UNDIFFERENTIATED and U levels +still show up in the table. This is because tabulation respects the +factor levels and level order, exactly as the split and +table function do. If empty levels should be dropped then +rtables needs to know that at splitting time via the +split_fun argument in split_rows_by(). There +are a number of predefined functions. For this example +drop_split_levels() is required to drop the empty levels at +splitting time. Splitting is a big topic and will be eventually +addressed in a specific package vignette.

+
+lyt3 <- basic_table(show_colcounts = TRUE) %>%
+  split_cols_by(var = "ARM") %>%
+  split_rows_by("SEX", split_fun = drop_split_levels, child_labels = "visible") %>%
+  analyze(c("AGE", "BMRKR2"), s_summary)
+
+tbl3 <- build_table(lyt3, ADSL_M_F)
+tbl3
+
#                   A: Drug X      B: Placebo     C: Combination
+#                    (N=130)         (N=132)         (N=126)    
+# ——————————————————————————————————————————————————————————————
+# F                                                             
+#   AGE                                                         
+#     n                79              77               66      
+#     Mean (sd)   32.76 (6.09)    34.12 (7.06)     35.20 (7.43) 
+#     IQR             9.00            8.00             6.75     
+#     min - max   21.00 - 47.00   23.00 - 58.00   21.00 - 64.00 
+#   BMRKR2                                                      
+#     LOW              26              21               26      
+#     MEDIUM           21              38               17      
+#     HIGH             32              18               23      
+# M                                                             
+#   AGE                                                         
+#     n                51              55               60      
+#     Mean (sd)   35.57 (7.08)    37.44 (8.69)     35.38 (8.24) 
+#     IQR             11.00           9.00            11.00     
+#     min - max   23.00 - 50.00   21.00 - 62.00   20.00 - 69.00 
+#   BMRKR2                                                      
+#     LOW              21              23               11      
+#     MEDIUM           15              18               23      
+#     HIGH             15              14               26
+

In the table above the labels M and F are +not very descriptive. You can add the full labels as follows:

+
+ADSL_M_F_l <- ADSL_M_F %>%
+  mutate(lbl_sex = case_when(
+    SEX == "M" ~ "Male",
+    SEX == "F" ~ "Female",
+    SEX == "U" ~ "Unknown",
+    SEX == "UNDIFFERENTIATED" ~ "Undifferentiated"
+  ))
+
+lyt4 <- basic_table(show_colcounts = TRUE) %>%
+  split_cols_by(var = "ARM") %>%
+  split_rows_by("SEX", labels_var = "lbl_sex", split_fun = drop_split_levels, child_labels = "visible") %>%
+  analyze(c("AGE", "BMRKR2"), s_summary)
+
+tbl4 <- build_table(lyt4, ADSL_M_F_l)
+tbl4
+
#                   A: Drug X      B: Placebo     C: Combination
+#                    (N=130)         (N=132)         (N=126)    
+# ——————————————————————————————————————————————————————————————
+# Female                                                        
+#   AGE                                                         
+#     n                79              77               66      
+#     Mean (sd)   32.76 (6.09)    34.12 (7.06)     35.20 (7.43) 
+#     IQR             9.00            8.00             6.75     
+#     min - max   21.00 - 47.00   23.00 - 58.00   21.00 - 64.00 
+#   BMRKR2                                                      
+#     LOW              26              21               26      
+#     MEDIUM           21              38               17      
+#     HIGH             32              18               23      
+# Male                                                          
+#   AGE                                                         
+#     n                51              55               60      
+#     Mean (sd)   35.57 (7.08)    37.44 (8.69)     35.38 (8.24) 
+#     IQR             11.00           9.00            11.00     
+#     min - max   23.00 - 50.00   21.00 - 62.00   20.00 - 69.00 
+#   BMRKR2                                                      
+#     LOW              21              23               11      
+#     MEDIUM           15              18               23      
+#     HIGH             15              14               26
+

For the next table variation we only stratify by gender for the +AGE analysis. To do this the nested argument +has to be set to FALSE in analyze() call:

+
+lyt5 <- basic_table(show_colcounts = TRUE) %>%
+  split_cols_by(var = "ARM") %>%
+  split_rows_by("SEX", labels_var = "lbl_sex", split_fun = drop_split_levels, child_labels = "visible") %>%
+  analyze("AGE", s_summary, show_labels = "visible") %>%
+  analyze("BMRKR2", s_summary, nested = FALSE, show_labels = "visible")
+
+tbl5 <- build_table(lyt5, ADSL_M_F_l)
+tbl5
+
#                   A: Drug X      B: Placebo     C: Combination
+#                    (N=130)         (N=132)         (N=126)    
+# ——————————————————————————————————————————————————————————————
+# Female                                                        
+#   AGE                                                         
+#     n                79              77               66      
+#     Mean (sd)   32.76 (6.09)    34.12 (7.06)     35.20 (7.43) 
+#     IQR             9.00            8.00             6.75     
+#     min - max   21.00 - 47.00   23.00 - 58.00   21.00 - 64.00 
+# Male                                                          
+#   AGE                                                         
+#     n                51              55               60      
+#     Mean (sd)   35.57 (7.08)    37.44 (8.69)     35.38 (8.24) 
+#     IQR             11.00           9.00            11.00     
+#     min - max   23.00 - 50.00   21.00 - 62.00   20.00 - 69.00 
+# BMRKR2                                                        
+#   LOW                47              44               37      
+#   MEDIUM             36              56               40      
+#   HIGH               47              32               49
+

Once we split the rows into groups (Male and +Female here) one might want to summarize groups: usually by +showing count and column percentages. This is especially important if we +have missing data. For example, if we create the above table but add +missing data to the AGE variable:

+
+insert_NAs <- function(x) {
+  x[sample(c(TRUE, FALSE), length(x), TRUE, prob = c(0.2, 0.8))] <- NA
+  x
+}
+
+set.seed(1)
+ADSL_NA <- ADSL_M_F_l %>%
+  mutate(AGE = insert_NAs(AGE))
+
+lyt6 <- basic_table(show_colcounts = TRUE) %>%
+  split_cols_by(var = "ARM") %>%
+  split_rows_by(
+    "SEX",
+    labels_var = "lbl_sex",
+    split_fun = drop_split_levels,
+    child_labels = "visible"
+  ) %>%
+  analyze("AGE", s_summary) %>%
+  analyze("BMRKR2", s_summary, nested = FALSE, show_labels = "visible")
+
+tbl6 <- build_table(lyt6, filter(ADSL_NA, SEX %in% c("M", "F")))
+tbl6
+
#                 A: Drug X      B: Placebo     C: Combination
+#                  (N=130)         (N=132)         (N=126)    
+# ————————————————————————————————————————————————————————————
+# Female                                                      
+#   n                65              61               54      
+#   Mean (sd)   32.71 (6.07)    34.33 (7.31)     34.61 (6.78) 
+#   IQR             9.00            10.00            6.75     
+#   min - max   21.00 - 47.00   23.00 - 58.00   21.00 - 54.00 
+# Male                                                        
+#   n                44              44               50      
+#   Mean (sd)   35.66 (6.78)    36.93 (8.18)     35.64 (8.42) 
+#   IQR             10.50           8.25            10.75     
+#   min - max   24.00 - 48.00   21.00 - 58.00   20.00 - 69.00 
+# BMRKR2                                                      
+#   LOW              47              44               37      
+#   MEDIUM           36              56               40      
+#   HIGH             47              32               49
+

Here it is not easy to see how many females and males there are in +each arm as n represents the number of non-missing data +elements in the variables. Groups within rows that are defined by +splitting can be summarized with summarize_row_groups(), +for example:

+
+lyt7 <- basic_table(show_colcounts = TRUE) %>%
+  split_cols_by(var = "ARM") %>%
+  split_rows_by("SEX", labels_var = "lbl_sex", split_fun = drop_split_levels) %>%
+  summarize_row_groups() %>%
+  analyze("AGE", s_summary) %>%
+  analyze("BMRKR2", afun = s_summary, nested = FALSE, show_labels = "visible")
+
+tbl7 <- build_table(lyt7, filter(ADSL_NA, SEX %in% c("M", "F")))
+tbl7
+
#                 A: Drug X      B: Placebo     C: Combination
+#                  (N=130)         (N=132)         (N=126)    
+# ————————————————————————————————————————————————————————————
+# Female         79 (60.8%)      77 (58.3%)       66 (52.4%)  
+#   n                65              61               54      
+#   Mean (sd)   32.71 (6.07)    34.33 (7.31)     34.61 (6.78) 
+#   IQR             9.00            10.00            6.75     
+#   min - max   21.00 - 47.00   23.00 - 58.00   21.00 - 54.00 
+# Male           51 (39.2%)      55 (41.7%)       60 (47.6%)  
+#   n                44              44               50      
+#   Mean (sd)   35.66 (6.78)    36.93 (8.18)     35.64 (8.42) 
+#   IQR             10.50           8.25            10.75     
+#   min - max   24.00 - 48.00   21.00 - 58.00   20.00 - 69.00 
+# BMRKR2                                                      
+#   LOW              47              44               37      
+#   MEDIUM           36              56               40      
+#   HIGH             47              32               49
+

There are a couple of things to note here:

+
    +
  • Group summaries produce “content” rows. Visually, it’s impossible to +distinguish data rows from content rows. Their difference is justified +(and it’s an important design decision) because when we paginate tables +the content rows are by default repeated if a group gets divided via +pagination.
  • +
  • Conceptually the content rows summarize the patient population which +is analyzed and hence are often the count & group percentages +(default behavior of summarize_row_groups()).
  • +
+

We can recreate this default behavior (count percentage) by defining +a cfun for illustrative purposes here as it results in the +same table as above:

+
+lyt8 <- basic_table(show_colcounts = TRUE) %>%
+  split_cols_by(var = "ARM") %>%
+  split_rows_by("SEX", labels_var = "lbl_sex", split_fun = drop_split_levels) %>%
+  summarize_row_groups(cfun = function(df, labelstr, .N_col, ...) {
+    in_rows(
+      rcell(nrow(df) * c(1, 1 / .N_col), format = "xx (xx.xx%)"),
+      .labels = labelstr
+    )
+  }) %>%
+  analyze("AGE", s_summary) %>%
+  analyze("BEP01FL", afun = s_summary, nested = FALSE, show_labels = "visible")
+
+tbl8 <- build_table(lyt8, filter(ADSL_NA, SEX %in% c("M", "F")))
+tbl8
+
#                 A: Drug X      B: Placebo     C: Combination
+#                  (N=130)         (N=132)         (N=126)    
+# ————————————————————————————————————————————————————————————
+# Female         79 (60.77%)     77 (58.33%)     66 (52.38%)  
+#   n                65              61               54      
+#   Mean (sd)   32.71 (6.07)    34.33 (7.31)     34.61 (6.78) 
+#   IQR             9.00            10.00            6.75     
+#   min - max   21.00 - 47.00   23.00 - 58.00   21.00 - 54.00 
+# Male           51 (39.23%)     55 (41.67%)     60 (47.62%)  
+#   n                44              44               50      
+#   Mean (sd)   35.66 (6.78)    36.93 (8.18)     35.64 (8.42) 
+#   IQR             10.50           8.25            10.75     
+#   min - max   24.00 - 48.00   21.00 - 58.00   20.00 - 69.00 
+# BEP01FL                                                     
+#   Y                67              63               65      
+#   N                63              69               61
+

Note that cfun, like afun (which is used in +analyze()), can operate on either variables, passed via the +x argument, or data.frames or +tibbles, which are passed via the df argument +(afun can optionally request df too). Unlike +afun, cfun must accept labelstr +as the second argument which gives the default group label (factor level +from splitting) and hence it could be modified:

+
+lyt9 <- basic_table() %>%
+  split_cols_by(var = "ARM") %>%
+  split_rows_by("SEX", labels_var = "lbl_sex", split_fun = drop_split_levels, child_labels = "hidden") %>%
+  summarize_row_groups(cfun = function(df, labelstr, .N_col, ...) {
+    in_rows(
+      rcell(nrow(df) * c(1, 1 / .N_col), format = "xx (xx.xx%)"),
+      .labels = paste0(labelstr, ": count (perc.)")
+    )
+  }) %>%
+  analyze("AGE", s_summary) %>%
+  analyze("BEP01FL", s_summary, nested = FALSE, show_labels = "visible")
+
+tbl9 <- build_table(lyt9, filter(ADSL_NA, SEX %in% c("M", "F")))
+tbl9
+
#                           A: Drug X      B: Placebo     C: Combination
+# ——————————————————————————————————————————————————————————————————————
+# Female: count (perc.)    79 (60.77%)     77 (58.33%)     66 (52.38%)  
+#   n                          65              61               54      
+#   Mean (sd)             32.71 (6.07)    34.33 (7.31)     34.61 (6.78) 
+#   IQR                       9.00            10.00            6.75     
+#   min - max             21.00 - 47.00   23.00 - 58.00   21.00 - 54.00 
+# Male: count (perc.)      51 (39.23%)     55 (41.67%)     60 (47.62%)  
+#   n                          44              44               50      
+#   Mean (sd)             35.66 (6.78)    36.93 (8.18)     35.64 (8.42) 
+#   IQR                       10.50           8.25            10.75     
+#   min - max             24.00 - 48.00   21.00 - 58.00   20.00 - 69.00 
+# BEP01FL                                                               
+#   Y                          67              63               65      
+#   N                          63              69               61
+
+
+

Using Layouts +

+

Layouts have a couple of advantages over tabulating the tables +directly:

+
    +
  • the creation of layouts requires the analyst to describe the problem +in an abstract way +
      +
    • i.e. they separate the analyses description from the actual +data
    • +
    +
  • +
  • referencing variable names happens via strings (no non-standard +evaluation (NSE) is needed, though this is arguably either a feature or +a shortcoming)
  • +
  • layouts can be reused
  • +
+

Here is an example that demonstrates the reusability of layouts:

+
+adsl_lyt <- basic_table(show_colcounts = TRUE) %>%
+  split_cols_by("ARM") %>%
+  analyze(c("AGE", "SEX"), afun = s_summary)
+
+adsl_lyt
+
# A Pre-data Table Layout
+# 
+# Column-Split Structure:
+# ARM (lvls) 
+# 
+# Row-Split Structure:
+# AGE:SEX (** multivar analysis **)
+

We can now build a table for ADSL

+
+adsl_tbl <- build_table(adsl_lyt, ADSL)
+adsl_tbl
+
#                        A: Drug X      B: Placebo     C: Combination
+#                         (N=134)         (N=134)         (N=132)    
+# ———————————————————————————————————————————————————————————————————
+# AGE                                                                
+#   n                       134             134             132      
+#   Mean (sd)          33.77 (6.55)    35.43 (7.90)     35.43 (7.72) 
+#   IQR                    11.00           10.00           10.00     
+#   min - max          21.00 - 50.00   21.00 - 62.00   20.00 - 69.00 
+# SEX                                                                
+#   F                       79              77               66      
+#   M                       51              55               60      
+#   U                        3               2               4       
+#   UNDIFFERENTIATED         1               0               2
+

or for all patients that are older than 18:

+
+adsl_f_tbl <- build_table(lyt, ADSL %>% filter(AGE > 18))
+
# Warning in min(x): no non-missing arguments to min; returning Inf
+
# Warning in max(x): no non-missing arguments to max; returning -Inf
+
+adsl_f_tbl
+
#                      A: Drug X      B: Placebo     C: Combination
+#                       (N=134)         (N=134)         (N=132)    
+# —————————————————————————————————————————————————————————————————
+# F                                                                
+#   AGE                                                            
+#     n                   79              77               66      
+#     Mean (sd)      32.76 (6.09)    34.12 (7.06)     35.20 (7.43) 
+#     IQR                9.00            8.00             6.75     
+#     min - max      21.00 - 47.00   23.00 - 58.00   21.00 - 64.00 
+#   BMRKR2                                                         
+#     LOW                 26              21               26      
+#     MEDIUM              21              38               17      
+#     HIGH                32              18               23      
+# M                                                                
+#   AGE                                                            
+#     n                   51              55               60      
+#     Mean (sd)      35.57 (7.08)    37.44 (8.69)     35.38 (8.24) 
+#     IQR                11.00           9.00            11.00     
+#     min - max      23.00 - 50.00   21.00 - 62.00   20.00 - 69.00 
+#   BMRKR2                                                         
+#     LOW                 21              23               11      
+#     MEDIUM              15              18               23      
+#     HIGH                15              14               26      
+# U                                                                
+#   AGE                                                            
+#     n                    3               2               4       
+#     Mean (sd)      31.67 (3.21)    31.00 (5.66)     35.25 (3.10) 
+#     IQR                3.00            4.00             3.25     
+#     min - max      28.00 - 34.00   27.00 - 35.00   31.00 - 38.00 
+#   BMRKR2                                                         
+#     LOW                  2               1               1       
+#     MEDIUM               1               0               2       
+#     HIGH                 0               1               1       
+# UNDIFFERENTIATED                                                 
+#   AGE                                                            
+#     n                    1               0               2       
+#     Mean (sd)       28.00 (NA)          NA          45.00 (1.41) 
+#     IQR                0.00             NA              1.00     
+#     min - max      28.00 - 28.00    Inf - -Inf     44.00 - 46.00 
+#   BMRKR2                                                         
+#     LOW                  1               0               2       
+#     MEDIUM               0               0               0       
+#     HIGH                 0               0               0
+
+
+
+

Adverse Events +

+

There are a number of different adverse event tables. We will now +present two tables that show adverse events by ID and then by grade and +by ID.

+

This time we won’t use the ADAE dataset from random.cdisc.data +but rather generate a dataset on the fly (see Adrian’s +2016 Phuse paper):

+
+set.seed(1)
+
+lookup <- tribble(
+  ~AEDECOD,                          ~AEBODSYS,                                         ~AETOXGR,
+  "HEADACHE",                        "NERVOUS SYSTEM DISORDERS",                        "5",
+  "BACK PAIN",                       "MUSCULOSKELETAL AND CONNECTIVE TISSUE DISORDERS", "2",
+  "GINGIVAL BLEEDING",               "GASTROINTESTINAL DISORDERS",                      "1",
+  "HYPOTENSION",                     "VASCULAR DISORDERS",                              "3",
+  "FAECES SOFT",                     "GASTROINTESTINAL DISORDERS",                      "2",
+  "ABDOMINAL DISCOMFORT",            "GASTROINTESTINAL DISORDERS",                      "1",
+  "DIARRHEA",                        "GASTROINTESTINAL DISORDERS",                      "1",
+  "ABDOMINAL FULLNESS DUE TO GAS",   "GASTROINTESTINAL DISORDERS",                      "1",
+  "NAUSEA (INTERMITTENT)",           "GASTROINTESTINAL DISORDERS",                      "2",
+  "WEAKNESS",                        "MUSCULOSKELETAL AND CONNECTIVE TISSUE DISORDERS", "3",
+  "ORTHOSTATIC HYPOTENSION",         "VASCULAR DISORDERS",                              "4"
+)
+
+normalize <- function(x) x / sum(x)
+weightsA <- normalize(c(0.1, dlnorm(seq(0, 5, length.out = 25), meanlog = 3)))
+weightsB <- normalize(c(0.2, dlnorm(seq(0, 5, length.out = 25))))
+
+N_pop <- 300
+ADSL2 <- data.frame(
+  USUBJID = seq(1, N_pop, by = 1),
+  ARM = sample(c("ARM A", "ARM B"), N_pop, TRUE),
+  SEX = sample(c("F", "M"), N_pop, TRUE),
+  AGE = 20 + rbinom(N_pop, size = 40, prob = 0.7)
+)
+
+l.adae <- mapply(
+  ADSL2$USUBJID,
+  ADSL2$ARM,
+  ADSL2$SEX,
+  ADSL2$AGE,
+  FUN = function(id, arm, sex, age) {
+    n_ae <- sample(0:25, 1, prob = if (arm == "ARM A") weightsA else weightsB)
+    i <- sample(seq_len(nrow(lookup)), size = n_ae, replace = TRUE, prob = c(6, rep(1, 10)) / 16)
+    lookup[i, ] %>%
+      mutate(
+        AESEQ = seq_len(n()),
+        USUBJID = id, ARM = arm, SEX = sex, AGE = age
+      )
+  },
+  SIMPLIFY = FALSE
+)
+
+ADAE2 <- do.call(rbind, l.adae)
+ADAE2 <- ADAE2 %>%
+  mutate(
+    ARM = factor(ARM, levels = c("ARM A", "ARM B")),
+    AEDECOD = as.factor(AEDECOD),
+    AEBODSYS = as.factor(AEBODSYS),
+    AETOXGR = factor(AETOXGR, levels = as.character(1:5))
+  ) %>%
+  select(USUBJID, ARM, AGE, SEX, AESEQ, AEDECOD, AEBODSYS, AETOXGR)
+
+ADAE2
+
# # A tibble: 3,118 × 8
+#    USUBJID ARM     AGE SEX   AESEQ AEDECOD               AEBODSYS        AETOXGR
+#      <dbl> <fct> <dbl> <chr> <int> <fct>                 <fct>           <fct>  
+#  1       1 ARM A    45 F         1 NAUSEA (INTERMITTENT) GASTROINTESTIN… 2      
+#  2       1 ARM A    45 F         2 HEADACHE              NERVOUS SYSTEM… 5      
+#  3       1 ARM A    45 F         3 HEADACHE              NERVOUS SYSTEM… 5      
+#  4       1 ARM A    45 F         4 HEADACHE              NERVOUS SYSTEM… 5      
+#  5       1 ARM A    45 F         5 HEADACHE              NERVOUS SYSTEM… 5      
+#  6       1 ARM A    45 F         6 HEADACHE              NERVOUS SYSTEM… 5      
+#  7       1 ARM A    45 F         7 HEADACHE              NERVOUS SYSTEM… 5      
+#  8       1 ARM A    45 F         8 HEADACHE              NERVOUS SYSTEM… 5      
+#  9       1 ARM A    45 F         9 HEADACHE              NERVOUS SYSTEM… 5      
+# 10       1 ARM A    45 F        10 FAECES SOFT           GASTROINTESTIN… 2      
+# # ℹ 3,108 more rows
+
+

Adverse Events By ID +

+

We start by defining an events summary function:

+
+s_events_patients <- function(x, labelstr, .N_col) {
+  in_rows(
+    "Total number of patients with at least one event" =
+      rcell(length(unique(x)) * c(1, 1 / .N_col), format = "xx (xx.xx%)"),
+    "Total number of events" = rcell(length(x), format = "xx")
+  )
+}
+

So, for a population of 5 patients where

+
    +
  • one patient has 2 AEs
  • +
  • one patient has 1 AE +
  • +
  • three patients have no AEs
  • +
+

we would get the following summary:

+
+s_events_patients(x = c("id 1", "id 1", "id 2"), .N_col = 5)
+
# RowsVerticalSection (in_rows) object print method:
+# ----------------------------
+#                                           row_name formatted_cell indent_mod
+# 1 Total number of patients with at least one event     2 (40.00%)          0
+# 2                           Total number of events              3          0
+#                                          row_label
+# 1 Total number of patients with at least one event
+# 2                           Total number of events
+

The .N_col argument is a special keyword argument by +which build_table() passes the population size for each +respective column. For a list of keyword arguments for the functions +passed to afun in analyze(), refer to the +documentation with ?analyze.

+

We now use the s_events_patients summary function in a +tabulation:

+
+adae_lyt <- basic_table(show_colcounts = TRUE) %>%
+  split_cols_by("ARM") %>%
+  analyze("USUBJID", s_events_patients)
+
+adae_tbl <- build_table(adae_lyt, ADAE2)
+adae_tbl
+
#                                                       ARM A         ARM B    
+#                                                     (N=2060)       (N=1058)  
+# —————————————————————————————————————————————————————————————————————————————
+# Total number of patients with at least one event   114 (5.53%)   150 (14.18%)
+# Total number of events                                2060           1058
+

Note that the column Ns are wrong as by default they are +set to the number of rows per group (i.e. number of AEs per +arm here). This also affects the percentages. For this table we are +interested in the number of patients per column/arm which is usually +taken from ADSL (var ADSL2 here).

+

rtables handles this by allowing us to override how the +column counts are computed. We can specify an alt_counts_df +in build_table(). When we do this, rtables +calculates the column counts by applying the same column faceting to +alt_counts_df as it does to the primary data during +tabulation:

+
+adae_adsl_tbl <- build_table(adae_lyt, ADAE2, alt_counts_df = ADSL2)
+adae_adsl_tbl
+
#                                                       ARM A          ARM B    
+#                                                      (N=146)        (N=154)   
+# ——————————————————————————————————————————————————————————————————————————————
+# Total number of patients with at least one event   114 (78.08%)   150 (97.40%)
+# Total number of events                                 2060           1058
+

Alternatively, if the desired column counts are already calculated, +they can be specified directly via the col_counts argument +to build_table(), though specifying an +alt_counts_df is the preferred mechanism (the number of +rows will be used, but no duplicate checking!!!).

+

We next calculate this information per system organ class:

+
+adae_soc_lyt <- basic_table(show_colcounts = TRUE) %>%
+  split_cols_by("ARM") %>%
+  analyze("USUBJID", s_events_patients) %>%
+  split_rows_by("AEBODSYS", child_labels = "visible", nested = FALSE) %>%
+  summarize_row_groups("USUBJID", cfun = s_events_patients)
+
+adae_soc_tbl <- build_table(adae_soc_lyt, ADAE2, alt_counts_df = ADSL2)
+adae_soc_tbl
+
#                                                         ARM A          ARM B    
+#                                                        (N=146)        (N=154)   
+# ————————————————————————————————————————————————————————————————————————————————
+# Total number of patients with at least one event     114 (78.08%)   150 (97.40%)
+# Total number of events                                   2060           1058    
+# GASTROINTESTINAL DISORDERS                                                      
+#   Total number of patients with at least one event   114 (78.08%)   130 (84.42%)
+#   Total number of events                                 760            374     
+# MUSCULOSKELETAL AND CONNECTIVE TISSUE DISORDERS                                 
+#   Total number of patients with at least one event   98 (67.12%)    81 (52.60%) 
+#   Total number of events                                 273            142     
+# NERVOUS SYSTEM DISORDERS                                                        
+#   Total number of patients with at least one event   113 (77.40%)   133 (86.36%)
+#   Total number of events                                 787            420     
+# VASCULAR DISORDERS                                                              
+#   Total number of patients with at least one event   93 (63.70%)    75 (48.70%) 
+#   Total number of events                                 240            122
+

We now have to add a count table of AEDECOD for each +AEBODSYS. The default analyze() behavior for a +factor is to create the count table per level (using +rtab_inner):

+
+adae_soc_lyt2 <- basic_table(show_colcounts = TRUE) %>%
+  split_cols_by("ARM") %>%
+  split_rows_by("AEBODSYS", child_labels = "visible", indent_mod = 1) %>%
+  summarize_row_groups("USUBJID", cfun = s_events_patients) %>%
+  analyze("AEDECOD", indent_mod = -1)
+
+adae_soc_tbl2 <- build_table(adae_soc_lyt2, ADAE2, alt_counts_df = ADSL2)
+adae_soc_tbl2
+
#                                                           ARM A          ARM B    
+#                                                          (N=146)        (N=154)   
+# ——————————————————————————————————————————————————————————————————————————————————
+#   GASTROINTESTINAL DISORDERS                                                      
+#     Total number of patients with at least one event   114 (78.08%)   130 (84.42%)
+#     Total number of events                                 760            374     
+#     ABDOMINAL DISCOMFORT                                   113             65     
+#     ABDOMINAL FULLNESS DUE TO GAS                          119             65     
+#     BACK PAIN                                               0              0      
+#     DIARRHEA                                               107             53     
+#     FAECES SOFT                                            122             58     
+#     GINGIVAL BLEEDING                                      147             71     
+#     HEADACHE                                                0              0      
+#     HYPOTENSION                                             0              0      
+#     NAUSEA (INTERMITTENT)                                  152             62     
+#     ORTHOSTATIC HYPOTENSION                                 0              0      
+#     WEAKNESS                                                0              0      
+#   MUSCULOSKELETAL AND CONNECTIVE TISSUE DISORDERS                                 
+#     Total number of patients with at least one event   98 (67.12%)    81 (52.60%) 
+#     Total number of events                                 273            142     
+#     ABDOMINAL DISCOMFORT                                    0              0      
+#     ABDOMINAL FULLNESS DUE TO GAS                           0              0      
+#     BACK PAIN                                              135             75     
+#     DIARRHEA                                                0              0      
+#     FAECES SOFT                                             0              0      
+#     GINGIVAL BLEEDING                                       0              0      
+#     HEADACHE                                                0              0      
+#     HYPOTENSION                                             0              0      
+#     NAUSEA (INTERMITTENT)                                   0              0      
+#     ORTHOSTATIC HYPOTENSION                                 0              0      
+#     WEAKNESS                                               138             67     
+#   NERVOUS SYSTEM DISORDERS                                                        
+#     Total number of patients with at least one event   113 (77.40%)   133 (86.36%)
+#     Total number of events                                 787            420     
+#     ABDOMINAL DISCOMFORT                                    0              0      
+#     ABDOMINAL FULLNESS DUE TO GAS                           0              0      
+#     BACK PAIN                                               0              0      
+#     DIARRHEA                                                0              0      
+#     FAECES SOFT                                             0              0      
+#     GINGIVAL BLEEDING                                       0              0      
+#     HEADACHE                                               787            420     
+#     HYPOTENSION                                             0              0      
+#     NAUSEA (INTERMITTENT)                                   0              0      
+#     ORTHOSTATIC HYPOTENSION                                 0              0      
+#     WEAKNESS                                                0              0      
+#   VASCULAR DISORDERS                                                              
+#     Total number of patients with at least one event   93 (63.70%)    75 (48.70%) 
+#     Total number of events                                 240            122     
+#     ABDOMINAL DISCOMFORT                                    0              0      
+#     ABDOMINAL FULLNESS DUE TO GAS                           0              0      
+#     BACK PAIN                                               0              0      
+#     DIARRHEA                                                0              0      
+#     FAECES SOFT                                             0              0      
+#     GINGIVAL BLEEDING                                       0              0      
+#     HEADACHE                                                0              0      
+#     HYPOTENSION                                            104             58     
+#     NAUSEA (INTERMITTENT)                                   0              0      
+#     ORTHOSTATIC HYPOTENSION                                136             64     
+#     WEAKNESS                                                0              0
+

The indent_mod argument enables relative indenting +changes if the tree structure of the table does not result in the +desired indentation by default.

+

This table so far is however not the usual adverse event table as it +counts the total number of events and not the number of subjects for one +or more events for a particular term. To get the correct table we need +to write a custom analysis function:

+
+table_count_once_per_id <- function(df, termvar = "AEDECOD", idvar = "USUBJID") {
+  x <- df[[termvar]]
+  id <- df[[idvar]]
+
+  counts <- table(x[!duplicated(id)])
+
+  in_rows(
+    .list = as.vector(counts),
+    .labels = names(counts)
+  )
+}
+
+table_count_once_per_id(ADAE2)
+
# RowsVerticalSection (in_rows) object print method:
+# ----------------------------
+#                         row_name formatted_cell indent_mod
+# 1           ABDOMINAL DISCOMFORT             23          0
+# 2  ABDOMINAL FULLNESS DUE TO GAS             21          0
+# 3                      BACK PAIN             20          0
+# 4                       DIARRHEA              7          0
+# 5                    FAECES SOFT             11          0
+# 6              GINGIVAL BLEEDING             15          0
+# 7                       HEADACHE            100          0
+# 8                    HYPOTENSION             16          0
+# 9          NAUSEA (INTERMITTENT)             21          0
+# 10       ORTHOSTATIC HYPOTENSION             14          0
+# 11                      WEAKNESS             16          0
+#                        row_label
+# 1           ABDOMINAL DISCOMFORT
+# 2  ABDOMINAL FULLNESS DUE TO GAS
+# 3                      BACK PAIN
+# 4                       DIARRHEA
+# 5                    FAECES SOFT
+# 6              GINGIVAL BLEEDING
+# 7                       HEADACHE
+# 8                    HYPOTENSION
+# 9          NAUSEA (INTERMITTENT)
+# 10       ORTHOSTATIC HYPOTENSION
+# 11                      WEAKNESS
+

So the desired AE table is:

+
+adae_soc_lyt3 <- basic_table(show_colcounts = TRUE) %>%
+  split_cols_by("ARM") %>%
+  split_rows_by("AEBODSYS", child_labels = "visible", indent_mod = 1) %>%
+  summarize_row_groups("USUBJID", cfun = s_events_patients) %>%
+  analyze("AEDECOD", afun = table_count_once_per_id, show_labels = "hidden", indent_mod = -1)
+
+adae_soc_tbl3 <- build_table(adae_soc_lyt3, ADAE2, alt_counts_df = ADSL2)
+adae_soc_tbl3
+
#                                                           ARM A          ARM B    
+#                                                          (N=146)        (N=154)   
+# ——————————————————————————————————————————————————————————————————————————————————
+#   GASTROINTESTINAL DISORDERS                                                      
+#     Total number of patients with at least one event   114 (78.08%)   130 (84.42%)
+#     Total number of events                                 760            374     
+#     ABDOMINAL DISCOMFORT                                    24             28     
+#     ABDOMINAL FULLNESS DUE TO GAS                           18             26     
+#     BACK PAIN                                               0              0      
+#     DIARRHEA                                                17             17     
+#     FAECES SOFT                                             17             14     
+#     GINGIVAL BLEEDING                                       18             25     
+#     HEADACHE                                                0              0      
+#     HYPOTENSION                                             0              0      
+#     NAUSEA (INTERMITTENT)                                   20             20     
+#     ORTHOSTATIC HYPOTENSION                                 0              0      
+#     WEAKNESS                                                0              0      
+#   MUSCULOSKELETAL AND CONNECTIVE TISSUE DISORDERS                                 
+#     Total number of patients with at least one event   98 (67.12%)    81 (52.60%) 
+#     Total number of events                                 273            142     
+#     ABDOMINAL DISCOMFORT                                    0              0      
+#     ABDOMINAL FULLNESS DUE TO GAS                           0              0      
+#     BACK PAIN                                               58             45     
+#     DIARRHEA                                                0              0      
+#     FAECES SOFT                                             0              0      
+#     GINGIVAL BLEEDING                                       0              0      
+#     HEADACHE                                                0              0      
+#     HYPOTENSION                                             0              0      
+#     NAUSEA (INTERMITTENT)                                   0              0      
+#     ORTHOSTATIC HYPOTENSION                                 0              0      
+#     WEAKNESS                                                40             36     
+#   NERVOUS SYSTEM DISORDERS                                                        
+#     Total number of patients with at least one event   113 (77.40%)   133 (86.36%)
+#     Total number of events                                 787            420     
+#     ABDOMINAL DISCOMFORT                                    0              0      
+#     ABDOMINAL FULLNESS DUE TO GAS                           0              0      
+#     BACK PAIN                                               0              0      
+#     DIARRHEA                                                0              0      
+#     FAECES SOFT                                             0              0      
+#     GINGIVAL BLEEDING                                       0              0      
+#     HEADACHE                                               113            133     
+#     HYPOTENSION                                             0              0      
+#     NAUSEA (INTERMITTENT)                                   0              0      
+#     ORTHOSTATIC HYPOTENSION                                 0              0      
+#     WEAKNESS                                                0              0      
+#   VASCULAR DISORDERS                                                              
+#     Total number of patients with at least one event   93 (63.70%)    75 (48.70%) 
+#     Total number of events                                 240            122     
+#     ABDOMINAL DISCOMFORT                                    0              0      
+#     ABDOMINAL FULLNESS DUE TO GAS                           0              0      
+#     BACK PAIN                                               0              0      
+#     DIARRHEA                                                0              0      
+#     FAECES SOFT                                             0              0      
+#     GINGIVAL BLEEDING                                       0              0      
+#     HEADACHE                                                0              0      
+#     HYPOTENSION                                             44             31     
+#     NAUSEA (INTERMITTENT)                                   0              0      
+#     ORTHOSTATIC HYPOTENSION                                 49             44     
+#     WEAKNESS                                                0              0
+

Note that we are missing the overall summary in the first two rows. +This can be added with an initial analyze() call.

+
+adae_soc_lyt4 <- basic_table(show_colcounts = TRUE) %>%
+  split_cols_by("ARM") %>%
+  analyze("USUBJID", afun = s_events_patients) %>%
+  split_rows_by("AEBODSYS", child_labels = "visible", indent_mod = 1, section_div = "") %>%
+  summarize_row_groups("USUBJID", cfun = s_events_patients) %>%
+  analyze("AEDECOD", table_count_once_per_id, show_labels = "hidden", indent_mod = -1)
+
+adae_soc_tbl4 <- build_table(adae_soc_lyt4, ADAE2, alt_counts_df = ADSL2)
+adae_soc_tbl4
+
#                                                           ARM A          ARM B    
+#                                                          (N=146)        (N=154)   
+# ——————————————————————————————————————————————————————————————————————————————————
+# Total number of patients with at least one event       114 (78.08%)   150 (97.40%)
+# Total number of events                                     2060           1058    
+#   GASTROINTESTINAL DISORDERS                                                      
+#     Total number of patients with at least one event   114 (78.08%)   130 (84.42%)
+#     Total number of events                                 760            374     
+#     ABDOMINAL DISCOMFORT                                    24             28     
+#     ABDOMINAL FULLNESS DUE TO GAS                           18             26     
+#     BACK PAIN                                               0              0      
+#     DIARRHEA                                                17             17     
+#     FAECES SOFT                                             17             14     
+#     GINGIVAL BLEEDING                                       18             25     
+#     HEADACHE                                                0              0      
+#     HYPOTENSION                                             0              0      
+#     NAUSEA (INTERMITTENT)                                   20             20     
+#     ORTHOSTATIC HYPOTENSION                                 0              0      
+#     WEAKNESS                                                0              0      
+# 
+#   MUSCULOSKELETAL AND CONNECTIVE TISSUE DISORDERS                                 
+#     Total number of patients with at least one event   98 (67.12%)    81 (52.60%) 
+#     Total number of events                                 273            142     
+#     ABDOMINAL DISCOMFORT                                    0              0      
+#     ABDOMINAL FULLNESS DUE TO GAS                           0              0      
+#     BACK PAIN                                               58             45     
+#     DIARRHEA                                                0              0      
+#     FAECES SOFT                                             0              0      
+#     GINGIVAL BLEEDING                                       0              0      
+#     HEADACHE                                                0              0      
+#     HYPOTENSION                                             0              0      
+#     NAUSEA (INTERMITTENT)                                   0              0      
+#     ORTHOSTATIC HYPOTENSION                                 0              0      
+#     WEAKNESS                                                40             36     
+# 
+#   NERVOUS SYSTEM DISORDERS                                                        
+#     Total number of patients with at least one event   113 (77.40%)   133 (86.36%)
+#     Total number of events                                 787            420     
+#     ABDOMINAL DISCOMFORT                                    0              0      
+#     ABDOMINAL FULLNESS DUE TO GAS                           0              0      
+#     BACK PAIN                                               0              0      
+#     DIARRHEA                                                0              0      
+#     FAECES SOFT                                             0              0      
+#     GINGIVAL BLEEDING                                       0              0      
+#     HEADACHE                                               113            133     
+#     HYPOTENSION                                             0              0      
+#     NAUSEA (INTERMITTENT)                                   0              0      
+#     ORTHOSTATIC HYPOTENSION                                 0              0      
+#     WEAKNESS                                                0              0      
+# 
+#   VASCULAR DISORDERS                                                              
+#     Total number of patients with at least one event   93 (63.70%)    75 (48.70%) 
+#     Total number of events                                 240            122     
+#     ABDOMINAL DISCOMFORT                                    0              0      
+#     ABDOMINAL FULLNESS DUE TO GAS                           0              0      
+#     BACK PAIN                                               0              0      
+#     DIARRHEA                                                0              0      
+#     FAECES SOFT                                             0              0      
+#     GINGIVAL BLEEDING                                       0              0      
+#     HEADACHE                                                0              0      
+#     HYPOTENSION                                             44             31     
+#     NAUSEA (INTERMITTENT)                                   0              0      
+#     ORTHOSTATIC HYPOTENSION                                 49             44     
+#     WEAKNESS                                                0              0
+

Finally, if we wanted to prune the 0 count rows we can do that with +the trim_rows() function:

+
+trim_rows(adae_soc_tbl4)
+
#                                                           ARM A          ARM B    
+#                                                          (N=146)        (N=154)   
+# ——————————————————————————————————————————————————————————————————————————————————
+# Total number of patients with at least one event       114 (78.08%)   150 (97.40%)
+# Total number of events                                     2060           1058    
+#   GASTROINTESTINAL DISORDERS                                                      
+#     Total number of patients with at least one event   114 (78.08%)   130 (84.42%)
+#     Total number of events                                 760            374     
+#     ABDOMINAL DISCOMFORT                                    24             28     
+#     ABDOMINAL FULLNESS DUE TO GAS                           18             26     
+#     DIARRHEA                                                17             17     
+#     FAECES SOFT                                             17             14     
+#     GINGIVAL BLEEDING                                       18             25     
+#     NAUSEA (INTERMITTENT)                                   20             20     
+# 
+#   MUSCULOSKELETAL AND CONNECTIVE TISSUE DISORDERS                                 
+#     Total number of patients with at least one event   98 (67.12%)    81 (52.60%) 
+#     Total number of events                                 273            142     
+#     BACK PAIN                                               58             45     
+#     WEAKNESS                                                40             36     
+# 
+#   NERVOUS SYSTEM DISORDERS                                                        
+#     Total number of patients with at least one event   113 (77.40%)   133 (86.36%)
+#     Total number of events                                 787            420     
+#     HEADACHE                                               113            133     
+# 
+#   VASCULAR DISORDERS                                                              
+#     Total number of patients with at least one event   93 (63.70%)    75 (48.70%) 
+#     Total number of events                                 240            122     
+#     HYPOTENSION                                             44             31     
+#     ORTHOSTATIC HYPOTENSION                                 49             44
+

Pruning is a larger topic with a separate +rtables package vignette.

+
+
+

Adverse Events By ID and By Grade +

+

The adverse events table by ID and by grade shows how many patients +had at least one adverse event per grade for different subsets of the +data (e.g. defined by system organ class).

+

For this table we do not show the zero count grades. Note that we add +the “overall” groups with a custom split function.

+
+table_count_grade_once_per_id <- function(df,
+                                          labelstr = "",
+                                          gradevar = "AETOXGR",
+                                          idvar = "USUBJID",
+                                          grade_levels = NULL) {
+  id <- df[[idvar]]
+  grade <- df[[gradevar]]
+
+  if (!is.null(grade_levels)) {
+    stopifnot(all(grade %in% grade_levels))
+    grade <- factor(grade, levels = grade_levels)
+  }
+
+  id_sel <- !duplicated(id)
+
+  in_rows(
+    "--Any Grade--" = sum(id_sel),
+    .list = as.list(table(grade[id_sel]))
+  )
+}
+
+table_count_grade_once_per_id(ex_adae, grade_levels = 1:5)
+
# RowsVerticalSection (in_rows) object print method:
+# ----------------------------
+#        row_name formatted_cell indent_mod     row_label
+# 1 --Any Grade--            365          0 --Any Grade--
+# 2             1            131          0             1
+# 3             2             70          0             2
+# 4             3             74          0             3
+# 5             4             25          0             4
+# 6             5             65          0             5
+

All of the layouting concepts needed to create this table have +already been introduced so far:

+
+adae_grade_lyt <- basic_table(show_colcounts = TRUE) %>%
+  split_cols_by("ARM") %>%
+  analyze(
+    "AETOXGR",
+    afun = table_count_grade_once_per_id,
+    extra_args = list(grade_levels = 1:5),
+    var_labels = "- Any adverse events -",
+    show_labels = "visible"
+  ) %>%
+  split_rows_by("AEBODSYS", child_labels = "visible", indent_mod = 1) %>%
+  summarize_row_groups(cfun = table_count_grade_once_per_id, format = "xx", indent_mod = 1) %>%
+  split_rows_by("AEDECOD", child_labels = "visible", indent_mod = -2) %>%
+  analyze(
+    "AETOXGR",
+    afun = table_count_grade_once_per_id,
+    extra_args = list(grade_levels = 1:5),
+    show_labels = "hidden"
+  )
+
+adae_grade_tbl <- build_table(adae_grade_lyt, ADAE2, alt_counts_df = ADSL2)
+adae_grade_tbl
+
#                                                      ARM A     ARM B 
+#                                                     (N=146)   (N=154)
+# —————————————————————————————————————————————————————————————————————
+# - Any adverse events -                                               
+#   --Any Grade--                                       114       150  
+#   1                                                   32        34   
+#   2                                                   22        30   
+#   3                                                   11        21   
+#   4                                                    8         6   
+#   5                                                   41        59   
+#   GASTROINTESTINAL DISORDERS                                         
+#         --Any Grade--                                 114       130  
+#         1                                             77        96   
+#         2                                             37        34   
+#         3                                              0         0   
+#         4                                              0         0   
+#         5                                              0         0   
+#     ABDOMINAL DISCOMFORT                                             
+#       --Any Grade--                                   68        49   
+#       1                                               68        49   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     ABDOMINAL FULLNESS DUE TO GAS                                    
+#       --Any Grade--                                   73        51   
+#       1                                               73        51   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     BACK PAIN                                                        
+#       --Any Grade--                                    0         0   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     DIARRHEA                                                         
+#       --Any Grade--                                   68        40   
+#       1                                               68        40   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     FAECES SOFT                                                      
+#       --Any Grade--                                   76        44   
+#       1                                                0         0   
+#       2                                               76        44   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     GINGIVAL BLEEDING                                                
+#       --Any Grade--                                   80        52   
+#       1                                               80        52   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     HEADACHE                                                         
+#       --Any Grade--                                    0         0   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     HYPOTENSION                                                      
+#       --Any Grade--                                    0         0   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     NAUSEA (INTERMITTENT)                                            
+#       --Any Grade--                                   83        50   
+#       1                                                0         0   
+#       2                                               83        50   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     ORTHOSTATIC HYPOTENSION                                          
+#       --Any Grade--                                    0         0   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     WEAKNESS                                                         
+#       --Any Grade--                                    0         0   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#   MUSCULOSKELETAL AND CONNECTIVE TISSUE DISORDERS                    
+#         --Any Grade--                                 98        81   
+#         1                                              0         0   
+#         2                                             58        45   
+#         3                                             40        36   
+#         4                                              0         0   
+#         5                                              0         0   
+#     ABDOMINAL DISCOMFORT                                             
+#       --Any Grade--                                    0         0   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     ABDOMINAL FULLNESS DUE TO GAS                                    
+#       --Any Grade--                                    0         0   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     BACK PAIN                                                        
+#       --Any Grade--                                   79        62   
+#       1                                                0         0   
+#       2                                               79        62   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     DIARRHEA                                                         
+#       --Any Grade--                                    0         0   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     FAECES SOFT                                                      
+#       --Any Grade--                                    0         0   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     GINGIVAL BLEEDING                                                
+#       --Any Grade--                                    0         0   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     HEADACHE                                                         
+#       --Any Grade--                                    0         0   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     HYPOTENSION                                                      
+#       --Any Grade--                                    0         0   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     NAUSEA (INTERMITTENT)                                            
+#       --Any Grade--                                    0         0   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     ORTHOSTATIC HYPOTENSION                                          
+#       --Any Grade--                                    0         0   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     WEAKNESS                                                         
+#       --Any Grade--                                   73        43   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                               73        43   
+#       4                                                0         0   
+#       5                                                0         0   
+#   NERVOUS SYSTEM DISORDERS                                           
+#         --Any Grade--                                 113       133  
+#         1                                              0         0   
+#         2                                              0         0   
+#         3                                              0         0   
+#         4                                              0         0   
+#         5                                             113       133  
+#     ABDOMINAL DISCOMFORT                                             
+#       --Any Grade--                                    0         0   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     ABDOMINAL FULLNESS DUE TO GAS                                    
+#       --Any Grade--                                    0         0   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     BACK PAIN                                                        
+#       --Any Grade--                                    0         0   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     DIARRHEA                                                         
+#       --Any Grade--                                    0         0   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     FAECES SOFT                                                      
+#       --Any Grade--                                    0         0   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     GINGIVAL BLEEDING                                                
+#       --Any Grade--                                    0         0   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     HEADACHE                                                         
+#       --Any Grade--                                   113       133  
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                               113       133  
+#     HYPOTENSION                                                      
+#       --Any Grade--                                    0         0   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     NAUSEA (INTERMITTENT)                                            
+#       --Any Grade--                                    0         0   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     ORTHOSTATIC HYPOTENSION                                          
+#       --Any Grade--                                    0         0   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     WEAKNESS                                                         
+#       --Any Grade--                                    0         0   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#   VASCULAR DISORDERS                                                 
+#         --Any Grade--                                 93        75   
+#         1                                              0         0   
+#         2                                              0         0   
+#         3                                             44        31   
+#         4                                             49        44   
+#         5                                              0         0   
+#     ABDOMINAL DISCOMFORT                                             
+#       --Any Grade--                                    0         0   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     ABDOMINAL FULLNESS DUE TO GAS                                    
+#       --Any Grade--                                    0         0   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     BACK PAIN                                                        
+#       --Any Grade--                                    0         0   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     DIARRHEA                                                         
+#       --Any Grade--                                    0         0   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     FAECES SOFT                                                      
+#       --Any Grade--                                    0         0   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     GINGIVAL BLEEDING                                                
+#       --Any Grade--                                    0         0   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     HEADACHE                                                         
+#       --Any Grade--                                    0         0   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     HYPOTENSION                                                      
+#       --Any Grade--                                   66        43   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                               66        43   
+#       4                                                0         0   
+#       5                                                0         0   
+#     NAUSEA (INTERMITTENT)                                            
+#       --Any Grade--                                    0         0   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0   
+#     ORTHOSTATIC HYPOTENSION                                          
+#       --Any Grade--                                   70        54   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                               70        54   
+#       5                                                0         0   
+#     WEAKNESS                                                         
+#       --Any Grade--                                    0         0   
+#       1                                                0         0   
+#       2                                                0         0   
+#       3                                                0         0   
+#       4                                                0         0   
+#       5                                                0         0
+
+
+
+

Response Table +

+

The response table that we will create here is composed of 3 +parts:

+
    +
  1. Binary response table
  2. +
  3. Unstratified analysis comparison vs. control group
  4. +
  5. Multinomial response table
  6. +
+

Let’s start with the first part which is fairly simple to derive:

+
+ADRS_BESRSPI <- ex_adrs %>%
+  filter(PARAMCD == "BESRSPI") %>%
+  mutate(
+    rsp = factor(AVALC %in% c("CR", "PR"), levels = c(TRUE, FALSE), labels = c("Responders", "Non-Responders")),
+    is_rsp = (rsp == "Responders")
+  )
+
+s_proportion <- function(x, .N_col) {
+  in_rows(
+    .list = lapply(
+      as.list(table(x)),
+      function(xi) rcell(xi * c(1, 1 / .N_col), format = "xx.xx (xx.xx%)")
+    )
+  )
+}
+
+rsp_lyt <- basic_table(show_colcounts = TRUE) %>%
+  split_cols_by("ARMCD", ref_group = "ARM A") %>%
+  analyze("rsp", s_proportion, show_labels = "hidden")
+
+rsp_tbl <- build_table(rsp_lyt, ADRS_BESRSPI)
+rsp_tbl
+
#                       ARM A            ARM B             ARM C     
+#                      (N=134)          (N=134)           (N=132)    
+# ———————————————————————————————————————————————————————————————————
+# Responders       114.00 (85.07%)   90.00 (67.16%)   120.00 (90.91%)
+# Non-Responders   20.00 (14.93%)    44.00 (32.84%)    12.00 (9.09%)
+

Note that we did set the ref_group argument in +split_cols_by() which for the current table had no effect +as we only use the cell data for the responder and non-responder counts. +The ref_group argument is needed for the part 2 and 3 of +the table.

+

We will now look the implementation of part 2: unstratified analysis +comparison vs. control group. Let’s start with the analysis +function:

+
+s_unstrat_resp <- function(x, .ref_group, .in_ref_col) {
+  if (.in_ref_col) {
+    return(in_rows(
+      "Difference in Response Rates (%)" = rcell(numeric(0)),
+      "95% CI (Wald, with correction)" = rcell(numeric(0)),
+      "p-value (Chi-Squared Test)" = rcell(numeric(0)),
+      "Odds Ratio (95% CI)" = rcell(numeric(0))
+    ))
+  }
+
+  fit <- stats::prop.test(
+    x = c(sum(x), sum(.ref_group)),
+    n = c(length(x), length(.ref_group)),
+    correct = FALSE
+  )
+
+  fit_glm <- stats::glm(
+    formula = rsp ~ group,
+    data = data.frame(
+      rsp = c(.ref_group, x),
+      group = factor(rep(c("ref", "x"), times = c(length(.ref_group), length(x))), levels = c("ref", "x"))
+    ),
+    family = binomial(link = "logit")
+  )
+
+  in_rows(
+    "Difference in Response Rates (%)" = non_ref_rcell(
+      (mean(x) - mean(.ref_group)) * 100,
+      .in_ref_col,
+      format = "xx.xx"
+    ),
+    "95% CI (Wald, with correction)" = non_ref_rcell(
+      fit$conf.int * 100,
+      .in_ref_col,
+      format = "(xx.xx, xx.xx)"
+    ),
+    "p-value (Chi-Squared Test)" = non_ref_rcell(
+      fit$p.value,
+      .in_ref_col,
+      format = "x.xxxx | (<0.0001)"
+    ),
+    "Odds Ratio (95% CI)" = non_ref_rcell(
+      c(
+        exp(stats::coef(fit_glm)[-1]),
+        exp(stats::confint.default(fit_glm, level = .95)[-1, , drop = FALSE])
+      ),
+      .in_ref_col,
+      format = "xx.xx (xx.xx - xx.xx)"
+    )
+  )
+}
+
+s_unstrat_resp(
+  x = ADRS_BESRSPI %>% filter(ARM == "A: Drug X") %>% pull(is_rsp),
+  .ref_group = ADRS_BESRSPI %>% filter(ARM == "B: Placebo") %>% pull(is_rsp),
+  .in_ref_col = FALSE
+)
+
# RowsVerticalSection (in_rows) object print method:
+# ----------------------------
+#                           row_name     formatted_cell indent_mod
+# 1 Difference in Response Rates (%)              17.91          0
+# 2   95% CI (Wald, with correction)      (7.93, 27.89)          0
+# 3       p-value (Chi-Squared Test)             0.0006          0
+# 4              Odds Ratio (95% CI) 2.79 (1.53 - 5.06)          0
+#                          row_label
+# 1 Difference in Response Rates (%)
+# 2   95% CI (Wald, with correction)
+# 3       p-value (Chi-Squared Test)
+# 4              Odds Ratio (95% CI)
+

Hence we can now add the next vignette to the table:

+
+rsp_lyt2 <- basic_table(show_colcounts = TRUE) %>%
+  split_cols_by("ARMCD", ref_group = "ARM A") %>%
+  analyze("rsp", s_proportion, show_labels = "hidden") %>%
+  analyze(
+    "is_rsp", s_unstrat_resp,
+    show_labels = "visible",
+    var_labels = "Unstratified Response Analysis"
+  )
+
+rsp_tbl2 <- build_table(rsp_lyt2, ADRS_BESRSPI)
+rsp_tbl2
+
#                                           ARM A              ARM B                ARM C       
+#                                          (N=134)            (N=134)              (N=132)      
+# ——————————————————————————————————————————————————————————————————————————————————————————————
+# Responders                           114.00 (85.07%)     90.00 (67.16%)      120.00 (90.91%)  
+# Non-Responders                       20.00 (14.93%)      44.00 (32.84%)       12.00 (9.09%)   
+# Unstratified Response Analysis                                                                
+#   Difference in Response Rates (%)                           -17.91                5.83       
+#   95% CI (Wald, with correction)                        (-27.89, -7.93)       (-1.94, 13.61)  
+#   p-value (Chi-Squared Test)                                 0.0006               0.1436      
+#   Odds Ratio (95% CI)                                  0.36 (0.20 - 0.65)   1.75 (0.82 - 3.75)
+

Next we will add part 3: the multinomial response table. To do so, we +are adding a row-split by response level, and then doing the same thing +as we did for the binary response table above.

+
+s_prop <- function(df, .N_col) {
+  in_rows(
+    "95% CI (Wald, with correction)" = rcell(binom.test(nrow(df), .N_col)$conf.int * 100, format = "(xx.xx, xx.xx)")
+  )
+}
+
+s_prop(
+  df = ADRS_BESRSPI %>% filter(ARM == "A: Drug X", AVALC == "CR"),
+  .N_col = sum(ADRS_BESRSPI$ARM == "A: Drug X")
+)
+
# RowsVerticalSection (in_rows) object print method:
+# ----------------------------
+#                         row_name formatted_cell indent_mod
+# 1 95% CI (Wald, with correction) (49.38, 66.67)          0
+#                        row_label
+# 1 95% CI (Wald, with correction)
+

We can now create the final response table with all three parts:

+
+rsp_lyt3 <- basic_table(show_colcounts = TRUE) %>%
+  split_cols_by("ARMCD", ref_group = "ARM A") %>%
+  analyze("rsp", s_proportion, show_labels = "hidden") %>%
+  analyze(
+    "is_rsp", s_unstrat_resp,
+    show_labels = "visible", var_labels = "Unstratified Response Analysis"
+  ) %>%
+  split_rows_by(
+    var = "AVALC",
+    split_fun = reorder_split_levels(neworder = c("CR", "PR", "SD", "NON CR/PD", "PD", "NE"), drlevels = TRUE),
+    nested = FALSE
+  ) %>%
+  summarize_row_groups() %>%
+  analyze("AVALC", afun = s_prop)
+
+rsp_tbl3 <- build_table(rsp_lyt3, ADRS_BESRSPI)
+rsp_tbl3
+
#                                           ARM A              ARM B                ARM C       
+#                                          (N=134)            (N=134)              (N=132)      
+# ——————————————————————————————————————————————————————————————————————————————————————————————
+# Responders                           114.00 (85.07%)     90.00 (67.16%)      120.00 (90.91%)  
+# Non-Responders                       20.00 (14.93%)      44.00 (32.84%)       12.00 (9.09%)   
+# Unstratified Response Analysis                                                                
+#   Difference in Response Rates (%)                           -17.91                5.83       
+#   95% CI (Wald, with correction)                        (-27.89, -7.93)       (-1.94, 13.61)  
+#   p-value (Chi-Squared Test)                                 0.0006               0.1436      
+#   Odds Ratio (95% CI)                                  0.36 (0.20 - 0.65)   1.75 (0.82 - 3.75)
+# CR                                     78 (58.2%)          55 (41.0%)           97 (73.5%)    
+#   95% CI (Wald, with correction)     (49.38, 66.67)      (32.63, 49.87)       (65.10, 80.79)  
+# PR                                     36 (26.9%)          35 (26.1%)           23 (17.4%)    
+#   95% CI (Wald, with correction)     (19.58, 35.20)      (18.92, 34.41)       (11.38, 24.99)  
+# SD                                     20 (14.9%)          44 (32.8%)           12 (9.1%)     
+#   95% CI (Wald, with correction)      (9.36, 22.11)      (24.97, 41.47)       (4.79, 15.34)
+

In the case that we wanted to rename the levels of AVALC +and remove the CI for NE we could do that as follows:

+
+rsp_label <- function(x) {
+  rsp_full_label <- c(
+    CR = "Complete Response (CR)",
+    PR = "Partial Response (PR)",
+    SD = "Stable Disease (SD)",
+    `NON CR/PD` = "Non-CR or Non-PD (NON CR/PD)",
+    PD = "Progressive Disease (PD)",
+    NE = "Not Evaluable (NE)",
+    Missing = "Missing",
+    `NE/Missing` = "Missing or unevaluable"
+  )
+  stopifnot(all(x %in% names(rsp_full_label)))
+  rsp_full_label[x]
+}
+
+
+rsp_lyt4 <- basic_table(show_colcounts = TRUE) %>%
+  split_cols_by("ARMCD", ref_group = "ARM A") %>%
+  analyze("rsp", s_proportion, show_labels = "hidden") %>%
+  analyze(
+    "is_rsp", s_unstrat_resp,
+    show_labels = "visible", var_labels = "Unstratified Response Analysis"
+  ) %>%
+  split_rows_by(
+    var = "AVALC",
+    split_fun = keep_split_levels(c("CR", "PR", "SD", "PD"), reorder = TRUE),
+    nested = FALSE
+  ) %>%
+  summarize_row_groups(cfun = function(df, labelstr, .N_col) {
+    in_rows(nrow(df) * c(1, 1 / .N_col), .formats = "xx (xx.xx%)", .labels = rsp_label(labelstr))
+  }) %>%
+  analyze("AVALC", afun = s_prop) %>%
+  analyze("AVALC", afun = function(x, .N_col) {
+    in_rows(rcell(sum(x == "NE") * c(1, 1 / .N_col), format = "xx.xx (xx.xx%)"), .labels = rsp_label("NE"))
+  }, nested = FALSE)
+
+rsp_tbl4 <- build_table(rsp_lyt4, ADRS_BESRSPI)
+rsp_tbl4
+
#                                           ARM A              ARM B                ARM C       
+#                                          (N=134)            (N=134)              (N=132)      
+# ——————————————————————————————————————————————————————————————————————————————————————————————
+# Responders                           114.00 (85.07%)     90.00 (67.16%)      120.00 (90.91%)  
+# Non-Responders                       20.00 (14.93%)      44.00 (32.84%)       12.00 (9.09%)   
+# Unstratified Response Analysis                                                                
+#   Difference in Response Rates (%)                           -17.91                5.83       
+#   95% CI (Wald, with correction)                        (-27.89, -7.93)       (-1.94, 13.61)  
+#   p-value (Chi-Squared Test)                                 0.0006               0.1436      
+#   Odds Ratio (95% CI)                                  0.36 (0.20 - 0.65)   1.75 (0.82 - 3.75)
+# Complete Response (CR)                 78 (58.21%)        55 (41.04%)          97 (73.48%)    
+#   95% CI (Wald, with correction)     (49.38, 66.67)      (32.63, 49.87)       (65.10, 80.79)  
+# Partial Response (PR)                  36 (26.87%)        35 (26.12%)          23 (17.42%)    
+#   95% CI (Wald, with correction)     (19.58, 35.20)      (18.92, 34.41)       (11.38, 24.99)  
+# Stable Disease (SD)                    20 (14.93%)        44 (32.84%)           12 (9.09%)    
+#   95% CI (Wald, with correction)      (9.36, 22.11)      (24.97, 41.47)       (4.79, 15.34)   
+# Progressive Disease (PD)                0 (0.00%)          0 (0.00%)            0 (0.00%)     
+#   95% CI (Wald, with correction)      (0.00, 2.72)        (0.00, 2.72)         (0.00, 2.76)   
+# Not Evaluable (NE)                    0.00 (0.00%)        0.00 (0.00%)         0.00 (0.00%)
+

Note that the table is missing the rows gaps to make it more +readable. The row spacing feature is on the rtables roadmap +and will be implemented in future.

+
+
+

Time to Event Analysis Table +

+

The time to event analysis table that will be constructed consists of +four parts:

+
    +
  1. Overall subject counts
  2. +
  3. Censored subjects summary
  4. +
  5. Cox proportional-hazards analysis
  6. +
  7. Time-to-event analysis
  8. +
+

The table is constructed by sequential use of the +analyze() function, with four custom analysis functions +corresponding to each of the four parts listed above. In addition the +table includes referential footnotes relevant to the table contents. The +table will be faceted column-wise by arm.

+

First we will start by loading the necessary packages and preparing +the data to be used in the construction of this table.

+
+library(survival)
+
+adtte <- ex_adaette %>%
+  dplyr::filter(PARAMCD == "AETTE2", SAFFL == "Y")
+
+# Add censoring to data for example
+adtte[adtte$AVAL > 1.0, ] <- adtte[adtte$AVAL > 1.0, ] %>% mutate(AVAL = 1.0, CNSR = 1)
+
+adtte2 <- adtte %>%
+  mutate(CNSDTDSC = ifelse(CNSDTDSC == "", "__none__", CNSDTDSC))
+

The adtte dataset will be used in preparing the models +while the adtte2 dataset handles missing values in the +“Censor Date Description” column and will be used to produce the final +table. We add censoring into the data for example purposes.

+

Next we create a basic analysis function, a_count_subjs +which prints the overall unique subject counts and percentages within +the data.

+
+a_count_subjs <- function(x, .N_col) {
+  in_rows(
+    "Subjects with Adverse Events n (%)" = rcell(length(unique(x)) * c(1, 1 / .N_col), format = "xx (xx.xx%)")
+  )
+}
+

Then an analysis function is created to generate the counts of +censored subjects for each level of a factor variable in the dataset. In +this case the cnsr_counter function will be applied with +the CNSDTDSC variable which contains a censor date +description for each censored subject.

+
+cnsr_counter <- function(df, .var, .N_col) {
+  x <- df[!duplicated(df$USUBJID), .var]
+  x <- x[x != "__none__"]
+  lapply(table(x), function(xi) rcell(xi * c(1, 1 / .N_col), format = "xx (xx.xx%)"))
+}
+

This function generates counts and fractions of unique subjects +corresponding to each factor level, excluding missing values (uncensored +patients).

+

A Cox proportional-hazards (Cox P-H) analysis is generated next with +a third custom analysis function, a_cph. Prior to creating +the analysis function, the Cox P-H model is fit to our data using the +coxph() and Surv() functions from the +survival package. Then this model is used as input to the +a_cph analysis function which returns hazard ratios, 95% +confidence intervals, and p-values comparing against the reference group +- in this case the leftmost column.

+
+cph <- coxph(Surv(AVAL, CNSR == 0) ~ ACTARM + STRATA1, ties = "exact", data = adtte)
+
+a_cph <- function(df, .var, .in_ref_col, .ref_full, full_cox_fit) {
+  if (.in_ref_col) {
+    ret <- replicate(3, list(rcell(NULL)))
+  } else {
+    curtrt <- df[[.var]][1]
+    coefs <- coef(full_cox_fit)
+    sel_pos <- grep(curtrt, names(coefs), fixed = TRUE)
+    hrval <- exp(coefs[sel_pos])
+    sdf <- survdiff(Surv(AVAL, CNSR == 0) ~ ACTARM + STRATA1, data = rbind(df, .ref_full))
+    pval <- (1 - pchisq(sdf$chisq, length(sdf$n) - 1)) / 2
+    ci_val <- exp(unlist(confint(full_cox_fit)[sel_pos, ]))
+    ret <- list(
+      rcell(hrval, format = "xx.x"),
+      rcell(ci_val, format = "(xx.x, xx.x)"),
+      rcell(pval, format = "x.xxxx | (<0.0001)")
+    )
+  }
+  in_rows(
+    .list = ret,
+    .names = c("Hazard ratio", "95% confidence interval", "p-value (one-sided stratified log rank)")
+  )
+}
+

The fourth and final analysis function, a_tte, generates +a time to first adverse event table with three rows corresponding to +Median, 95% Confidence Interval, and Min Max respectively. First a +survival table is constructed from the summary table of a survival model +using the survfit() and Surv() functions from +the survival package. This table is then given as input to +a_tte which produces the table of time to first adverse +event consisting of the previously mentioned summary statistics.

+
+surv_tbl <- as.data.frame(
+  summary(survfit(Surv(AVAL, CNSR == 0) ~ ACTARM, data = adtte, conf.type = "log-log"))$table
+) %>%
+  dplyr::mutate(
+    ACTARM = factor(gsub("ACTARM=", "", row.names(.)), levels = levels(adtte$ACTARM)),
+    ind = FALSE
+  )
+
+a_tte <- function(df, .var, kp_table) {
+  ind <- grep(df[[.var]][1], row.names(kp_table), fixed = TRUE)
+  minmax <- range(df[["AVAL"]])
+  mm_val_str <- format_value(minmax, format = "xx.x, xx.x")
+  rowfn <- list()
+  if (all(df$CNSR[df$AVAL == minmax[2]])) {
+    mm_val_str <- paste0(mm_val_str, "*")
+    rowfn <- "* indicates censoring"
+  }
+  in_rows(
+    Median = kp_table[ind, "median", drop = TRUE],
+    "95% confidence interval" = unlist(kp_table[ind, c("0.95LCL", "0.95UCL")]),
+    "Min Max" = mm_val_str,
+    .formats = c("xx.xx", "xx.xx - xx.xx", "xx"),
+    .row_footnotes = list(NULL, NULL, rowfn)
+  )
+}
+

Additionally, the a_tte function creates a referential +footnote within the table to indicate where censoring occurred in the +data.

+

Now we are able to use these four analysis functions to build our +time to event analysis table.

+
+lyt <- basic_table(show_colcounts = TRUE) %>%
+  ## Column faceting
+  split_cols_by("ARM", ref_group = "A: Drug X") %>%
+  ## Overall count
+  analyze("USUBJID", a_count_subjs, show_labels = "hidden") %>%
+  ## Censored subjects summary
+  analyze("CNSDTDSC", cnsr_counter, var_labels = "Censored Subjects", show_labels = "visible") %>%
+  ## Cox P-H analysis
+  analyze("ARM", a_cph, extra_args = list(full_cox_fit = cph), show_labels = "hidden") %>%
+  ## Time-to-event analysis
+  analyze(
+    "ARM", a_tte,
+    var_labels = "Time to first adverse event", show_labels = "visible",
+    extra_args = list(kp_table = surv_tbl),
+    table_names = "kapmeier"
+  )
+
+tbl_tte <- build_table(lyt, adtte2)
+

We set the show_colcounts argument of +basic_table() to TRUE to first print the total +subject counts for each column. Next we use split_cols_by() +to split the table into three columns corresponding to the three +different levels of ARM, and specify that the first arm, +"A: Drug X" should act as the reference group to be +compared against - this reference group is used for the Cox P-H +analysis. Then we call analyze() sequentially using each of +the four custom analysis functions as argument afun and +specifying additional arguments where necessary. Then we use +build_table() to construct our rtable using +the adtte2 dataset.

+

Finally, we annotate the table using the +fnotes_at_path() function to specify that product-limit +estimates are used to calculate the statistics listed under the “Time to +first adverse event” heading within the table. The referential footnote +created earlier in the time-to-event analysis function +(a_tte) is also displayed.

+
+fnotes_at_path(
+  tbl_tte,
+  c("ma_USUBJID_CNSDTDSC_ARM_kapmeier", "kapmeier")
+) <- "Product-limit (Kaplan-Meier) estimates."
+
+tbl_tte
+
#                                             A: Drug X      B: Placebo     C: Combination
+#                                              (N=134)         (N=134)         (N=132)    
+# ————————————————————————————————————————————————————————————————————————————————————————
+# Subjects with Adverse Events n (%)        134 (100.00%)   134 (100.00%)   132 (100.00%) 
+# Censored Subjects                                                                       
+#   Clinical Cut Off                          6 (4.48%)       3 (2.24%)      14 (10.61%)  
+#   Completion or Discontinuation             9 (6.72%)       5 (3.73%)       9 (6.82%)   
+#   End of AE Reporting Period               14 (10.45%)      7 (5.22%)      14 (10.61%)  
+#   Preferred Term                           11 (8.21%)       5 (3.73%)       13 (9.85%)  
+# Hazard ratio                                                   0.7             1.0      
+# 95% confidence interval                                    (0.5, 0.9)       (0.8, 1.4)  
+# p-value (one-sided stratified log rank)                      0.1070           0.4880    
+# Time to first adverse event {1}                                                         
+#   Median                                      0.23            0.39             0.29     
+#   95% confidence interval                  0.18 - 0.33     0.29 - 0.49     0.22 - 0.35  
+#   Min Max {2}                               0.0, 1.0*       0.0, 1.0*       0.0, 1.0*   
+# ————————————————————————————————————————————————————————————————————————————————————————
+# 
+# {1} - Product-limit (Kaplan-Meier) estimates.
+# {2} - * indicates censoring
+# ————————————————————————————————————————————————————————————————————————————————————————
+
+
+
+ + + + +
+ + + + + + + diff --git a/v0.6.9/articles/col_counts.html b/v0.6.9/articles/col_counts.html new file mode 100644 index 000000000..caafdfe01 --- /dev/null +++ b/v0.6.9/articles/col_counts.html @@ -0,0 +1,405 @@ + + + + + + + + +Column Counts and Formats • rtables + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

The Old Way +

+

Many tables call for column counts to be displayed in the header +material of a table (i.e., interspersed with the column labels).

+

Historically, rtables supported this only for so-called +leaf or individual columns.

+
+

Setting column counts to visible at Layout time +

+

Display of column counts (off by default) was primarily achieved via +passing show_colcounts = TRUE to basic_table , +e.g.

+ +
# 
+# Attaching package: 'dplyr'
+
# The following objects are masked from 'package:stats':
+# 
+#     filter, lag
+
# The following objects are masked from 'package:base':
+# 
+#     intersect, setdiff, setequal, union
+ +
# Loading required package: formatters
+
# 
+# Attaching package: 'formatters'
+
# The following object is masked from 'package:base':
+# 
+#     %||%
+
# Loading required package: magrittr
+
# 
+# Attaching package: 'rtables'
+
# The following object is masked from 'package:utils':
+# 
+#     str
+
+lyt <- basic_table(show_colcounts = TRUE) %>%
+  split_cols_by("ARM") %>%
+  split_cols_by("SEX", split_fun = keep_split_levels(c("F", "M"))) %>%
+  analyze("AGE")
+
+tbl <- build_table(lyt, ex_adsl)
+tbl
+
#           A: Drug X        B: Placebo       C: Combination  
+#          F        M        F        M         F         M   
+#        (N=79)   (N=51)   (N=77)   (N=55)   (N=66)    (N=60) 
+# ————————————————————————————————————————————————————————————
+# Mean   32.76    35.57    34.12    37.44     35.20     35.38
+

The format of the counts could also be controlled by the +colcount_format argument to basic_table.

+

We had no way of displaying (or, in fact, even easily calculating) +the ARM facet counts.

+
+
+

Modifying counts on an existing table +

+

(Leaf-)column counts could be altered after the fact via the +col_counts<- getter:

+
+col_counts(tbl) <- c(17, 18, 19, 17, 18, 19)
+tbl
+
#           A: Drug X        B: Placebo       C: Combination  
+#          F        M        F        M         F         M   
+#        (N=17)   (N=18)   (N=19)   (N=17)   (N=18)    (N=19) 
+# ————————————————————————————————————————————————————————————
+# Mean   32.76    35.57    34.12    37.44     35.20     35.38
+

NB doing this has never updated percentages that +appear within the table as they are calculated at table-creation time, +so this can lead to misleading results when not used with care.

+
+
+

Hiding counts +

+

We did not provide a user-visible way to toggle column count display +after table creation, though we did support showing a blank space for +particular counts by setting them to NA:

+
+col_counts(tbl) <- c(17, 18, NA, 17, 18, 19)
+tbl
+
#           A: Drug X        B: Placebo      C: Combination  
+#          F        M        F       M         F         M   
+#        (N=17)   (N=18)           (N=17)   (N=18)    (N=19) 
+# ———————————————————————————————————————————————————————————
+# Mean   32.76    35.57    34.12   37.44     35.20     35.38
+

These mechanisms will all continue to work for the forseeable future, +though new code is advised use the new API discussed below.

+
+
+
+

Higher Level Column Counts +

+

Starting in rtables version 6.8.0, the +concept of column counts is modeled and handled with much more +granularity than previously. Each facet in column space now has a column +count (whether or not it is displayed), which will appear directly under +the corresponding column label (spanning the same number of rows) when +set to be visible.

+
+

Setting Column Counts to Visible at Layout Time +

+

The primary way for users to create tables which displays these +“high-level” column counts is to create a layout that specifies they +should be visible.

+

We do this with the new show_colcounts argument now +accepted by all split_cols_by* layout functions.

+
+lyt2 <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  split_cols_by("SEX",
+    split_fun = keep_split_levels(c("F", "M")),
+    show_colcounts = TRUE
+  ) %>%
+  analyze("AGE")
+
+tbl2 <- build_table(lyt2, ex_adsl)
+tbl2
+
#           A: Drug X        B: Placebo       C: Combination  
+#          F        M        F        M         F         M   
+#        (N=79)   (N=51)   (N=77)   (N=55)   (N=66)    (N=60) 
+# ————————————————————————————————————————————————————————————
+# Mean   32.76    35.57    34.12    37.44     35.20     35.38
+
+lyt3 <- basic_table() %>%
+  split_cols_by("ARM", show_colcounts = TRUE) %>%
+  split_cols_by("SEX", split_fun = keep_split_levels(c("F", "M"))) %>%
+  analyze("AGE")
+
+tbl3 <- build_table(lyt3, ex_adsl)
+tbl3
+
#          A: Drug X      B: Placebo      C: Combination  
+#           (N=134)         (N=134)           (N=132)     
+#          F       M       F       M        F         M   
+# ————————————————————————————————————————————————————————
+# Mean   32.76   35.57   34.12   37.44    35.20     35.38
+

As before, these column counts are calculated at table creation time, +using alt_counts_df if it is provided (or simply +df otherwise).

+

Column formats are set at layout time via the +colcount_format argument of the specific +split_cols_by call.

+
+
+

Manipulating Column Counts In An Existing Table +

+

Manipulation of column counts (beyond the old setters provided for +backwards compatibility) is path based. In other words, when we set a +column count (e.g., to NA so it displays as a blank) or set the +visibilty of a set of column counts, we do so by indicating them via +column paths. The ability to alter column count formats on an existing +table is currently not offered by any exported functions.

+

Column paths can be obtained via col_paths for the leaf +columns, or via make_col_df(tbl, visible_only = FALSE)$path +for all addressable facets.

+
+

Setting individual column counts +

+

The facet_colcount getter and setter queries and sets +the column count for a facet in column space (note it needs not be a +leaf facet). E.g.,

+
+facet_colcount(tbl3, c("ARM", "C: Combination"))
+
# [1] 132
+
+facet_colcount(tbl3, c("ARM", "C: Combination")) <- 75
+tbl3
+
#          A: Drug X      B: Placebo      C: Combination  
+#           (N=134)         (N=134)           (N=75)      
+#          F       M       F       M        F         M   
+# ————————————————————————————————————————————————————————
+# Mean   32.76   35.57   34.12   37.44    35.20     35.38
+

For convenience (primarily because it was needed internally), we also +provide rm_all_colcounts which sets all column +counts for a particular table to NA at all levels of +nesting. We do not expect this to be particularly useful to +end-users.

+
+
+

Setting Col Count Visibility +

+

Typically we do not set column count visibility individually. *This +is due to a constraint where direct leaf siblings (e.g. F and M under +one of the arms in our layout) must have the same visibility for their +column counts in order for the rendering machinery to work.

+

Instead, we can reset the column count visibility of groups of +siblings via the facet_colcounts_visible (note the ‘s’) +setter. This function accepts a path which ends in the name associated +with a splitting instruction in the layout (e.g., c("ARM"), +c("ARM", "B: Placebo", "SEX"), etc) and resets the +visibility of all direct children of that path.

+
+facet_colcounts_visible(tbl3, c("ARM", "A: Drug X", "SEX")) <- TRUE
+tbl3
+
#           A: Drug X                                       
+#            (N=134)        B: Placebo      C: Combination  
+#          F        M         (N=134)           (N=75)      
+#        (N=79)   (N=51)     F       M        F         M   
+# ——————————————————————————————————————————————————————————
+# Mean   32.76    35.57    34.12   37.44    35.20     35.38
+

NOTE as we can see here, the visibility of column +counts can have an “unbalanced design”, provided the direct-siblings +agreeing constraint is met. This leads to things not lining up directly +as one might expect (it does not generate any blank spaces the way +setting a visible column count to NA does).

+

Currently paths with "*" in them do not work within +facet_colcounts_visible, but that capability is likely to +be added in future releases.

+

colcount_visible getters and setters do also exist which +retrieve and set individual column counts’ visiblities, but these are +largely an internal detail and in virtually all cases end users should +avoid calling them directly.

+
+## BEWARE, the following is expected to show error
+tbl4 <- tbl3
+colcount_visible(tbl4, c("ARM", "A: Drug X", "SEX", "F")) <- FALSE
+tbl4
+
+# Expected Error message
+# Error in h(simpleError(msg, call)) :
+#  error in evaluating the argument 'x' in selecting a method for function 'toString': Detected different colcount visibility among sibling facets (those arising from the same split_cols_by* layout instruction). This is not supported.
+# Set count values to NA if you want a blank space to appear as the displayed count for particular facets.
+# First disagreement occured at paths:
+# ARM[A: Drug X]->SEX[F]
+# ARM[A: Drug X]->SEX[M]
+

Note currently this restriction is currently only enforced for leaf +columns due to technical implementation details but how a table renders +should be considered undefined behavior when it contains a group of +sibling column facets arising from the same layout instruction whose +column count visiblities disagree. That may become an error in future +versions without warning.

+
+
+

Advanced Settings +

+

By using make_col_df() we can see the full path to any +column count. One example application is to add a NA value +that would print to the default value is "", that will show +nothing. To change (for now uniformly only) the output string in case of +missing values in the column counts you can use +colcount_na_str:

+
+coldf <- make_col_df(tbl3)
+facet_colcount(tbl3, coldf$path[[1]][c(1, 2)]) <- NA_integer_
+print(tbl3) # Keeps the missing space
+
#           A: Drug X                                       
+#                           B: Placebo      C: Combination  
+#          F        M         (N=134)           (N=75)      
+#        (N=79)   (N=51)     F       M        F         M   
+# ——————————————————————————————————————————————————————————
+# Mean   32.76    35.57    34.12   37.44    35.20     35.38
+
+colcount_na_str(tbl3) <- "NaN"
+tbl3 # Shows NaN
+
#           A: Drug X                                       
+#              NaN          B: Placebo      C: Combination  
+#          F        M         (N=134)           (N=75)      
+#        (N=79)   (N=51)     F       M        F         M   
+# ——————————————————————————————————————————————————————————
+# Mean   32.76    35.57    34.12   37.44    35.20     35.38
+
+
+
+
+
+ + + + +
+ + + + + + + diff --git a/v0.6.9/articles/custom_appearance.html b/v0.6.9/articles/custom_appearance.html new file mode 100644 index 000000000..97a1a0fc2 --- /dev/null +++ b/v0.6.9/articles/custom_appearance.html @@ -0,0 +1,1166 @@ + + + + + + + + +Customizing Appearance • rtables + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + + +
+

Customizing Appearance +

+

In this vignette, we describe the various ways we can modify and +customize the appearance of rtables.

+

Loading the package:

+ +
+

Rows and cell values alignments +

+

It is possible to align the content by assigning "left", +"center" (default), and "right" to +.aligns and align arguments in +in_rows() and rcell(), respectively. It is +also possible to use decimal, dec_right, and +dec_left for decimal alignments. The first takes all +numerical values and aligns the decimal character . in +every value of the column that has align = "decimal". Also +numeric without decimal values are aligned according to an imaginary +. if specified as such. dec_left and +dec_right behave similarly, with the difference that if the +column present empty spaces at left or right, it pushes values towards +left or right taking the one value that has most decimal characters, if +right, or non-decimal values if left. For more details, please read the +related documentation page help("decimal_align").

+

Please consider using ?in_rows and ?rcell +for further clarifications on the two arguments, and use +formatters::list_valid_aligns() to see all available +alignment options.

+

In the following we show two simplified examples that use +align and .aligns, respectively.

+
+# In rcell we use align.
+lyt <- basic_table() %>%
+  analyze("AGE", function(x) {
+    in_rows(
+      left = rcell("l", align = "left"),
+      right = rcell("r", align = "right"),
+      center = rcell("c", align = "center")
+    )
+  })
+
+tbl <- build_table(lyt, DM)
+tbl
+
#          all obs
+# ————————————————
+# left     l      
+# right          r
+# center      c
+
+# In in_rows, we use .aligns. This can either set the general value or the
+#   single values (see NB).
+lyt2 <- basic_table() %>%
+  analyze("AGE", function(x) {
+    in_rows(
+      left = rcell("l"),
+      right = rcell("r"),
+      center = rcell("c"),
+      .aligns = c("right")
+    ) # NB: .aligns = c("right", "left", "center")
+  })
+
+tbl2 <- build_table(lyt2, DM)
+tbl2
+
#          all obs
+# ————————————————
+# left           l
+# right          r
+# center         c
+

These concepts can be well applied to any clinical table as shown in +the following, more complex, example.

+
+lyt3 <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  split_rows_by("SEX") %>%
+  analyze(c("AGE", "STRATA1"), function(x) {
+    if (is.numeric(x)) {
+      in_rows(
+        "mean" = rcell(mean(x)),
+        "sd" = rcell(sd(x)),
+        .formats = c("xx.x"), .aligns = "left"
+      )
+    } else if (is.factor(x)) {
+      rcell(length(unique(x)), align = "right")
+    } else {
+      stop("Unsupported type")
+    }
+  }, show_labels = "visible", na_str = "NE")
+
+tbl3 <- build_table(lyt3, ex_adsl)
+tbl3
+
#                    A: Drug X   B: Placebo   C: Combination
+# ——————————————————————————————————————————————————————————
+# F                                                         
+#   AGE                                                     
+#     mean           32.8        34.1         35.2          
+#     sd             6.1         7.1          7.4           
+#   STRATA1                                                 
+#     STRATA1                3            3                3
+# M                                                         
+#   AGE                                                     
+#     mean           35.6        37.4         35.4          
+#     sd             7.1         8.7          8.2           
+#   STRATA1                                                 
+#     STRATA1                3            3                3
+# U                                                         
+#   AGE                                                     
+#     mean           31.7        31.0         35.2          
+#     sd             3.2         5.7          3.1           
+#   STRATA1                                                 
+#     STRATA1                3            2                3
+# UNDIFFERENTIATED                                          
+#   AGE                                                     
+#     mean           28.0        NE           45.0          
+#     sd             NE          NE           1.4           
+#   STRATA1                                                 
+#     STRATA1                1            0                2
+
+
+

Top-left Materials +

+

The sequence of strings printed in the area between the column header +display and the first row label can be modified during pre-processing +using label position argument in row splits split_rows_by, +with the append_topleft function, and during +post-processing using the top_left() function. Note: +Indenting is automatically added label_pos = "topleft".

+

Within the layout initializer:

+
+lyt <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  split_rows_by("STRATA1") %>%
+  analyze("AGE") %>%
+  append_topleft("New top_left material here")
+
+build_table(lyt, DM)
+
# New top_left material here   A: Drug X   B: Placebo   C: Combination
+# ————————————————————————————————————————————————————————————————————
+# A                                                                   
+#   Mean                         32.53       32.30          35.76     
+# B                                                                   
+#   Mean                         35.46       32.42          34.39     
+# C                                                                   
+#   Mean                         36.34       34.45          33.54
+

Specify label position using the split_rows function. +Notice the position of STRATA1 and SEX.

+
+lyt <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  split_rows_by("STRATA1", label_pos = "topleft") %>%
+  split_rows_by("SEX", label_pos = "topleft") %>%
+  analyze("AGE")
+
+build_table(lyt, DM)
+
# STRATA1                                                     
+#   SEX                A: Drug X   B: Placebo   C: Combination
+# ————————————————————————————————————————————————————————————
+# A                                                           
+#   F                                                         
+#     Mean               30.91       32.91          35.95     
+#   M                                                         
+#     Mean               35.07       31.09          35.60     
+#   U                                                         
+#     Mean                NA           NA             NA      
+#   UNDIFFERENTIATED                                          
+#     Mean                NA           NA             NA      
+# B                                                           
+#   F                                                         
+#     Mean               34.85       32.88          34.42     
+#   M                                                         
+#     Mean               36.64       32.09          34.37     
+#   U                                                         
+#     Mean                NA           NA             NA      
+#   UNDIFFERENTIATED                                          
+#     Mean                NA           NA             NA      
+# C                                                           
+#   F                                                         
+#     Mean               35.19       36.00          34.32     
+#   M                                                         
+#     Mean               37.39       32.81          32.83     
+#   U                                                         
+#     Mean                NA           NA             NA      
+#   UNDIFFERENTIATED                                          
+#     Mean                NA           NA             NA
+

Post-processing using the top_left() function:

+
+lyt <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  split_rows_by("SEX") %>%
+  analyze(c("AGE", "STRATA1"), function(x) {
+    if (is.numeric(x)) {
+      in_rows(
+        "mean" = rcell(mean(x)),
+        "sd" = rcell(sd(x)),
+        .formats = c("xx.x"), .aligns = "left"
+      )
+    } else if (is.factor(x)) {
+      rcell(length(unique(x)), align = "right")
+    } else {
+      stop("Unsupported type")
+    }
+  }, show_labels = "visible", na_str = "NE") %>%
+  build_table(ex_adsl)
+
+# Adding top-left material
+top_left(lyt) <- "New top-left material here"
+
+lyt
+
# New top-left material here   A: Drug X   B: Placebo   C: Combination
+# ————————————————————————————————————————————————————————————————————
+# F                                                                   
+#   AGE                                                               
+#     mean                     32.8        34.1         35.2          
+#     sd                       6.1         7.1          7.4           
+#   STRATA1                                                           
+#     STRATA1                          3            3                3
+# M                                                                   
+#   AGE                                                               
+#     mean                     35.6        37.4         35.4          
+#     sd                       7.1         8.7          8.2           
+#   STRATA1                                                           
+#     STRATA1                          3            3                3
+# U                                                                   
+#   AGE                                                               
+#     mean                     31.7        31.0         35.2          
+#     sd                       3.2         5.7          3.1           
+#   STRATA1                                                           
+#     STRATA1                          3            2                3
+# UNDIFFERENTIATED                                                    
+#   AGE                                                               
+#     mean                     28.0        NE           45.0          
+#     sd                       NE          NE           1.4           
+#   STRATA1                                                           
+#     STRATA1                          1            0                2
+
+
+

Table Inset +

+

Table title, table body, referential footnotes and and main footers +can be inset from the left alignment of the titles and provenance footer +materials. This can be modified within the layout initializer +basic_table() using the inset argument or +during post-processing with table_inset().

+

Using the layout initializer:

+
+lyt <- basic_table(inset = 5) %>%
+  analyze("AGE")
+
+build_table(lyt, DM)
+
#             all obs
+#      ——————————————
+#      Mean    34.22
+

Using the post-processing function:

+

Without inset -

+
+lyt <- basic_table() %>%
+  analyze("AGE")
+
+tbl <- build_table(lyt, DM)
+tbl
+
#        all obs
+# ——————————————
+# Mean    34.22
+

With an inset of 5 characters -

+
+table_inset(tbl) <- 5
+tbl
+
#             all obs
+#      ——————————————
+#      Mean    34.22
+

Below is an example with a table produced for clinical data. Compare +the inset of the table and main footer between the two tables.

+

Without inset -

+
+analysisfun <- function(x, ...) {
+  in_rows(
+    row1 = 5,
+    row2 = c(1, 2),
+    .row_footnotes = list(row1 = "row 1 rfn"),
+    .cell_footnotes = list(row2 = "row 2 cfn")
+  )
+}
+
+lyt <- basic_table(
+  title = "Title says Whaaaat", subtitles = "Oh, ok.",
+  main_footer = "ha HA! Footer!", prov_footer = "provenaaaaance"
+) %>%
+  split_cols_by("ARM") %>%
+  analyze("AGE", afun = analysisfun)
+
+result <- build_table(lyt, ex_adsl)
+result
+
# Title says Whaaaat
+# Oh, ok.
+# 
+# ——————————————————————————————————————————————————
+#            A: Drug X   B: Placebo   C: Combination
+# ——————————————————————————————————————————————————
+# row1 {1}       5           5              5       
+# row2       1, 2 {2}     1, 2 {2}       1, 2 {2}   
+# ——————————————————————————————————————————————————
+# 
+# {1} - row 1 rfn
+# {2} - row 2 cfn
+# ——————————————————————————————————————————————————
+# 
+# ha HA! Footer!
+# 
+# provenaaaaance
+

With inset -

+

Notice, the inset does not apply to any title materials (main title, +subtitles, page titles), or provenance footer materials. Inset settings +is applied to top-left materials, referential footnotes main footer +materials and any horizontal dividers.

+
+table_inset(result) <- 5
+result
+
# Title says Whaaaat
+# Oh, ok.
+# 
+#      ——————————————————————————————————————————————————
+#                 A: Drug X   B: Placebo   C: Combination
+#      ——————————————————————————————————————————————————
+#      row1 {1}       5           5              5       
+#      row2       1, 2 {2}     1, 2 {2}       1, 2 {2}   
+#      ——————————————————————————————————————————————————
+# 
+#      {1} - row 1 rfn
+#      {2} - row 2 cfn
+#      ——————————————————————————————————————————————————
+# 
+#      ha HA! Footer!
+# 
+# provenaaaaance
+
+
+

Horizontal Separation +

+

A character value can be specified to modify the horizontal +separation between column headers and the table. Horizontal separation +applies when:

+
    +
  1. separating title + subtitles from the column labels + top left +materials,
  2. +
  3. column labels + top left material from row labels + cells,
  4. +
  5. row labels + cells from footer content, and
  6. +
  7. Referential footnotes from main + provenance content there would be +something on both sides of the divider.
  8. +
+

Below, we replace the default line with “=”.

+
+tbl <- basic_table() %>%
+  split_cols_by("Species") %>%
+  add_colcounts() %>%
+  analyze(c("Sepal.Length", "Petal.Width"), function(x) {
+    in_rows(
+      mean_sd = c(mean(x), sd(x)),
+      var = var(x),
+      min_max = range(x),
+      .formats = c("xx.xx (xx.xx)", "xx.xxx", "xx.x - xx.x"),
+      .labels = c("Mean (sd)", "Variance", "Min - Max")
+    )
+  }) %>%
+  build_table(iris, hsep = "=")
+tbl
+
#                  setosa      versicolor     virginica 
+#                  (N=50)        (N=50)        (N=50)   
+# ======================================================
+# Sepal.Length                                          
+#   Mean (sd)    5.01 (0.35)   5.94 (0.52)   6.59 (0.64)
+#   Variance        0.124         0.266         0.404   
+#   Min - Max     4.3 - 5.8     4.9 - 7.0     4.9 - 7.9 
+# Petal.Width                                           
+#   Mean (sd)    0.25 (0.11)   1.33 (0.20)   2.03 (0.27)
+#   Variance        0.011         0.039         0.075   
+#   Min - Max     0.1 - 0.6     1.0 - 1.8     1.4 - 2.5
+
+
+

Section Dividers +

+

A character value can be specified as a section divider which succeed +every group defined by a split instruction. Note, a trailing divider at +the end of the table is never printed.

+

Below, a “+” is repeated and used as a section divider.

+
+lyt <- basic_table() %>%
+  split_cols_by("Species") %>%
+  analyze(head(names(iris), -1), afun = function(x) {
+    list(
+      "mean / sd" = rcell(c(mean(x), sd(x)), format = "xx.xx (xx.xx)"),
+      "range" = rcell(diff(range(x)), format = "xx.xx")
+    )
+  }, section_div = "+")
+
+build_table(lyt, iris)
+
#                  setosa      versicolor     virginica 
+# ——————————————————————————————————————————————————————
+# Sepal.Length                                          
+#   mean / sd    5.01 (0.35)   5.94 (0.52)   6.59 (0.64)
+#   range           1.50          2.10          3.00    
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++
+# Sepal.Width                                           
+#   mean / sd    3.43 (0.38)   2.77 (0.31)   2.97 (0.32)
+#   range           2.10          1.40          1.60    
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++
+# Petal.Length                                          
+#   mean / sd    1.46 (0.17)   4.26 (0.47)   5.55 (0.55)
+#   range           0.90          2.10          2.40    
+# ++++++++++++++++++++++++++++++++++++++++++++++++++++++
+# Petal.Width                                           
+#   mean / sd    0.25 (0.11)   1.33 (0.20)   2.03 (0.27)
+#   range           0.50          0.80          1.10
+

Section dividers can be set to ” ” to create a blank line.

+
+lyt <- basic_table() %>%
+  split_cols_by("Species") %>%
+  analyze(head(names(iris), -1), afun = function(x) {
+    list(
+      "mean / sd" = rcell(c(mean(x), sd(x)), format = "xx.xx (xx.xx)"),
+      "range" = rcell(diff(range(x)), format = "xx.xx")
+    )
+  }, section_div = " ")
+
+build_table(lyt, iris)
+
#                  setosa      versicolor     virginica 
+# ——————————————————————————————————————————————————————
+# Sepal.Length                                          
+#   mean / sd    5.01 (0.35)   5.94 (0.52)   6.59 (0.64)
+#   range           1.50          2.10          3.00    
+#                                                       
+# Sepal.Width                                           
+#   mean / sd    3.43 (0.38)   2.77 (0.31)   2.97 (0.32)
+#   range           2.10          1.40          1.60    
+#                                                       
+# Petal.Length                                          
+#   mean / sd    1.46 (0.17)   4.26 (0.47)   5.55 (0.55)
+#   range           0.90          2.10          2.40    
+#                                                       
+# Petal.Width                                           
+#   mean / sd    0.25 (0.11)   1.33 (0.20)   2.03 (0.27)
+#   range           0.50          0.80          1.10
+

Separation characters can be specified for different row splits. +However, only one will be printed if they “pile up” next to each +other.

+
+lyt <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  split_rows_by("RACE", section_div = "=") %>%
+  split_rows_by("STRATA1", section_div = "~") %>%
+  analyze("AGE", mean, var_labels = "Age", format = "xx.xx")
+
+build_table(lyt, DM)
+
#                                             A: Drug X   B: Placebo   C: Combination
+# ———————————————————————————————————————————————————————————————————————————————————
+# ASIAN                                                                              
+#   A                                                                                
+#     mean                                      32.19       33.90          36.81     
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+#   B                                                                                
+#     mean                                      34.12       31.62          34.73     
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+#   C                                                                                
+#     mean                                      36.21       33.00          32.39     
+# ===================================================================================
+# BLACK OR AFRICAN AMERICAN                                                          
+#   A                                                                                
+#     mean                                      31.50       28.57          33.62     
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+#   B                                                                                
+#     mean                                      35.60       30.83          33.67     
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+#   C                                                                                
+#     mean                                      35.50       34.18          35.00     
+# ===================================================================================
+# WHITE                                                                              
+#   A                                                                                
+#     mean                                      37.67       31.33          33.17     
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+#   B                                                                                
+#     mean                                      39.86       39.00          34.75     
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+#   C                                                                                
+#     mean                                      39.75       44.67          36.75     
+# ===================================================================================
+# AMERICAN INDIAN OR ALASKA NATIVE                                                   
+#   A                                                                                
+#     mean                                       NA           NA             NA      
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+#   B                                                                                
+#     mean                                       NA           NA             NA      
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+#   C                                                                                
+#     mean                                       NA           NA             NA      
+# ===================================================================================
+# MULTIPLE                                                                           
+#   A                                                                                
+#     mean                                       NA           NA             NA      
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+#   B                                                                                
+#     mean                                       NA           NA             NA      
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+#   C                                                                                
+#     mean                                       NA           NA             NA      
+# ===================================================================================
+# NATIVE HAWAIIAN OR OTHER PACIFIC ISLANDER                                          
+#   A                                                                                
+#     mean                                       NA           NA             NA      
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+#   B                                                                                
+#     mean                                       NA           NA             NA      
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+#   C                                                                                
+#     mean                                       NA           NA             NA      
+# ===================================================================================
+# OTHER                                                                              
+#   A                                                                                
+#     mean                                       NA           NA             NA      
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+#   B                                                                                
+#     mean                                       NA           NA             NA      
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+#   C                                                                                
+#     mean                                       NA           NA             NA      
+# ===================================================================================
+# UNKNOWN                                                                            
+#   A                                                                                
+#     mean                                       NA           NA             NA      
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+#   B                                                                                
+#     mean                                       NA           NA             NA      
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+#   C                                                                                
+#     mean                                       NA           NA             NA
+
+
+

Indent Modifier +

+

Tables by default have indenting at each level of splitting. A custom +indent value can be supplied with the indent_mod argument +within a split function to modify this default. Compare the indenting of +the tables below:

+

Default Indent -

+
+basic_table(
+  title = "Study XXXXXXXX",
+  subtitles = c("subtitle YYYYYYYYYY", "subtitle2 ZZZZZZZZZ"),
+  main_footer = "Analysis was done using cool methods that are correct",
+  prov_footer = "file: /path/to/stuff/that/lives/there HASH:1ac41b242a"
+) %>%
+  split_cols_by("ARM") %>%
+  split_rows_by("SEX") %>%
+  split_rows_by("STRATA1") %>%
+  analyze("AGE", mean, format = "xx.x") %>%
+  build_table(DM)
+
# Study XXXXXXXX
+# subtitle YYYYYYYYYY
+# subtitle2 ZZZZZZZZZ
+# 
+# ——————————————————————————————————————————————————————————
+#                    A: Drug X   B: Placebo   C: Combination
+# ——————————————————————————————————————————————————————————
+# F                                                         
+#   A                                                       
+#     mean             30.9         32.9           36.0     
+#   B                                                       
+#     mean             34.9         32.9           34.4     
+#   C                                                       
+#     mean             35.2         36.0           34.3     
+# M                                                         
+#   A                                                       
+#     mean             35.1         31.1           35.6     
+#   B                                                       
+#     mean             36.6         32.1           34.4     
+#   C                                                       
+#     mean             37.4         32.8           32.8     
+# U                                                         
+#   A                                                       
+#     mean              NA           NA             NA      
+#   B                                                       
+#     mean              NA           NA             NA      
+#   C                                                       
+#     mean              NA           NA             NA      
+# UNDIFFERENTIATED                                          
+#   A                                                       
+#     mean              NA           NA             NA      
+#   B                                                       
+#     mean              NA           NA             NA      
+#   C                                                       
+#     mean              NA           NA             NA      
+# ——————————————————————————————————————————————————————————
+# 
+# Analysis was done using cool methods that are correct
+# 
+# file: /path/to/stuff/that/lives/there HASH:1ac41b242a
+

Modified indent -

+
+basic_table(
+  title = "Study XXXXXXXX",
+  subtitles = c("subtitle YYYYYYYYYY", "subtitle2 ZZZZZZZZZ"),
+  main_footer = "Analysis was done using cool methods that are correct",
+  prov_footer = "file: /path/to/stuff/that/lives/there HASH:1ac41b242a"
+) %>%
+  split_cols_by("ARM") %>%
+  split_rows_by("SEX", indent_mod = 3) %>%
+  split_rows_by("STRATA1", indent_mod = 5) %>%
+  analyze("AGE", mean, format = "xx.x") %>%
+  build_table(DM)
+
# Study XXXXXXXX
+# subtitle YYYYYYYYYY
+# subtitle2 ZZZZZZZZZ
+# 
+# ——————————————————————————————————————————————————————————————————
+#                            A: Drug X   B: Placebo   C: Combination
+# ——————————————————————————————————————————————————————————————————
+#       F                                                           
+#                   A                                               
+#                     mean     30.9         32.9           36.0     
+#                   B                                               
+#                     mean     34.9         32.9           34.4     
+#                   C                                               
+#                     mean     35.2         36.0           34.3     
+#       M                                                           
+#                   A                                               
+#                     mean     35.1         31.1           35.6     
+#                   B                                               
+#                     mean     36.6         32.1           34.4     
+#                   C                                               
+#                     mean     37.4         32.8           32.8     
+#       U                                                           
+#                   A                                               
+#                     mean      NA           NA             NA      
+#                   B                                               
+#                     mean      NA           NA             NA      
+#                   C                                               
+#                     mean      NA           NA             NA      
+#       UNDIFFERENTIATED                                            
+#                   A                                               
+#                     mean      NA           NA             NA      
+#                   B                                               
+#                     mean      NA           NA             NA      
+#                   C                                               
+#                     mean      NA           NA             NA      
+# ——————————————————————————————————————————————————————————————————
+# 
+# Analysis was done using cool methods that are correct
+# 
+# file: /path/to/stuff/that/lives/there HASH:1ac41b242a
+
+
+

Variable Label Visibility +

+

With split instructions, visibility of the label for the variable +being split can be modified to visible, hidden +and topleft with the show_labels argument, +label_pos argument, and child_labels argument +where applicable. Note: this is NOT the name of the levels contained in +the variable. For analyze calls, indicates that the variable should be +visible only if multiple variables are analyzed at the same level of +nesting.

+

Visibility of labels for the groups generated by a split can also be +modified using the child_label argument with a split call. +The child_label argument can force labels to be visible in +addition to content rows but we cannot hide or move the content +rows.

+

Notice the placement of the “AGE” label in this example:

+
+lyt <- basic_table(show_colcounts = TRUE) %>%
+  split_cols_by(var = "ARM") %>%
+  split_rows_by("SEX", split_fun = drop_split_levels, child_labels = "visible") %>%
+  split_rows_by("STRATA1") %>%
+  analyze("AGE", mean, show_labels = "default")
+
+build_table(lyt, DM)
+
#               A: Drug X          B: Placebo       C: Combination 
+#                (N=121)            (N=106)            (N=129)     
+# —————————————————————————————————————————————————————————————————
+# F                                                                
+#   A                                                              
+#     mean   30.9090909090909   32.9090909090909        35.95      
+#   B                                                              
+#     mean   34.8518518518519   32.8823529411765   34.4210526315789
+#   C                                                              
+#     mean   35.1904761904762          36          34.3181818181818
+# M                                                                
+#   A                                                              
+#     mean   35.0714285714286   31.0909090909091         35.6      
+#   B                                                              
+#     mean   36.6428571428571   32.0869565217391   34.3684210526316
+#   C                                                              
+#     mean   37.3913043478261       32.8125        32.8333333333333
+

When set to default, the label AGE is not repeated since +there is only one variable being analyzed at the same level of nesting. +Override this by setting the show_labels argument as +“visible”.

+
+lyt2 <- basic_table(show_colcounts = TRUE) %>%
+  split_cols_by(var = "ARM") %>%
+  split_rows_by("SEX", split_fun = drop_split_levels, child_labels = "hidden") %>%
+  split_rows_by("STRATA1") %>%
+  analyze("AGE", mean, show_labels = "visible")
+
+build_table(lyt2, DM)
+
#               A: Drug X          B: Placebo       C: Combination 
+#                (N=121)            (N=106)            (N=129)     
+# —————————————————————————————————————————————————————————————————
+# A                                                                
+#   AGE                                                            
+#     mean   30.9090909090909   32.9090909090909        35.95      
+# B                                                                
+#   AGE                                                            
+#     mean   34.8518518518519   32.8823529411765   34.4210526315789
+# C                                                                
+#   AGE                                                            
+#     mean   35.1904761904762          36          34.3181818181818
+# A                                                                
+#   AGE                                                            
+#     mean   35.0714285714286   31.0909090909091         35.6      
+# B                                                                
+#   AGE                                                            
+#     mean   36.6428571428571   32.0869565217391   34.3684210526316
+# C                                                                
+#   AGE                                                            
+#     mean   37.3913043478261       32.8125        32.8333333333333
+

Below is an example using the label_pos argument for +modifying label visibility:

+

Label order will mirror the order of split_rows_by +calls. If the labels of any subgroups should be hidden, the +label_pos argument should be set to hidden.

+

“SEX” label position is hidden -

+
+basic_table(
+  title = "Study XXXXXXXX",
+  subtitles = c("subtitle YYYYYYYYYY", "subtitle2 ZZZZZZZZZ"),
+  main_footer = "Analysis was done using cool methods that are correct",
+  prov_footer = "file: /path/to/stuff/that/lives/there HASH:1ac41b242a"
+) %>%
+  split_cols_by("ARM") %>%
+  split_rows_by("SEX", split_fun = drop_split_levels, label_pos = "visible") %>%
+  split_rows_by("STRATA1", label_pos = "hidden") %>%
+  analyze("AGE", mean, format = "xx.x") %>%
+  build_table(DM)
+
# Study XXXXXXXX
+# subtitle YYYYYYYYYY
+# subtitle2 ZZZZZZZZZ
+# 
+# ————————————————————————————————————————————————————
+#              A: Drug X   B: Placebo   C: Combination
+# ————————————————————————————————————————————————————
+# SEX                                                 
+#   F                                                 
+#     A                                               
+#       mean     30.9         32.9           36.0     
+#     B                                               
+#       mean     34.9         32.9           34.4     
+#     C                                               
+#       mean     35.2         36.0           34.3     
+#   M                                                 
+#     A                                               
+#       mean     35.1         31.1           35.6     
+#     B                                               
+#       mean     36.6         32.1           34.4     
+#     C                                               
+#       mean     37.4         32.8           32.8     
+# ————————————————————————————————————————————————————
+# 
+# Analysis was done using cool methods that are correct
+# 
+# file: /path/to/stuff/that/lives/there HASH:1ac41b242a
+

“SEX” label position is with the top-left materials -

+
+basic_table(
+  title = "Study XXXXXXXX",
+  subtitles = c("subtitle YYYYYYYYYY", "subtitle2 ZZZZZZZZZ"),
+  main_footer = "Analysis was done using cool methods that are correct",
+  prov_footer = "file: /path/to/stuff/that/lives/there HASH:1ac41b242a"
+) %>%
+  split_cols_by("ARM") %>%
+  split_rows_by("SEX", split_fun = drop_split_levels, label_pos = "topleft") %>%
+  split_rows_by("STRATA1", label_pos = "hidden") %>%
+  analyze("AGE", mean, format = "xx.x") %>%
+  build_table(DM)
+
# Study XXXXXXXX
+# subtitle YYYYYYYYYY
+# subtitle2 ZZZZZZZZZ
+# 
+# ——————————————————————————————————————————————————
+# SEX        A: Drug X   B: Placebo   C: Combination
+# ——————————————————————————————————————————————————
+# F                                                 
+#   A                                               
+#     mean     30.9         32.9           36.0     
+#   B                                               
+#     mean     34.9         32.9           34.4     
+#   C                                               
+#     mean     35.2         36.0           34.3     
+# M                                                 
+#   A                                               
+#     mean     35.1         31.1           35.6     
+#   B                                               
+#     mean     36.6         32.1           34.4     
+#   C                                               
+#     mean     37.4         32.8           32.8     
+# ——————————————————————————————————————————————————
+# 
+# Analysis was done using cool methods that are correct
+# 
+# file: /path/to/stuff/that/lives/there HASH:1ac41b242a
+
+
+

Cell, Label, and Annotation Wrapping +

+

An rtable can be rendered with a customized width by +setting custom rendering widths for cell contents, row labels, and +titles/footers.

+

This is demonstrated using the sample data and table below. In this +section we aim to render this table with a reduced width since the table +has very wide contents in several cells, labels, and titles/footers.

+
+trimmed_data <- ex_adsl %>%
+  filter(SEX %in% c("M", "F")) %>%
+  filter(RACE %in% levels(RACE)[1:2])
+
+levels(trimmed_data$ARM)[1] <- "Incredibly long column name to be wrapped"
+levels(trimmed_data$ARM)[2] <- "This_column_name_should_be_split_somewhere"
+
+wide_tbl <- basic_table(
+  title = "Title that is too long and also needs to be wrapped to a smaller width",
+  subtitles = "Subtitle that is also long and also needs to be wrapped to a smaller width",
+  main_footer = "Footnote that is wider than expected for this table.",
+  prov_footer = "Provenance footer material that is also wider than expected for this table."
+) %>%
+  split_cols_by("ARM") %>%
+  split_rows_by("RACE", split_fun = drop_split_levels) %>%
+  analyze(
+    c("AGE", "EOSDY"),
+    na_str = "Very long cell contents to_be_wrapped_and_splitted",
+    inclNAs = TRUE
+  ) %>%
+  build_table(trimmed_data)
+
+wide_tbl
+
# Title that is too long and also needs to be wrapped to a smaller width
+# Subtitle that is also long and also needs to be wrapped to a smaller width
+# 
+# ————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
+#                                 Incredibly long column name to be wrapped            This_column_name_should_be_split_somewhere                         C: Combination                  
+# ————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
+# ASIAN                                                                                                                                                                                   
+#   AGE                                                                                                                                                                                   
+#     Mean                                          32.50                                                36.68                                                36.99                       
+#   EOSDY                                                                                                                                                                                 
+#     Mean                    Very long cell contents to_be_wrapped_and_splitted   Very long cell contents to_be_wrapped_and_splitted   Very long cell contents to_be_wrapped_and_splitted
+# BLACK OR AFRICAN AMERICAN                                                                                                                                                               
+#   AGE                                                                                                                                                                                   
+#     Mean                                          34.27                                                34.93                                                33.71                       
+#   EOSDY                                                                                                                                                                                 
+#     Mean                    Very long cell contents to_be_wrapped_and_splitted   Very long cell contents to_be_wrapped_and_splitted   Very long cell contents to_be_wrapped_and_splitted
+# ————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
+# 
+# Footnote that is wider than expected for this table.
+# 
+# Provenance footer material that is also wider than expected for this table.
+

In the following sections we will use the toString() +function to render the table in string form. This resulting string +representation is ready to be printed or written to a plain text file, +but we will use the strsplit() function in combination with +the matrix() function to preview the rendered wrapped table +in matrix form within this vignette.

+
+

Cell & Label Wrapping +

+

The width of a rendered table can be customized by wrapping column +widths. This is done by setting custom width values via the +widths argument of the toString() function. +The length of the vector passed to the widths argument must +be equal to the total number of columns in the table, including the row +labels column, with each value of the vector corresponding to the +maximum width (in characters) allowed in each column, from left to +right.

+

Similarly, wrapping can be applied when exporting a table via one of +the four export_as_* functions and when implementing +pagination via the paginate_table() function from the +rtables package. In these cases, the rendered column widths +are set using the colwidths argument which takes input in +the same format as the widths argument of +toString().

+

For example, wide_tbl has four columns (1 row label +column and 3 content columns) which we will set the widths of below to +use in the rendered table. We set the width of the row label column to +10 characters and the widths of each of the 3 content columns to 8 +characters. Any words longer than the specified width are broken and +continued on the following line. By default there are 3 spaces +separating each of the columns in the rendered table but this can be +customized via the col_gap argument to +toString() if further width customization is desired.

+
+result_wrap_cells <- toString(wide_tbl, widths = c(10, 8, 8, 8))
+matrix_wrap_cells <- matrix(strsplit(result_wrap_cells, "\n")[[1]], ncol = 1)
+matrix_wrap_cells
+
#       [,1]                                                                         
+#  [1,] "Title that is too long and also needs to be wrapped to a smaller width"     
+#  [2,] "Subtitle that is also long and also needs to be wrapped to a smaller width" 
+#  [3,] ""                                                                           
+#  [4,] "———————————————————————————————————————————"                                
+#  [5,] "             Incredib   This_col           "                                
+#  [6,] "             ly long    umn_name           "                                
+#  [7,] "              column    _should_           "                                
+#  [8,] "               name     be_split           "                                
+#  [9,] "              to be     _somewhe   C: Combi"                                
+# [10,] "             wrapped       re       nation "                                
+# [11,] "———————————————————————————————————————————"                                
+# [12,] "ASIAN                                      "                                
+# [13,] "  AGE                                      "                                
+# [14,] "    Mean      32.50      36.68      36.99  "                                
+# [15,] "  EOSDY                                    "                                
+# [16,] "    Mean       Very       Very       Very  "                                
+# [17,] "               long       long       long  "                                
+# [18,] "               cell       cell       cell  "                                
+# [19,] "             contents   contents   contents"                                
+# [20,] "             to_be_wr   to_be_wr   to_be_wr"                                
+# [21,] "             apped_an   apped_an   apped_an"                                
+# [22,] "             d_splitt   d_splitt   d_splitt"                                
+# [23,] "                ed         ed         ed   "                                
+# [24,] "BLACK OR                                   "                                
+# [25,] "AFRICAN                                    "                                
+# [26,] "AMERICAN                                   "                                
+# [27,] "  AGE                                      "                                
+# [28,] "    Mean      34.27      34.93      33.71  "                                
+# [29,] "  EOSDY                                    "                                
+# [30,] "    Mean       Very       Very       Very  "                                
+# [31,] "               long       long       long  "                                
+# [32,] "               cell       cell       cell  "                                
+# [33,] "             contents   contents   contents"                                
+# [34,] "             to_be_wr   to_be_wr   to_be_wr"                                
+# [35,] "             apped_an   apped_an   apped_an"                                
+# [36,] "             d_splitt   d_splitt   d_splitt"                                
+# [37,] "                ed         ed         ed   "                                
+# [38,] "———————————————————————————————————————————"                                
+# [39,] ""                                                                           
+# [40,] "Footnote that is wider than expected for this table."                       
+# [41,] ""                                                                           
+# [42,] "Provenance footer material that is also wider than expected for this table."
+

In the resulting output we can see that the table has been correctly +rendered using wrapping with a total width of 43 characters, but that +the titles and footers remain wider than the rendered table.

+
+
+ +

In addition to wrapping column widths, titles and footers can be +wrapped by setting tf_wrap = TRUE in +toString() and setting the max_width argument +of toString() to the maximum width (in characters) allowed +for titles/footers. The four export_as_* functions and +paginate_table() can also wrap titles/footers by setting +the same two arguments. In the following code, we set +max_width = 43 so that the rendered table and all of its +annotations have a maximum width of 43 characters.

+
+result_wrap_cells_tf <- toString(
+  wide_tbl,
+  widths = c(10, 8, 8, 8),
+  tf_wrap = TRUE,
+  max_width = 43
+)
+matrix_wrap_cells_tf <- matrix(strsplit(result_wrap_cells_tf, "\n")[[1]], ncol = 1)
+matrix_wrap_cells_tf
+
#       [,1]                                         
+#  [1,] "Title that is too long and also needs to be"
+#  [2,] "wrapped to a smaller width"                 
+#  [3,] "Subtitle that is also long and also needs"  
+#  [4,] "to be wrapped to a smaller width"           
+#  [5,] ""                                           
+#  [6,] "———————————————————————————————————————————"
+#  [7,] "             Incredib   This_col           "
+#  [8,] "             ly long    umn_name           "
+#  [9,] "              column    _should_           "
+# [10,] "               name     be_split           "
+# [11,] "              to be     _somewhe   C: Combi"
+# [12,] "             wrapped       re       nation "
+# [13,] "———————————————————————————————————————————"
+# [14,] "ASIAN                                      "
+# [15,] "  AGE                                      "
+# [16,] "    Mean      32.50      36.68      36.99  "
+# [17,] "  EOSDY                                    "
+# [18,] "    Mean       Very       Very       Very  "
+# [19,] "               long       long       long  "
+# [20,] "               cell       cell       cell  "
+# [21,] "             contents   contents   contents"
+# [22,] "             to_be_wr   to_be_wr   to_be_wr"
+# [23,] "             apped_an   apped_an   apped_an"
+# [24,] "             d_splitt   d_splitt   d_splitt"
+# [25,] "                ed         ed         ed   "
+# [26,] "BLACK OR                                   "
+# [27,] "AFRICAN                                    "
+# [28,] "AMERICAN                                   "
+# [29,] "  AGE                                      "
+# [30,] "    Mean      34.27      34.93      33.71  "
+# [31,] "  EOSDY                                    "
+# [32,] "    Mean       Very       Very       Very  "
+# [33,] "               long       long       long  "
+# [34,] "               cell       cell       cell  "
+# [35,] "             contents   contents   contents"
+# [36,] "             to_be_wr   to_be_wr   to_be_wr"
+# [37,] "             apped_an   apped_an   apped_an"
+# [38,] "             d_splitt   d_splitt   d_splitt"
+# [39,] "                ed         ed         ed   "
+# [40,] "———————————————————————————————————————————"
+# [41,] ""                                           
+# [42,] "Footnote that is wider than expected for"   
+# [43,] "this table."                                
+# [44,] ""                                           
+# [45,] "Provenance footer material that is also"    
+# [46,] "wider than expected for this table."
+
+
+
+
+
+ + + + +
+ + + + + + + diff --git a/v0.6.9/articles/dev-guide/dg_debug_rtables.html b/v0.6.9/articles/dev-guide/dg_debug_rtables.html new file mode 100644 index 000000000..e58f2924a --- /dev/null +++ b/v0.6.9/articles/dev-guide/dg_debug_rtables.html @@ -0,0 +1,323 @@ + + + + + + + + +Debugging in {rtables} and Beyond • rtables + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

Debugging +

+

This is a short and non-comprehensive guide to debugging +rtables. Regardless, it is to be considered valid for +personal use at your discretion.

+
+

Coding in Practice +

+
    +
  • It is easy to read and find problems
  • +
  • It is not clever if it is impossible to debug
  • +
+
+
+

Some Definitions +

+
    +
  • +Coding Error - Code does not do what you intended +-> Bug in the punch card
  • +
  • +Unexpected Input - Defensive programming FAIL FAST +FAIL LOUD (FFFL) -> useful and not too time consuming
  • +
  • +Bug in Dependency -> never use dependencies if +you can!
  • +
+
+
+

Considerations About FFFL +

+

Errors should be as close as possible to the source. For example, bad +inputs should be found very early. The worst possible example is a +software that is silently giving incorrect results. Common things that +we can catch early are missing values, column length == 0, +or length > 1.

+
+
+

General Suggestions +

+
    +
  • Robust code base does not attempt doing possibly problematic +operations.
  • +
  • Read Error Messages
  • +
  • +debugcall you can add the signature (formals)
  • +
  • +trace is powerful because you can add the reaction
  • +
  • +tracer is very good and precise to find where it +happens
  • +
+

options(error = recover) is one of the best tools to +debug at it is a core tool when developing that allows you to step into +any point of the function call sequence.

+

dump.frames and debugger: it saves it to a +file or an object and then you call debugger to step in it as you did +recover.

+
+
+

+warn Global Option +

+
    +
  • +<0 ignored
  • +
  • +0 top level function call
  • +
  • +1 immediately as they occur
  • +
  • +>=2 throws errors
  • +
+

<<- for recover or +debugger gives it to the global environment

+
+
+

direct-modification techniques +

+
    +
  • PRINT / CAT is always a low level debugging that can be used. It is +helpful for server jobs where maybe only terminal or console output is +available and no browser() can be used. For example, you +can print the position or state of a function at a certain point until +you find the break point.
  • +
  • comment blocks -> does not work with pipes (you can use +identity() it is a step that does nothing but does not +break the pipes)
  • +
  • +browser() bombing
  • +
+
+
+

Regression Tests +

+

Almost every bug should become a regression test.

+
+
+

Debugging with Pipes +

+
    +
  • Pipes are better to write code but horrible to debug
  • +
  • T in pipe %T>% does print it midway
  • +
  • +debug_pipe() -> it is like the T pipe going into +browser()
  • +
+
+
+

Shiny Debugging +

+

More difficult due to reactivity.

+
+
+

General Suggestion +

+

DO NOT BE CLEVER WITH CODE - ONLY IF YOU HAVE TO, CLEVER IS ALSO +SUBJECTIVE AND IT WILL CHANGE WITH TIME.

+
+
+
+

Debugging in rtables +

+

We invite the smart developer to use the provided examples as a way +to get an “interactive” and dynamic view of the internal algorithms as +they are routinely executed when constructing tables with +rtables. This is achieved by using browser() +and debugonce() on internal and exported functions +(rtables::: or rtables::), as we will see in a +moment. We invite you to continuously and autonomously explore the +multiple S3 and S4 objects that constitute the +complexity and power of rtables. To do so, we will use the +following functions:

+
    +
  • +methods(generic_function): This function lists the +methods that are available for a generic function. Specifically for +S4 generic functions, +showMethods(generic_function) gives more detailed +information about each method (e.g. inheritance).
  • +
  • +class(object): This function returns the class of an +object. If the class is not one of the built-in classes in R, you can +use this information to search for its documentation and examples. +help(class) may be informative as it will call the +documentation of the specific class. Similarly, the ? +operator will bring up the documentation page for different +S4 methods. For S3 methods it is necessary to +postfix the class name with a dot (e.g. ?summary.lm).
  • +
  • +getClass(class): This describes the type of class in a +compact way, the slots that it has, and the relationships that it may +have with the other classes that may inherit from or be inherited by it. +With getClass(object) we can see to which values the slots +of the object are assigned. It is possible to use +str(object, max.level = 2) to see less formal and more +compact descriptions of the slots, but it may be problematic when there +are one or more objects in the class slots. Hence, the maximum number of +levels should always be limited to 2 or 3 (max.level = 2). +Similarly, attributes() can be used to retrieve some +information, but we need to remember that storing important variables in +this way is not encouraged. Information regarding the type of class can +be retrieved with mode() and indirectly by +summary() and is.S4(). +*getAnywhere(function) is very useful to get the source +code of internal functions and specific generics. It works very well +with S3 methods, and will display the relevant namespace +for each of the methods found. Similarly, +getMethod(S4_generic, S4_class) can retrieve the source +code of class-specific S4 methods.
  • +
  • +eval(debugcall(generic_function(obj))): this is a very +useful way to browse a S4 method, specifically for a +defined object, without having to manually insert browser() +into the code. It is also possible to do similarly with R > 3.4.0 +where debug*() calls can have the triggering signature +(class) specified. Both of these are modern and simplified wrappers of +the tracing function trace().
  • +
+
+
+
+ + + + +
+ + + + + + + diff --git a/v0.6.9/articles/dev-guide/dg_notes.html b/v0.6.9/articles/dev-guide/dg_notes.html new file mode 100644 index 000000000..fa19ead4d --- /dev/null +++ b/v0.6.9/articles/dev-guide/dg_notes.html @@ -0,0 +1,487 @@ + + + + + + + + +Sparse Notes on {rtables} Internals • rtables + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

Disclaimer +

+

This is a collection of notes divided by issues and it is a working +document that will end up being a developer vignette one day.

+
+
+

+section_div notes +

+

Everything in the layout is built over split objects, that reside in +00_tabletrees.R. There section_div is defined +internally in each split object as child_section_div and +assigned to NA_character as default. This needs to be in +all split objects that need to have a separator divisor. Object-wise, +the virtual class Split contains section_div +and it has the following sub-classes. I tagged with “X” constructor that +allows for section_div to be assigned to a value different +than NA_character, and "NX" otherwise.

+ +
## Loading required package: formatters
+
## 
+## Attaching package: 'formatters'
+
## The following object is masked from 'package:base':
+## 
+##     %||%
+
## Loading required package: magrittr
+
## 
+## Attaching package: 'rtables'
+
## The following object is masked from 'package:utils':
+## 
+##     str
+
+getClass("Split")
+
## Virtual Class "Split" [package "rtables"]
+## 
+## Slots:
+##                                                                               
+## Name:                  payload                    name             split_label
+## Class:                     ANY               character               character
+##                                                                               
+## Name:             split_format            split_na_str    split_label_position
+## Class:              FormatSpec               character               character
+##                                                                               
+## Name:              content_fun          content_format          content_na_str
+## Class:              listOrNULL              FormatSpec               character
+##                                                                               
+## Name:              content_var          label_children              extra_args
+## Class:               character                 logical                    list
+##                                                                               
+## Name:          indent_modifier content_indent_modifier      content_extra_args
+## Class:                 integer                 integer                    list
+##                                                                               
+## Name:        page_title_prefix       child_section_div    child_show_colcounts
+## Class:               character               character                 logical
+##                               
+## Name:    child_colcount_format
+## Class:              FormatSpec
+## 
+## Known Subclasses: 
+## Class "CustomizableSplit", directly
+## Class "AllSplit", directly
+## Class "VarStaticCutSplit", directly
+## Class "VarDynCutSplit", directly
+## Class "VAnalyzeSplit", directly
+## Class "CompoundSplit", directly
+## Class "VarLevelSplit", by class "CustomizableSplit", distance 2
+## Class "MultiVarSplit", by class "CustomizableSplit", distance 2
+## Class "RootSplit", by class "AllSplit", distance 2
+## Class "ManualSplit", by class "AllSplit", distance 2
+## Class "CumulativeCutSplit", by class "VarStaticCutSplit", distance 2
+## Class "AnalyzeVarSplit", by class "VAnalyzeSplit", distance 2
+## Class "AnalyzeColVarSplit", by class "VAnalyzeSplit", distance 2
+## Class "AnalyzeMultiVars", by class "CompoundSplit", distance 2
+## Class "VarLevWBaselineSplit", by class "VarLevelSplit", distance 3
+
+# Known Subclasses:
+# ? Class "CustomizableSplit", directly # vclass used for grouping different split types (I guess)
+# Class "AllSplit", directly            # NX
+# Class "VarStaticCutSplit", directly   # X via make_static_cut_split
+# Class "VarDynCutSplit", directly      # X
+# Class "VAnalyzeSplit", directly       # X
+# ? Class "CompoundSplit", directly   # Used only for AnalyzeMultiVars (maybe not needed?)
+# Class "VarLevelSplit", by class "CustomizableSplit", distance 2            # X
+# Class "MultiVarSplit", by class "CustomizableSplit", distance 2            # X
+# Class "RootSplit", by class "AllSplit", distance 2                         # NX
+# Class "ManualSplit", by class "AllSplit", distance 2                       # X
+# Class "CumulativeCutSplit", by class "VarStaticCutSplit", distance 2       # X via make_static_cut_split
+# Class "AnalyzeVarSplit", by class "VAnalyzeSplit", distance 2         # Virtual
+# Class "AnalyzeColVarSplit", by class "VAnalyzeSplit", distance 2           # X
+# Class "AnalyzeMultiVars", by class "CompoundSplit", distance 2             # X
+# Class "VarLevWBaselineSplit", by class "VarLevelSplit", distance 3         # NX
+

This can be updated only by related layout functions. The most +important, that are covered by tests are analyze and +split_rows_by.

+

Now it is relevant to understand where this information is saved in +the table object built by build_table. To do that we need +to see where it is present and how it is assigned. Let’s go back to +00tabletree.Rand look for +trailing_section_div. As classes definitions goes, you will +notice from the search that trailing_section_div is present +in the virtual classes TableRow and +VTableTree. In the following is the class hierarchy that +makes `trailing_section_div:

+
+getClass("TableRow")
+
## Virtual Class "TableRow" [package "rtables"]
+## 
+## Slots:
+##                                                                            
+## Name:              leaf_value           var_analyzed                  label
+## Class:                    ANY              character              character
+##                                                                            
+## Name:           row_footnotes   trailing_section_div                  level
+## Class:                   list              character                integer
+##                                                                            
+## Name:                    name               col_info                 format
+## Class:              character InstantiatedColumnInfo             FormatSpec
+##                                                                            
+## Name:                  na_str        indent_modifier            table_inset
+## Class:              character                integer                integer
+## 
+## Extends: 
+## Class "VLeaf", directly
+## Class "VTableNodeInfo", directly
+## Class "VNodeInfo", by class "VLeaf", distance 2
+## 
+## Known Subclasses: "DataRow", "ContentRow", "LabelRow"
+
+# Extends:
+# Class "VLeaf", directly
+# Class "VTableNodeInfo", directly
+# Class "VNodeInfo", by class "VLeaf", distance 2
+#
+# Known Subclasses: "DataRow", "ContentRow", "LabelRow"
+
+getClass("VTableTree")
+
## Virtual Class "VTableTree" [package "rtables"]
+## 
+## Slots:
+##                                                                            
+## Name:                children               rowspans               labelrow
+## Class:                   list             data.frame               LabelRow
+##                                                                            
+## Name:             page_titles         horizontal_sep     header_section_div
+## Class:              character              character              character
+##                                                                            
+## Name:    trailing_section_div               col_info                 format
+## Class:              character InstantiatedColumnInfo             FormatSpec
+##                                                                            
+## Name:                  na_str        indent_modifier            table_inset
+## Class:              character                integer                integer
+##                                                                            
+## Name:                   level                   name             main_title
+## Class:                integer              character              character
+##                                                                            
+## Name:               subtitles            main_footer      provenance_footer
+## Class:              character              character              character
+## 
+## Extends: 
+## Class "VTableNodeInfo", directly
+## Class "VTree", directly
+## Class "VTitleFooter", directly
+## Class "VNodeInfo", by class "VTableNodeInfo", distance 2
+## 
+## Known Subclasses: "ElementaryTable", "TableTree"
+
+# Extends:
+# Class "VTableNodeInfo", directly
+# Class "VTree", directly
+# Class "VTitleFooter", directly
+# Class "VNodeInfo", by class "VTableNodeInfo", distance 2
+#
+# Known Subclasses: "ElementaryTable", "TableTree"
+

Always check the constructors after finding the classes. In the above +case for example, the DataRow and ContentRow +share the constructor, so we do not need to add identical getter and +setters for these two classes but only for the virtual class +TableRow. Different is the story for LabelRow +which needs to be handle differently. Now, to understand why only these +two have this feature, lets see the structure of a table built with +section dividers:

+
+lyt <- basic_table() %>%
+  split_rows_by("ARM", section_div = "+") %>%
+  split_rows_by("STRATA1", section_div = "") %>%
+  analyze("AGE",
+    afun = function(x) list("Mean" = mean(x), "Standard deviation" = sd(x)),
+    format = list("Mean" = "xx.", "Standard deviation" = "xx."),
+    section_div = "~"
+  )
+
+tbl <- build_table(lyt, DM)
+
+print(tbl)
+
##                          all obs
+## ————————————————————————————————
+## A: Drug X                       
+##   A                             
+##     Mean                   33   
+## ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+##     Standard deviation      7   
+## 
+##   B                             
+##     Mean                   35   
+## ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+##     Standard deviation      7   
+## 
+##   C                             
+##     Mean                   36   
+## ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+##     Standard deviation      9   
+## ++++++++++++++++++++++++++++++++
+## B: Placebo                      
+##   A                             
+##     Mean                   32   
+## ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+##     Standard deviation      6   
+## 
+##   B                             
+##     Mean                   32   
+## ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+##     Standard deviation      6   
+## 
+##   C                             
+##     Mean                   34   
+## ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+##     Standard deviation      7   
+## ++++++++++++++++++++++++++++++++
+## C: Combination                  
+##   A                             
+##     Mean                   36   
+## ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+##     Standard deviation      7   
+## 
+##   B                             
+##     Mean                   34   
+## ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+##     Standard deviation      6   
+## 
+##   C                             
+##     Mean                   34   
+## ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+##     Standard deviation      6
+
+print(class(tbl)) # TableTree
+
## [1] "TableTree"
+## attr(,"package")
+## [1] "rtables"
+
+# methods("trailing_section_div") # to see this please do devtools::load_all()
+# [1] trailing_section_div,LabelRow-method
+# trailing_section_div,TableRow-method
+# trailing_section_div,VTableTree-method
+

In the above, we show that trailing_section_div has +methods for TableRow virtual object, LabelRow, +and VTableTree. These three make the whole +section_div structure as the VTableTree is +present in TableTree and ElementaryTable that +are the two main table objects. If these are not +NA_character_ then the section_div is printed +at split divisions. The LabelRow and TableRow +are different as their assignment allows the row-wise modification of +separators. When we have a special case for a ContentRow, +as it is represented as content_table(obj) which is a +one-line ElementaryTable, while label row is turned off. +Please take a moment to check the following setter:

+
+setMethod("section_div<-", "VTableTree", function(obj, value, only_sep_sections = FALSE) {
+  char_v <- as.character(value)
+  tree_depths <- unname(vapply(collect_leaves(obj), tt_level, numeric(1)))
+  max_tree_depth <- max(tree_depths)
+  stopifnot(is.logical(only_sep_sections))
+  .check_char_vector_for_section_div(char_v, max_tree_depth, nrow(obj))
+
+  # Automatic establishment of intent
+  if (length(char_v) < nrow(obj)) {
+    only_sep_sections <- TRUE
+  }
+
+  # Case where only separators or splits need to change externally
+  if (only_sep_sections && length(char_v) < nrow(obj)) {
+    if (length(char_v) == 1) {
+      char_v <- rep(char_v, max_tree_depth - 1) # -1 is the data row
+    }
+    # Case where char_v is longer than the max depth
+    char_v <- char_v[seq_len(min(max_tree_depth, length(char_v)))]
+    # Filling up with NAs the rest of the tree depth section div chr vector
+    missing_char_v_len <- max_tree_depth - length(char_v)
+    char_v <- c(char_v, rep(NA_character_, missing_char_v_len))
+    # char_v <- unlist(
+    #   lapply(tree_depths, function(tree_depth_i) char_v[seq_len(tree_depth_i)]),
+    #   use.names = FALSE
+    # )
+  }
+
+  # Retrieving if it is a contentRow (no need for labelrow to be visible in this case)
+  content_row_tbl <- content_table(obj)
+  is_content_table <- isS4(content_row_tbl) && nrow(content_row_tbl) > 0
+
+  # Main table structure change
+  if (labelrow_visible(obj) || is_content_table) {
+    if (only_sep_sections) {
+      # Only tables are modified
+      trailing_section_div(tt_labelrow(obj)) <- NA_character_
+      trailing_section_div(obj) <- char_v[1]
+      section_div(tree_children(obj), only_sep_sections = only_sep_sections) <- char_v[-1]
+    } else {
+      # All leaves are modified
+      trailing_section_div(tt_labelrow(obj)) <- char_v[1]
+      trailing_section_div(obj) <- NA_character_
+      section_div(tree_children(obj), only_sep_sections = only_sep_sections) <- char_v[-1]
+    }
+  } else {
+    section_div(tree_children(obj), only_sep_sections = only_sep_sections) <- char_v
+  }
+  obj
+})
+

only_sep_sections is a parameter that is used to change +only the separators (between splits) and not the data rows. It is +happening forcefully if set to TRUE, but it is +automatically activated when section_div(tbl) <- char_v +is a character vector of length < nrow(tbl). Notice that +the exception for ContentRow is activated by the switcher +is_content_table. This is because content rows do not have +visible label row. You see that in the main table structure change we +have two blocks depending on only_sep_sections. If +TRUE only the VTableTree are modified leading +to only split section separators to be modified. Also consider looking +at section_div getter and tests in +test-accessors.R to have more insights on the structure. +Also to understand exactly how this is bound to output, please check the +result of make_row_df() for the column +trailing_sep. Indeed, an alternative and iterative method +is used by make_row_df to retrieve the information about +the separators for each table row. Being it a trailing separator by +definition, we added header_section_div as a function and a +parameter of basic_table, so to possibly add an empty line +after the header (e.g. header_section_div(tbl) = " "). This +is not a trailing separator, but it is a separator that is added after +the header. To close the circle, please check how +trailing_sep and header_section_div is +propagated and printed/used in formatters::toString.

+
+
+
+ + + + +
+ + + + + + + diff --git a/v0.6.9/articles/dev-guide/dg_split_machinery.html b/v0.6.9/articles/dev-guide/dg_split_machinery.html new file mode 100644 index 000000000..3b66b269f --- /dev/null +++ b/v0.6.9/articles/dev-guide/dg_split_machinery.html @@ -0,0 +1,1486 @@ + + + + + + + + +Split Machinery • rtables + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

Disclaimer +

+

This article is intended for use by developers only and will contain +low-level explanations of the topics covered. For user-friendly +vignettes, please see the Articles +page on the rtables website.

+

Any code or prose which appears in the version of this article on the +main branch of the repository may reflect a specific state +of things that can be more or less recent. This guide describes very +important pieces of the split machinery that are unlikely to change. +Regardless, we invite the reader to keep in mind that the current +repository code may have drifted from the following material in this +document, and it is always the best practice to read the code directly +on main.

+

Please keep in mind that rtables is still under active +development, and it has seen the efforts of multiple contributors across +different years. Therefore, there may be legacy mechanisms and ongoing +transformations that could look different in the future.

+

Being that this a working document that may be subjected to both +deprecation and updates, we keep xxx comments to indicate +placeholders for warnings and to-do’s that need further work.

+
+
+

Introduction +

+

The scope of this article is understanding how rtables +creates facets by splitting the incoming data into hierarchical groups +that go from the root node to singular rcells. The latter +level, also called the leaf-level, contains the final partition that is +subjected to analysis functions. More details from the user perspective +can be found in the Split +Functions vignette and in function documentation like +?split_rows_by and ?split_funcs.

+

The following article will describe how the split machinery works in +the row domain. Further information on how the split machinery works in +the column domain will be covered in a separate article.

+
+
+

Process and Methods +

+

Beforehand, we encourage the reader to familiarize themselves with +the Debugging +in {rtables} article from the rtables Developers Guide. +This document is generally valid for R programming, but has been +tailored to study and understand complex packages that rely heavily on +S3 and S4 object programming like rtables.

+

Here, we explore and study the split machinery with a growing amount +of complexity, following relevant functions and methods throughout their +execution. By going from basic to complex and by discussing important +and special cases, we hope to be able to give you a good understanding +of how the split machinery works.

+

In practice, the majority of the split engine resides in the source +file R/split_funs.R, with occasional incursion into +R/make_split_fun.R for custom split function creation and +rarer references to other more general tabulation files.

+
+
+

+do_split +

+

The split machinery is so fundamental to rtables that +relevant functions like do_split are executed even when no +split is requested. The following example shows how we can enter +do_split and start understanding the class hierarchy and +the main split engine.

+
+library(rtables)
+# debugonce(rtables:::do_split) # Uncomment me to enter the function!!!
+basic_table() %>%
+  build_table(DM)
+
##    all obs
+## ——————————
+

In the following code, we copied the do_split function +code to allow the reader to go through the general structure with +enhanced comments and sections. Each section in the code reflects +roughly one section of this article.

+
+# rtables 0.6.2
+### NB This is called at EACH level of recursive splitting
+do_split <- function(spl,
+                     df,
+                     vals = NULL,
+                     labels = NULL,
+                     trim = FALSE,
+                     spl_context) {
+  # - CHECKS - #
+  ## This will error if, e.g., df does not have columns
+  ##  required by spl, or generally any time the split (spl)
+  ##  can not be applied to df
+  check_validsplit(spl, df)
+
+  # - SPLIT FUNCTION - #
+  ## In special cases, we need to partition data (split)
+  ##  in a very specific way, e.g. depending on the data or
+  ##  external values. These can be achieved by using a custom
+  ##  split function.
+
+  ## note the <- here!!!
+  if (!is.null(splfun <- split_fun(spl))) {
+    ## Currently split functions take df, vals, labels and
+    ## return list(values = ..., datasplit = ..., labels = ...),
+    ## with an optional additional 'extras' element
+    if (func_takes(splfun, ".spl_context")) {
+      ret <- tryCatch(
+        splfun(df, spl, vals, labels,
+          trim = trim,
+          .spl_context = spl_context
+        ),
+        error = function(e) e
+      ) ## rawvalues(spl_context))
+    } else {
+      ret <- tryCatch(splfun(df, spl, vals, labels, trim = trim),
+        error = function(e) e
+      )
+    }
+    if (is(ret, "error")) {
+      stop(
+        "Error applying custom split function: ", ret$message, "\n\tsplit: ",
+        class(spl), " (", payloadmsg(spl), ")\n",
+        "\toccured at path: ",
+        spl_context_to_disp_path(spl_context), "\n"
+      )
+    }
+  } else {
+    # - .apply_split_inner - #
+    ## This is called when no split function is provided. Please note that this function
+    ##  will also probably be called when the split function is provided, as long as the
+    ##  main splitting method is not willingly modified by the split function.
+    ret <- .apply_split_inner(df = df, spl = spl, vals = vals, labels = labels, trim = trim)
+  }
+
+  # - EXTRA - #
+  ## this adds .ref_full and .in_ref_col
+  if (is(spl, "VarLevWBaselineSplit")) {
+    ret <- .add_ref_extras(spl, df, ret)
+  }
+
+  # - FIXUPVALS - #
+  ## This:
+  ##  - guarantees that ret$values contains SplitValue objects
+  ##  - removes the extras element since its redundant after the above
+  ##  - ensures datasplit and values lists are named according to labels
+  ##  - ensures labels are character not factor
+  ret <- .fixupvals(ret)
+
+  # - RETURN - #
+  ret
+}
+

We will see where and how input parameters are used. The most +important parameters are spl and df - the +split objects and the input data.frame, respectively.

+
+

Checks and Classes +

+

We will start by looking at the first function called from +do_split. This will give us a good overview of how the +split itself is defined. This function is, of course, the check function +(check_validsplit) that is used to verify if the split is +valid for the data. In the following we will describe the split-class +hierarchy step-by-step, but we invite the reader to explore this further +on their own as well.

+

Let’s first search the package for check_validsplit. You +will find that it is defined as a generic in +R/split_funs.R, where it is applied to the following +“split” classes: VarLevelSplit, MultiVarSplit, +VAnalyzeSplit, CompoundSplit, and +Split. Another way to find this information, which is more +useful for more spread out and complicated objects, is by using +showMethods(check_validsplit). The virtual class +VAnalyzeSplit (by convention virtual classes start with +“V”) defines the main parent of the analysis split which we discuss in +detail in the related vignette vignette() (xxx). From this, +we can see that the analyze() calls actually mimic split +objects as they create different results under a specific final split +(or node). Now, notice that check_validsplit is also called +in another location, the main R/tt_dotabulation.R source +file. This is again something related to making “analyze” rows as it +mainly checks for VAnalyzeSplit. See the Tabulation +article for more details. We will discuss the other classes as they +appear in our examples. See more about class hierarchy in the Table +Hierarchy article.

+

For the moment, we see with class(spl) (from the main +do_split function) that we are dealing with an +AllSplit object. By calling +showMethods(check_validsplit) we produce the following:

+
# rtables 0.6.2
+Function: check_validsplit (package rtables)
+spl="AllSplit"
+    (inherited from: spl="Split")
+spl="CompoundSplit"
+spl="MultiVarSplit"
+spl="Split"
+spl="VAnalyzeSplit"
+spl="VarLevelSplit"
+

This means that each of the listed classes has a dedicated definition +of check_validsplit that may largely differ from the +others. Only the class AllSplit does not have its own +function definition as it is inherited from the Split +class. Therefore, we understand that AllSplit is a parent +class of Split. This is one of the first definitions of a +virtual class in the package and it is the only one that does not +include the “V” prefix. These classes are defined along with their +constructors in R/00tabletrees.R. Reading about how +AllSplit is structured can be useful in understanding how +split objects are expected to work. Please see the comments in the +following:

+
+# rtables 0.6.2
+setClass("AllSplit", contains = "Split")
+
+AllSplit <- function(split_label = "",
+                     cfun = NULL,
+                     cformat = NULL,
+                     cna_str = NA_character_,
+                     split_format = NULL,
+                     split_na_str = NA_character_,
+                     split_name = NULL,
+                     extra_args = list(),
+                     indent_mod = 0L,
+                     cindent_mod = 0L,
+                     cvar = "",
+                     cextra_args = list(),
+                     ...) {
+  if (is.null(split_name)) { # If the split has no name
+    if (nzchar(split_label)) { # (std is "")
+      split_name <- split_label
+    } else {
+      split_name <- "all obs" # No label, a standard split with all
+      # observations is assigned.
+    }
+  }
+  new("AllSplit",
+    split_label = split_label,
+    content_fun = cfun,
+    content_format = cformat,
+    content_na_str = cna_str,
+    split_format = split_format,
+    split_na_str = split_na_str,
+    name = split_name,
+    label_children = FALSE,
+    extra_args = extra_args,
+    indent_modifier = as.integer(indent_mod),
+    content_indent_modifier = as.integer(cindent_mod),
+    content_var = cvar,
+    split_label_position = "hidden",
+    content_extra_args = cextra_args,
+    page_title_prefix = NA_character_,
+    child_section_div = NA_character_
+  )
+}
+

We can also print this information by calling +getClass("AllSplit") for the general slot definition, or by +calling getClass(spl). Note that the first call will give +also a lot of information about the class hierarchy. For more +information regarding class hierarchy, please refer to the relevant +article here. +We will discuss the majority of the slots by the end of this document. +Now, let’s see if we can find some of the values described in the +constructor within our object. To do so, we will show the more compact +representation given by str. When there are multiple and +hierarchical slots that contain objects themselves, calling +str will be much less or not at all informative if the +maximum level of nesting is not set +(e.g. max.level = 2).

+
# rtables 0.6.2
+Browse[2]> str(spl, max.level = 2)
+Formal class 'AllSplit' [package "rtables"] with 17 slots
+  ..@ payload                : NULL
+  ..@ name                   : chr "all obs"
+  ..@ split_label            : chr ""
+  ..@ split_format           : NULL
+  ..@ split_na_str           : chr NA
+  ..@ split_label_position   : chr "hidden"
+  ..@ content_fun            : NULL
+  ..@ content_format         : NULL
+  ..@ content_na_str         : chr NA
+  ..@ content_var            : chr ""
+  ..@ label_children         : logi FALSE
+  ..@ extra_args             : list()
+  ..@ indent_modifier        : int 0
+  ..@ content_indent_modifier: int 0
+  ..@ content_extra_args     : list()
+  ..@ page_title_prefix      : chr NA
+  ..@ child_section_div      : chr NA
+

Details about these slots will become necessary in future examples, +and we will deal with them at that time. Now, we gave you a hint of the +complex class hierarchy that makes up rtables, and how to +explore it autonomously. Let’s go forward in do_split. In +our case, with AllSplit inherited from Split, +we are sure that the called function will be the following (read the +comment!):

+
+# rtables 0.6.2
+## Default does nothing, add methods as they become required
+setMethod(
+  "check_validsplit", "Split",
+  function(spl, df) invisible(NULL)
+)
+
+
+

Split Functions and .apply_split_inner +

+

Before diving into custom split functions, we need to take a moment +to analyze how .apply_split_inner works. This function is +routinely called whether or not we have a split function. Let’s see why +this is the case by entering it with +debugonce(.apply_split_inner). Of course, we are still +currently browsing within do_split in debug mode from the +first example. We print and comment on the function in the +following:

+
+# rtables 0.6.2
+.apply_split_inner <- function(spl, df, vals = NULL, labels = NULL, trim = FALSE) {
+  # - INPUTS - #
+  # In this case .applysplit_rawvals will attempt to find the split values if vals is NULL.
+  # Please notice that there may be a non-mutually exclusive set or subset of elements that
+  # will constitute the split.
+
+  # - SPLIT VALS - #
+  ## Try to calculate values first - most of the time we can
+  if (is.null(vals)) {
+    vals <- .applysplit_rawvals(spl, df)
+  }
+
+  # - EXTRA PARAMETERS - #
+  # This call extracts extra parameters from the split, according to the split values
+  extr <- .applysplit_extras(spl, df, vals)
+
+  # If there are no values to do the split upon, we return an empty final split
+  if (is.null(vals)) {
+    return(list(
+      values = list(),
+      datasplit = list(),
+      labels = list(),
+      extras = list()
+    ))
+  }
+
+  # - DATA SUBSETTING - #
+  dpart <- .applysplit_datapart(spl, df, vals)
+
+  # - LABEL RETRIEVAL - #
+  if (is.null(labels)) {
+    labels <- .applysplit_partlabels(spl, df, vals, labels)
+  } else {
+    stopifnot(names(labels) == names(vals))
+  }
+
+  # - TRIM - #
+  ## Get rid of columns that would not have any observations,
+  ## but only if there were any rows to start with - if not
+  ## we're in a manually constructed table column tree
+  if (trim) {
+    hasdata <- sapply(dpart, function(x) nrow(x) > 0)
+    if (nrow(df) > 0 && length(dpart) > sum(hasdata)) { # some empties
+      dpart <- dpart[hasdata]
+      vals <- vals[hasdata]
+      extr <- extr[hasdata]
+      labels <- labels[hasdata]
+    }
+  }
+
+  # - ORDER RESULTS - #
+  # Finds relevant order depending on spl_child_order()
+  if (is.null(spl_child_order(spl)) || is(spl, "AllSplit")) {
+    vord <- seq_along(vals)
+  } else {
+    vord <- match(
+      spl_child_order(spl),
+      vals
+    )
+    vord <- vord[!is.na(vord)]
+  }
+
+  ## FIXME: should be an S4 object, not a list
+  ret <- list(
+    values = vals[vord],
+    datasplit = dpart[vord],
+    labels = labels[vord],
+    extras = extr[vord]
+  )
+  ret
+}
+

After reading through .apply_split_inner, we see that +there are some fundamental functions - defined strictly for internal use +(by convention they start with “.”) - that are generics and depend on +the kind of split in input. R/split_funs.R is very kind and +groups generic definitions at the beginning of the file. These functions +are the main dispatchers for the majority of the split machinery. This +is a clear example that shows how using S4 logic enables +better clarity and flexibility in programming, allowing for easy +extension of the program. For compactness we also show the +showMethods result for each generic.

+
+# rtables 0.6.2
+# Retrieves the values that will constitute the splits (facets), not necessarily a unique list.
+# They could come from the data cuts for example -> it can be anything that produces a set of strings.
+setGeneric(
+  ".applysplit_rawvals",
+  function(spl, df) standardGeneric(".applysplit_rawvals")
+)
+# Browse[2]> showMethods(.applysplit_rawvals)
+# Function: .applysplit_rawvals (package rtables)
+# spl="AllSplit"
+# spl="ManualSplit"
+# spl="MultiVarSplit"
+# spl="VAnalyzeSplit"
+# spl="VarLevelSplit"
+# spl="VarStaticCutSplit"
+# Nothing here is inherited from the virtual class Split!!!
+
+# Contains the subset of the data (default, but these can overlap and can also NOT be mutually exclusive).
+setGeneric(
+  ".applysplit_datapart",
+  function(spl, df, vals) standardGeneric(".applysplit_datapart")
+)
+# Same as .applysplit_rawvals
+
+# Extract the extra parameter for the split
+setGeneric(
+  ".applysplit_extras",
+  function(spl, df, vals) standardGeneric(".applysplit_extras")
+)
+# Browse[2]> showMethods(.applysplit_extras)
+# Function: .applysplit_extras (package rtables)
+# spl="AllSplit"
+#     (inherited from: spl="Split")
+# spl="Split"
+# This means there is only a function for the virtual class Split.
+#  So all splits behave the same!!!
+
+# Split label retrieval and assignment if visible.
+setGeneric(
+  ".applysplit_partlabels",
+  function(spl, df, vals, labels) standardGeneric(".applysplit_partlabels")
+)
+# Browse[2]> showMethods(.applysplit_partlabels)
+# Function: .applysplit_partlabels (package rtables)
+# spl="AllSplit"
+#     (inherited from: spl="Split")
+# spl="MultiVarSplit"
+# spl="Split"
+# spl="VarLevelSplit"
+
+setGeneric(
+  "check_validsplit", # our friend
+  function(spl, df) standardGeneric("check_validsplit")
+)
+# Note: check_validsplit is an internal function but may one day be exported.
+#       This is why it does not have the "." prefix.
+
+setGeneric(
+  ".applysplit_ref_vals",
+  function(spl, df, vals) standardGeneric(".applysplit_ref_vals")
+)
+# Browse[2]> showMethods(.applysplit_ref_vals)
+# Function: .applysplit_ref_vals (package rtables)
+# spl="Split"
+# spl="VarLevWBaselineSplit"
+

Now, we know that .applysplit_extras is the function +that will be called first. This is because we did not specify any +vals and it is therefore NULL. This is an +S4 generic function as can be seen by +showMethod(.applysplit_extras), and its definition can be +seen in the following:

+
# rtables 0.6.2
+Browse[3]> getMethod(".applysplit_rawvals", "AllSplit")
+Method Definition:
+
+function (spl, df)
+obj_name(spl)
+
+Signatures:
+        spl
+target  "AllSplit"
+defined "AllSplit"
+
+# What is obj_name -> slot in spl
+Browse[3]> obj_name(spl)
+[1] "all obs"
+
+# coming from
+Browse[3]> getMethod("obj_name", "Split")
+Method Definition:
+
+function (obj)
+obj@name ##### Slot that we could see from str(spl, max.level = 2)
+
+Signatures:
+        obj
+target  "Split"
+defined "Split"
+

Then we have .applysplit_extras, which simply extracts +the extra arguments from the split objects and assigns them to their +relative split values. This function will be covered in more detail in a +later section. If still no split values are available, the function will +exit here with an empty split. Otherwise, the data will be divided into +different splits or data subsets (facets) with +.applysplit_datapart. In our current example, the resulting +list comprises the whole input dataset (do +getMethod(".applysplit_datapart", "AllSplit") and the list +will be evident: function (spl, df, vals) list(df)).

+

Next, split labels are checked. If they are not present, split values +(vals) will be used with +.applysplit_partlabels, transformed into +as.character(vals) if applied to a Split +object. Otherwise, the inserted labels are checked against the names of +split values.

+

Lastly, the split values are ordered according to +spl_child_order. In our case, which concerns the general +AllSplit, the sorting will not happen, i.e. it will be +dependent simply on the number of split values +(seq_along(vals)).

+
+
+
+

A Simple Split +

+

In the following, we demonstrate how row splits work using the +features that we have already described. We will add two splits and see +how the behavior of do_split changes. Note that if we do +not add an analyze call the split will behave as before, +giving an empty table with all observations. By default, calling +analyze on a variable will calculate the mean for each data +subset that has been generated by the splits. We want to go beyond the +first call of do_split that is by design applied on all +observations, with the purpose of generating the root split that +contains all data and all splits (indeed AllSplit). To +achieve this we use debug(rtables:::do_split) instead of +debugonce(rtables:::do_split) as we will need to step into +each of the splits. Alternatively, it is possible to use the more +powerful trace function to enter in cases where input is +from a specific class. To do so, the following can be used: +trace("do_split", quote(if(!is(spl, "AllSplit")) browser()), where = asNamespace("rtables")). +Note that we specify the namespace with where. Multiple +tracer elements can be added with expression(E1, E2), which +is the same as c(quote(E1), quote(E2)). Specific +steps can be specified with the at parameter. +Remember to call +untrace("do_split", quote(if(!is(spl, "AllSplit")) browser()), where = asNamespace("rtables")) +once finished to remove the trace.

+
+# rtables 0.6.2
+library(rtables)
+library(dplyr)
+
+# This filter is added to avoid having too many calls to do_split
+DM_tmp <- DM %>%
+  filter(ARM %in% names(table(DM$ARM)[1:2])) %>% # limit to two
+  filter(SEX %in% c("M", "F")) %>% # limit to two
+  mutate(SEX = factor(SEX), ARM = factor(ARM)) # to drop unused levels
+
+# debug(rtables:::do_split)
+lyt <- basic_table() %>%
+  split_rows_by("ARM") %>%
+  split_rows_by("SEX") %>%
+  analyze("BMRKR1") # analyze() is needed for the table to have non-label rows
+
+lyt %>%
+  build_table(DM_tmp)
+
##              all obs
+## ————————————————————
+## A: Drug X           
+##   F                 
+##     Mean      6.06  
+##   M                 
+##     Mean      5.42  
+## B: Placebo          
+##   F                 
+##     Mean      6.24  
+##   M                 
+##     Mean      5.97
+
+# undebug(rtables:::do_split)
+

Before continuing, we want to check the formal class of +spl.

+
# rtables 0.6.2
+Browse[2]> str(spl, max.level = 2)
+Formal class 'VarLevelSplit' [package "rtables"] with 20 slots
+  ..@ value_label_var        : chr "ARM"
+  ..@ value_order            : chr [1:2] "A: Drug X" "B: Placebo"
+  ..@ split_fun              : NULL
+  ..@ payload                : chr "ARM"
+  ..@ name                   : chr "ARM"
+  ..@ split_label            : chr "ARM"
+  ..@ split_format           : NULL
+  ..@ split_na_str           : chr NA
+  ..@ split_label_position   : chr "hidden"
+  ..@ content_fun            : NULL
+  ..@ content_format         : NULL
+  ..@ content_na_str         : chr NA
+  ..@ content_var            : chr ""
+  ..@ label_children         : logi NA
+  ..@ extra_args             : list()
+  ..@ indent_modifier        : int 0
+  ..@ content_indent_modifier: int 0
+  ..@ content_extra_args     : list()
+  ..@ page_title_prefix      : chr NA
+  ..@ child_section_div      : chr NA
+

From this, we can directly infer that the class is different now +(VarLevelSplit) and understand that the split label will be +hidden (split_label_position slot). Moreover, we see a +specific value order with specific split values. +VarLevelSplit also seems to have three more slots than +AllSplit. What are they precisely?

+
+# rtables 0.6.2
+slots_as <- getSlots("AllSplit") # inherits virtual class Split and is general class for all splits
+# getClass("CustomizableSplit") # -> Extends: "Split", Known Subclasses: Class "VarLevelSplit", directly
+slots_cs <- getSlots("CustomizableSplit") # Adds split function
+slots_vls <- getSlots("VarLevelSplit")
+
+slots_cs[!(names(slots_cs) %in% names(slots_as))]
+#        split_fun
+# "functionOrNULL"
+slots_vls[!(names(slots_vls) %in% names(slots_cs))]
+# value_label_var     value_order
+#     "character"           "ANY"
+

Remember to always check the constructor and class definition in +R/00tabletrees.R if exploratory tools do not suffice. Now, +check_validsplit(spl, df) will use a different method than +before (getMethod("check_validsplit", "VarLevelSplit")). It +uses the internal utility function .checkvarsok to check if +vars, i.e. the payload, is actually present in +names(df).

+

The next relevant function will be .apply_split_inner, +and we will exactly what changes using +debugonce(.apply_split_inner). Of course, this function is +called directly as no custom split function is provided. Since parameter +vals is not specified (NULL), the split values +are retrieved from df by using the split payload to select +specific columns (varvec <- df[[spl_payload(spl)]]). +Whenever no split values are specified they are retrieved from the +selected column as unique values (character) or levels +(factor).

+

Next, .applysplit_datapart creates a named list of +facets or data subsets. In this case, the result is actually a mutually +exclusive partition of the data. This is because we did not specify any +split values and as such the column content was retrieved via +unique (in case of a character vector) or +levels (in case of factors). +.applysplit_partlabels is a bit less linear as it has to +take into account the possibility of having specified labels in the +payload. Instead of looking at the function source code with +getMethod(".applysplit_partlabels", "VarLevelSplit"), we +can enter the S4 generic function in debugging mode as +follows:

+
+# rtables 0.6.2
+eval(debugcall(.applysplit_partlabels(spl, df, vals, labels)))
+# We leave to the smart developer to see how the labels are assigned
+
+# Remember to undebugcall() similarly!
+

In our case, the final labels are vals because they were +not explicitly assigned. Their order is retrieved from the split object +(spl_child_order(spl)) and matched with current split +values. The returned list is then processed as it was before.

+

If we continue with the next call of do_split, the same +procedure is followed for the second ARM split. This is +applied to the partition that was created in the first split. The main +df is now constituted by a subset (facet) of the total +data, determined by the first split. This will be repeated iteratively +for as many data splits as requested. Before concluding this iteration, +we take a moment to discuss in detail how +.fixupvals(partinfo) works. This is not a generic function +and the source code can be easily accessed. We suggest running through +it with debugonce(.fixupvals) to understand what it does in +practice. The fundamental aspects of .fixupvals(partinfo) +are as follows:

+
    +
  • Ensures that labels are character and not factor.
  • +
  • Ensures that the splits of data and list of values are named +according to labels.
  • +
  • Guarantees that ret$values contains +SplitValue objects.
  • +
  • Removes the list element extra since it is now included +in the SplitValue.
  • +
+

Note that this function can occasionally be called more than once on +the same return object (a named list for now). Of course, after the +first call only checks are applied.

+
# rtables 0.6.2
+
+# Can find the following core function:
+# vals <- make_splvalue_vec(vals, extr, labels = labels)
+# ---> Main list of SplitValue objects: iterative call of
+#      new("SplitValue", value = val, extra = extr, label = label)
+
+# Structure of ret before calling .fixupvals
+Browse[2]> str(ret, max.level = 2)
+List of 4
+ $ values   : chr [1:2] "A: Drug X" "B: Placebo"
+ $ datasplit:List of 2
+  ..$ A: Drug X : tibble [121 × 8] (S3: tbl_df/tbl/data.frame)
+  ..$ B: Placebo: tibble [106 × 8] (S3: tbl_df/tbl/data.frame)
+ $ labels   : Named chr [1:2] "A: Drug X" "B: Placebo"
+  ..- attr(*, "names")= chr [1:2] "A: Drug X" "B: Placebo"
+ $ extras   :List of 2
+  ..$ : list()
+  ..$ : list()
+
+# Structure of ret after the function call
+Browse[2]> str(.fixupvals(ret), max.level = 2)
+List of 3
+ $ values   :List of 2
+  ..$ A: Drug X :Formal class 'SplitValue' [package "rtables"] with 3 slots
+  ..$ B: Placebo:Formal class 'SplitValue' [package "rtables"] with 3 slots
+ $ datasplit:List of 2
+  ..$ A: Drug X : tibble [121 × 8] (S3: tbl_df/tbl/data.frame)
+  ..$ B: Placebo: tibble [106 × 8] (S3: tbl_df/tbl/data.frame)
+ $ labels   : Named chr [1:2] "A: Drug X" "B: Placebo"
+  ..- attr(*, "names")= chr [1:2] "A: Drug X" "B: Placebo"
+
+# The SplitValue object is fundamental
+Browse[2]> str(ret$values)
+List of 2
+ $ A: Drug X :Formal class 'SplitValue' [package "rtables"] with 3 slots
+  .. ..@ extra: list()
+  .. ..@ value: chr "A: Drug X"
+  .. ..@ label: chr "A: Drug X"
+ $ B: Placebo:Formal class 'SplitValue' [package "rtables"] with 3 slots
+  .. ..@ extra: list()
+  .. ..@ value: chr "B: Placebo"
+  .. ..@ label: chr "B: Placebo"
+
+

Pre-Made Split Functions +

+

We start by examining a split function that is already defined in +rtables. Its scope is filtering out specific values as +follows:

+
+library(rtables)
+# debug(rtables:::do_split) # uncomment to see into the main split function
+basic_table() %>%
+  split_rows_by("SEX", split_fun = drop_split_levels) %>%
+  analyze("BMRKR1") %>%
+  build_table(DM)
+
##          all obs
+## ————————————————
+## F               
+##   Mean    6.04  
+## M               
+##   Mean    5.64
+
+# undebug(rtables:::do_split)
+
+# This produces the same output as before (when filters were used)
+

After the root split, we enter the split based on SEX. +As we have specified a split function, we can retrieve the split +function by using splfun <- split_fun(spl) and enter an +if-else statement for the two possible cases: whether there is split +context or not. In both cases, an error catching framework is used to +give informative errors in case of failure. Later we will see more in +depth how this works.

+

We invite the reader to always keep an eye on +spl_context, as it is fundamental to more sophisticated +splits, e.g. in the cases where the split itself depends mainly on +preceding splits or values. When the split function is called, please +take a moment to look at how drop_split_levels is defined. +You will see that the function is fundamentally a wrapper of +.apply_split_inner that drops empty factor levels, +therefore avoiding empty splits.

+
+# rtables 0.6.2
+# > drop_split_levels
+function(df,
+         spl,
+         vals = NULL,
+         labels = NULL,
+         trim = FALSE) {
+  # Retrieve split column
+  var <- spl_payload(spl)
+  df2 <- df
+
+  ## This call is exactly the one we used when filtering to get rid of empty levels
+  df2[[var]] <- factor(df[[var]])
+
+  ## Our main function!
+  .apply_split_inner(spl, df2,
+    vals = vals,
+    labels = labels,
+    trim = trim
+  )
+}
+

There are many pre-made split functions included in +rtables. A list of these functions can be found in the Split +Functions vignette, or via ?split_funcs. We leave it to +the developer to look into how some of these split functions work, in +particular trim_levels_to_map may be of interest.

+
+
+

Creating Custom Split Functions +

+

Now we will create a custom split function. Firstly, we will see how +the system manages error messages. For a general understanding of how +custom split functions are created, please read the Custom +Split Functions section of the Advanced Usage vignette or see +?custom_split_funs. In the following code we use +browser() to enter our custom split functions. We invite +the reader to activate options(error = recover) to +investigate cases where we encounter an error. Note that you can revert +to default behavior by restarting your R session, by +caching the default option value, or by using callr to +retrieve the default as follows: +default_opts <- callr::r(function(){options()}); options(error = default_opts$error).

+
+# rtables 0.6.2
+# Table call with only the function changing
+simple_table <- function(DM, f) {
+  lyt <- basic_table() %>%
+    split_rows_by("ARM", split_fun = f) %>%
+    analyze("BMRKR1")
+
+  lyt %>%
+    build_table(DM)
+}
+# First round will fail because there are unused arguments
+exploratory_split_fun <- function(df, spl) NULL
+# debug(rtables:::do_split)
+err_msg <- tryCatch(simple_table(DM, exploratory_split_fun), error = function(e) e)
+# undebug(rtables:::do_split)
+
+message(err_msg$message)
+
## Error applying custom split function: unused arguments (vals, labels, trim = trim)
+##  split: VarLevelSplit (ARM)
+##  occured at path: root
+

The commented debugging lines above will allow you to inspect the +error. Alternatively, using the recover option will allow you the +possibility to select the frame number, i.e. the trace level, to enter. +Selecting the last frame number (10 in this case) will allow you to see +the value of ret from rtables:::do_split that +causes the error and how the informative error message that follows is +created.

+
# rtables 0.6.2
+# Debugging level
+10: tt_dotabulation.R#627: do_split(spl, df, spl_context = spl_context)
+
+# Original call and final error
+> simple_table(DM, exploratory_split_fun)
+Error in do_split(spl, df, spl_context = spl_context) :
+  Error applying custom split function: unused arguments (vals, labels, trim = trim) # This is main error
+    split: VarLevelSplit (ARM) # Split reference
+    occured at path: root # Path level (where it occurred)
+

The previous split function fails because +exploratory_split_fun is given more arguments than it +accepts. A simple way to avoid this is to add ... to the +function call. Now let’s construct an interesting split function (and +error):

+
+# rtables 0.6.2
+f_brakes_if <- function(split_col = NULL, error = FALSE) {
+  function(df, spl, ...) { # order matters! more than naming
+    # browser() # To check how it works
+    if (is.null(split_col)) { # Retrieves the default
+      split_col <- spl_variable(spl) # Internal accessor to split obj
+    }
+    my_payload <- split_col # Changing split column value
+
+    vals <- levels(df[[my_payload]]) # Extracting values to split
+    datasplit <- lapply(seq_along(vals), function(i) {
+      df[df[[my_payload]] == vals[[i]], ]
+    })
+    names(datasplit) <- as.character(vals)
+
+    # Error
+    if (isTRUE(error)) {
+      # browser() # If you need to check how it works
+      mystery_error_values <- sapply(datasplit, function(x) mean(x$BMRKR1))
+      if (any(mystery_error_values > 6)) {
+        stop(
+          "It should not be more than 6! Should it be? Found in split values: ",
+          names(datasplit)[which(mystery_error_values > 6)]
+        )
+      }
+    }
+
+    # Handy function to return a split result!!
+    make_split_result(vals, datasplit, vals)
+  }
+}
+simple_table(DM, f_brakes_if()) # works!
+
##                  all obs
+## ————————————————————————
+## A: Drug X               
+##   Mean            5.79  
+## B: Placebo              
+##   Mean            6.11  
+## C: Combination          
+##   Mean            5.69
+
+simple_table(DM, f_brakes_if(split_col = "STRATA1")) # works!
+
##          all obs
+## ————————————————
+## A               
+##   Mean    5.95  
+## B               
+##   Mean    5.90  
+## C               
+##   Mean    5.71
+
+# simple_table(DM, f_brakes_if(error = TRUE)) # does not work, but returns an informative message
+
+# Error in do_split(spl, df, spl_context = spl_context) :
+# Error applying custom split function: It should not be more than 6! Should it be? Found in split values: B: Placebo
+# split: VarLevelSplit (ARM)
+# occurred at path: root
+

Now we will take a moment to dwell on the machinery included in +rtables to create custom split functions. Before doing so, +please read the relevant documentation at ?make_split_fun. +Most of the pre-made split functions included in rtables +are or will be written with make_split_fun as it is a more +stable constructor for such functions than was previously used. We +invite the reader to take a look at make_split_fun.R. The +majority of the functions here should be understandable with the +knowledge you have gained from this guide so far. It is important to +note that if no core split function is specified, which is commonly the +case, make_split_fun calls do_base_split +directly, which is a minimal wrapper of the well-known +do_split. drop_facet_levels, for example, is a +pre-processing function that at its core simply removes empty factor +levels from the split “column”, thus avoiding showing empty lines.

+

It is also possible to provide a list of functions, as it can be seen +in the examples of ?make_split_fun. Note that pre- and +post-processing requires a list as input to support the possibility of +combining multiple functions. In contrast, the core splitting function +must be a single function call as it is not expected to have stacked +features. This rarely needs to be modified and the majority of the +included split functions work with pre- or post-processing. Included +post-processing functions are interesting as they interact with the +split object, e.g. by reordering the facets or by adding an overall +facet (add_overall_facet). The attentive reader will have +noticed that the core function relies on do_split and many +of the post-processing functions rely on make_split_result, +which is the best way to get the correct split return structure. Note +that modifying the core split only works in the row space at the +moment.

+
+

+.spl_context - Adding Context to Our Splits +

+

The best way to understand what split context does, and how to use +it, is to read the Leveraging +.spl_context section of the Advanced Usage vignette, +and to use browser() within a split function to see how it +is structured. As .spl_context is needed for rewriting core +functions, we propose a wrapper of do_base_split here, +which is a handy redirection to the standard do_split +without the split function part (i.e. it is a wrapper of +.apply_split_inner, the real core splitting machinery). Out +of curiosity, we set trim = TRUE here. This trimming only +works when there is a mixed table (some values are 0s and some have +content), for which it will trim 0s. This is rarely the case, and we +encourage using the replacement functions +trim_levels_to_group and trim_levels_to_map +for trimming. Nowadays, it should even be impossible to set it +differently from trim = FALSE.

+

(write an issue informative error for not list xxx).

+
+# rtables 0.6.2
+browsing_f <- function(df, spl, .spl_context, ...) {
+  # browser()
+  # do_base_split(df, spl, ...) # order matters!! This would fail if done
+  do_base_split(spl = spl, df = df, vals = NULL, labels = NULL, trim = TRUE)
+}
+
+fnc_tmp <- function(innervar) { # Exploring trim_levels_in_facets (check its form)
+  function(ret, ...) {
+    # browser()
+    for (var in innervar) { # of course AGE is not here, so nothing is dropped!!
+      ret$datasplit <- lapply(ret$datasplit, function(df) {
+        df[[var]] <- factor(df[[var]])
+        df
+      })
+    }
+    ret
+  }
+}
+
+basic_table() %>%
+  split_rows_by("ARM") %>%
+  split_rows_by("STRATA1") %>%
+  split_rows_by_cuts("AGE",
+    cuts = c(0, 50, 100),
+    cutlabels = c("young", "old")
+  ) %>%
+  split_rows_by("SEX", split_fun = make_split_fun(
+    pre = list(drop_facet_levels), # This is dropping the SEX levels (AGE is upper level)
+    core_split = browsing_f,
+    post = list(fnc_tmp("AGE")) # To drop these we should use a split_fun in the above level
+  )) %>%
+  summarize_row_groups() %>%
+  build_table(DM)
+
# The following is the .spl_contest printout:
+Browse[1]> .spl_context
+    split     value full_parent_df all_cols_n      all obs
+1    root      root   c("S1", ....        356 TRUE, TR....
+2     ARM A: Drug X   c("S6", ....        121 TRUE, TR....
+3 STRATA1         A   c("S14",....         36 TRUE, TR....
+4     AGE     young   c("S14",....         36 TRUE, TR....
+
+# NOTE: make_split_fun(pre = list(drop_facet_levels)) and drop_split_levels
+#       do the same thing in this case
+

Here we can see what the split column variable is +(split, first column) at this level of the splitting +procedure. value is the current split value that is being +dealt with. For the next column, let’s see the number of rows of these +data frames: +sapply(.spl_context$full_parent_df, nrow) # [1] 356 121 36 36. +Indeed, the root level contains the full input data frame, +while the other levels are subgroups of the full data according to the +split value. all_cols_n shows exactly the numbers just +described. all obs is the current filter applied to the +columns. Applying this to the root data (or the row subgroup data) +reveals the current column-wise facet (or row-wise for a row split). It +is also possible to use the same information to make complex splits in +the column space by using the full data frame and the value splits to +select the interested values. This is something we will change and +simplify within rtables as the need becomes apparent.

+
+
+
+

Extra Arguments: extra_args +

+

This functionality is well-known and used in the setting of analysis +functions (a somewhat complicated example of this can be found in the Example +Complex Analysis Function vignette), but we will show here how this +can also apply to splits.

+
+# rtables 0.6.2
+
+# Let's use the tracer!!
+my_tracer <- quote(if (length(spl@extra_args) > 0) browser())
+trace(
+  what = "do_split",
+  tracer = my_tracer,
+  where = asNamespace("rtables")
+)
+
+custom_mean_var <- function(var) {
+  function(df, labelstr, na.rm = FALSE, ...) {
+    # browser()
+    mean(df[[var]], na.rm = na.rm)
+  }
+}
+
+DM_ageNA <- DM
+DM_ageNA$AGE[1] <- NA
+
+basic_table() %>%
+  split_rows_by("ARM") %>%
+  split_rows_by("SEX", split_fun = drop_split_levels) %>%
+  summarize_row_groups(
+    cfun = custom_mean_var("AGE"),
+    extra_args = list(na.rm = TRUE), format = "xx.x",
+    label_fstr = "label %s"
+  ) %>%
+  # content_extra_args, c_extra_args are different slots!! (xxx)
+  split_rows_by("STRATA1", split_fun = keep_split_levels("A")) %>%
+  analyze("AGE") %>% # check with the extra_args (xxx)
+  build_table(DM_ageNA)
+# You can pass extra_args down to other splits. It is possible this will not not
+#   work. Should it? That is why extra_args lives only in splits (xxx) check if it works
+#   as is. Difficult to find an use case for this. Maybe it could work for the ref_group
+#   info. That does not work with nesting already (fairly sure that it will break stuff).
+#   Does it make sense to have more than one ref_group at any point of the analysis? No docs,
+#   send a warning if users try to nest things with ref_group (that is passed around via
+#   extra_args)
+
+# As we can see that was not possible. What if we now force it a bit?
+my_split_fun <- function(df, spl, .spl_context, ...) {
+  spl@extra_args <- list(na.rm = TRUE)
+  # does not work because do_split is not changing the object
+  # the split does not do anything with it
+  drop_split_levels(df, spl)
+} # does not work
+
+basic_table() %>%
+  split_rows_by("ARM") %>%
+  split_rows_by("SEX", split_fun = my_split_fun) %>%
+  analyze("AGE", inclNAs = TRUE, afun = mean) %>% # include_NAs is set FALSE
+  build_table(DM_ageNA)
+# extra_args is in available in cols but not in rows, because different columns
+#  may need it for different col space. Row-wise it seems not necessary.
+#  The only thing that works is adding it to analyze (xxx) check if it is worth adding
+
+# We invite the developer now to test all the test files of this package with the tracer on
+# therefore -> extra_args is not currently used in splits (xxx could be wrong)
+# could be not being hooked up
+untrace(what = "do_split", where = asNamespace("rtables"))
+
+# Let's try with the other variables identically
+my_tracer <- quote(if (!is.null(vals) || !is.null(labels) || isTRUE(trim)) {
+  print("A LOT TO SAY")
+  message("CANT BLOCK US ALL")
+  stop("NOW FOR SURE")
+  browser()
+})
+trace(
+  what = "do_split",
+  tracer = my_tracer,
+  where = asNamespace("rtables")
+)
+# Run tests by copying the above in setup-fakedata.R (then devtools::test())
+untrace(
+  what = "do_split",
+  where = asNamespace("rtables")
+)
+

As we have demonstrated, all of the above seem like impossible cases +and are to be considered as vestigial and to be deprecated.

+
+
+
+

+MultiVarSplit & CompoundSplit +Examples +

+

The final part of this article is still under construction, hence the +non-specific mentions and the to do list. xxx CompoundSplit +generates facets from one variable (e.g. cumulative distributions) while +MultiVarSplit uses different variables for the split. See +AnalyzeMultiVars, which inherits from +CompoundSplit for more details on how it analyzes the same +facets multiple times. MultiVarColSplit works with +analyze_colvars, which is out of the scope of this article. +.set_kids_sect_sep adds things between children (can be set +from split).

+

First, we want to see how the MultiVarSplit class +behaves for an example case taken from +?split_rows_by_multivar.

+
+# rtables 0.6.2
+
+my_tracer <- quote(if (is(spl, "MultiVarSplit")) browser())
+trace(
+  what = "do_split",
+  tracer = my_tracer,
+  where = asNamespace("rtables")
+)
+# We want also to take a look at the following:
+debugonce(rtables:::.apply_split_inner)
+lyt <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  split_rows_by_multivar(c("BMRKR1", "BMRKR1"),
+    varlabels = c("SD", "MEAN")
+  ) %>%
+  split_rows_by("COUNTRY",
+    split_fun = keep_split_levels("PAK")
+  ) %>% # xxx for #690 #691
+  summarize_row_groups() %>%
+  analyze(c("AGE", "SEX"))
+
+build_table(lyt, DM)
+
+# xxx check empty space on top -> check if it is a bug, file it
+untrace(
+  what = "do_split",
+  where = asNamespace("rtables")
+)
+

If we print the output, we will notice that the two groups (one +called “SEX” and the other “STRATA1”) are identical along the columns. +This is because no subgroup was actually created. This is an interesting +way to personalize splits with the help of custom split functions and +their split context, and to have widely different subgroups in the +table.

+

We invite the reader to try to understand why +split_rows_by_multivar can have other row splits under it +(see xxx comment in the previous code), while +split_cols_by_multivar does not. This is a known bug at the +moment, and we will work towards a fix for this. Known issues are often +linked in the source code by their GitHub issue number +(e.g. #690).

+

Lastly, we will briefly show an example of a split by cut function +and how to replace it to solve the empty age groups problem as we did +before. We propose the same simplified situation:

+
+# rtables 0.6.2
+
+cutfun <- function(x) {
+  # browser()
+  cutpoints <- c(0, 50, 100)
+  names(cutpoints) <- c("", "Younger", "Older")
+  cutpoints
+}
+
+tbl <- basic_table(show_colcounts = TRUE) %>%
+  split_rows_by("ARM", split_fun = drop_and_remove_levels(c("B: Placebo", "C: Combination"))) %>%
+  split_rows_by("STRATA1") %>%
+  split_rows_by_cutfun("AGE", cutfun = cutfun) %>%
+  # split_rows_by_cuts("AGE", cuts = c(0, 50, 100),
+  #                    cutlabels = c("young", "old")) %>% # Works the same
+  split_rows_by("SEX", split_fun = drop_split_levels) %>%
+  summarize_row_groups() %>% # This is degenerate!!!
+  build_table(DM)
+
+tbl
+
##                  all obs 
+##                  (N=356) 
+## —————————————————————————
+## A: Drug X                
+##   A                      
+##     AGE                  
+##       Younger            
+##         F       22 (6.2%)
+##         M       14 (3.9%)
+##       Older              
+##   B                      
+##     AGE                  
+##       Younger            
+##         F       26 (7.3%)
+##         M       14 (3.9%)
+##       Older              
+##         F       1 (0.3%) 
+##   C                      
+##     AGE                  
+##       Younger            
+##         F       19 (5.3%)
+##         M       21 (5.9%)
+##       Older              
+##         F       2 (0.6%) 
+##         M       2 (0.6%)
+

For both row split cases (*_cuts and +*_cutfun), we have empty levels that are not dropped. This +is to be expected and can be avoided by using a dedicated split +function. Intentionally looking at the future split is possible in order +to determine if an element is present in it. At the moment it is not +possible to add spl_fun to dedicated split functions like +split_rows_by_cuts.

+

Note that in the previous table we only used +summarize_row_groups, with no analyze calls. +This rendered the table nicely, but it is not the standard method to use +as summarize_row_groups is intended only to +decorate row groups, i.e. rows with labels. Internally, these rows are +called content rows and that is why analysis functions in +summarize_row_groups are called cfun instead +of afun. Indeed, the tabulation machinery also presents +these two differently as is described in the Tabulation +with Row Structure section of the Tabulation vignette.

+

We can try to construct the split function for cuts manually with +make_split_fun:

+
+my_count_afun <- function(x, .N_col, .spl_context, ...) {
+  # browser()
+  out <- list(c(length(x), length(x) / .N_col))
+  names(out) <- .spl_context$value[nrow(.spl_context)] # workaround (xxx #689)
+  in_rows(
+    .list = out,
+    .formats = c("xx (xx.x%)")
+  )
+}
+# ?make_split_fun # To check for docs/examples
+
+# Core split
+cuts_core <- function(spl, df, vals, labels, .spl_context) {
+  # browser() # file an issue xxx
+  # variables that are split on are converted to factor during the original clean-up
+  # cut split are not doing it but it is an exception. xxx
+  # young_v <- as.numeric(df[["AGE"]]) < 50
+  # current solution:
+  young_v <- as.numeric(as.character(df[["AGE"]])) < 50
+  make_split_result(c("young", "old"),
+    datasplit = list(df[young_v, ], df[!young_v, ]),
+    labels = c("Younger", "Older")
+  )
+}
+drop_empties <- function(splret, spl, fulldf, ...) {
+  # browser()
+  nrows_data_split <- vapply(splret$datasplit, nrow, numeric(1))
+  to_keep <- nrows_data_split > 0
+  make_split_result(
+    splret$values[to_keep],
+    splret$datasplit[to_keep],
+    splret$labels[to_keep]
+  )
+}
+gen_split <- make_split_fun(
+  core_split = cuts_core,
+  post = list(drop_empties)
+)
+
+tbl <- basic_table(show_colcounts = TRUE) %>%
+  split_rows_by("ARM", split_fun = keep_split_levels(c("A: Drug X"))) %>%
+  split_rows_by("STRATA1") %>%
+  split_rows_by("AGE", split_fun = gen_split) %>%
+  analyze("SEX") %>% # It is the last step!! No need of BMRKR1 right?
+  # split_rows_by("SEX", split_fun = drop_split_levels,
+  #               child_labels = "hidden") %>% # close issue #689. would it work for
+  # analyze_colvars? probably (xxx)
+  # analyze("BMRKR1", afun = my_count_afun) %>%  # This is NOT degenerate!!! BMRKR1 is only placeholder
+  build_table(DM)
+
+tbl
+

Alternatively, we could choose to prune these rows out with +prune_table!

+
+# rtables 0.6.2
+
+tbl <- basic_table(show_colcounts = TRUE) %>%
+  split_rows_by("ARM", split_fun = keep_split_levels(c("A: Drug X"))) %>%
+  split_rows_by("STRATA1") %>%
+  split_rows_by_cuts(
+    "AGE",
+    cuts = c(0, 50, 100),
+    cutlabels = c("young", "old")
+  ) %>%
+  split_rows_by("SEX", split_fun = drop_split_levels) %>%
+  summarize_row_groups() %>% # This is degenerate!!! # we keep it until #689
+  build_table(DM)
+
+tbl
+
##              all obs 
+##              (N=356) 
+## —————————————————————
+## A: Drug X            
+##   A                  
+##     young            
+##       F     22 (6.2%)
+##       M     14 (3.9%)
+##     old              
+##   B                  
+##     young            
+##       F     26 (7.3%)
+##       M     14 (3.9%)
+##     old              
+##       F     1 (0.3%) 
+##   C                  
+##     young            
+##       F     19 (5.3%)
+##       M     21 (5.9%)
+##     old              
+##       F     2 (0.6%) 
+##       M     2 (0.6%)
+
+# Trying with pruning
+prune_table(tbl) # (xxx) what is going on here? it is degenerate so it has no real leaves
+
## NULL
+
+# It is degenerate -> what to do?
+# The same mechanism is applied in the case of NULL leaves, they are rolled up in the
+#  table tree
+
    +
  1. add the pre-processing with z-scoring
  2. +
+
+
+
+ + + + +
+ + + + + + + diff --git a/v0.6.9/articles/dev-guide/dg_table_hierarchy.html b/v0.6.9/articles/dev-guide/dg_table_hierarchy.html new file mode 100644 index 000000000..cadccccb7 --- /dev/null +++ b/v0.6.9/articles/dev-guide/dg_table_hierarchy.html @@ -0,0 +1,318 @@ + + + + + + + + +Table Hierarchy • rtables + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

Disclaimer +

+

This article is intended for use by developers only and will contain +low-level explanations of the topics covered. For user-friendly +vignettes, please see the Articles +page on the rtables website.

+

Any code or prose which appears in the version of this article on the +main branch of the repository may reflect a specific state +of things that can be more or less recent. This guide describes very +important aspects of table hierarchy that are unlikely to change. +Regardless, we invite the reader to keep in mind that the current +repository code may have drifted from the following material in this +document, and it is always the best practice to read the code directly +on main.

+

Please keep in mind that rtables is still under active +development, and it has seen the efforts of multiple contributors across +different years. Therefore, there may be legacy mechanisms and ongoing +transformations that could look different in the future.

+
+
+

Introduction +

+

The scope of this vignette is to understand the structure of +rtable objects, class hierarchy with an exploration of tree +structures as S4 objects. Exploring table structure enables a better +understanding of rtables concepts such as split machinery, +tabulation, pagination and export. More details from the user’s +perspective of table structure can be found in the relevant +vignettes.

+

isS4 getclass - for class structure

+
+
+

Process and Methods +

+

We invite developers to use the provided examples to interactively +explore the rtables hierarchy. The most helpful command is +getClass for a list of the slots associated with a class, +in addition to related classes and their relative distances.

+
+
+

Representation of Information before generation +

+
+
+

Table Representation +

+

PredataAxisLayout class is used to define the data +subset instructions for tabulation. 2 sub-classes (one for each axis): +PredataColLayout, PredataRowLayout

+
+
+

Slots, Parent-Child Relationships +

+
+
+

Content (summary row groups) +

+

Splits are core functionality for rtables as tabulation +and calculations are often required on subsets of the data.

+
+
+

Split Machinery +

+
+library(rtables)
+getClass("TreePos")
+
## Class "TreePos" [package "rtables"]
+## 
+## Slots:
+##                                                       
+## Name:       splits    s_values sval_labels      subset
+## Class:        list        list   character   SubsetDef
+

TreePos class contains split information as a list of +the splits, split label values, and the subsets of the data that are +generated by the split.

+

AllSplit RootSplit +MultiVarSplit VarStaticCutSplit +CumulativeCutSplit VarDynCutSplit +CompoundSplit VarLevWBaselineSplit

+

The highest level of the table hierarchy belong to +TableTree. The code below identifies the slots associated +with with this class.

+
+getClass("TableTree")
+
## Class "TableTree" [package "rtables"]
+## 
+## Slots:
+##                                                                            
+## Name:                 content      page_title_prefix               children
+## Class:        ElementaryTable              character                   list
+##                                                                            
+## Name:                rowspans               labelrow            page_titles
+## Class:             data.frame               LabelRow              character
+##                                                                            
+## Name:          horizontal_sep     header_section_div   trailing_section_div
+## Class:              character              character              character
+##                                                                            
+## Name:                col_info                 format                 na_str
+## Class: InstantiatedColumnInfo             FormatSpec              character
+##                                                                            
+## Name:         indent_modifier            table_inset                  level
+## Class:                integer                integer                integer
+##                                                                            
+## Name:                    name             main_title              subtitles
+## Class:              character              character              character
+##                                                     
+## Name:             main_footer      provenance_footer
+## Class:              character              character
+## 
+## Extends: 
+## Class "VTableTree", directly
+## Class "VTableNodeInfo", by class "VTableTree", distance 2
+## Class "VTree", by class "VTableTree", distance 2
+## Class "VTitleFooter", by class "VTableTree", distance 2
+## Class "VNodeInfo", by class "VTableTree", distance 3
+

As an S4 object, the slots can be accessed using @ +(similar to the use of $ for list objects). You’ll notice +there are classes that fall under “Extends”. The classes contained here +have a relationship to the TableTree object and are +“virtual” classes. To avoid the repetition of slots and carrying the +same data (set of slots for example) that multiple classes may need, +rtables extensively uses virtual classes. A virtual class +cannot be instantiated, the purpose is for other classes to inherit +information from it.

+
+lyt <- basic_table(title = "big title") %>%
+  split_rows_by("SEX", page_by = TRUE) %>%
+  analyze("AGE")
+
+tt <- build_table(lyt, DM)
+
+# Though we don't recommend using str for studying rtable objects,
+# we do find it useful in this instance to visualize the parent/child relationships.
+str(tt, max.level = 2)
+
## Formal class 'TableTree' [package "rtables"] with 20 slots
+##   ..@ content             :Formal class 'ElementaryTable' [package "rtables"] with 19 slots
+##   ..@ page_title_prefix   : chr "SEX"
+##   ..@ children            :List of 4
+##   ..@ rowspans            :'data.frame': 0 obs. of  0 variables
+##   ..@ labelrow            :Formal class 'LabelRow' [package "rtables"] with 13 slots
+##   ..@ page_titles         : chr(0) 
+##   ..@ horizontal_sep      : chr "—"
+##   ..@ header_section_div  : chr NA
+##   ..@ trailing_section_div: chr NA
+##   ..@ col_info            :Formal class 'InstantiatedColumnInfo' [package "rtables"] with 9 slots
+##   ..@ format              : NULL
+##   ..@ na_str              : chr NA
+##   ..@ indent_modifier     : int 0
+##   ..@ table_inset         : int 0
+##   ..@ level               : int 1
+##   ..@ name                : chr "SEX"
+##   ..@ main_title          : chr "big title"
+##   ..@ subtitles           : chr(0) 
+##   ..@ main_footer         : chr(0) 
+##   ..@ provenance_footer   : chr(0)
+
## Warning: str provides a low level, implementation-detail-specific description
+## of the TableTree object structure. See table_structure(.) for a summary of
+## table struture intended for end users.
+
+
+

Tree Paths +

+

Root to Leaves, are vectors of vectors Tables are tree, nodes in the +tree can have summaries associated with them. Tables are trees because +of the nested structure. There is also the benefit of keeping and +repeating necessary information when trying to paginate a table.

+

Children of ElementaryTables are row objects. +TableTree can have children that are either row objects or +other table objects.

+
+

TODO: +

+

Create Tree Diagram showing class hierarchy.

+
+
+
+
+ + + + +
+ + + + + + + diff --git a/v0.6.9/articles/dev-guide/dg_tabulation.html b/v0.6.9/articles/dev-guide/dg_tabulation.html new file mode 100644 index 000000000..94ae888a1 --- /dev/null +++ b/v0.6.9/articles/dev-guide/dg_tabulation.html @@ -0,0 +1,438 @@ + + + + + + + + +Tabulation • rtables + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

Disclaimer +

+

This article is intended for use by developers only and will contain +low-level explanations of the topics covered. For user-friendly +vignettes, please see the Articles +page on the rtables website.

+

Any code or prose which appears in the version of this article on the +main branch of the repository may reflect a specific state +of things that can be more or less recent. This guide describes very +important aspects of the tabulation process that are unlikely to change. +Regardless, we invite the reader to keep in mind that the current +repository code may have drifted from the following material in this +document, and it is always the best practice to read the code directly +on main.

+

Please keep in mind that rtables is still under active +development, and it has seen the efforts of multiple contributors across +different years. Therefore, there may be legacy mechanisms and ongoing +transformations that could look different in the future.

+

Being that this a working document that may be subjected to both +deprecation and updates, we keep xxx comments to indicate +placeholders for warnings and to-do’s that need further work.

+
+
+

Introduction +

+

Tabulation in rtables is a process that takes a +pre-defined layout and applies it to data. The layout object, with all +of its splits and analyzes, can be applied to different +data to produce valid tables. This process happens principally within +the tt_dotabulation.R file and the user-facing function +build_table that resides in it. We will occasionally use +functions and methods that are present in other files, like +colby_construction.R or make_subset_expr.R. We +assume the reader is already familiar with the documentation for +build_table. We suggest reading the Split +Machinery article prior to this one, as it is instrumental in +understanding how the layout object, which is essentially built out of +splits, is tabulated when data is supplied.

+
+
+

Tabulation +

+

We enter into build_table using debugonce +to see how it works.

+
+# rtables 0.6.2
+library(rtables)
+debugonce(build_table)
+
+# A very simple layout
+lyt <- basic_table() %>%
+  split_rows_by("STRATA1") %>%
+  split_rows_by("SEX", split_fun = drop_split_levels) %>%
+  split_cols_by("ARM") %>%
+  analyze("BMRKR1")
+
+# lyt must be a PreDataTableLayouts object
+is(lyt, "PreDataTableLayouts")
+
+lyt %>% build_table(DM)
+

Now let’s look within our build_table call. After the +initial check that the layout is a pre-data table layout, it checks if +the column layout is defined (clayout accessor), i.e. it +does not have any column split. If that is the case, a +All obs column is added automatically with all +observations. After this, there are a couple of defensive programming +calls that do checks and transformations as we finally have the data. +These can be divided into two categories: those that mainly concern the +layout, which are defined as generics, and those that concern the data, +which are instead a function as they are not dependent on the layout +class. Indeed, the layout is structured and can be divided into +clayout and rlayout (column and row layout). +The first one is used to create cinfo, which is the general +object and container of the column splits and information. The second +one contains the obligatory all data split, i.e. the root split +(accessible with root_spl), and the row splits’ vectors +which are iterative splits in the row space. In the following, we +consider the initial checks and defensive programming.

+
+## do checks and defensive programming now that we have the data
+lyt <- fix_dyncuts(lyt, df) # Create the splits that depends on data
+lyt <- set_def_child_ord(lyt, df) # With the data I set the same order for all splits
+lyt <- fix_analyze_vis(lyt) # Checks if the analyze last split should be visible
+# If there is only one you will not get the variable name, otherwise you get it if you
+# have multivar. Default is NA. You can do it now only because you are sure to
+# have the whole layout.
+df <- fix_split_vars(lyt, df, char_ok = is.null(col_counts))
+# checks if split vars are present
+
+lyt[] # preserve names - warning if names longer, repeats the name value if only one
+lyt@.Data # might not preserve the names # it works only when it is another class that inherits from lists
+# We suggest doing extensive testing about these behaviors in order to do choose the appropriate one
+

Along with the various checks and defensive programming, we find +PreDataAxisLayout which is a virtual class that both row +and column layouts inherit from. Virtual classes are handy for group +classes that need to share things like labels or functions that need to +be applicable to their relative classes. See more information about the +rtables class hierarchy in the dedicated article here.

+

Now, we continue with build_table. After the checks, we +notice TreePos() which is a constructor for an object that +retains a representation of the tree position along with split values +and labels. This is mainly used by create_colinfo, which we +enter now with debugonce(create_colinfo). This function +creates the object that represents the column splits and everything else +that may be related to the columns. In particular, the column counts are +calculated in this function. The parameter inputs are as follows:

+
+cinfo <- create_colinfo(
+  lyt, # Main layout with col split info
+  df, # df used for splits and col counts if no alt_counts_df is present
+  rtpos, # TreePos (does not change out of this function)
+  counts = col_counts, # If we want to overwrite the calculations with df/alt_counts_df
+  alt_counts_df = alt_counts_df, # alternative data for col counts
+  total = col_total, # calculated from build_table inputs (nrow of df or alt_counts_df)
+  topleft # topleft information added into build_table
+)
+

create_colinfo is in make_subset_expr.R. +Here, we see that if topleft is present in +build_table, it will override the one in lyt. +Entering create_colinfo, we will see the following +calls:

+
+clayout <- clayout(lyt) # Extracts column split and info
+
+if (is.null(topleft)) {
+  topleft <- top_left(lyt) # If top_left is not present in build_table, it is taken from lyt
+}
+
+ctree <- coltree(clayout, df = df, rtpos = rtpos) # Main constructor of LayoutColTree
+# The above is referenced as generic and principally represented as
+# setMethod("coltree", "PreDataColLayout", (located in `tree_accessor.R`).
+# This is a call that restructures information from clayout, df, and rtpos
+# to get a more compact column tree layout. Part of this design is related
+# to past implementations.
+
+cexprs <- make_col_subsets(ctree, df) # extracts expressions in a compact fashion.
+# WARNING: removing NAs at this step is automatic. This should
+# be coupled with a warning for NAs in the split (xxx)
+
+colextras <- col_extra_args(ctree) # retrieves extra_args from the tree. It may not be used
+

Next in the function is the determination of the column counts. +Currently, this happens only at the leaf level, but it can certainly be +calculated independently for all levels (this is an open issue in +rtables, i.e. how to print other levels’ totals). +Precedence for column counts may be not documented (“xxx todo”). The +main use case is when you are analyzing a participation-level dataset, +with multiple records per subject, and you would like to retain the +total numbers of subjects per column, often taken from a subject-level +dataset, to use as column counts. Originally, counts were only able to +be added as a vector, but it is often the case that users would like the +possibility to use alt_counts_df. The cinfo +object (InstantiatedColumnInfo) is created with all the +above information.

+

If we continue inside build_table, we see +.make_ctab used to make a root split. This is a general +procedure that generates the initial root split as a content row. +ctab is applied to this content row, which is a row that +contains only a label. From ?summarize_row_groups, you know +that this is how rtables defines label rows, i.e. as +content rows. .make_ctab is very similar to the function +that actual creates the table rows, .make_tablerows. Note +that this function uses parent_cfun and +.make_caller to retrieve the content function inserted in +above levels. Here we split the structural handling of the table object +and the row-creation engine, which are divided by a +.make_tablerows call. If you search the package, you will +find that this function is only called twice, once in +.make_ctab and once in .make_analyzed_tab. +These two are the final elements of the table construction: the creation +of rows.

+

Going back to build_table, you will see that the row +layout is actually a list of split vectors. The fundamental line, +kids <- lapply(seq_along(rlyt), function(i) {, allows us +to appreciate this. Going forward we see how +recursive_applysplit is applied to each split vector. It +may be worthwhile to check what this vector looks like in our test +case.

+
+# rtables 0.6.2
+# A very simple layout
+lyt <- basic_table() %>%
+  split_rows_by("STRATA1") %>%
+  split_rows_by("SEX", split_fun = drop_split_levels) %>%
+  split_cols_by("ARM") %>%
+  analyze("BMRKR1")
+
+rlyt <- rtables:::rlayout(lyt)
+str(rlyt, max.level = 2)
+
Formal class 'PreDataRowLayout' [package "rtables"] with 2 slots
+  ..@ .Data     :List of 2 # rlyt is a rtables object (PreDataRowLayout) that is also a list!
+  ..@ root_split:Formal class 'RootSplit' [package "rtables"] with 17 slots # another object!
+  # If you do summarize_row_groups before anything you act on the root split. We need this to
+  # have a place for the content that is valid for the whole table.
+
+str(rtables:::root_spl(rlyt), max.level = 2) # it is still a split
+
+str(rlyt[[1]], max.level = 3) # still a rtables object (SplitVector) that is a list
+Formal class 'SplitVector' [package "rtables"] with 1 slot
+  ..@ .Data:List of 3
+  .. ..$ :Formal class 'VarLevelSplit' [package "rtables"] with 20 slots
+  .. ..$ :Formal class 'VarLevelSplit' [package "rtables"] with 20 slots
+  .. ..$ :Formal class 'AnalyzeMultiVars' [package "rtables"] with 17 slots
+

The last print is very informative. We can see from the layout +construction that this object is built with 2 +VarLevelSplits on the rows and one final +AnalyzeMultiVars, which is the leaf analysis split that has +the final level rows. The second split vector is the following +AnalyzeVarSplit.

+

xxx To get multiple split vectors, you need to escape the nesting +with nest = FALSE or by adding a split_rows_by +call after an analyze call.

+
# rtables 0.6.2
+str(rlyt[[2]], max.level = 5)
+Formal class 'SplitVector' [package "rtables"] with 1 slot
+  ..@ .Data:List of 1
+  .. ..$ :Formal class 'AnalyzeVarSplit' [package "rtables"] with 21 slots
+  .. .. .. ..@ analysis_fun           :function (x, ...)
+  .. .. .. .. ..- attr(*, "srcref")= 'srcref' int [1:8] 1723 5 1732 5 5 5 4198 4207
+  .. .. .. .. .. ..- attr(*, "srcfile")=Classes 'srcfilealias', 'srcfile' <environment: 0x560d8e67b750>
+  .. .. .. ..@ default_rowlabel       : chr "Var3 Counts"
+  .. .. .. ..@ include_NAs            : logi FALSE
+  .. .. .. ..@ var_label_position     : chr "default"
+  .. .. .. ..@ payload                : chr "VAR3"
+  .. .. .. ..@ name                   : chr "VAR3"
+  .. .. .. ..@ split_label            : chr "Var3 Counts"
+  .. .. .. ..@ split_format           : NULL
+  .. .. .. ..@ split_na_str           : chr NA
+  .. .. .. ..@ split_label_position   : chr(0)
+  .. .. .. ..@ content_fun            : NULL
+  .. .. .. ..@ content_format         : NULL
+  .. .. .. ..@ content_na_str         : chr(0)
+  .. .. .. ..@ content_var            : chr ""
+  .. .. .. ..@ label_children         : logi FALSE
+  .. .. .. ..@ extra_args             : list()
+  .. .. .. ..@ indent_modifier        : int 0
+  .. .. .. ..@ content_indent_modifier: int 0
+  .. .. .. ..@ content_extra_args     : list()
+  .. .. .. ..@ page_title_prefix      : chr NA
+  .. .. .. ..@ child_section_div      : chr NA
+

Continuing in recursive_applysplit, this is made up of +two main calls: one to .make_ctab which makes the content +row and calculates the counts if specified, and +.make_split_kids. This eventually contains +recursive_applysplit which is applied if the split vector +is built of Splits that are not analyze +splits. It being a generic is very handy here to switch between +different downstream processes. In our case (rlyt[[1]]) we +will call the method getMethod(".make_split_kids", "Split") +twice before getting to the analysis split. There, we have a (xxx) +multi-variable split which applies .make_split_kids to each +of its elements, in turn calling the main +getMethod(".make_split_kids", "VAnalyzeSplit") which would +in turn go to .make_analyzed_tab.

+

There are interesting edge cases here for different split cases, like +split_by_multivars or when one of the splits has a +reference group. In the internal code here, it is called +baseline. If we follow this variable across the function +layers, we will see that where the split (do_split) happens +(in getMethod(".make_split_kids", "Split")) we have a +second split for the reference group. This is done to make this +available in each row to calculate, for example, differences from the +reference group.

+

Now we move towards .make_tablerows, and here analysis +functions become key as this is the place where these are applied and +analyzed. First, the external tryCatch is used to cache +errors at a higher level, so as to differentiate the two major blocks. +The function parameters here are quite intuitive, with the exception of +spl_context. This is a fundamental parameter that keeps +information about splits so that it can be visible from analysis +functions. If you look into this value, you will see that is carried and +updated everywhere a split happens, except for columns. Column-related +information is added last, when in gen_onerv, which is the +lowest level where one result value is produced. From +.make_tablerows we go to gen_rowvalues, aside +from some row and referential footers handling. +gen_rowvalues unpacks the cinfo object and +crosses it with the arriving row split information to generate rows. In +particular, rawvals <- mapply(gen_onerv, maps the +columns to generate a list of values corresponding to a table row. +Looking at the final if in gen_onerv we see +if (!is(val, "RowsVerticalSection")) and the function +in_rows is called. We invite the reader to explore what the +building blocks of in_rows are, and how +.make_tablerows constructs a data row +(DataRow) or a content row (ContentRow) +depending on whether it is called from .make_ctab or +.make_analyzed_tab.

+

.make_tablerows either makes a content table or an +“analysis table”. gen_rowvalues generates a list of stacks +(RowsVerticalSection, more than one rows potentially!) for +each column.

+

To add: conceptual part -> calculating things by column and +putting them side by side and slicing them by rows and putting it +together -> rtables is row dominant.

+
+
+
+ + + + +
+ + + + + + + diff --git a/v0.6.9/articles/example_analysis_coxreg.html b/v0.6.9/articles/example_analysis_coxreg.html new file mode 100644 index 000000000..37d1af6a7 --- /dev/null +++ b/v0.6.9/articles/example_analysis_coxreg.html @@ -0,0 +1,673 @@ + + + + + + + + +Example Complex Analysis Function: Modelling Cox Regression • rtables + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

Introduction +

+

In this vignette we will demonstrate how a complex analysis function +can be constructed in order to build highly-customized tables with +rtables. This example will detail the steps in creating an +analysis function to calculate a basic univariable Cox regression +summary table to analyze the treatment effect of the ARM +variable and any covariate/interaction effects for a survival analysis. +For a Cox regression analysis function with more customization options +and the capability of fitting multivariable Cox regression models, see +the summarize_coxreg() +function from the tern +package, which builds upon the concepts used in the construction of this +example.

+

The packages used in this vignette are:

+ +
+
+

Data Pre-Processing +

+

First, we prepare the data that will be used to generate a table in +this example. We will use the example ADTTE (Time-To-Event +Analysis) dataset ex_adtte from the formatters +package, which contains our treatment variable ARM, several +variables that can be chosen as covariates, and censor variable +CNSR from which we will derive the event variable +EVENT required for our model. For the purpose of this +example, we will use age (AGE) and race (RACE) +as our covariates.

+

We prepare the data as needed to observe the desired effects in our +summary table. PARAMCD is filtered so that only records of +overall survival (OS) are included, and we filter and mutate to include +only the levels of interest in our covariates. The ARM +variable is mutated to indicate that "B: Placebo" should be +used as the reference level of our treatment variable, and the +EVENT variable is derived from CNSR.

+
+adtte <- ex_adtte
+
+anl <- adtte %>%
+  dplyr::filter(PARAMCD == "OS") %>%
+  dplyr::filter(ARM %in% c("A: Drug X", "B: Placebo")) %>%
+  dplyr::filter(RACE %in% c("ASIAN", "BLACK OR AFRICAN AMERICAN", "WHITE")) %>%
+  dplyr::mutate(RACE = droplevels(RACE)) %>%
+  dplyr::mutate(ARM = droplevels(stats::relevel(ARM, "B: Placebo"))) %>%
+  dplyr::mutate(EVENT = 1 - CNSR)
+
+
+

Creating Helper Functions: Cox Regression Model Calculations +

+
+

+tidy Method for summary.coxph Objects: +tidy.summary.coxph +

+

This method allows the tidy function from the +broom package to operate on summary.coxph +output, extracting the values of interest to this analysis and returning +a tidied tibble::tibble() object.

+
+tidy.summary.coxph <- function(x, ...) {
+  is(x, "summary.coxph")
+  pval <- x$coefficients
+  confint <- x$conf.int
+  levels <- rownames(pval)
+  pval <- tibble::as_tibble(pval)
+  confint <- tibble::as_tibble(confint)
+
+  ret <- cbind(pval[, grepl("Pr", names(pval))], confint)
+  ret$level <- levels
+  ret$n <- x[["n"]]
+  ret
+}
+
+
+

Function to Estimate Interaction Effects: +h_coxreg_inter_effect +

+

The h_coxreg_inter_effect helper function is used within +the following helper function, +h_coxreg_extract_interaction, to estimate interaction +effects from a given model for a given covariate. The function +calculates the desired statistics from the given model and returns a +data.frame with label information for each row as well as +the statistics n, hr (hazard ratio), +lcl (CI lower bound), ucl (CI upper bound), +pval (effect p-value), and pval_inter +(interaction p-value). If a numeric covariate is selected, the median +value is used as the sole “level” for which an interaction effect is +calculated. For non-numeric covariates, an interaction effect is +calculated for each level of the covariate, with each result returned on +a separate row.

+
+h_coxreg_inter_effect <- function(x,
+                                  effect,
+                                  covar,
+                                  mod,
+                                  label,
+                                  control,
+                                  data) {
+  if (is.numeric(x)) {
+    betas <- stats::coef(mod)
+    attrs <- attr(stats::terms(mod), "term.labels")
+    term_indices <- grep(pattern = effect, x = attrs[!grepl("strata\\(", attrs)])
+    betas <- betas[term_indices]
+    betas_var <- diag(stats::vcov(mod))[term_indices]
+    betas_cov <- stats::vcov(mod)[term_indices[1], term_indices[2]]
+    xval <- stats::median(x)
+    effect_index <- !grepl(covar, names(betas))
+    coef_hat <- betas[effect_index] + xval * betas[!effect_index]
+    coef_se <- sqrt(betas_var[effect_index] + xval^2 * betas_var[!effect_index] + 2 * xval * betas_cov)
+    q_norm <- stats::qnorm((1 + control$conf_level) / 2)
+  } else {
+    var_lvl <- paste0(effect, levels(data[[effect]])[-1]) # [-1]: reference level
+    giv_lvl <- paste0(covar, levels(data[[covar]]))
+    design_mat <- expand.grid(effect = var_lvl, covar = giv_lvl)
+    design_mat <- design_mat[order(design_mat$effect, design_mat$covar), ]
+    design_mat <- within(data = design_mat, expr = {
+      inter <- paste0(effect, ":", covar)
+      rev_inter <- paste0(covar, ":", effect)
+    })
+    split_by_variable <- design_mat$effect
+    interaction_names <- paste(design_mat$effect, design_mat$covar, sep = "/")
+    mmat <- stats::model.matrix(mod)[1, ]
+    mmat[!mmat == 0] <- 0
+    design_mat <- apply(X = design_mat, MARGIN = 1, FUN = function(x) {
+      mmat[names(mmat) %in% x[-which(names(x) == "covar")]] <- 1
+      mmat
+    })
+    colnames(design_mat) <- interaction_names
+    coef <- stats::coef(mod)
+    vcov <- stats::vcov(mod)
+    betas <- as.matrix(coef)
+    coef_hat <- t(design_mat) %*% betas
+    dimnames(coef_hat)[2] <- "coef"
+    coef_se <- apply(design_mat, 2, function(x) {
+      vcov_el <- as.logical(x)
+      y <- vcov[vcov_el, vcov_el]
+      y <- sum(y)
+      y <- sqrt(y)
+      y
+    })
+    q_norm <- stats::qnorm((1 + control$conf_level) / 2)
+    y <- cbind(coef_hat, `se(coef)` = coef_se)
+    y <- apply(y, 1, function(x) {
+      x["hr"] <- exp(x["coef"])
+      x["lcl"] <- exp(x["coef"] - q_norm * x["se(coef)"])
+      x["ucl"] <- exp(x["coef"] + q_norm * x["se(coef)"])
+      x
+    })
+    y <- t(y)
+    y <- by(y, split_by_variable, identity)
+    y <- lapply(y, as.matrix)
+    attr(y, "details") <- paste0(
+      "Estimations of ", effect, " hazard ratio given the level of ", covar, " compared to ",
+      effect, " level ", levels(data[[effect]])[1], "."
+    )
+    xval <- levels(data[[covar]])
+  }
+  data.frame(
+    effect = "Covariate:",
+    term = rep(covar, length(xval)),
+    term_label = as.character(paste0("  ", xval)),
+    level = as.character(xval),
+    n = NA,
+    hr = if (is.numeric(x)) exp(coef_hat) else y[[1]][, "hr"],
+    lcl = if (is.numeric(x)) exp(coef_hat - q_norm * coef_se) else y[[1]][, "lcl"],
+    ucl = if (is.numeric(x)) exp(coef_hat + q_norm * coef_se) else y[[1]][, "ucl"],
+    pval = NA,
+    pval_inter = NA,
+    stringsAsFactors = FALSE
+  )
+}
+
+
+

Function to Extract Effect Information: +h_coxreg_extract_interaction +

+

Using the previous two helper functions, +h_coxreg_extract_interaction uses ANOVA to extract +information from the given model about the given covariate. This +function will extract different information depending on whether the +effect of interest is a treatment/main effect or an interaction effect, +and returns a data.frame with label information for each +row (corresponding to each effect) as well as the statistics +n, hr, lcl, ucl, +pval, and pval_inter (for interaction effects +only). This helper function is used directly within our analysis +function to analyze the Cox regression model and extract relevant +information to be processed and displayed within our output table.

+
+h_coxreg_extract_interaction <- function(effect, covar, mod, data) {
+  control <- list(pval_method = "wald", ties = "exact", conf_level = 0.95, interaction = FALSE)
+  test_statistic <- c(wald = "Wald", likelihood = "LR")[control$pval_method]
+  mod_aov <- withCallingHandlers(
+    expr = car::Anova(mod, test.statistic = test_statistic, type = "III"),
+    message = function(m) invokeRestart("muffleMessage")
+  )
+  msum <- if (!any(attr(stats::terms(mod), "order") == 2)) summary(mod, conf.int = control$conf_level) else mod_aov
+  sum_anova <- broom::tidy(msum)
+  if (!any(attr(stats::terms(mod), "order") == 2)) {
+    effect_aov <- mod_aov[effect, , drop = TRUE]
+    pval <- effect_aov[[grep(pattern = "Pr", x = names(effect_aov)), drop = TRUE]]
+    sum_main <- sum_anova[grepl(effect, sum_anova$level), ]
+    term_label <- if (effect == covar) {
+      paste0(levels(data[[covar]])[2], " vs control (", levels(data[[covar]])[1], ")")
+    } else {
+      unname(formatters::var_labels(data, fill = TRUE)[[covar]])
+    }
+    y <- data.frame(
+      effect = ifelse(covar == effect, "Treatment:", "Covariate:"),
+      term = covar, term_label = term_label,
+      level = levels(data[[effect]])[2],
+      n = mod[["n"]], hr = unname(sum_main["exp(coef)"]), lcl = unname(sum_main[grep("lower", names(sum_main))]),
+      ucl = unname(sum_main[grep("upper", names(sum_main))]), pval = pval,
+      stringsAsFactors = FALSE
+    )
+    y$pval_inter <- NA
+    y
+  } else {
+    pval <- sum_anova[sum_anova$term == effect, ][["p.value"]]
+
+    ## Test the interaction effect
+    pval_inter <- sum_anova[grep(":", sum_anova$term), ][["p.value"]]
+    covar_test <- data.frame(
+      effect = "Covariate:",
+      term = covar, term_label = unname(formatters::var_labels(data, fill = TRUE)[[covar]]),
+      level = "",
+      n = mod$n, hr = NA, lcl = NA, ucl = NA, pval = pval,
+      pval_inter = pval_inter,
+      stringsAsFactors = FALSE
+    )
+    ## Estimate the interaction
+    y <- h_coxreg_inter_effect(
+      data[[covar]],
+      covar = covar,
+      effect = effect,
+      mod = mod,
+      label = unname(formatters::var_labels(data, fill = TRUE)[[covar]]),
+      control = control,
+      data = data
+    )
+    rbind(covar_test, y)
+  }
+}
+
+
+
+

Creating a Helper Function: cached_model +

+

Next, we will create a helper function, cached_model, +which will be used within our analysis function to cache and return the +fitted Cox regression model for the current covariate. The +df argument will be directly inherited from the +df argument passed to the analysis function, which contains +the full dataset being analyzed. The cov argument will be +the covariate that is being analyzed depending on the current row +context. If the treatment effect is currently being analyzed, this value +will be an empty string. The cache_env parameter will be an +environment object which is used to store the model for the current +covariate, also passed down from the analysis function. Of course, this +function can also be run outside of the analysis function and will still +cache and return a Cox regression model.

+

Using these arguments, the cached_model function first +checks if a model for the given covariate cov is already +stored in the caching environment cache_env. If so, then +this model is retrieved and returned by cached_model. If +not, the model must be constructed. This is done by first constructing +the model formula, model_form, starting with only the +treatment effect (ARM) and adding a covariate effect if one +is currently being analyzed. Then a Cox regression model is fit using +df and the model formula, and this model is both returned +and stored in the caching environment object as +cache_env[[cov]].

+
+cached_model <- function(df, cov, cache_env) {
+  ## Check if a model already exists for
+  ## `cov` in the caching environment
+  if (!is.null(cache_env[[cov]])) {
+    ## If model already exists, retrieve it from cache_env
+    model <- cache_env[[cov]]
+  } else {
+    ## Build model formula
+    model_form <- paste0("survival::Surv(AVAL, EVENT) ~ ARM")
+    if (length(cov) > 0) {
+      model_form <- paste(c(model_form, cov), collapse = " * ")
+    } else {
+      cov <- "ARM"
+    }
+    ## Calculate Cox regression model
+    model <- survival::coxph(
+      formula = stats::as.formula(model_form),
+      data = df,
+      ties = "exact"
+    )
+    ## Store model in the caching environment
+    cache_env[[cov]] <- model
+  }
+  model
+}
+
+
+

Creating the Analysis Function: a_cox_summary +

+

With our data prepared and helper function created, we can proceed to +construct our analysis function a_cox_summary, which will +be used to populate all of the rows in our table. In order to be used to +generate both data rows (for interaction effects) and content rows (for +main effects), we must create a function that can be used as both +afun in analyze and cfun in +summarize_row_groups. Therefore, our function must accept +the labelstr parameter.

+

The arguments of our analysis function will be as follows:

+
    +
  • +df - a data.frame of the full dataset +required to fit the Cox regression model.
  • +
  • +labelstr - the string label for the +variable being analyzed in the current row/column split context.
  • +
  • +.spl_context - a data.frame containing the +value column which is used by this analysis function to +determine the name of the variable/covariate in the current split. For +more details on the information stored by .spl_context see +?analyze.
  • +
  • +stat and format - strings +that indicate which statistic column we are currently in and what format +should be applied to print the statistic.
  • +
  • +cache_env - an environment object that can +be used to store cached models so that we can prevent repeatedly fitting +the same model. Instead, each model will be generated once per covariate +and then reused. This argument will be passed directly to the +cached_model helper function we defined previously.
  • +
  • +cov_main - a logical value indicating +whether or not the current row is summarizing covariate main +effects.
  • +
+

The analysis function works within a given row/column split context +by using the current covariate (cov) and the +cached_model function to obtain the desired Cox regression +model. From this model, the h_coxreg_extract_interaction +function is able to extract information/statistics relevant to the +analysis and store it in a data.frame. The rows in this +data.frame that are of interest in the current row/column +split context are then extracted and the statistic to be printed in the +current column is retrieved from these rows. Finally, the formatted +cells with this statistic are returned as a +VerticalRowsSection object. For more detail see the +commented function code below, where the purpose of each line within +a_cox_summary is described.

+
+a_cox_summary <- function(df,
+                          labelstr = "",
+                          .spl_context,
+                          stat,
+                          format,
+                          cache_env,
+                          cov_main = FALSE) {
+  ## Get current covariate (variable used in latest row split)
+  cov <- tail(.spl_context$value, 1)
+
+  ## If currently analyzing treatment effect (ARM) replace empty
+  ## value of cov with "ARM" so the correct model row is analyzed
+  if (length(cov) == 0) cov <- "ARM"
+
+  ## Use cached_model to get the fitted Cox regression
+  ## model for the current covariate
+  model <- cached_model(df = df, cov = cov, cache_env = cache_env)
+
+  ## Extract levels of cov to be used as row labels for interaction effects.
+  ## If cov is numeric, the median value of cov is used as a row label instead
+  cov_lvls <- if (is.factor(df[[cov]])) levels(df[[cov]]) else as.character(median(df[[cov]]))
+
+  ## Use function to calculate and extract information relevant to cov from the model
+  cov_rows <- h_coxreg_extract_interaction(effect = "ARM", covar = cov, mod = model, data = df)
+  ## Effect p-value is only printed for treatment effect row
+  if (!cov == "ARM") cov_rows[, "pval"] <- NA_real_
+  ## Extract rows containing statistics for cov from model information
+  if (!cov_main) {
+    ## Extract rows for main effect
+    cov_rows <- cov_rows[cov_rows$level %in% cov_lvls, ]
+  } else {
+    ## Extract all non-main effect rows
+    cov_rows <- cov_rows[nchar(cov_rows$level) == 0, ]
+  }
+  ## Extract value(s) of statistic for current column and variable/levels
+  stat_vals <- as.list(apply(cov_rows[stat], 1, function(x) x, simplify = FALSE))
+  ## Assign labels: covariate name for main effect (content) rows, ARM comparison description
+  ## for treatment effect (content) row, cov_lvls for interaction effect (data) rows
+  nms <- if (cov_main) labelstr else if (cov == "ARM") cov_rows$term_label else cov_lvls
+  ## Return formatted/labelled row
+  in_rows(
+    .list = stat_vals,
+    .names = nms,
+    .labels = nms,
+    .formats = setNames(rep(format, length(nms)), nms),
+    .format_na_strs = setNames(rep("", length(nms)), nms)
+  )
+}
+
+
+

Selecting Parameters +

+

We are able to customize our Cox regression summary using this +analysis function by selecting covariates (and their labels), statistics +(and their labels), and statistic formats to use when generating the +output table. We also initialize a new environment object to be used by +the analysis function as the caching environment to store our models in. +For the purpose of this example, we will choose all 5 of the possible +statistics to include in the table: n, hazard ratio, confidence +interval, effect p-value, and interaction p-value.

+
+my_covs <- c("AGE", "RACE") ## Covariates
+my_cov_labs <- c("Age", "Race") ## Covariate labels
+my_stats <- list("n", "hr", c("lcl", "ucl"), "pval", "pval_inter") ## Statistics
+my_stat_labs <- c("n", "Hazard Ratio", "95% CI", "p-value\n(effect)", "p-value\n(interaction)") ## Statistic labels
+my_formats <- c(
+  n = "xx", hr = "xx.xx", lcl = "(xx.xx, xx.xx)", pval = "xx.xxxx", pval_inter = "xx.xxxx" ## Statistic formats
+)
+my_env <- new.env()
+ny_cache_env <- replicate(length(my_stats), list(my_env)) ## Caching environment
+
+
+

Constructing the Table +

+

Finally, the table layout can be constructed and used to build the +desired table.

+

We first split our basic_table using +split_cols_by_multivar to ensure that each statistic exists +in its own column. To do so, we choose a variable (in this case +STUDYID) which shares the same value in every row, and use +it as the split variable for every column so that the full dataset is +used to compute the model for every column. We use the +extra_args argument for which each list element’s element +positions correspond to the children of (columns generated by) this +split. These arguments are inherited by all following layout elements +operating within this split, which use these elements as argument +inputs. To elaborate on this, we have three elements in +extra_args: stat, format, and +cache_env - each of which are arguments of +a_cox_summary and have length equal to the number of +columns (as defined above). For each use of our analysis function +following this column split, depending on the current column context, +the corresponding element of each of these three list elements will be +inherited from extra_args and used as input. For example, +if analyze_colvars is called with +a_cox_summary as afun and is performing +calculations for column 1, my_stats[1] ("n") +will be given as argument stat, my_formats[1] +("xx") as argument format, and +my_cache_env[1] (my_env) as +cache_env. This is useful for our table since we want each +column to print out values for a different statistic and apply its +corresponding format.

+

Next, we can use summarize_row_groups to generate the +content row for treatment effect. This is the first instance where +extra_args from the column split will be inherited and used +as argument input in cfun.

+

After generating the treatment effect row, we want to add rows for +covariates. We use split_rows_by_multivar to split rows by +covariate and apply appropriate labels.

+

Following this row split, we use summarize_row_groups +with a_cox_summary as cfun to generate one +content row for each covariate main effect. Once again the contents of +extra_args from the column split are inherited as input. +Here we specify cov_main = TRUE in the +extra_args argument so that main effects rather than +interactions are considered. Since this is not a split, this instance of +extra_args is not inherited by any following layout +elements. As cov_main is a singular value, +cov_main = TRUE will be used within every column +context.

+

The last part of our table is the covariate interaction effects. We +use analyze_colvars with a_cox_summary as +afun, and again inherit extra_args from the +column split. Using an rtables “analyze” function generates +data rows, with one row corresponding to each covariate level (or median +value, for numeric covariates), nested under the content row (main +effect) for that same covariate.

+
+lyt <- basic_table() %>%
+  ## Column split: one column for each statistic
+  split_cols_by_multivar(
+    vars = rep("STUDYID", length(my_stats)),
+    varlabels = my_stat_labs,
+    extra_args = list(
+      stat = my_stats,
+      format = my_formats,
+      cache_env = ny_cache_env
+    )
+  ) %>%
+  ## Create content row for treatment effect
+  summarize_row_groups(cfun = a_cox_summary) %>%
+  ## Row split: one content row for each covariate
+  split_rows_by_multivar(
+    vars = my_covs,
+    varlabels = my_cov_labs,
+    split_label = "Covariate:",
+    indent_mod = -1 ## Align split label left
+  ) %>%
+  ## Create content rows for covariate main effects
+  summarize_row_groups(
+    cfun = a_cox_summary,
+    extra_args = list(cov_main = TRUE)
+  ) %>%
+  ## Create data rows for covariate interaction effects
+  analyze_colvars(afun = a_cox_summary)
+

Using our pre-processed anl dataset, we can now build +and output our final Cox regression summary table.

+
+cox_tbl <- build_table(lyt, anl)
+cox_tbl
+
##                                                                         p-value       p-value   
+##                                      n    Hazard Ratio      95% CI      (effect)   (interaction)
+## ————————————————————————————————————————————————————————————————————————————————————————————————
+## A: Drug X vs control (B: Placebo)   247       0.97       (0.71, 1.32)    0.8243                 
+## Covariate:                                                                                      
+##   Age                               247                                               0.7832    
+##     34                                        0.92       (0.68, 1.26)                           
+##   Race                              247                                               0.7441    
+##     ASIAN                                     1.03       (0.68, 1.57)                           
+##     BLACK OR AFRICAN AMERICAN                 0.78       (0.41, 1.49)                           
+##     WHITE                                     1.06       (0.55, 2.04)
+
+
+
+ + + + +
+ + + + + + + diff --git a/v0.6.9/articles/exploratory_analysis.html b/v0.6.9/articles/exploratory_analysis.html new file mode 100644 index 000000000..af1360d53 --- /dev/null +++ b/v0.6.9/articles/exploratory_analysis.html @@ -0,0 +1,595 @@ + + + + + + + + +Exploratory Analysis • rtables + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + + +
+

Introduction +

+

In this vignette, we would like to introduce how +qtable() can be used to easily create cross tabulations for +exploratory data analysis. qtable() is an extension of +table() from base R and can do much beyond creating two-way +contingency tables. The function has a simple to use interface while +internally it builds layouts using the rtables +framework.

+
+
+

Getting Started +

+

Load packages used in this vignette:

+ +

Let’s start by seeing what table() can do:

+
+table(ex_adsl$ARM)
+
# 
+#      A: Drug X     B: Placebo C: Combination 
+#            134            134            132
+
+table(ex_adsl$SEX, ex_adsl$ARM)
+
#                   
+#                    A: Drug X B: Placebo C: Combination
+#   F                       79         77             66
+#   M                       51         55             60
+#   U                        3          2              4
+#   UNDIFFERENTIATED         1          0              2
+

We can easily recreate the cross-tables above with +qtable() by specifying a data.frame with variable(s) to +tabulate. The col_vars and row_vars arguments +control how to split the data across columns and rows respectively.

+
+qtable(ex_adsl, col_vars = "ARM")
+
#         A: Drug X   B: Placebo   C: Combination
+#          (N=134)     (N=134)        (N=132)    
+# ———————————————————————————————————————————————
+# count      134         134            132
+
+qtable(ex_adsl, col_vars = "ARM", row_vars = "SEX")
+
#                    A: Drug X   B: Placebo   C: Combination
+# count               (N=134)     (N=134)        (N=132)    
+# ——————————————————————————————————————————————————————————
+# F                     79           77             66      
+# M                     51           55             60      
+# U                      3           2              4       
+# UNDIFFERENTIATED       1           0              2
+

Aside from the display style, the main difference is that +qtable() will add (N=xx) in the table header by default. +This can be removed with show_colcounts.

+
+qtable(ex_adsl, "ARM", show_colcounts = FALSE)
+
# count            all obs
+# ————————————————————————
+# A: Drug X          134  
+# B: Placebo         134  
+# C: Combination     132
+

Any variables used as the row or column facets should not have any +empty strings (““). This is because non empty values are required as +labels when generating the table. The code below will generate an +error.

+
+tmp_adsl <- ex_adsl
+tmp_adsl$new <- rep_len(c("", "A", "B"), nrow(tmp_adsl))
+
+qtable(tmp_adsl, row_vars = "new")
+
+
+

Nested Tables +

+

Providing more than one variable name for the row or column structure +in qtable() will create a nested table. Arbitrary nesting +is supported in each dimension.

+
+qtable(ex_adsl, row_vars = c("SEX", "STRATA1"), col_vars = c("ARM", "STRATA2"))
+
#                       A: Drug X        B: Placebo       C: Combination  
+#                      S1       S2       S1       S2       S1        S2   
+# count              (N=73)   (N=61)   (N=67)   (N=67)   (N=56)    (N=76) 
+# ————————————————————————————————————————————————————————————————————————
+# F                                                                       
+#   A                  12       9        11       13        7        11   
+#   B                  14       11       12       15        9        12   
+#   C                  17       16       13       13       14        13   
+# M                                                                       
+#   A                  5        11       10       9         6        14   
+#   B                  13       8        7        10        9        12   
+#   C                  8        6        13       6         8        11   
+# U                                                                       
+#   A                  1        0        1        0         1         0   
+#   B                  1        0        0        1         0         1   
+#   C                  1        0        0        0         1         1   
+# UNDIFFERENTIATED                                                        
+#   A                  0        0        0        0         0         1   
+#   C                  1        0        0        0         1         0
+

Note that by default, unobserved factor levels within a facet are not +included in the table. This can be modified with +drop_levels. The code below adds a row of 0s for +STRATA1 level “B” nested under the SEX level +“UNDIFFERENTIATED”.

+
+qtable(
+  ex_adsl,
+  row_vars = c("SEX", "STRATA1"),
+  col_vars = c("ARM", "STRATA2"),
+  drop_levels = FALSE
+)
+
#                       A: Drug X        B: Placebo       C: Combination  
+#                      S1       S2       S1       S2       S1        S2   
+# count              (N=73)   (N=61)   (N=67)   (N=67)   (N=56)    (N=76) 
+# ————————————————————————————————————————————————————————————————————————
+# F                                                                       
+#   A                  12       9        11       13        7        11   
+#   B                  14       11       12       15        9        12   
+#   C                  17       16       13       13       14        13   
+# M                                                                       
+#   A                  5        11       10       9         6        14   
+#   B                  13       8        7        10        9        12   
+#   C                  8        6        13       6         8        11   
+# U                                                                       
+#   A                  1        0        1        0         1         0   
+#   B                  1        0        0        1         0         1   
+#   C                  1        0        0        0         1         1   
+# UNDIFFERENTIATED                                                        
+#   A                  0        0        0        0         0         1   
+#   B                  0        0        0        0         0         0   
+#   C                  1        0        0        0         1         0
+

In contrast, table() cannot return a nested table. +Rather it produces a list of contingency tables when more than two +variables are used as inputs.

+
+table(ex_adsl$SEX, ex_adsl$STRATA1, ex_adsl$ARM, ex_adsl$STRATA2)
+
# , ,  = A: Drug X,  = S1
+# 
+#                   
+#                     A  B  C
+#   F                12 14 17
+#   M                 5 13  8
+#   U                 1  1  1
+#   UNDIFFERENTIATED  0  0  1
+# 
+# , ,  = B: Placebo,  = S1
+# 
+#                   
+#                     A  B  C
+#   F                11 12 13
+#   M                10  7 13
+#   U                 1  0  0
+#   UNDIFFERENTIATED  0  0  0
+# 
+# , ,  = C: Combination,  = S1
+# 
+#                   
+#                     A  B  C
+#   F                 7  9 14
+#   M                 6  9  8
+#   U                 1  0  1
+#   UNDIFFERENTIATED  0  0  1
+# 
+# , ,  = A: Drug X,  = S2
+# 
+#                   
+#                     A  B  C
+#   F                 9 11 16
+#   M                11  8  6
+#   U                 0  0  0
+#   UNDIFFERENTIATED  0  0  0
+# 
+# , ,  = B: Placebo,  = S2
+# 
+#                   
+#                     A  B  C
+#   F                13 15 13
+#   M                 9 10  6
+#   U                 0  1  0
+#   UNDIFFERENTIATED  0  0  0
+# 
+# , ,  = C: Combination,  = S2
+# 
+#                   
+#                     A  B  C
+#   F                11 12 13
+#   M                14 12 11
+#   U                 0  1  1
+#   UNDIFFERENTIATED  1  0  0
+

With some help from stats::ftable() the nested structure +can be achieved in two steps.

+
+t1 <- ftable(ex_adsl[, c("SEX", "STRATA1", "ARM", "STRATA2")])
+ftable(t1, row.vars = c("SEX", "STRATA1"))
+
#                          ARM     A: Drug X    B: Placebo    C: Combination   
+#                          STRATA2        S1 S2         S1 S2             S1 S2
+# SEX              STRATA1                                                     
+# F                A                      12  9         11 13              7 11
+#                  B                      14 11         12 15              9 12
+#                  C                      17 16         13 13             14 13
+# M                A                       5 11         10  9              6 14
+#                  B                      13  8          7 10              9 12
+#                  C                       8  6         13  6              8 11
+# U                A                       1  0          1  0              1  0
+#                  B                       1  0          0  1              0  1
+#                  C                       1  0          0  0              1  1
+# UNDIFFERENTIATED A                       0  0          0  0              0  1
+#                  B                       0  0          0  0              0  0
+#                  C                       1  0          0  0              1  0
+
+
+

NA Values +

+

So far in all the examples we have seen, we used counts to summarize +the data in each table cell as this is the default analysis used by +qtable(). Internally, a single analysis variable specified +by avar is used to generate the counts in the table. The +default analysis variable is the first variable in data. In +the case of ex_adsl this is “STUDYID”.

+

Let’s see what happens when we introduce some NA values +into the analysis variable:

+
+tmp_adsl <- ex_adsl
+tmp_adsl[[1]] <- NA_character_
+
+qtable(tmp_adsl, row_vars = "ARM", col_vars = "SEX")
+
#                     F         M        U     UNDIFFERENTIATED
+# count            (N=222)   (N=166)   (N=9)        (N=3)      
+# —————————————————————————————————————————————————————————————
+# A: Drug X           0         0        0            0        
+# B: Placebo          0         0        0            0        
+# C: Combination      0         0        0            0
+

The resulting table is showing 0’s across all cells because all the +values of the analysis variable are NA.

+

Keep this behavior in mind when doing quick exploratory analysis +using the default counts aggregate function of qtable.

+

If this does not suit your purpose, you can either pre-process your +data to re-code the NA values or use another analysis +function. We will see how the latter is done in the Custom Aggregation section.

+
+# Recode NA values
+tmp_adsl[[1]] <- addNA(tmp_adsl[[1]])
+
+qtable(tmp_adsl, row_vars = "ARM", col_vars = "SEX")
+
#                     F         M        U     UNDIFFERENTIATED
+# count            (N=222)   (N=166)   (N=9)        (N=3)      
+# —————————————————————————————————————————————————————————————
+# A: Drug X          79        51        3            1        
+# B: Placebo         77        55        2            0        
+# C: Combination     66        60        4            2
+

In addition, row and column variables should have NA +levels explicitly labelled as above. If this is not done, the columns +and/or rows will not reflect the full data.

+
+tmp_adsl$new1 <- factor(NA_character_, levels = c("X", "Y", "Z"))
+qtable(tmp_adsl, row_vars = "ARM", col_vars = "new1")
+
#                    X       Y       Z  
+# count            (N=0)   (N=0)   (N=0)
+# ——————————————————————————————————————
+# A: Drug X          0       0       0  
+# B: Placebo         0       0       0  
+# C: Combination     0       0       0
+

Explicitly labeling the NA levels in the column facet +adds a column to the table:

+
+tmp_adsl$new2 <- addNA(tmp_adsl$new1)
+levels(tmp_adsl$new2)[4] <- "<NA>" # NA needs to be a recognizible string
+qtable(tmp_adsl, row_vars = "ARM", col_vars = "new2")
+
#                    X       Y       Z      <NA>  
+# count            (N=0)   (N=0)   (N=0)   (N=400)
+# ————————————————————————————————————————————————
+# A: Drug X          0       0       0       134  
+# B: Placebo         0       0       0       134  
+# C: Combination     0       0       0       132
+
+
+

Custom Aggregation +

+

A powerful feature of qtable() is that the user can +define the type of function used to summarize the data in each facet. We +can specify the type of analysis summary using the afun +argument:

+
+qtable(ex_adsl, row_vars = "STRATA2", col_vars = "ARM", avar = "AGE", afun = mean)
+
#              A: Drug X   B: Placebo   C: Combination
+# AGE - mean    (N=134)     (N=134)        (N=132)    
+# ————————————————————————————————————————————————————
+# S1             34.10       36.46          35.70     
+# S2             33.38       34.40          35.24
+

Note that the analysis variable AGE and analysis +function name are included in the top right header of the table.

+

If the analysis function returns a vector of 2 or 3 elements, the +result is displayed in multi-valued single cells.

+
+qtable(ex_adsl, row_vars = "STRATA2", col_vars = "ARM", avar = "AGE", afun = range)
+
#                A: Drug X    B: Placebo    C: Combination
+# AGE - range     (N=134)       (N=134)        (N=132)    
+# ————————————————————————————————————————————————————————
+# S1            23.0 / 48.0   24.0 / 62.0    20.0 / 69.0  
+# S2            21.0 / 50.0   21.0 / 58.0    23.0 / 64.0
+

If you want to use an analysis function with more than 3 summary +elements, you can use a list. In this case, the values are displayed in +the table as multiple stacked cells within each facet. If the list +elements are named, the names are used as row labels.

+
+fivenum2 <- function(x) {
+  setNames(as.list(fivenum(x)), c("min", "Q1", "MED", "Q3", "max"))
+}
+qtable(ex_adsl, row_vars = "STRATA2", col_vars = "ARM", avar = "AGE", afun = fivenum2)
+
#                  A: Drug X   B: Placebo   C: Combination
+# AGE - fivenum2    (N=134)     (N=134)        (N=132)    
+# ————————————————————————————————————————————————————————
+# S1                                                      
+#   min              23.00       24.00          20.00     
+#   Q1               28.00       30.00          30.50     
+#   MED              34.00       36.00          35.00     
+#   Q3               39.00       40.50          40.00     
+#   max              48.00       62.00          69.00     
+# S2                                                      
+#   min              21.00       21.00          23.00     
+#   Q1               29.00       29.50          30.00     
+#   MED              32.00       32.00          34.50     
+#   Q3               38.00       39.50          38.00     
+#   max              50.00       58.00          64.00
+

More advanced formatting can be controlled with +in_rows(). See function documentation for more details.

+
+meansd_range <- function(x) {
+  in_rows(
+    "Mean (sd)" = rcell(c(mean(x), sd(x)), format = "xx.xx (xx.xx)"),
+    "Range" = rcell(range(x), format = "xx - xx")
+  )
+}
+
+qtable(ex_adsl, row_vars = "STRATA2", col_vars = "ARM", avar = "AGE", afun = meansd_range)
+
#                       A: Drug X      B: Placebo    C: Combination
+# AGE - meansd_range     (N=134)        (N=134)         (N=132)    
+# —————————————————————————————————————————————————————————————————
+# S1                                                               
+#   Mean (sd)          34.10 (6.71)   36.46 (7.72)    35.70 (8.22) 
+#   Range                23 - 48        24 - 62         20 - 69    
+# S2                                                               
+#   Mean (sd)          33.38 (6.40)   34.40 (7.99)    35.24 (7.39) 
+#   Range                21 - 50        21 - 58         23 - 64
+
+
+

Marginal Summaries +

+

Another feature of qtable() is the ability to quickly +add marginal summary rows with the summarize_groups +argument. This summary will add to the table the count of non-NA records +of the analysis variable at each level of nesting. For example, compare +these two tables:

+
+qtable(
+  ex_adsl,
+  row_vars = c("STRATA1", "STRATA2"), col_vars = "ARM",
+  avar = "AGE", afun = mean
+)
+
#              A: Drug X   B: Placebo   C: Combination
+# AGE - mean    (N=134)     (N=134)        (N=132)    
+# ————————————————————————————————————————————————————
+# A                                                   
+#   S1           31.61       36.68          34.00     
+#   S2           34.40       33.55          34.35     
+# B                                                   
+#   S1           34.57       37.68          35.83     
+#   S2           32.79       34.77          36.68     
+# C                                                   
+#   S1           35.26       35.38          36.58     
+#   S2           32.95       34.89          34.72
+
+qtable(
+  ex_adsl,
+  row_vars = c("STRATA1", "STRATA2"), col_vars = "ARM",
+  summarize_groups = TRUE, avar = "AGE", afun = mean
+)
+
#                  A: Drug X    B: Placebo   C: Combination
+# AGE - mean        (N=134)      (N=134)        (N=132)    
+# —————————————————————————————————————————————————————————
+# A                38 (28.4%)   44 (32.8%)     40 (30.3%)  
+#   S1             18 (13.4%)   22 (16.4%)     14 (10.6%)  
+#     AGE - mean     31.61        36.68          34.00     
+#   S2             20 (14.9%)   22 (16.4%)     26 (19.7%)  
+#     AGE - mean     34.40        33.55          34.35     
+# B                47 (35.1%)   45 (33.6%)     43 (32.6%)  
+#   S1             28 (20.9%)   19 (14.2%)     18 (13.6%)  
+#     AGE - mean     34.57        37.68          35.83     
+#   S2             19 (14.2%)   26 (19.4%)     25 (18.9%)  
+#     AGE - mean     32.79        34.77          36.68     
+# C                49 (36.6%)   45 (33.6%)     49 (37.1%)  
+#   S1             27 (20.1%)   26 (19.4%)     24 (18.2%)  
+#     AGE - mean     35.26        35.38          36.58     
+#   S2             22 (16.4%)   19 (14.2%)     25 (18.9%)  
+#     AGE - mean     32.95        34.89          34.72
+

In the second table, there are marginal summary rows for each level +of the two row facet variables: STRATA1 and +STRATA2. The number 18 in the second row gives the count of +observations part of ARM level “A: Drug X”, +STRATA1 level “A”, and STRATA2 level “S1”. The +percent is calculated as the cell count divided by the column count +given in the table header. So we can see that the mean AGE +of 31.61 in that subgroup is based on 18 subjects which correspond to +13.4% of the subjects in arm “A: Drug X”.

+

See ?summarize_row_groups for how to add marginal +summary rows when using the core rtables framework.

+
+
+

Table Decorations +

+

Tables generated with qtable() can include annotations +such as titles, subtitles and footnotes like so:

+
+qtable(
+  ex_adsl,
+  row_vars = "STRATA2", col_vars = "ARM",
+  title = "Strata 2 Summary",
+  subtitle = paste0("STUDY ", ex_adsl$STUDYID[1]),
+  main_footer = paste0("Date: ", as.character(Sys.Date()))
+)
+
# Strata 2 Summary
+# STUDY AB12345
+# 
+# ———————————————————————————————————————————————
+#         A: Drug X   B: Placebo   C: Combination
+# count    (N=134)     (N=134)        (N=132)    
+# ———————————————————————————————————————————————
+# S1         73           67             56      
+# S2         61           67             76      
+# ———————————————————————————————————————————————
+# 
+# Date: 2024-06-27
+
+
+

Summary +

+

Here is what we have learned in this vignette:

+ +

As the intended use of qtable() is for exploratory data +analysis, there is limited functionality for building very complex +tables. For details on how to get started with the core +rtables layout functionality see the introduction +vignette.

+
+
+
+ + + + +
+ + + + + + + diff --git a/v0.6.9/articles/format_precedence.html b/v0.6.9/articles/format_precedence.html new file mode 100644 index 000000000..2ac7cddf5 --- /dev/null +++ b/v0.6.9/articles/format_precedence.html @@ -0,0 +1,623 @@ + + + + + + + + +Format Precedence and NA Handling • rtables + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + + +
+

Formats Precedence +

+

Users of the rtables package can specify the format in +which the numbers in the reporting tables are printed. Formatting +functionality is provided by the formatters +R package. See formatters::list_valid_format_labels() for a +list of all available formats. The format can be specified by the user +in a few different places. It may happen that, for a single table +layout, the format is specified in more than one place. In such a case, +the final format that will be applied depends on format precedence rules +defined by rtables. In this vignette, we describe the basic +rules of rtables format precedence.

+

The examples shown in this vignette utilize the example +ADSL dataset, a demographic table that summarizes the +variables content for different population subsets (encoded in the +columns).

+
+library(rtables)
+ADSL <- ex_adsl
+

Note that all ex_* data which is currently attached to +the rtables package is provided by the formatters +package and was created using the publicly available random.cdisc.data +R package.

+
+

Format Precedence and Inheritance Rules +

+

The format in which numbers are printed can be specified by the user +in a few different places. In the context of precedence, it is important +which level of the split hierarchy formats are specified at. In general, +there are two such levels: the cell level and the +so-called parent table level. The concept of the cell +and the parent table results from the way in which the +rtables package stores resulting tables. It models the +resulting tables as hierarchical, tree-like objects with the cells (as +leaves) containing multiple values. Particularly noteworthy in this +context is the fact that the actual table splitting occurs in a +row-dominant way (even if column splitting is present in the layout). +rtables provides user-end function +table_structure() that prints the structure of a given +table object.

+

For a simple illustration, consider the following example:

+
+lyt <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  split_rows_by("SEX") %>%
+  analyze(vars = "AGE", afun = mean)
+
+adsl_analyzed <- build_table(lyt, ADSL)
+adsl_analyzed
+
#                       A: Drug X          B: Placebo       C: Combination 
+# —————————————————————————————————————————————————————————————————————————
+# F                                                                        
+#   mean             32.7594936708861   34.1168831168831   35.1969696969697
+# M                                                                        
+#   mean             35.5686274509804   37.4363636363636   35.3833333333333
+# U                                                                        
+#   mean             31.6666666666667          31               35.25      
+# UNDIFFERENTIATED                                                         
+#   mean                    28                 NA                 45
+
+table_structure(adsl_analyzed)
+
# [TableTree] SEX
+#  [TableTree] F
+#   [ElementaryTable] AGE (1 x 3)
+#  [TableTree] M
+#   [ElementaryTable] AGE (1 x 3)
+#  [TableTree] U
+#   [ElementaryTable] AGE (1 x 3)
+#  [TableTree] UNDIFFERENTIATED
+#   [ElementaryTable] AGE (1 x 3)
+

In this table, there are 4 sub-tables under the SEX +table. These are: F, M, U, and +UNDIFFERENTIATED. Each of these sub-tables has one +sub-table AGE. For example, for the first AGE +sub-table, its parent table is F.

+

The concept of hierarchical, tree-like representations of resulting +tables translates directly to format precedence and inheritance rules. +As a general principle, the format being finally applied for the cell is +the one that is the most specific, that is, the one which is the closest +to the cell in a given path in the tree. Hence, the +precedence-inheritance chain looks like the following:

+
parent_table -> parent_table -> ... -> parent_table -> cell
+

In such a chain, the outermost parent_table is the least +specific place to specify the format, while the cell is the +most specific one. In cases where the format is specified by the user in +more than one place, the one which is most specific will be applied in +the cell. If no specific format has been selected by the user for the +split, then the default format will be applied. The default format is +"xx" and it yields the same formatting as the +as.character() function. In the following sections of this +vignette, we will illustrate the format precedence rules with a few +examples.

+
+
+

Standard Format +

+

Below is a simple layout that does not explicitly set a format for +the output of the analysis function. In such a case, the default format +is applied.

+
+lyt0 <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  analyze(vars = "AGE", afun = mean)
+
+build_table(lyt0, ADSL)
+
#           A: Drug X          B: Placebo       C: Combination 
+# —————————————————————————————————————————————————————————————
+# mean   33.7686567164179   35.4328358208955   35.4318181818182
+
+
+

Cell Format +

+

The format of a cell can be explicitly specified via the +rcell() or in_rows() functions. The former is +essentially a collection of data objects while the latter is a +collection of rcell() objects. As previously mentioned, +this is the most specific place where the format can be specified by the +user.

+
+lyt1 <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  analyze(vars = "AGE", afun = function(x) {
+    rcell(mean(x), format = "xx.xx", label = "Mean")
+  })
+
+build_table(lyt1, ADSL)
+
#        A: Drug X   B: Placebo   C: Combination
+# ——————————————————————————————————————————————
+# Mean     33.77       35.43          35.43
+
+lyt1a <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  analyze(vars = "AGE", afun = function(x) {
+    in_rows(
+      "Mean" = rcell(mean(x)),
+      .formats = "xx.xx"
+    )
+  })
+
+build_table(lyt1a, ADSL)
+
#        A: Drug X   B: Placebo   C: Combination
+# ——————————————————————————————————————————————
+# Mean     33.77       35.43          35.43
+

If the format is specified in both of these places at the same time, +the one specified via in_rows() takes highest precedence. +Technically, in this case, the format defined in rcell() +will simply be overwritten by the one defined in in_rows(). +This is because the format specified in in_rows() is +applied to the cells not the rows (overriding the previously specified +cell-specific values), which indicates that the precedence rules +described above are still in place.

+
+lyt2 <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  analyze(vars = "AGE", afun = function(x) {
+    in_rows(
+      "Mean" = rcell(mean(x), format = "xx.xxx"),
+      .formats = "xx.xx"
+    )
+  })
+
+build_table(lyt2, ADSL)
+
#        A: Drug X   B: Placebo   C: Combination
+# ——————————————————————————————————————————————
+# Mean     33.77       35.43          35.43
+
+
+

Parent Table Format and Inheritance +

+

In addition to the cell level, the format can be specified at the +parent table level. If no format has been set by the user for a cell, +the most specific format for that cell is the one defined at its +innermost parent table split (if any).

+
+lyt3 <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  analyze(vars = "AGE", mean, format = "xx.x")
+
+build_table(lyt3, ADSL)
+
#        A: Drug X   B: Placebo   C: Combination
+# ——————————————————————————————————————————————
+# mean     33.8         35.4           35.4
+

If the cell format is also specified for a cell, then the parent +table format is ignored for this cell since the cell format is more +specific and therefore takes precedence.

+
+lyt4 <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  analyze(
+    vars = "AGE", afun = function(x) {
+      rcell(mean(x), format = "xx.xx", label = "Mean")
+    },
+    format = "xx.x"
+  )
+
+build_table(lyt4, ADSL)
+
#        A: Drug X   B: Placebo   C: Combination
+# ——————————————————————————————————————————————
+# Mean     33.77       35.43          35.43
+
+lyt4a <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  analyze(
+    vars = "AGE", afun = function(x) {
+      in_rows(
+        "Mean" = rcell(mean(x)),
+        "SD" = rcell(sd(x)),
+        .formats = "xx.xx"
+      )
+    },
+    format = "xx.x"
+  )
+
+build_table(lyt4a, ADSL)
+
#        A: Drug X   B: Placebo   C: Combination
+# ——————————————————————————————————————————————
+# Mean     33.77       35.43          35.43     
+# SD       6.55         7.90           7.72
+

In the following, slightly more complicated, example, we can observe +partial inheritance. That is, only SD cells inherit the +parent table’s format while the Mean cells do not.

+
+lyt5 <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  analyze(
+    vars = "AGE", afun = function(x) {
+      in_rows(
+        "Mean" = rcell(mean(x), format = "xx.xx"),
+        "SD" = rcell(sd(x))
+      )
+    },
+    format = "xx.x"
+  )
+
+build_table(lyt5, ADSL)
+
#        A: Drug X   B: Placebo   C: Combination
+# ——————————————————————————————————————————————
+# Mean     33.77       35.43          35.43     
+# SD        6.6         7.9            7.7
+
+
+
+

+NA Handling +

+

Consider the following layout and the resulting table created:

+
+lyt6 <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  split_rows_by("SEX") %>%
+  analyze(vars = "AGE", afun = mean, format = "xx.xx")
+
+build_table(lyt6, ADSL)
+
#                    A: Drug X   B: Placebo   C: Combination
+# ——————————————————————————————————————————————————————————
+# F                                                         
+#   mean               32.76       34.12          35.20     
+# M                                                         
+#   mean               35.57       37.44          35.38     
+# U                                                         
+#   mean               31.67       31.00          35.25     
+# UNDIFFERENTIATED                                          
+#   mean               28.00         NA           45.00
+

In the output the cell corresponding to the +UNDIFFERENTIATED level of SEX and the +B: Placebo level of ARM is displayed as +NA. This occurs because there were no non-NA +values under this facet that could be used to compute the mean. +rtables allows the user to specify a string to display when +cell values are NA. Similar to formats for numbers, the +user can specify a string to replace NA with the parameter +format_na_str or .format_na_str. This can be +specified at the cell or parent table level. NA string +precedence and inheritance rules are the same as those for number format +precedence, described in the previous section of this vignette. We will +illustrate this with a few examples.

+
+

Replacing NA Values at the Cell Level +

+

At the cell level, it is possible to replace NA values +with a custom string by means of the format_na_str +parameter in rcell() or .format_na_str +parameter in in_rows().

+
+lyt7 <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  split_rows_by("SEX") %>%
+  analyze(vars = "AGE", afun = function(x) {
+    rcell(mean(x), format = "xx.xx", label = "Mean", format_na_str = "<missing>")
+  })
+
+build_table(lyt7, ADSL)
+
#                    A: Drug X   B: Placebo   C: Combination
+# ——————————————————————————————————————————————————————————
+# F                                                         
+#   Mean               32.76       34.12          35.20     
+# M                                                         
+#   Mean               35.57       37.44          35.38     
+# U                                                         
+#   Mean               31.67       31.00          35.25     
+# UNDIFFERENTIATED                                          
+#   Mean               28.00     <missing>        45.00
+
+lyt7a <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  split_rows_by("SEX") %>%
+  analyze(vars = "AGE", afun = function(x) {
+    in_rows(
+      "Mean" = rcell(mean(x), format = "xx.xx"),
+      .format_na_strs = "<MISSING>"
+    )
+  })
+
+build_table(lyt7a, ADSL)
+
#                    A: Drug X   B: Placebo   C: Combination
+# ——————————————————————————————————————————————————————————
+# F                                                         
+#   Mean               32.76       34.12          35.20     
+# M                                                         
+#   Mean               35.57       37.44          35.38     
+# U                                                         
+#   Mean               31.67       31.00          35.25     
+# UNDIFFERENTIATED                                          
+#   Mean               28.00     <MISSING>        45.00
+

If the NA string is specified in both of these places at +the same time, the one specified with in_rows() takes +precedence. Technically, in this case the NA replacement +string defined in rcell() will simply be overwritten by the +one defined in in_rows(). This is because the +NA string specified in in_rows() is applied to +the cells, not the rows (overriding the previously specified cell +specific values), which means that the precedence rules described above +are still in place.

+
+lyt8 <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  split_rows_by("SEX") %>%
+  analyze(vars = "AGE", afun = function(x) {
+    in_rows(
+      "Mean" = rcell(mean(x), format = "xx.xx", format_na_str = "<missing>"),
+      .format_na_strs = "<MISSING>"
+    )
+  })
+
+build_table(lyt8, ADSL)
+
#                    A: Drug X   B: Placebo   C: Combination
+# ——————————————————————————————————————————————————————————
+# F                                                         
+#   Mean               32.76       34.12          35.20     
+# M                                                         
+#   Mean               35.57       37.44          35.38     
+# U                                                         
+#   Mean               31.67       31.00          35.25     
+# UNDIFFERENTIATED                                          
+#   Mean               28.00     <MISSING>        45.00
+
+
+

Parent Table Replacement of NA Values and Inheritance +Principles +

+

In addition to the cell level, the string replacement for +NA values can be specified at the parent table level. If no +replacement string has been specified by the user for a cell, the most +specific NA string for that cell is the one defined at its +innermost parent table split (if any).

+
+lyt9 <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  split_rows_by("SEX") %>%
+  analyze(vars = "AGE", mean, format = "xx.xx", na_str = "not available")
+
+build_table(lyt9, ADSL)
+
#                    A: Drug X    B: Placebo     C: Combination
+# —————————————————————————————————————————————————————————————
+# F                                                            
+#   mean               32.76         34.12           35.20     
+# M                                                            
+#   mean               35.57         37.44           35.38     
+# U                                                            
+#   mean               31.67         31.00           35.25     
+# UNDIFFERENTIATED                                             
+#   mean               28.00     not available       45.00
+

If an NA value replacement string was also specified at +the cell level, then the one set at the parent table level is ignored +for this cell as the cell level format is more specific and therefore +takes precedence.

+
+lyt10 <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  split_rows_by("SEX") %>%
+  analyze(
+    vars = "AGE", afun = function(x) {
+      rcell(mean(x), format = "xx.xx", label = "Mean", format_na_str = "<missing>")
+    },
+    na_str = "not available"
+  )
+
+build_table(lyt10, ADSL)
+
#                    A: Drug X   B: Placebo   C: Combination
+# ——————————————————————————————————————————————————————————
+# F                                                         
+#   Mean               32.76       34.12          35.20     
+# M                                                         
+#   Mean               35.57       37.44          35.38     
+# U                                                         
+#   Mean               31.67       31.00          35.25     
+# UNDIFFERENTIATED                                          
+#   Mean               28.00     <missing>        45.00
+
+lyt10a <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  split_rows_by("SEX") %>%
+  analyze(
+    vars = "AGE", afun = function(x) {
+      in_rows(
+        "Mean" = rcell(mean(x)),
+        "SD" = rcell(sd(x)),
+        .formats = "xx.xx",
+        .format_na_strs = "<missing>"
+      )
+    },
+    na_str = "not available"
+  )
+
+build_table(lyt10a, ADSL)
+
#                    A: Drug X   B: Placebo   C: Combination
+# ——————————————————————————————————————————————————————————
+# F                                                         
+#   Mean               32.76       34.12          35.20     
+#   SD                 6.09         7.06           7.43     
+# M                                                         
+#   Mean               35.57       37.44          35.38     
+#   SD                 7.08         8.69           8.24     
+# U                                                         
+#   Mean               31.67       31.00          35.25     
+#   SD                 3.21         5.66           3.10     
+# UNDIFFERENTIATED                                          
+#   Mean               28.00     <missing>        45.00     
+#   SD               <missing>   <missing>         1.41
+

In the following, slightly more complicated example, we can observe +partial inheritance of NA strings. That is, only SD cells +inherit the parent table’s NA string, while the +Mean cells do not.

+
+lyt11 <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  split_rows_by("SEX") %>%
+  analyze(
+    vars = "AGE", afun = function(x) {
+      in_rows(
+        "Mean" = rcell(mean(x), format_na_str = "<missing>"),
+        "SD" = rcell(sd(x))
+      )
+    },
+    format = "xx.xx",
+    na_str = "not available"
+  )
+
+build_table(lyt11, ADSL)
+
#                      A: Drug X      B: Placebo     C: Combination
+# —————————————————————————————————————————————————————————————————
+# F                                                                
+#   Mean                 32.76           34.12           35.20     
+#   SD                   6.09            7.06             7.43     
+# M                                                                
+#   Mean                 35.57           37.44           35.38     
+#   SD                   7.08            8.69             8.24     
+# U                                                                
+#   Mean                 31.67           31.00           35.25     
+#   SD                   3.21            5.66             3.10     
+# UNDIFFERENTIATED                                                 
+#   Mean                 28.00         <missing>         45.00     
+#   SD               not available   not available        1.41
+
+
+
+
+ + + + +
+ + + + + + + diff --git a/v0.6.9/articles/images/rtables-basics.png b/v0.6.9/articles/images/rtables-basics.png new file mode 100644 index 000000000..4ef55dd4c Binary files /dev/null and b/v0.6.9/articles/images/rtables-basics.png differ diff --git a/v0.6.9/articles/index.html b/v0.6.9/articles/index.html new file mode 100644 index 000000000..8204cd220 --- /dev/null +++ b/v0.6.9/articles/index.html @@ -0,0 +1,176 @@ + +Articles • rtables + Skip to contents + + +
+
+
+ +
+

Getting Started

+
+ +
Introduction to {rtables}
+
+
Exploratory Analysis
+
+
+ + + + + +
+

Developer Guide

+

Articles intended for developer use only.

+ +
Split Machinery
+
+
Tabulation
+
+
Table Hierarchy
+
+
Debugging in {rtables} and Beyond
+
+
Sparse Notes on {rtables} Internals
+
+
+
+ + +
+ + + + + + + diff --git a/v0.6.9/articles/introduction.html b/v0.6.9/articles/introduction.html new file mode 100644 index 000000000..7817b3592 --- /dev/null +++ b/v0.6.9/articles/introduction.html @@ -0,0 +1,616 @@ + + + + + + + + +Introduction to {rtables} • rtables + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

Introduction +

+

The rtables package provides a framework to create, +tabulate, and output tables in R. Most of the design requirements for +rtables have their origin in studying tables that are +commonly used to report analyses from clinical trials; however, we were +careful to keep rtables a general purpose toolkit.

+

In this vignette, we give a short introduction into +rtables and tabulating a table.

+

The content in this vignette is based on the following two +resources:

+ +

The packages used in this vignette are rtables and +dplyr:

+ +
+
+

Overview +

+

To build a table using rtables two components are +required: A layout constructed using rtables functions, and +a data.frame of unaggregated data. These two elements are +combined to build a table object. Table objects contain information +about both the content and the structure of the table, as well as +instructions on how this information should be processed to construct +the table. After obtaining the table object, a formatted table can be +printed in ASCII format, or exported to a variety of other formats +(.txt, .pdf, .docx, etc.).

+

+
+
+

Data +

+

The data used in this vignette is a made up using random number +generators. The data content is relatively simple: one row per imaginary +person and one column per measurement: study arm, the country of origin, +gender, handedness, age, and weight.

+
+n <- 400
+
+set.seed(1)
+
+df <- tibble(
+  arm = factor(sample(c("Arm A", "Arm B"), n, replace = TRUE), levels = c("Arm A", "Arm B")),
+  country = factor(sample(c("CAN", "USA"), n, replace = TRUE, prob = c(.55, .45)), levels = c("CAN", "USA")),
+  gender = factor(sample(c("Female", "Male"), n, replace = TRUE), levels = c("Female", "Male")),
+  handed = factor(sample(c("Left", "Right"), n, prob = c(.6, .4), replace = TRUE), levels = c("Left", "Right")),
+  age = rchisq(n, 30) + 10
+) %>% mutate(
+  weight = 35 * rnorm(n, sd = .5) + ifelse(gender == "Female", 140, 180)
+)
+
+head(df)
+
# # A tibble: 6 × 6
+#   arm   country gender handed   age weight
+#   <fct> <fct>   <fct>  <fct>  <dbl>  <dbl>
+# 1 Arm A USA     Female Left    31.3   139.
+# 2 Arm B CAN     Female Right   50.5   116.
+# 3 Arm A USA     Male   Right   32.4   186.
+# 4 Arm A USA     Male   Right   34.6   169.
+# 5 Arm B USA     Female Right   43.0   160.
+# 6 Arm A USA     Female Right   43.2   126.
+

Note that we use factor variables so that the level order is +represented in the row or column order when we tabulate the information +of df below.

+
+
+

Building a Table +

+

The aim of this vignette is to build the following table step by +step:

+
#                     Arm A                     Arm B         
+#              Female        Male        Female        Male   
+#              (N=96)      (N=105)       (N=92)      (N=107)  
+# ————————————————————————————————————————————————————————————
+# CAN        45 (46.9%)   64 (61.0%)   46 (50.0%)   62 (57.9%)
+#   Left     32 (33.3%)   42 (40.0%)   26 (28.3%)   37 (34.6%)
+#     mean     38.87        40.43        40.33        37.68   
+#   Right    13 (13.5%)   22 (21.0%)   20 (21.7%)   25 (23.4%)
+#     mean     36.64        40.19        40.16        40.65   
+# USA        51 (53.1%)   41 (39.0%)   46 (50.0%)   45 (42.1%)
+#   Left     34 (35.4%)   19 (18.1%)   25 (27.2%)   25 (23.4%)
+#     mean     40.36        39.68        39.21        40.07   
+#   Right    17 (17.7%)   22 (21.0%)   21 (22.8%)   20 (18.7%)
+#     mean     36.94        39.80        38.53        39.02
+
+
+

Quick Start +

+

The table above can be achieved via the qtable() +function. If you are new to tabulation with the rtables +layout framework, you can use this convenience wrapper to create many +types of two-way frequency tables.

+

The purpose of qtable is to enable quick exploratory +data analysis. See the exploratory_analysis +vignette for more details.

+

Here is the code to recreate the table above:

+
+qtable(df,
+  row_vars = c("country", "handed"),
+  col_vars = c("arm", "gender"),
+  avar = "age",
+  afun = mean,
+  summarize_groups = TRUE,
+  row_labels = "mean"
+)
+
#                       Arm A                     Arm B         
+#                Female        Male        Female        Male   
+# age - mean     (N=96)      (N=105)       (N=92)      (N=107)  
+# ——————————————————————————————————————————————————————————————
+# CAN          45 (46.9%)   64 (61.0%)   46 (50.0%)   62 (57.9%)
+#   Left       32 (33.3%)   42 (40.0%)   26 (28.3%)   37 (34.6%)
+#     mean       38.87        40.43        40.33        37.68   
+#   Right      13 (13.5%)   22 (21.0%)   20 (21.7%)   25 (23.4%)
+#     mean       36.64        40.19        40.16        40.65   
+# USA          51 (53.1%)   41 (39.0%)   46 (50.0%)   45 (42.1%)
+#   Left       34 (35.4%)   19 (18.1%)   25 (27.2%)   25 (23.4%)
+#     mean       40.36        39.68        39.21        40.07   
+#   Right      17 (17.7%)   22 (21.0%)   21 (22.8%)   20 (18.7%)
+#     mean       36.94        39.80        38.53        39.02
+

From the qtable function arguments above we can see many +of the key concepts of the underlying rtables layout +framework. The user needs to define:

+
    +
  • Which variables should be used as facets in the row and/or column +space?
  • +
  • Which variable should be used in the summary analysis?
  • +
  • Which function should be used as a summary?
  • +
  • Should the table include any marginal summaries?
  • +
  • Are any labels needed to clarify the table content?
  • +
+

In the sections below we will look at translating each of these +questions to a set of features part of the rtables layout +framework. Now let’s take a look at building the example table with a +layout.

+
+
+

Layout Instructions +

+

In rtables a basic table is defined to have 0 rows and +one column representing all data. Analyzing a variable is one way of +adding a row:

+
+lyt <- basic_table() %>%
+  analyze("age", mean, format = "xx.x")
+
+tbl <- build_table(lyt, df)
+tbl
+
#        all obs
+# ——————————————
+# mean    39.4
+

In the code above we first described the table and assigned that +description to a variable lyt. We then built the table +using the actual data with build_table(). The description +of a table is called a table layout. basic_table() is the +start of every table layout and contains the information that we have in +one column representing all data. The analyze() instruction +adds to the layout that the age variable should be analyzed +with the mean() analysis function and the result should be +rounded to 1 decimal place.

+

Hence, a layout is “pre-data”, that is, it’s a description of how to +build a table once we get data. We can look at the layout isolated:

+
+lyt
+
# A Pre-data Table Layout
+# 
+# Column-Split Structure:
+#  () 
+# 
+# Row-Split Structure:
+# age (** analysis **)
+

The general layouting instructions are summarized below:

+ +

Using those functions, it is possible to create a wide variety of +tables as we will show in this document.

+
+
+

Adding Column Structure +

+

We will now add more structure to the columns by adding a column +split based on the factor variable arm:

+
+lyt <- basic_table() %>%
+  split_cols_by("arm") %>%
+  analyze("age", afun = mean, format = "xx.x")
+
+tbl <- build_table(lyt, df)
+tbl
+
#        Arm A   Arm B
+# ————————————————————
+# mean   39.5    39.4
+

The resulting table has one column per factor level of +arm. So the data represented by the first column is +df[df$arm == "ARM A", ]. Hence, the +split_cols_by() partitions the data among the columns by +default.

+

Column splitting can be done in a recursive/nested manner by adding +sequential split_cols_by() layout instruction. It’s also +possible to add a non-nested split. Here we splitting each arm further +by the gender:

+
+lyt <- basic_table() %>%
+  split_cols_by("arm") %>%
+  split_cols_by("gender") %>%
+  analyze("age", afun = mean, format = "xx.x")
+
+tbl <- build_table(lyt, df)
+tbl
+
#            Arm A           Arm B    
+#        Female   Male   Female   Male
+# ————————————————————————————————————
+# mean    38.8    40.1    39.6    39.2
+

The first column represents the data in df where +df$arm == "A" & df$gender == "Female" and the second +column the data in df where +df$arm == "A" & df$gender == "Male", and so on.

+

More information on column structure can be found in the col_counts +vignette.

+
+
+

Adding Row Structure +

+

So far, we have created layouts with analysis and column splitting +instructions, i.e. analyze() and +split_cols_by(), respectively. This resulted with a table +with multiple columns and one data row. We will add more row structure +by stratifying the mean analysis by country (i.e. adding a split in the +row space):

+
+lyt <- basic_table() %>%
+  split_cols_by("arm") %>%
+  split_cols_by("gender") %>%
+  split_rows_by("country") %>%
+  analyze("age", afun = mean, format = "xx.x")
+
+tbl <- build_table(lyt, df)
+tbl
+
#              Arm A           Arm B    
+#          Female   Male   Female   Male
+# ——————————————————————————————————————
+# CAN                                   
+#   mean    38.2    40.3    40.3    38.9
+# USA                                   
+#   mean    39.2    39.7    38.9    39.6
+

In this table the data used to derive the first data cell (average of +age of female Canadians in Arm A) is where +df$country == "CAN" & df$arm == "Arm A" & df$gender == "Female". +This cell value can also be calculated manually:

+
+mean(df$age[df$country == "CAN" & df$arm == "Arm A" & df$gender == "Female"])
+
# [1] 38.22447
+

Row structure can also be used to group the table into titled groups +of pages during rendering. We do this via ‘page by splits’, which are +declared via page_by = TRUE within a call to +split_rows_by:

+
+lyt <- basic_table() %>%
+  split_cols_by("arm") %>%
+  split_cols_by("gender") %>%
+  split_rows_by("country", page_by = TRUE) %>%
+  split_rows_by("handed") %>%
+  analyze("age", afun = mean, format = "xx.x")
+
+tbl <- build_table(lyt, df)
+cat(export_as_txt(tbl, page_type = "letter", page_break = "\n\n~~~~~~ Page Break ~~~~~~\n\n"))
+
# 
+# country: CAN
+# 
+# ————————————————————————————————————————
+#                Arm A           Arm B    
+#            Female   Male   Female   Male
+# ————————————————————————————————————————
+# Left                                    
+#   mean      38.9    40.4    40.3    37.7
+# Right                                   
+#   mean      36.6    40.2    40.2    40.6
+# 
+# 
+# ~~~~~~ Page Break ~~~~~~
+# 
+# 
+# country: USA
+# 
+# ————————————————————————————————————————
+#                Arm A           Arm B    
+#            Female   Male   Female   Male
+# ————————————————————————————————————————
+# Left                                    
+#   mean      40.4    39.7    39.2    40.1
+# Right                                   
+#   mean      36.9    39.8    38.5    39.0
+

We go into more detail on page-by splits and how to control the +page-group specific titles in the Title and footer vignette.

+

Note that if you print or render a table without pagination, the +page_by splits are currently rendered as normal row splits. This may +change in future releases.

+
+
+

Adding Group Information +

+

When adding row splits, we get by default label rows for each split +level, for example CAN and USA in the table +above. Besides the column space subsetting, we have now further +subsetted the data for each cell. It is often useful when defining a row +splitting to display information about each row group. In +rtables this is referred to as content information, +i.e. mean() on row 2 is a descendant of CAN +(visible via the indenting, though the table has an underlying tree +structure that is not of importance for this vignette). In order to add +content information and turn the CAN label row into a +content row, the summarize_row_groups() function is +required. By default, the count (nrows()) and percentage of +data relative to the column associated data is calculated:

+
+lyt <- basic_table() %>%
+  split_cols_by("arm") %>%
+  split_cols_by("gender") %>%
+  split_rows_by("country") %>%
+  summarize_row_groups() %>%
+  analyze("age", afun = mean, format = "xx.x")
+
+tbl <- build_table(lyt, df)
+tbl
+
#                   Arm A                     Arm B         
+#            Female        Male        Female        Male   
+# ——————————————————————————————————————————————————————————
+# CAN      45 (46.9%)   64 (61.0%)   46 (50.0%)   62 (57.9%)
+#   mean      38.2         40.3         40.3         38.9   
+# USA      51 (53.1%)   41 (39.0%)   46 (50.0%)   45 (42.1%)
+#   mean      39.2         39.7         38.9         39.6
+

The relative percentage for average age of female Canadians is +calculated as follows:

+
+df_cell <- subset(df, df$country == "CAN" & df$arm == "Arm A" & df$gender == "Female")
+df_col_1 <- subset(df, df$arm == "Arm A" & df$gender == "Female")
+
+c(count = nrow(df_cell), percentage = nrow(df_cell) / nrow(df_col_1))
+
#      count percentage 
+#   45.00000    0.46875
+

so the group percentages per row split sum up to 1 for each +column.

+

We can further split the row space by dividing each country by +handedness:

+
+lyt <- basic_table() %>%
+  split_cols_by("arm") %>%
+  split_cols_by("gender") %>%
+  split_rows_by("country") %>%
+  summarize_row_groups() %>%
+  split_rows_by("handed") %>%
+  analyze("age", afun = mean, format = "xx.x")
+
+tbl <- build_table(lyt, df)
+tbl
+
#                     Arm A                     Arm B         
+#              Female        Male        Female        Male   
+# ————————————————————————————————————————————————————————————
+# CAN        45 (46.9%)   64 (61.0%)   46 (50.0%)   62 (57.9%)
+#   Left                                                      
+#     mean      38.9         40.4         40.3         37.7   
+#   Right                                                     
+#     mean      36.6         40.2         40.2         40.6   
+# USA        51 (53.1%)   41 (39.0%)   46 (50.0%)   45 (42.1%)
+#   Left                                                      
+#     mean      40.4         39.7         39.2         40.1   
+#   Right                                                     
+#     mean      36.9         39.8         38.5         39.0
+

Next, we further add a count and percentage summary for handedness +within each country:

+
+lyt <- basic_table() %>%
+  split_cols_by("arm") %>%
+  split_cols_by("gender") %>%
+  split_rows_by("country") %>%
+  summarize_row_groups() %>%
+  split_rows_by("handed") %>%
+  summarize_row_groups() %>%
+  analyze("age", afun = mean, format = "xx.x")
+
+tbl <- build_table(lyt, df)
+tbl
+
#                     Arm A                     Arm B         
+#              Female        Male        Female        Male   
+# ————————————————————————————————————————————————————————————
+# CAN        45 (46.9%)   64 (61.0%)   46 (50.0%)   62 (57.9%)
+#   Left     32 (33.3%)   42 (40.0%)   26 (28.3%)   37 (34.6%)
+#     mean      38.9         40.4         40.3         37.7   
+#   Right    13 (13.5%)   22 (21.0%)   20 (21.7%)   25 (23.4%)
+#     mean      36.6         40.2         40.2         40.6   
+# USA        51 (53.1%)   41 (39.0%)   46 (50.0%)   45 (42.1%)
+#   Left     34 (35.4%)   19 (18.1%)   25 (27.2%)   25 (23.4%)
+#     mean      40.4         39.7         39.2         40.1   
+#   Right    17 (17.7%)   22 (21.0%)   21 (22.8%)   20 (18.7%)
+#     mean      36.9         39.8         38.5         39.0
+
+
+

Comparing with Other Tabulation Frameworks +

+

There are a number of other table frameworks available in +R, including:

+ +

There are a number of reasons to choose rtables (yet +another tables R package):

+
    +
  • Output tables in ASCII to text files.
  • +
  • Table rendering (ASCII, HTML, etc.) is separate from the data model. +Hence, one always has access to the non-rounded/non-formatted +numbers.
  • +
  • Pagination in both horizontal and vertical directions to meet the +health authority submission requirements.
  • +
  • Cell, row, column, and table reference system.
  • +
  • Titles, footers, and referential footnotes.
  • +
  • Path based access to cell content which is useful for automated +content generation.
  • +
+

More in depth comparisons of the various tabulation frameworks can be +found in the Overview +of table R packages chapter of the Tables in Clinical Trials with R +book compiled by the R Consortium Tables Working Group.

+
+
+

Summary +

+

In this vignette you have learned:

+
    +
  • Every cell has an associated subset of data - this means that much +of tabulation has to do with splitting/subsetting data.
  • +
  • Tables can be described with pre-data using layouts.
  • +
  • Tables are a form of visualization of data.
  • +
+

The other vignettes in the rtables package will provide +more detailed information about the rtables package. We +recommend that you continue with the tabulation_dplyr +vignette which compares the information derived by the table in this +vignette using dplyr.

+
+
+
+ + + + +
+ + + + + + + diff --git a/v0.6.9/articles/introspecting_tables.html b/v0.6.9/articles/introspecting_tables.html new file mode 100644 index 000000000..8e5216b4e --- /dev/null +++ b/v0.6.9/articles/introspecting_tables.html @@ -0,0 +1,475 @@ + + + + + + + + +Introspecting Tables • rtables + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +

The packages used in this vignette are rtables and +dplyr:

+ +
+

Introduction +

+

First, let’s set up a simple table.

+
+lyt <- basic_table() %>%
+  split_cols_by("ARMCD", show_colcounts = TRUE, colcount_format = "N=xx") %>%
+  split_cols_by("STRATA2", show_colcounts = TRUE) %>%
+  split_rows_by("STRATA1") %>%
+  add_overall_col("All") %>%
+  summarize_row_groups() %>%
+  analyze("AGE", afun = max, format = "xx.x")
+
+tbl <- build_table(lyt, ex_adsl)
+tbl
+
#                  ARM A                     ARM B                     ARM C                       
+#                  N=134                     N=134                     N=132                       
+#             S1           S2           S1           S2           S1           S2                  
+#           (N=73)       (N=61)       (N=67)       (N=67)       (N=56)       (N=76)         All    
+# —————————————————————————————————————————————————————————————————————————————————————————————————
+# A       18 (24.7%)   20 (32.8%)   22 (32.8%)   22 (32.8%)   14 (25.0%)   26 (34.2%)   122 (30.5%)
+#   max      40.0         46.0         62.0         50.0         47.0         45.0         62.0    
+# B       28 (38.4%)   19 (31.1%)   19 (28.4%)   26 (38.8%)   18 (32.1%)   25 (32.9%)   135 (33.8%)
+#   max      48.0         47.0         58.0         58.0         46.0         64.0         64.0    
+# C       27 (37.0%)   22 (36.1%)   26 (38.8%)   19 (28.4%)   24 (42.9%)   25 (32.9%)   143 (35.8%)
+#   max      48.0         50.0         48.0         51.0         69.0         50.0         69.0
+
+
+

Getting Started +

+

We can get basic table dimensions, the number of rows, and the number +of columns with the following code:

+
+dim(tbl)
+
# [1] 6 7
+
+nrow(tbl)
+
# [1] 6
+
+ncol(tbl)
+
# [1] 7
+
+
+

Detailed Table Structure +

+

The table_structure() function prints a summary of a +table’s row structure at one of two levels of detail. By default, it +summarizes the structure at the subtable level.

+ +
# [TableTree] STRATA1
+#  [TableTree] A [cont: 1 x 7]
+#   [ElementaryTable] AGE (1 x 7)
+#  [TableTree] B [cont: 1 x 7]
+#   [ElementaryTable] AGE (1 x 7)
+#  [TableTree] C [cont: 1 x 7]
+#   [ElementaryTable] AGE (1 x 7)
+

When the detail argument is set to "row", +however, it provides a more detailed row-level summary which acts as a +useful alternative to how we might normally use the str() +function to interrogate compound nested lists.

+
+table_structure(tbl, detail = "row") # or "subtable"
+
# TableTree: [STRATA1] (STRATA1)
+#   labelrow: [STRATA1] (STRATA1) - <not visible>
+#   children: 
+#     TableTree: [A] (A)
+#       labelrow: [A] (A) - <not visible>
+#       content:
+#         ElementaryTable: [A@content] ()
+#           labelrow: [] () - <not visible>
+#           children: 
+#             ContentRow: [A] (A)
+#       children: 
+#         ElementaryTable: [AGE] (AGE)
+#           labelrow: [AGE] (AGE) - <not visible>
+#           children: 
+#             DataRow: [max] (max)
+#     TableTree: [B] (B)
+#       labelrow: [B] (B) - <not visible>
+#       content:
+#         ElementaryTable: [B@content] ()
+#           labelrow: [] () - <not visible>
+#           children: 
+#             ContentRow: [B] (B)
+#       children: 
+#         ElementaryTable: [AGE] (AGE)
+#           labelrow: [AGE] (AGE) - <not visible>
+#           children: 
+#             DataRow: [max] (max)
+#     TableTree: [C] (C)
+#       labelrow: [C] (C) - <not visible>
+#       content:
+#         ElementaryTable: [C@content] ()
+#           labelrow: [] () - <not visible>
+#           children: 
+#             ContentRow: [C] (C)
+#       children: 
+#         ElementaryTable: [AGE] (AGE)
+#           labelrow: [AGE] (AGE) - <not visible>
+#           children: 
+#             DataRow: [max] (max)
+

Similarly, for columns we can see how the tree is structured with the +following call:

+ +
# [root] (no pos)
+#    [ARMCD] (no pos)
+#      [ARM A] (ARMCD: ARM A)
+#        [S1] (ARMCD: ARM A -> STRATA2: S1)
+#        [S2] (ARMCD: ARM A -> STRATA2: S2)
+#      [ARM B] (ARMCD: ARM B)
+#        [S1] (ARMCD: ARM B -> STRATA2: S1)
+#        [S2] (ARMCD: ARM B -> STRATA2: S2)
+#      [ARM C] (ARMCD: ARM C)
+#        [S1] (ARMCD: ARM C -> STRATA2: S1)
+#        [S2] (ARMCD: ARM C -> STRATA2: S2)
+#    [All] (no pos)
+#      [All] (All: All)
+

Further information about the column structure can be found in the +vignette on col_counts.

+

The make_row_df() and make_col_df() +functions each create a data.frame with a variety of +information about the table’s structure. Most useful for introspection +purposes are the label, name, +abs_rownumber, path and +node_class columns (the remainder of the information in the +returned data.frame is used for pagination)

+
+make_row_df(tbl)[, c("label", "name", "abs_rownumber", "path", "node_class")]
+
#   label name abs_rownumber         path node_class
+# 1     A    A             1 STRATA1,.... ContentRow
+# 2   max  max             2 STRATA1,....    DataRow
+# 3     B    B             3 STRATA1,.... ContentRow
+# 4   max  max             4 STRATA1,....    DataRow
+# 5     C    C             5 STRATA1,.... ContentRow
+# 6   max  max             6 STRATA1,....    DataRow
+

There is also a wrapper function, row_paths() available +for make_row_df to display only the row path structure:

+
+row_paths(tbl)
+
# [[1]]
+# [1] "STRATA1"  "A"        "@content" "A"       
+# 
+# [[2]]
+# [1] "STRATA1" "A"       "AGE"     "max"    
+# 
+# [[3]]
+# [1] "STRATA1"  "B"        "@content" "B"       
+# 
+# [[4]]
+# [1] "STRATA1" "B"       "AGE"     "max"    
+# 
+# [[5]]
+# [1] "STRATA1"  "C"        "@content" "C"       
+# 
+# [[6]]
+# [1] "STRATA1" "C"       "AGE"     "max"
+

By default make_row_df() summarizes only visible rows, +but setting visible_only to FALSE gives us a +structural summary of the table with the full hierarchy of subtables, +including those that are not represented directly by any visible +rows:

+
+make_row_df(tbl, visible_only = FALSE)[, c("label", "name", "abs_rownumber", "path", "node_class")]
+
#    label      name abs_rownumber         path      node_class
+# 1          STRATA1            NA      STRATA1       TableTree
+# 2                A            NA   STRATA1, A       TableTree
+# 3        A@content            NA STRATA1,.... ElementaryTable
+# 4      A         A             1 STRATA1,....      ContentRow
+# 5              AGE            NA STRATA1,.... ElementaryTable
+# 6    max       max             2 STRATA1,....         DataRow
+# 7                B            NA   STRATA1, B       TableTree
+# 8        B@content            NA STRATA1,.... ElementaryTable
+# 9      B         B             3 STRATA1,....      ContentRow
+# 10             AGE            NA STRATA1,.... ElementaryTable
+# 11   max       max             4 STRATA1,....         DataRow
+# 12               C            NA   STRATA1, C       TableTree
+# 13       C@content            NA STRATA1,.... ElementaryTable
+# 14     C         C             5 STRATA1,....      ContentRow
+# 15             AGE            NA STRATA1,.... ElementaryTable
+# 16   max       max             6 STRATA1,....         DataRow
+

make_col_df() similarly accepts +visible_only, though here the meaning is slightly +different, indicating whether only leaf columns should be +summarized (defaults to TRUE) or whether higher level +groups of columns - analogous to subtables in row space - should be +summarized as well.

+
+make_col_df(tbl)[, c("label", "name", "abs_pos", "path", "leaf_indices")]
+
#   label name abs_pos         path leaf_indices
+# 1    S1   S1       1 ARMCD, A....            1
+# 2    S2   S2       2 ARMCD, A....            2
+# 3    S1   S1       3 ARMCD, A....            3
+# 4    S2   S2       4 ARMCD, A....            4
+# 5    S1   S1       5 ARMCD, A....            5
+# 6    S2   S2       6 ARMCD, A....            6
+# 7   All  All       7     All, All            7
+
+make_col_df(tbl, visible_only = FALSE)[, c("label", "name", "abs_pos", "path", "leaf_indices")]
+
#    label  name abs_pos         path leaf_indices
+# 1  ARM A ARM A      NA ARMCD, ARM A         1, 2
+# 2     S1    S1       1 ARMCD, A....            1
+# 3     S2    S2       2 ARMCD, A....            2
+# 4  ARM B ARM B      NA ARMCD, ARM B         3, 4
+# 5     S1    S1       3 ARMCD, A....            3
+# 6     S2    S2       4 ARMCD, A....            4
+# 7  ARM C ARM C      NA ARMCD, ARM C         5, 6
+# 8     S1    S1       5 ARMCD, A....            5
+# 9     S2    S2       6 ARMCD, A....            6
+# 10   All   All       7     All, All            7
+

Similarly, there is wrapper function col_paths() +available, which displays only the column structure:

+
+col_paths(tbl)
+
# [[1]]
+# [1] "ARMCD"   "ARM A"   "STRATA2" "S1"     
+# 
+# [[2]]
+# [1] "ARMCD"   "ARM A"   "STRATA2" "S2"     
+# 
+# [[3]]
+# [1] "ARMCD"   "ARM B"   "STRATA2" "S1"     
+# 
+# [[4]]
+# [1] "ARMCD"   "ARM B"   "STRATA2" "S2"     
+# 
+# [[5]]
+# [1] "ARMCD"   "ARM C"   "STRATA2" "S1"     
+# 
+# [[6]]
+# [1] "ARMCD"   "ARM C"   "STRATA2" "S2"     
+# 
+# [[7]]
+# [1] "All" "All"
+

The row_paths_summary() and +col_paths_summary() functions wrap the respective +make_*_df functions, printing the name, +node_class, and path information (in the row +case), or the label and path information (in +the column case), indented to illustrate table structure:

+ +
# rowname    node_class    path                   
+# ————————————————————————————————————————————————
+# A          ContentRow    STRATA1, A, @content, A
+#   max      DataRow       STRATA1, A, AGE, max   
+# B          ContentRow    STRATA1, B, @content, B
+#   max      DataRow       STRATA1, B, AGE, max   
+# C          ContentRow    STRATA1, C, @content, C
+#   max      DataRow       STRATA1, C, AGE, max
+ +
# label    path                     
+# ——————————————————————————————————
+# ARM A    ARMCD, ARM A             
+#   S1     ARMCD, ARM A, STRATA2, S1
+#   S2     ARMCD, ARM A, STRATA2, S2
+# ARM B    ARMCD, ARM B             
+#   S1     ARMCD, ARM B, STRATA2, S1
+#   S2     ARMCD, ARM B, STRATA2, S2
+# ARM C    ARMCD, ARM C             
+#   S1     ARMCD, ARM C, STRATA2, S1
+#   S2     ARMCD, ARM C, STRATA2, S2
+# All      All, All
+
+
+

Insights on Value Format Structure +

+

We can gain insight into the value formatting structure of a table +using table_shell(), which returns a table with the same +output as print() but with the cell values replaced by +their underlying format strings (e.g. instead of 40.0, +xx.x is displayed, and so on). This is useful for +understanding the structure of the table, and for debugging purposes. +Another useful tool is the value_formats() function which +instead of a table returns a matrix of the format strings for each cell +value in the table.

+

See below the printout for the above examples:

+ +
#                  ARM A                     ARM B                     ARM C                      
+#                  N=134                     N=134                     N=132                      
+#             S1           S2           S1           S2           S1           S2                 
+#           (N=73)       (N=61)       (N=67)       (N=67)       (N=56)       (N=76)        All    
+# ————————————————————————————————————————————————————————————————————————————————————————————————
+# A       xx (xx.x%)   xx (xx.x%)   xx (xx.x%)   xx (xx.x%)   xx (xx.x%)   xx (xx.x%)   xx (xx.x%)
+#   max      xx.x         xx.x         xx.x         xx.x         xx.x         xx.x         xx.x   
+# B       xx (xx.x%)   xx (xx.x%)   xx (xx.x%)   xx (xx.x%)   xx (xx.x%)   xx (xx.x%)   xx (xx.x%)
+#   max      xx.x         xx.x         xx.x         xx.x         xx.x         xx.x         xx.x   
+# C       xx (xx.x%)   xx (xx.x%)   xx (xx.x%)   xx (xx.x%)   xx (xx.x%)   xx (xx.x%)   xx (xx.x%)
+#   max      xx.x         xx.x         xx.x         xx.x         xx.x         xx.x         xx.x
+ +
#     ARM A.S1     ARM A.S2     ARM B.S1     ARM B.S2     ARM C.S1    
+# A   "xx (xx.x%)" "xx (xx.x%)" "xx (xx.x%)" "xx (xx.x%)" "xx (xx.x%)"
+# max "xx.x"       "xx.x"       "xx.x"       "xx.x"       "xx.x"      
+# B   "xx (xx.x%)" "xx (xx.x%)" "xx (xx.x%)" "xx (xx.x%)" "xx (xx.x%)"
+# max "xx.x"       "xx.x"       "xx.x"       "xx.x"       "xx.x"      
+# C   "xx (xx.x%)" "xx (xx.x%)" "xx (xx.x%)" "xx (xx.x%)" "xx (xx.x%)"
+# max "xx.x"       "xx.x"       "xx.x"       "xx.x"       "xx.x"      
+#     ARM C.S2     All         
+# A   "xx (xx.x%)" "xx (xx.x%)"
+# max "xx.x"       "xx.x"      
+# B   "xx (xx.x%)" "xx (xx.x%)"
+# max "xx.x"       "xx.x"      
+# C   "xx (xx.x%)" "xx (xx.x%)"
+# max "xx.x"       "xx.x"
+
+
+

Applications +

+

Knowing the structure of an rtable object is helpful for +retrieving specific values from the table. For examples, see the Path +Based Cell Value Accessing section of the Subsetting and +Manipulating Table Contents vignette.

+

Understanding table structure is also important for post-processing +processes such as sorting and pruning. More details on this are covered +in the Pruning +and Sorting Tables vignette vignette.

+
+
+

Summary +

+

In this vignette you have learned a number of utility functions that +are available for examining the underlying structure of +rtable objects.

+
+
+
+ + + + +
+ + + + + + + diff --git a/v0.6.9/articles/manual_table_construction.html b/v0.6.9/articles/manual_table_construction.html new file mode 100644 index 000000000..5b1dcad94 --- /dev/null +++ b/v0.6.9/articles/manual_table_construction.html @@ -0,0 +1,252 @@ + + + + + + + + +Constructing rtables Manually • rtables + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + + +
+

Overview +

+

The main functions currently associated with rtables +are

+

Tables in rtables can be constructed via the layout or +rtabulate tabulation frameworks or also manually. Currently +manual table construction is the only way to define column spans. The +main functions for manual table constructions are:

+
    +
  • +rtable(): collection of rrow() objects, +column header and default format
  • +
  • +rrow(): collection of rcell() objects and +default format
  • +
  • +rcell(): collection of data objects and cell +format
  • +
+
+
+

Simple Example +

+ +
+tbl <- rtable(
+  header = c("Treatement\nN=100", "Comparison\nN=300"),
+  format = "xx (xx.xx%)",
+  rrow("A", c(104, .2), c(100, .4)),
+  rrow("B", c(23, .4), c(43, .5)),
+  rrow(),
+  rrow("this is a very long section header"),
+  rrow("estimate", rcell(55.23, "xx.xx", colspan = 2)),
+  rrow("95% CI", indent = 1, rcell(c(44.8, 67.4), format = "(xx.x, xx.x)", colspan = 2))
+)
+

Before we go into explaining the individual components used to create +this table we continue with the html conversion of the +rtable() object:

+
+as_html(tbl, width = "80%")
+
+
+
+

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TreatementComparison
N=100N=300
A104 (20.00%)100 (40.00%)
B23 (40.00%)43 (50.00%)
this is a very long section header
estimate55.23
95% CI(44.8, 67.4)
+
+
+

Next, the [ operator lets you access the cell +content.

+
+tbl[1, 1]
+
#      Treatement 
+#        N=100    
+# ————————————————
+# A   104 (20.00%)
+

and to format that cell run format_rcell(tbl[1,1])=.

+

Note that tbl[6, 1] and tbl[6, 2] display +both the same rcell because of the +colspan.

+
+
+
+ + + + +
+ + + + + + + diff --git a/v0.6.9/articles/sorting_pruning.html b/v0.6.9/articles/sorting_pruning.html new file mode 100644 index 000000000..f33639381 --- /dev/null +++ b/v0.6.9/articles/sorting_pruning.html @@ -0,0 +1,1154 @@ + + + + + + + + +Pruning and Sorting Tables • rtables + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + + +
+

Introduction +

+

Often we want to filter or reorder elements of a table in ways that +take into account the table structure. For example:

+
    +
  • Sorting subtables corresponding to factor levels so that most +commonly observed levels occur first in the table.
  • +
  • Sorting rows within a single subtable
  • +
  • Removing subtables which represent 0 observations or which after +other filtering contain 0 rows.
  • +
+
+
+

A Table In Need of Attention +

+
+library(rtables)
+library(dplyr)
+
+raw_lyt <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  split_cols_by("SEX") %>%
+  split_rows_by("RACE") %>%
+  summarize_row_groups() %>%
+  split_rows_by("STRATA1") %>%
+  summarize_row_groups() %>%
+  analyze("AGE")
+
+raw_tbl <- build_table(raw_lyt, DM)
+raw_tbl
+
#                                                                  A: Drug X                                              B: Placebo                                           C: Combination                   
+#                                                 F            M           U      UNDIFFERENTIATED       F            M           U      UNDIFFERENTIATED       F            M           U      UNDIFFERENTIATED
+# ——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
+# ASIAN                                       44 (62.9%)   35 (68.6%)   0 (NA%)       0 (NA%)        37 (66.1%)   31 (62.0%)   0 (NA%)       0 (NA%)        40 (65.6%)   44 (64.7%)   0 (NA%)       0 (NA%)     
+#   A                                         15 (21.4%)   12 (23.5%)   0 (NA%)       0 (NA%)        14 (25.0%)   6 (12.0%)    0 (NA%)       0 (NA%)        15 (24.6%)   16 (23.5%)   0 (NA%)       0 (NA%)     
+#     Mean                                      30.40        34.42        NA             NA            35.43        30.33        NA             NA            37.40        36.25        NA             NA       
+#   B                                         16 (22.9%)   8 (15.7%)    0 (NA%)       0 (NA%)        13 (23.2%)   16 (32.0%)   0 (NA%)       0 (NA%)        10 (16.4%)   12 (17.6%)   0 (NA%)       0 (NA%)     
+#     Mean                                      33.75        34.88        NA             NA            32.46        30.94        NA             NA            33.30        35.92        NA             NA       
+#   C                                         13 (18.6%)   15 (29.4%)   0 (NA%)       0 (NA%)        10 (17.9%)   9 (18.0%)    0 (NA%)       0 (NA%)        15 (24.6%)   16 (23.5%)   0 (NA%)       0 (NA%)     
+#     Mean                                      36.92        35.60        NA             NA            34.00        31.89        NA             NA            33.47        31.38        NA             NA       
+# BLACK OR AFRICAN AMERICAN                   18 (25.7%)   10 (19.6%)   0 (NA%)       0 (NA%)        12 (21.4%)   12 (24.0%)   0 (NA%)       0 (NA%)        13 (21.3%)   14 (20.6%)   0 (NA%)       0 (NA%)     
+#   A                                          5 (7.1%)     1 (2.0%)    0 (NA%)       0 (NA%)         5 (8.9%)     2 (4.0%)    0 (NA%)       0 (NA%)         4 (6.6%)     4 (5.9%)    0 (NA%)       0 (NA%)     
+#     Mean                                      31.20        33.00        NA             NA            28.00        30.00        NA             NA            30.75        36.50        NA             NA       
+#   B                                         7 (10.0%)     3 (5.9%)    0 (NA%)       0 (NA%)         3 (5.4%)     3 (6.0%)    0 (NA%)       0 (NA%)         6 (9.8%)     6 (8.8%)    0 (NA%)       0 (NA%)     
+#     Mean                                      36.14        34.33        NA             NA            29.67        32.00        NA             NA            36.33        31.00        NA             NA       
+#   C                                          6 (8.6%)    6 (11.8%)    0 (NA%)       0 (NA%)         4 (7.1%)    7 (14.0%)    0 (NA%)       0 (NA%)         3 (4.9%)     4 (5.9%)    0 (NA%)       0 (NA%)     
+#     Mean                                      31.33        39.67        NA             NA            34.50        34.00        NA             NA            33.00        36.50        NA             NA       
+# WHITE                                       8 (11.4%)    6 (11.8%)    0 (NA%)       0 (NA%)        7 (12.5%)    7 (14.0%)    0 (NA%)       0 (NA%)        8 (13.1%)    10 (14.7%)   0 (NA%)       0 (NA%)     
+#   A                                          2 (2.9%)     1 (2.0%)    0 (NA%)       0 (NA%)         3 (5.4%)     3 (6.0%)    0 (NA%)       0 (NA%)         1 (1.6%)     5 (7.4%)    0 (NA%)       0 (NA%)     
+#     Mean                                      34.00        45.00        NA             NA            29.33        33.33        NA             NA            35.00        32.80        NA             NA       
+#   B                                          4 (5.7%)     3 (5.9%)    0 (NA%)       0 (NA%)         1 (1.8%)     4 (8.0%)    0 (NA%)       0 (NA%)         3 (4.9%)     1 (1.5%)    0 (NA%)       0 (NA%)     
+#     Mean                                      37.00        43.67        NA             NA            48.00        36.75        NA             NA            34.33        36.00        NA             NA       
+#   C                                          2 (2.9%)     2 (3.9%)    0 (NA%)       0 (NA%)         3 (5.4%)     0 (0.0%)    0 (NA%)       0 (NA%)         4 (6.6%)     4 (5.9%)    0 (NA%)       0 (NA%)     
+#     Mean                                      35.50        44.00        NA             NA            44.67          NA         NA             NA            38.50        35.00        NA             NA       
+# AMERICAN INDIAN OR ALASKA NATIVE             0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)     
+#   A                                          0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)     
+#     Mean                                        NA           NA         NA             NA              NA           NA         NA             NA              NA           NA         NA             NA       
+#   B                                          0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)     
+#     Mean                                        NA           NA         NA             NA              NA           NA         NA             NA              NA           NA         NA             NA       
+#   C                                          0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)     
+#     Mean                                        NA           NA         NA             NA              NA           NA         NA             NA              NA           NA         NA             NA       
+# MULTIPLE                                     0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)     
+#   A                                          0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)     
+#     Mean                                        NA           NA         NA             NA              NA           NA         NA             NA              NA           NA         NA             NA       
+#   B                                          0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)     
+#     Mean                                        NA           NA         NA             NA              NA           NA         NA             NA              NA           NA         NA             NA       
+#   C                                          0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)     
+#     Mean                                        NA           NA         NA             NA              NA           NA         NA             NA              NA           NA         NA             NA       
+# NATIVE HAWAIIAN OR OTHER PACIFIC ISLANDER    0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)     
+#   A                                          0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)     
+#     Mean                                        NA           NA         NA             NA              NA           NA         NA             NA              NA           NA         NA             NA       
+#   B                                          0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)     
+#     Mean                                        NA           NA         NA             NA              NA           NA         NA             NA              NA           NA         NA             NA       
+#   C                                          0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)     
+#     Mean                                        NA           NA         NA             NA              NA           NA         NA             NA              NA           NA         NA             NA       
+# OTHER                                        0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)     
+#   A                                          0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)     
+#     Mean                                        NA           NA         NA             NA              NA           NA         NA             NA              NA           NA         NA             NA       
+#   B                                          0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)     
+#     Mean                                        NA           NA         NA             NA              NA           NA         NA             NA              NA           NA         NA             NA       
+#   C                                          0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)     
+#     Mean                                        NA           NA         NA             NA              NA           NA         NA             NA              NA           NA         NA             NA       
+# UNKNOWN                                      0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)     
+#   A                                          0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)     
+#     Mean                                        NA           NA         NA             NA              NA           NA         NA             NA              NA           NA         NA             NA       
+#   B                                          0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)     
+#     Mean                                        NA           NA         NA             NA              NA           NA         NA             NA              NA           NA         NA             NA       
+#   C                                          0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)         0 (0.0%)     0 (0.0%)    0 (NA%)       0 (NA%)     
+#     Mean                                        NA           NA         NA             NA              NA           NA         NA             NA              NA           NA         NA             NA
+
+
+

Trimming +

+
+

Trimming Rows +

+

Trimming represents a convenience wrapper around simple, direct +subsetting of the rows of a TableTree.

+

We use the trim_rows() function with our table and a +criteria function. All rows where the criteria function returns +TRUE will be removed, and all others will be retained.

+

NOTE: Each row is kept or removed completely +independently, with no awareness of the surrounding structure. This +means, for example, that a subtree could have all its analysis rows +removed and not be removed itself. For structure-aware filtering of a +table, we will use pruning described in the next section.

+

A trimming function accepts a TableRow object +and returns TRUE if the row should be removed.

+

The default trimming function removes rows in which all columns have +no values in them, i.e. that have all NA values or all +0 values:

+
+trim_rows(raw_tbl)
+
#                                                  A: Drug X                                              B: Placebo                                           C: Combination                   
+#                                 F            M           U      UNDIFFERENTIATED       F            M           U      UNDIFFERENTIATED       F            M           U      UNDIFFERENTIATED
+# ——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
+# ASIAN                       44 (62.9%)   35 (68.6%)   0 (NA%)       0 (NA%)        37 (66.1%)   31 (62.0%)   0 (NA%)       0 (NA%)        40 (65.6%)   44 (64.7%)   0 (NA%)       0 (NA%)     
+#   A                         15 (21.4%)   12 (23.5%)   0 (NA%)       0 (NA%)        14 (25.0%)   6 (12.0%)    0 (NA%)       0 (NA%)        15 (24.6%)   16 (23.5%)   0 (NA%)       0 (NA%)     
+#     Mean                      30.40        34.42        NA             NA            35.43        30.33        NA             NA            37.40        36.25        NA             NA       
+#   B                         16 (22.9%)   8 (15.7%)    0 (NA%)       0 (NA%)        13 (23.2%)   16 (32.0%)   0 (NA%)       0 (NA%)        10 (16.4%)   12 (17.6%)   0 (NA%)       0 (NA%)     
+#     Mean                      33.75        34.88        NA             NA            32.46        30.94        NA             NA            33.30        35.92        NA             NA       
+#   C                         13 (18.6%)   15 (29.4%)   0 (NA%)       0 (NA%)        10 (17.9%)   9 (18.0%)    0 (NA%)       0 (NA%)        15 (24.6%)   16 (23.5%)   0 (NA%)       0 (NA%)     
+#     Mean                      36.92        35.60        NA             NA            34.00        31.89        NA             NA            33.47        31.38        NA             NA       
+# BLACK OR AFRICAN AMERICAN   18 (25.7%)   10 (19.6%)   0 (NA%)       0 (NA%)        12 (21.4%)   12 (24.0%)   0 (NA%)       0 (NA%)        13 (21.3%)   14 (20.6%)   0 (NA%)       0 (NA%)     
+#   A                          5 (7.1%)     1 (2.0%)    0 (NA%)       0 (NA%)         5 (8.9%)     2 (4.0%)    0 (NA%)       0 (NA%)         4 (6.6%)     4 (5.9%)    0 (NA%)       0 (NA%)     
+#     Mean                      31.20        33.00        NA             NA            28.00        30.00        NA             NA            30.75        36.50        NA             NA       
+#   B                         7 (10.0%)     3 (5.9%)    0 (NA%)       0 (NA%)         3 (5.4%)     3 (6.0%)    0 (NA%)       0 (NA%)         6 (9.8%)     6 (8.8%)    0 (NA%)       0 (NA%)     
+#     Mean                      36.14        34.33        NA             NA            29.67        32.00        NA             NA            36.33        31.00        NA             NA       
+#   C                          6 (8.6%)    6 (11.8%)    0 (NA%)       0 (NA%)         4 (7.1%)    7 (14.0%)    0 (NA%)       0 (NA%)         3 (4.9%)     4 (5.9%)    0 (NA%)       0 (NA%)     
+#     Mean                      31.33        39.67        NA             NA            34.50        34.00        NA             NA            33.00        36.50        NA             NA       
+# WHITE                       8 (11.4%)    6 (11.8%)    0 (NA%)       0 (NA%)        7 (12.5%)    7 (14.0%)    0 (NA%)       0 (NA%)        8 (13.1%)    10 (14.7%)   0 (NA%)       0 (NA%)     
+#   A                          2 (2.9%)     1 (2.0%)    0 (NA%)       0 (NA%)         3 (5.4%)     3 (6.0%)    0 (NA%)       0 (NA%)         1 (1.6%)     5 (7.4%)    0 (NA%)       0 (NA%)     
+#     Mean                      34.00        45.00        NA             NA            29.33        33.33        NA             NA            35.00        32.80        NA             NA       
+#   B                          4 (5.7%)     3 (5.9%)    0 (NA%)       0 (NA%)         1 (1.8%)     4 (8.0%)    0 (NA%)       0 (NA%)         3 (4.9%)     1 (1.5%)    0 (NA%)       0 (NA%)     
+#     Mean                      37.00        43.67        NA             NA            48.00        36.75        NA             NA            34.33        36.00        NA             NA       
+#   C                          2 (2.9%)     2 (3.9%)    0 (NA%)       0 (NA%)         3 (5.4%)     0 (0.0%)    0 (NA%)       0 (NA%)         4 (6.6%)     4 (5.9%)    0 (NA%)       0 (NA%)     
+#     Mean                      35.50        44.00        NA             NA            44.67          NA         NA             NA            38.50        35.00        NA             NA
+
+
+

Trimming Columns +

+

There are currently no special utilities for trimming columns but we +can remove the empty columns with fairly straightforward column +subsetting using the col_counts() function:

+
+coltrimmed <- raw_tbl[, col_counts(raw_tbl) > 0]
+
# Note: method with signature 'VTableTree#missing#ANY' chosen for function '[',
+#  target signature 'TableTree#missing#logical'.
+#  "VTableTree#ANY#logical" would also be valid
+
+h_coltrimmed <- head(coltrimmed, n = 14)
+h_coltrimmed
+
#                                    A: Drug X                B: Placebo              C: Combination     
+#                                 F            M            F            M            F            M     
+# ———————————————————————————————————————————————————————————————————————————————————————————————————————
+# ASIAN                       44 (62.9%)   35 (68.6%)   37 (66.1%)   31 (62.0%)   40 (65.6%)   44 (64.7%)
+#   A                         15 (21.4%)   12 (23.5%)   14 (25.0%)   6 (12.0%)    15 (24.6%)   16 (23.5%)
+#     Mean                      30.40        34.42        35.43        30.33        37.40        36.25   
+#   B                         16 (22.9%)   8 (15.7%)    13 (23.2%)   16 (32.0%)   10 (16.4%)   12 (17.6%)
+#     Mean                      33.75        34.88        32.46        30.94        33.30        35.92   
+#   C                         13 (18.6%)   15 (29.4%)   10 (17.9%)   9 (18.0%)    15 (24.6%)   16 (23.5%)
+#     Mean                      36.92        35.60        34.00        31.89        33.47        31.38   
+# BLACK OR AFRICAN AMERICAN   18 (25.7%)   10 (19.6%)   12 (21.4%)   12 (24.0%)   13 (21.3%)   14 (20.6%)
+#   A                          5 (7.1%)     1 (2.0%)     5 (8.9%)     2 (4.0%)     4 (6.6%)     4 (5.9%) 
+#     Mean                      31.20        33.00        28.00        30.00        30.75        36.50   
+#   B                         7 (10.0%)     3 (5.9%)     3 (5.4%)     3 (6.0%)     6 (9.8%)     6 (8.8%) 
+#     Mean                      36.14        34.33        29.67        32.00        36.33        31.00   
+#   C                          6 (8.6%)    6 (11.8%)     4 (7.1%)    7 (14.0%)     3 (4.9%)     4 (5.9%) 
+#     Mean                      31.33        39.67        34.50        34.00        33.00        36.50
+

Now, it is interesting to see how this table is structured:

+
+table_structure(h_coltrimmed)
+
# [TableTree] RACE
+#  [TableTree] ASIAN [cont: 1 x 6]
+#   [TableTree] STRATA1
+#    [TableTree] A [cont: 1 x 6]
+#     [ElementaryTable] AGE (1 x 6)
+#    [TableTree] B [cont: 1 x 6]
+#     [ElementaryTable] AGE (1 x 6)
+#    [TableTree] C [cont: 1 x 6]
+#     [ElementaryTable] AGE (1 x 6)
+#  [TableTree] BLACK OR AFRICAN AMERICAN [cont: 1 x 6]
+#   [TableTree] STRATA1
+#    [TableTree] A [cont: 1 x 6]
+#     [ElementaryTable] AGE (1 x 6)
+#    [TableTree] B [cont: 1 x 6]
+#     [ElementaryTable] AGE (1 x 6)
+#    [TableTree] C [cont: 1 x 6]
+#     [ElementaryTable] AGE (1 x 6)
+

For a deeper understanding of the fundamental structures in +rtables, we suggest taking a look at slides 69-76 of this +Slide +deck.

+

In brief, it is important to notice how [TableTree] RACE +is the root of the table that is split (with +split_rows_by("RACE") %>%) into two subtables: +[TableTree] ASIAN [cont: 1 x 6] and +[TableTree] BLACK OR AFRICAN AMERICAN [cont: 1 x 6]. These +are then “described” with summarize_row_groups() %>%, +which creates for every split a “content” table containing 1 row (the 1 +in cont: 1 x 6), which when rendered takes the place of +LabelRow.

+

Each of these two subtables then contain a STRATA1 +table, representing the further split_rows_by("STRATA1") in +the layout, which, similar to the RACE table, is split into +subtables: one for each strata which have similar content tables; Each +individual strata subtable, then, contains an +ElementaryTable (whose children are individual rows) +generated by the analyze("AGE") layout directive, +i.e. [ElementaryTable] AGE (1 x 6).

+

This subtable and row structure is very important for both sorting +and pruning; values in “content” (ContentRow) and “value” +(DataRow) rows use different access functions and they +should be treated differently.

+

Another interesting function that can be used to understand the +connection between row names and their representational path is the +following:

+
+row_paths_summary(h_coltrimmed)
+
# rowname                      node_class    path                                                                
+# ———————————————————————————————————————————————————————————————————————————————————————————————————————————————
+# ASIAN                        ContentRow    RACE, ASIAN, @content, ASIAN                                        
+#   A                          ContentRow    RACE, ASIAN, STRATA1, A, @content, A                                
+#     Mean                     DataRow       RACE, ASIAN, STRATA1, A, AGE, Mean                                  
+#   B                          ContentRow    RACE, ASIAN, STRATA1, B, @content, B                                
+#     Mean                     DataRow       RACE, ASIAN, STRATA1, B, AGE, Mean                                  
+#   C                          ContentRow    RACE, ASIAN, STRATA1, C, @content, C                                
+#     Mean                     DataRow       RACE, ASIAN, STRATA1, C, AGE, Mean                                  
+# BLACK OR AFRICAN AMERICAN    ContentRow    RACE, BLACK OR AFRICAN AMERICAN, @content, BLACK OR AFRICAN AMERICAN
+#   A                          ContentRow    RACE, BLACK OR AFRICAN AMERICAN, STRATA1, A, @content, A            
+#     Mean                     DataRow       RACE, BLACK OR AFRICAN AMERICAN, STRATA1, A, AGE, Mean              
+#   B                          ContentRow    RACE, BLACK OR AFRICAN AMERICAN, STRATA1, B, @content, B            
+#     Mean                     DataRow       RACE, BLACK OR AFRICAN AMERICAN, STRATA1, B, AGE, Mean              
+#   C                          ContentRow    RACE, BLACK OR AFRICAN AMERICAN, STRATA1, C, @content, C            
+#     Mean                     DataRow       RACE, BLACK OR AFRICAN AMERICAN, STRATA1, C, AGE, Mean
+
+
+
+

Pruning +

+

Pruning is similar in outcome to trimming, but more powerful and more +complex, as it takes structure into account.

+

Pruning is applied recursively, in that at each structural unit +(subtable, row) it applies the pruning function both at that level and +to all it’s children (up to a user-specifiable maximum depth).

+

The default pruning function, for example, determines if a subtree is +empty by:

+
    +
  1. Removing all children which contain a single content row which +contains all zeros or all NAs
  2. +
  3. Removing rows which contain either all zeros or all +NAs
  4. +
  5. Removing the full subtree if no unpruned children remain
  6. +
+
+pruned <- prune_table(coltrimmed)
+pruned
+
#                                    A: Drug X                B: Placebo              C: Combination     
+#                                 F            M            F            M            F            M     
+# ———————————————————————————————————————————————————————————————————————————————————————————————————————
+# ASIAN                       44 (62.9%)   35 (68.6%)   37 (66.1%)   31 (62.0%)   40 (65.6%)   44 (64.7%)
+#   A                         15 (21.4%)   12 (23.5%)   14 (25.0%)   6 (12.0%)    15 (24.6%)   16 (23.5%)
+#     Mean                      30.40        34.42        35.43        30.33        37.40        36.25   
+#   B                         16 (22.9%)   8 (15.7%)    13 (23.2%)   16 (32.0%)   10 (16.4%)   12 (17.6%)
+#     Mean                      33.75        34.88        32.46        30.94        33.30        35.92   
+#   C                         13 (18.6%)   15 (29.4%)   10 (17.9%)   9 (18.0%)    15 (24.6%)   16 (23.5%)
+#     Mean                      36.92        35.60        34.00        31.89        33.47        31.38   
+# BLACK OR AFRICAN AMERICAN   18 (25.7%)   10 (19.6%)   12 (21.4%)   12 (24.0%)   13 (21.3%)   14 (20.6%)
+#   A                          5 (7.1%)     1 (2.0%)     5 (8.9%)     2 (4.0%)     4 (6.6%)     4 (5.9%) 
+#     Mean                      31.20        33.00        28.00        30.00        30.75        36.50   
+#   B                         7 (10.0%)     3 (5.9%)     3 (5.4%)     3 (6.0%)     6 (9.8%)     6 (8.8%) 
+#     Mean                      36.14        34.33        29.67        32.00        36.33        31.00   
+#   C                          6 (8.6%)    6 (11.8%)     4 (7.1%)    7 (14.0%)     3 (4.9%)     4 (5.9%) 
+#     Mean                      31.33        39.67        34.50        34.00        33.00        36.50   
+# WHITE                       8 (11.4%)    6 (11.8%)    7 (12.5%)    7 (14.0%)    8 (13.1%)    10 (14.7%)
+#   A                          2 (2.9%)     1 (2.0%)     3 (5.4%)     3 (6.0%)     1 (1.6%)     5 (7.4%) 
+#     Mean                      34.00        45.00        29.33        33.33        35.00        32.80   
+#   B                          4 (5.7%)     3 (5.9%)     1 (1.8%)     4 (8.0%)     3 (4.9%)     1 (1.5%) 
+#     Mean                      37.00        43.67        48.00        36.75        34.33        36.00   
+#   C                          2 (2.9%)     2 (3.9%)     3 (5.4%)     0 (0.0%)     4 (6.6%)     4 (5.9%) 
+#     Mean                      35.50        44.00        44.67          NA         38.50        35.00
+

We can also use the low_obs_pruner() pruning function +constructor to create a pruning function which removes subtrees with +content summaries whose first entries for each column sum or average are +below a specified number. (In the default summaries the first entry per +column is the count).

+
+pruned2 <- prune_table(coltrimmed, low_obs_pruner(10, "mean"))
+pruned2
+
#                   A: Drug X                B: Placebo              C: Combination     
+#                F            M            F            M            F            M     
+# ——————————————————————————————————————————————————————————————————————————————————————
+# ASIAN      44 (62.9%)   35 (68.6%)   37 (66.1%)   31 (62.0%)   40 (65.6%)   44 (64.7%)
+#   A        15 (21.4%)   12 (23.5%)   14 (25.0%)   6 (12.0%)    15 (24.6%)   16 (23.5%)
+#     Mean     30.40        34.42        35.43        30.33        37.40        36.25   
+#   B        16 (22.9%)   8 (15.7%)    13 (23.2%)   16 (32.0%)   10 (16.4%)   12 (17.6%)
+#     Mean     33.75        34.88        32.46        30.94        33.30        35.92   
+#   C        13 (18.6%)   15 (29.4%)   10 (17.9%)   9 (18.0%)    15 (24.6%)   16 (23.5%)
+#     Mean     36.92        35.60        34.00        31.89        33.47        31.38
+

Note that because the pruning is being applied recursively, only the +ASIAN subtree remains because even though the full +BLACK OR AFRICAN AMERICAN subtree encompassed enough +observations, the strata within it did not. We can take care of this by +setting the stop_depth for pruning to 1.

+
+pruned3 <- prune_table(coltrimmed, low_obs_pruner(10, "sum"), stop_depth = 1)
+pruned3
+
#                                    A: Drug X                B: Placebo              C: Combination     
+#                                 F            M            F            M            F            M     
+# ———————————————————————————————————————————————————————————————————————————————————————————————————————
+# ASIAN                       44 (62.9%)   35 (68.6%)   37 (66.1%)   31 (62.0%)   40 (65.6%)   44 (64.7%)
+#   A                         15 (21.4%)   12 (23.5%)   14 (25.0%)   6 (12.0%)    15 (24.6%)   16 (23.5%)
+#     Mean                      30.40        34.42        35.43        30.33        37.40        36.25   
+#   B                         16 (22.9%)   8 (15.7%)    13 (23.2%)   16 (32.0%)   10 (16.4%)   12 (17.6%)
+#     Mean                      33.75        34.88        32.46        30.94        33.30        35.92   
+#   C                         13 (18.6%)   15 (29.4%)   10 (17.9%)   9 (18.0%)    15 (24.6%)   16 (23.5%)
+#     Mean                      36.92        35.60        34.00        31.89        33.47        31.38   
+# BLACK OR AFRICAN AMERICAN   18 (25.7%)   10 (19.6%)   12 (21.4%)   12 (24.0%)   13 (21.3%)   14 (20.6%)
+#   A                          5 (7.1%)     1 (2.0%)     5 (8.9%)     2 (4.0%)     4 (6.6%)     4 (5.9%) 
+#     Mean                      31.20        33.00        28.00        30.00        30.75        36.50   
+#   B                         7 (10.0%)     3 (5.9%)     3 (5.4%)     3 (6.0%)     6 (9.8%)     6 (8.8%) 
+#     Mean                      36.14        34.33        29.67        32.00        36.33        31.00   
+#   C                          6 (8.6%)    6 (11.8%)     4 (7.1%)    7 (14.0%)     3 (4.9%)     4 (5.9%) 
+#     Mean                      31.33        39.67        34.50        34.00        33.00        36.50   
+# WHITE                       8 (11.4%)    6 (11.8%)    7 (12.5%)    7 (14.0%)    8 (13.1%)    10 (14.7%)
+#   A                          2 (2.9%)     1 (2.0%)     3 (5.4%)     3 (6.0%)     1 (1.6%)     5 (7.4%) 
+#     Mean                      34.00        45.00        29.33        33.33        35.00        32.80   
+#   B                          4 (5.7%)     3 (5.9%)     1 (1.8%)     4 (8.0%)     3 (4.9%)     1 (1.5%) 
+#     Mean                      37.00        43.67        48.00        36.75        34.33        36.00   
+#   C                          2 (2.9%)     2 (3.9%)     3 (5.4%)     0 (0.0%)     4 (6.6%)     4 (5.9%) 
+#     Mean                      35.50        44.00        44.67          NA         38.50        35.00
+

We can also see that pruning to a lower number of observations, say, +to a total of 16, with no stop_depth removes +some but not all of the strata from our third race +(WHITE).

+
+pruned4 <- prune_table(coltrimmed, low_obs_pruner(16, "sum"))
+pruned4
+
#                                    A: Drug X                B: Placebo              C: Combination     
+#                                 F            M            F            M            F            M     
+# ———————————————————————————————————————————————————————————————————————————————————————————————————————
+# ASIAN                       44 (62.9%)   35 (68.6%)   37 (66.1%)   31 (62.0%)   40 (65.6%)   44 (64.7%)
+#   A                         15 (21.4%)   12 (23.5%)   14 (25.0%)   6 (12.0%)    15 (24.6%)   16 (23.5%)
+#     Mean                      30.40        34.42        35.43        30.33        37.40        36.25   
+#   B                         16 (22.9%)   8 (15.7%)    13 (23.2%)   16 (32.0%)   10 (16.4%)   12 (17.6%)
+#     Mean                      33.75        34.88        32.46        30.94        33.30        35.92   
+#   C                         13 (18.6%)   15 (29.4%)   10 (17.9%)   9 (18.0%)    15 (24.6%)   16 (23.5%)
+#     Mean                      36.92        35.60        34.00        31.89        33.47        31.38   
+# BLACK OR AFRICAN AMERICAN   18 (25.7%)   10 (19.6%)   12 (21.4%)   12 (24.0%)   13 (21.3%)   14 (20.6%)
+#   A                          5 (7.1%)     1 (2.0%)     5 (8.9%)     2 (4.0%)     4 (6.6%)     4 (5.9%) 
+#     Mean                      31.20        33.00        28.00        30.00        30.75        36.50   
+#   B                         7 (10.0%)     3 (5.9%)     3 (5.4%)     3 (6.0%)     6 (9.8%)     6 (8.8%) 
+#     Mean                      36.14        34.33        29.67        32.00        36.33        31.00   
+#   C                          6 (8.6%)    6 (11.8%)     4 (7.1%)    7 (14.0%)     3 (4.9%)     4 (5.9%) 
+#     Mean                      31.33        39.67        34.50        34.00        33.00        36.50   
+# WHITE                       8 (11.4%)    6 (11.8%)    7 (12.5%)    7 (14.0%)    8 (13.1%)    10 (14.7%)
+#   B                          4 (5.7%)     3 (5.9%)     1 (1.8%)     4 (8.0%)     3 (4.9%)     1 (1.5%) 
+#     Mean                      37.00        43.67        48.00        36.75        34.33        36.00
+
+
+

Sorting +

+
+

Sorting Fundamentals +

+

Sorting of an rtables table is done at a +path, meaning a sort operation will occur at a particular +location within the table, and the direct children of the +element at that path will be reordered. This occurs whether those +children are subtables themselves, or individual rows. Sorting is done +via the sort_at_path() function, which accepts both a (row) +path and a scoring function.

+

A score function accepts a subtree or TableRow +and returns a single orderable (typically numeric) value. Within the +subtable currently being sorted, the children are then reordered by the +value of the score function. Importantly, “content” +(ContentRow) and “values” (DataRow) need to be +treated differently in the scoring function as they are retrieved: the +content of a subtable is retrieved via the +content _table accessor.

+

The cont_n_allcols() scoring function provided by +rtables, works by scoring subtables by the sum of the first +elements in the first row of the subtable’s content table. Note +that this function fails if the child being scored does not have a +content function (i.e., if summarize_row_groups() was not +used at the corresponding point in the layout). We can see this in it’s +definition, below:

+
+cont_n_allcols
+
# function (tt) 
+# {
+#     ctab <- content_table(tt)
+#     if (NROW(ctab) == 0) {
+#         stop("cont_n_allcols score function used at subtable [", 
+#             obj_name(tt), "] that has no content table.")
+#     }
+#     sum(sapply(row_values(tree_children(ctab)[[1]]), function(cv) cv[1]))
+# }
+# <bytecode: 0x55b02dd5ec10>
+# <environment: namespace:rtables>
+

Therefore, a fundamental difference between pruning and sorting is +that sorting occurs at particular places in the table, as defined by a +path.

+

For example, we can sort the strata values (ContentRow) +by observation counts within just the ASIAN subtable:

+
+sort_at_path(pruned, path = c("RACE", "ASIAN", "STRATA1"), scorefun = cont_n_allcols)
+
#                                    A: Drug X                B: Placebo              C: Combination     
+#                                 F            M            F            M            F            M     
+# ———————————————————————————————————————————————————————————————————————————————————————————————————————
+# ASIAN                       44 (62.9%)   35 (68.6%)   37 (66.1%)   31 (62.0%)   40 (65.6%)   44 (64.7%)
+#   A                         15 (21.4%)   12 (23.5%)   14 (25.0%)   6 (12.0%)    15 (24.6%)   16 (23.5%)
+#     Mean                      30.40        34.42        35.43        30.33        37.40        36.25   
+#   C                         13 (18.6%)   15 (29.4%)   10 (17.9%)   9 (18.0%)    15 (24.6%)   16 (23.5%)
+#     Mean                      36.92        35.60        34.00        31.89        33.47        31.38   
+#   B                         16 (22.9%)   8 (15.7%)    13 (23.2%)   16 (32.0%)   10 (16.4%)   12 (17.6%)
+#     Mean                      33.75        34.88        32.46        30.94        33.30        35.92   
+# BLACK OR AFRICAN AMERICAN   18 (25.7%)   10 (19.6%)   12 (21.4%)   12 (24.0%)   13 (21.3%)   14 (20.6%)
+#   A                          5 (7.1%)     1 (2.0%)     5 (8.9%)     2 (4.0%)     4 (6.6%)     4 (5.9%) 
+#     Mean                      31.20        33.00        28.00        30.00        30.75        36.50   
+#   B                         7 (10.0%)     3 (5.9%)     3 (5.4%)     3 (6.0%)     6 (9.8%)     6 (8.8%) 
+#     Mean                      36.14        34.33        29.67        32.00        36.33        31.00   
+#   C                          6 (8.6%)    6 (11.8%)     4 (7.1%)    7 (14.0%)     3 (4.9%)     4 (5.9%) 
+#     Mean                      31.33        39.67        34.50        34.00        33.00        36.50   
+# WHITE                       8 (11.4%)    6 (11.8%)    7 (12.5%)    7 (14.0%)    8 (13.1%)    10 (14.7%)
+#   A                          2 (2.9%)     1 (2.0%)     3 (5.4%)     3 (6.0%)     1 (1.6%)     5 (7.4%) 
+#     Mean                      34.00        45.00        29.33        33.33        35.00        32.80   
+#   B                          4 (5.7%)     3 (5.9%)     1 (1.8%)     4 (8.0%)     3 (4.9%)     1 (1.5%) 
+#     Mean                      37.00        43.67        48.00        36.75        34.33        36.00   
+#   C                          2 (2.9%)     2 (3.9%)     3 (5.4%)     0 (0.0%)     4 (6.6%)     4 (5.9%) 
+#     Mean                      35.50        44.00        44.67          NA         38.50        35.00
+
+# B and C are swapped as the global count (sum of all column counts) of strata C is higher than the one of strata B
+
+
+

Wildcards in Sort Paths +

+

Unlike other uses of pathing (currently), a sorting path can contain +“*“. This indicates that the children of each subtable matching he +* element of the path should be sorted +separately as indicated by the remainder of the path +after the * and the score function.

+

Thus we can extend our sorting of strata within the +ASIAN subtable to all race-specific subtables by using the +wildcard:

+
+sort_at_path(pruned, path = c("RACE", "*", "STRATA1"), scorefun = cont_n_allcols)
+
#                                    A: Drug X                B: Placebo              C: Combination     
+#                                 F            M            F            M            F            M     
+# ———————————————————————————————————————————————————————————————————————————————————————————————————————
+# ASIAN                       44 (62.9%)   35 (68.6%)   37 (66.1%)   31 (62.0%)   40 (65.6%)   44 (64.7%)
+#   A                         15 (21.4%)   12 (23.5%)   14 (25.0%)   6 (12.0%)    15 (24.6%)   16 (23.5%)
+#     Mean                      30.40        34.42        35.43        30.33        37.40        36.25   
+#   C                         13 (18.6%)   15 (29.4%)   10 (17.9%)   9 (18.0%)    15 (24.6%)   16 (23.5%)
+#     Mean                      36.92        35.60        34.00        31.89        33.47        31.38   
+#   B                         16 (22.9%)   8 (15.7%)    13 (23.2%)   16 (32.0%)   10 (16.4%)   12 (17.6%)
+#     Mean                      33.75        34.88        32.46        30.94        33.30        35.92   
+# BLACK OR AFRICAN AMERICAN   18 (25.7%)   10 (19.6%)   12 (21.4%)   12 (24.0%)   13 (21.3%)   14 (20.6%)
+#   C                          6 (8.6%)    6 (11.8%)     4 (7.1%)    7 (14.0%)     3 (4.9%)     4 (5.9%) 
+#     Mean                      31.33        39.67        34.50        34.00        33.00        36.50   
+#   B                         7 (10.0%)     3 (5.9%)     3 (5.4%)     3 (6.0%)     6 (9.8%)     6 (8.8%) 
+#     Mean                      36.14        34.33        29.67        32.00        36.33        31.00   
+#   A                          5 (7.1%)     1 (2.0%)     5 (8.9%)     2 (4.0%)     4 (6.6%)     4 (5.9%) 
+#     Mean                      31.20        33.00        28.00        30.00        30.75        36.50   
+# WHITE                       8 (11.4%)    6 (11.8%)    7 (12.5%)    7 (14.0%)    8 (13.1%)    10 (14.7%)
+#   B                          4 (5.7%)     3 (5.9%)     1 (1.8%)     4 (8.0%)     3 (4.9%)     1 (1.5%) 
+#     Mean                      37.00        43.67        48.00        36.75        34.33        36.00   
+#   A                          2 (2.9%)     1 (2.0%)     3 (5.4%)     3 (6.0%)     1 (1.6%)     5 (7.4%) 
+#     Mean                      34.00        45.00        29.33        33.33        35.00        32.80   
+#   C                          2 (2.9%)     2 (3.9%)     3 (5.4%)     0 (0.0%)     4 (6.6%)     4 (5.9%) 
+#     Mean                      35.50        44.00        44.67          NA         38.50        35.00
+
+# All subtables, i.e. ASIAN, BLACK..., and WHITE, are reordered separately
+

The above is equivalent to separately calling the following:

+
+tmptbl <- sort_at_path(pruned, path = c("RACE", "ASIAN", "STRATA1"), scorefun = cont_n_allcols)
+tmptbl <- sort_at_path(tmptbl, path = c("RACE", "BLACK OR AFRICAN AMERICAN", "STRATA1"), scorefun = cont_n_allcols)
+tmptbl <- sort_at_path(tmptbl, path = c("RACE", "WHITE", "STRATA1"), scorefun = cont_n_allcols)
+tmptbl
+
#                                    A: Drug X                B: Placebo              C: Combination     
+#                                 F            M            F            M            F            M     
+# ———————————————————————————————————————————————————————————————————————————————————————————————————————
+# ASIAN                       44 (62.9%)   35 (68.6%)   37 (66.1%)   31 (62.0%)   40 (65.6%)   44 (64.7%)
+#   A                         15 (21.4%)   12 (23.5%)   14 (25.0%)   6 (12.0%)    15 (24.6%)   16 (23.5%)
+#     Mean                      30.40        34.42        35.43        30.33        37.40        36.25   
+#   C                         13 (18.6%)   15 (29.4%)   10 (17.9%)   9 (18.0%)    15 (24.6%)   16 (23.5%)
+#     Mean                      36.92        35.60        34.00        31.89        33.47        31.38   
+#   B                         16 (22.9%)   8 (15.7%)    13 (23.2%)   16 (32.0%)   10 (16.4%)   12 (17.6%)
+#     Mean                      33.75        34.88        32.46        30.94        33.30        35.92   
+# BLACK OR AFRICAN AMERICAN   18 (25.7%)   10 (19.6%)   12 (21.4%)   12 (24.0%)   13 (21.3%)   14 (20.6%)
+#   C                          6 (8.6%)    6 (11.8%)     4 (7.1%)    7 (14.0%)     3 (4.9%)     4 (5.9%) 
+#     Mean                      31.33        39.67        34.50        34.00        33.00        36.50   
+#   B                         7 (10.0%)     3 (5.9%)     3 (5.4%)     3 (6.0%)     6 (9.8%)     6 (8.8%) 
+#     Mean                      36.14        34.33        29.67        32.00        36.33        31.00   
+#   A                          5 (7.1%)     1 (2.0%)     5 (8.9%)     2 (4.0%)     4 (6.6%)     4 (5.9%) 
+#     Mean                      31.20        33.00        28.00        30.00        30.75        36.50   
+# WHITE                       8 (11.4%)    6 (11.8%)    7 (12.5%)    7 (14.0%)    8 (13.1%)    10 (14.7%)
+#   B                          4 (5.7%)     3 (5.9%)     1 (1.8%)     4 (8.0%)     3 (4.9%)     1 (1.5%) 
+#     Mean                      37.00        43.67        48.00        36.75        34.33        36.00   
+#   A                          2 (2.9%)     1 (2.0%)     3 (5.4%)     3 (6.0%)     1 (1.6%)     5 (7.4%) 
+#     Mean                      34.00        45.00        29.33        33.33        35.00        32.80   
+#   C                          2 (2.9%)     2 (3.9%)     3 (5.4%)     0 (0.0%)     4 (6.6%)     4 (5.9%) 
+#     Mean                      35.50        44.00        44.67          NA         38.50        35.00
+

It is possible to understand better pathing with +table_structure() that highlights the tree-like structure +and the node names:

+ +
# [TableTree] RACE
+#  [TableTree] ASIAN [cont: 1 x 6]
+#   [TableTree] STRATA1
+#    [TableTree] A [cont: 1 x 6]
+#     [ElementaryTable] AGE (1 x 6)
+#    [TableTree] B [cont: 1 x 6]
+#     [ElementaryTable] AGE (1 x 6)
+#    [TableTree] C [cont: 1 x 6]
+#     [ElementaryTable] AGE (1 x 6)
+#  [TableTree] BLACK OR AFRICAN AMERICAN [cont: 1 x 6]
+#   [TableTree] STRATA1
+#    [TableTree] A [cont: 1 x 6]
+#     [ElementaryTable] AGE (1 x 6)
+#    [TableTree] B [cont: 1 x 6]
+#     [ElementaryTable] AGE (1 x 6)
+#    [TableTree] C [cont: 1 x 6]
+#     [ElementaryTable] AGE (1 x 6)
+#  [TableTree] WHITE [cont: 1 x 6]
+#   [TableTree] STRATA1
+#    [TableTree] A [cont: 1 x 6]
+#     [ElementaryTable] AGE (1 x 6)
+#    [TableTree] B [cont: 1 x 6]
+#     [ElementaryTable] AGE (1 x 6)
+#    [TableTree] C [cont: 1 x 6]
+#     [ElementaryTable] AGE (1 x 6)
+

or with row_paths_summary:

+ +
# rowname                      node_class    path                                                                
+# ———————————————————————————————————————————————————————————————————————————————————————————————————————————————
+# ASIAN                        ContentRow    RACE, ASIAN, @content, ASIAN                                        
+#   A                          ContentRow    RACE, ASIAN, STRATA1, A, @content, A                                
+#     Mean                     DataRow       RACE, ASIAN, STRATA1, A, AGE, Mean                                  
+#   B                          ContentRow    RACE, ASIAN, STRATA1, B, @content, B                                
+#     Mean                     DataRow       RACE, ASIAN, STRATA1, B, AGE, Mean                                  
+#   C                          ContentRow    RACE, ASIAN, STRATA1, C, @content, C                                
+#     Mean                     DataRow       RACE, ASIAN, STRATA1, C, AGE, Mean                                  
+# BLACK OR AFRICAN AMERICAN    ContentRow    RACE, BLACK OR AFRICAN AMERICAN, @content, BLACK OR AFRICAN AMERICAN
+#   A                          ContentRow    RACE, BLACK OR AFRICAN AMERICAN, STRATA1, A, @content, A            
+#     Mean                     DataRow       RACE, BLACK OR AFRICAN AMERICAN, STRATA1, A, AGE, Mean              
+#   B                          ContentRow    RACE, BLACK OR AFRICAN AMERICAN, STRATA1, B, @content, B            
+#     Mean                     DataRow       RACE, BLACK OR AFRICAN AMERICAN, STRATA1, B, AGE, Mean              
+#   C                          ContentRow    RACE, BLACK OR AFRICAN AMERICAN, STRATA1, C, @content, C            
+#     Mean                     DataRow       RACE, BLACK OR AFRICAN AMERICAN, STRATA1, C, AGE, Mean              
+# WHITE                        ContentRow    RACE, WHITE, @content, WHITE                                        
+#   A                          ContentRow    RACE, WHITE, STRATA1, A, @content, A                                
+#     Mean                     DataRow       RACE, WHITE, STRATA1, A, AGE, Mean                                  
+#   B                          ContentRow    RACE, WHITE, STRATA1, B, @content, B                                
+#     Mean                     DataRow       RACE, WHITE, STRATA1, B, AGE, Mean                                  
+#   C                          ContentRow    RACE, WHITE, STRATA1, C, @content, C                                
+#     Mean                     DataRow       RACE, WHITE, STRATA1, C, AGE, Mean
+

Note in the latter we see content rows as those with paths following +@content, e.g., ASIAN, @content, ASIAN. The +first of these at a given path (i.e., +<path>, @content, <> are the rows which will be +used by the scoring functions which begin with cont_.

+

We can directly sort the ethnicity by observations in increasing +order:

+
+ethsort <- sort_at_path(pruned, path = c("RACE"), scorefun = cont_n_allcols, decreasing = FALSE)
+ethsort
+
#                                    A: Drug X                B: Placebo              C: Combination     
+#                                 F            M            F            M            F            M     
+# ———————————————————————————————————————————————————————————————————————————————————————————————————————
+# WHITE                       8 (11.4%)    6 (11.8%)    7 (12.5%)    7 (14.0%)    8 (13.1%)    10 (14.7%)
+#   A                          2 (2.9%)     1 (2.0%)     3 (5.4%)     3 (6.0%)     1 (1.6%)     5 (7.4%) 
+#     Mean                      34.00        45.00        29.33        33.33        35.00        32.80   
+#   B                          4 (5.7%)     3 (5.9%)     1 (1.8%)     4 (8.0%)     3 (4.9%)     1 (1.5%) 
+#     Mean                      37.00        43.67        48.00        36.75        34.33        36.00   
+#   C                          2 (2.9%)     2 (3.9%)     3 (5.4%)     0 (0.0%)     4 (6.6%)     4 (5.9%) 
+#     Mean                      35.50        44.00        44.67          NA         38.50        35.00   
+# BLACK OR AFRICAN AMERICAN   18 (25.7%)   10 (19.6%)   12 (21.4%)   12 (24.0%)   13 (21.3%)   14 (20.6%)
+#   A                          5 (7.1%)     1 (2.0%)     5 (8.9%)     2 (4.0%)     4 (6.6%)     4 (5.9%) 
+#     Mean                      31.20        33.00        28.00        30.00        30.75        36.50   
+#   B                         7 (10.0%)     3 (5.9%)     3 (5.4%)     3 (6.0%)     6 (9.8%)     6 (8.8%) 
+#     Mean                      36.14        34.33        29.67        32.00        36.33        31.00   
+#   C                          6 (8.6%)    6 (11.8%)     4 (7.1%)    7 (14.0%)     3 (4.9%)     4 (5.9%) 
+#     Mean                      31.33        39.67        34.50        34.00        33.00        36.50   
+# ASIAN                       44 (62.9%)   35 (68.6%)   37 (66.1%)   31 (62.0%)   40 (65.6%)   44 (64.7%)
+#   A                         15 (21.4%)   12 (23.5%)   14 (25.0%)   6 (12.0%)    15 (24.6%)   16 (23.5%)
+#     Mean                      30.40        34.42        35.43        30.33        37.40        36.25   
+#   B                         16 (22.9%)   8 (15.7%)    13 (23.2%)   16 (32.0%)   10 (16.4%)   12 (17.6%)
+#     Mean                      33.75        34.88        32.46        30.94        33.30        35.92   
+#   C                         13 (18.6%)   15 (29.4%)   10 (17.9%)   9 (18.0%)    15 (24.6%)   16 (23.5%)
+#     Mean                      36.92        35.60        34.00        31.89        33.47        31.38
+

Within each ethnicity separately, sort the strata by number of +females in arm C (i.e. column position 5):

+
+sort_at_path(pruned, path = c("RACE", "*", "STRATA1"), cont_n_onecol(5))
+
#                                    A: Drug X                B: Placebo              C: Combination     
+#                                 F            M            F            M            F            M     
+# ———————————————————————————————————————————————————————————————————————————————————————————————————————
+# ASIAN                       44 (62.9%)   35 (68.6%)   37 (66.1%)   31 (62.0%)   40 (65.6%)   44 (64.7%)
+#   A                         15 (21.4%)   12 (23.5%)   14 (25.0%)   6 (12.0%)    15 (24.6%)   16 (23.5%)
+#     Mean                      30.40        34.42        35.43        30.33        37.40        36.25   
+#   C                         13 (18.6%)   15 (29.4%)   10 (17.9%)   9 (18.0%)    15 (24.6%)   16 (23.5%)
+#     Mean                      36.92        35.60        34.00        31.89        33.47        31.38   
+#   B                         16 (22.9%)   8 (15.7%)    13 (23.2%)   16 (32.0%)   10 (16.4%)   12 (17.6%)
+#     Mean                      33.75        34.88        32.46        30.94        33.30        35.92   
+# BLACK OR AFRICAN AMERICAN   18 (25.7%)   10 (19.6%)   12 (21.4%)   12 (24.0%)   13 (21.3%)   14 (20.6%)
+#   B                         7 (10.0%)     3 (5.9%)     3 (5.4%)     3 (6.0%)     6 (9.8%)     6 (8.8%) 
+#     Mean                      36.14        34.33        29.67        32.00        36.33        31.00   
+#   A                          5 (7.1%)     1 (2.0%)     5 (8.9%)     2 (4.0%)     4 (6.6%)     4 (5.9%) 
+#     Mean                      31.20        33.00        28.00        30.00        30.75        36.50   
+#   C                          6 (8.6%)    6 (11.8%)     4 (7.1%)    7 (14.0%)     3 (4.9%)     4 (5.9%) 
+#     Mean                      31.33        39.67        34.50        34.00        33.00        36.50   
+# WHITE                       8 (11.4%)    6 (11.8%)    7 (12.5%)    7 (14.0%)    8 (13.1%)    10 (14.7%)
+#   C                          2 (2.9%)     2 (3.9%)     3 (5.4%)     0 (0.0%)     4 (6.6%)     4 (5.9%) 
+#     Mean                      35.50        44.00        44.67          NA         38.50        35.00   
+#   B                          4 (5.7%)     3 (5.9%)     1 (1.8%)     4 (8.0%)     3 (4.9%)     1 (1.5%) 
+#     Mean                      37.00        43.67        48.00        36.75        34.33        36.00   
+#   A                          2 (2.9%)     1 (2.0%)     3 (5.4%)     3 (6.0%)     1 (1.6%)     5 (7.4%) 
+#     Mean                      34.00        45.00        29.33        33.33        35.00        32.80
+
+
+

Sorting Within an Analysis Subtable +

+

When sorting within an analysis subtable (e.g., the subtable +generated when your analysis function generates more than one row per +group of data), the name of that subtable (generally the name of the +variable being analyzed) must appear in the path, even if +the variable label is not displayed when the table is +printed.

+

To show the differences between sorting an analysis subtable +(DataRow), and a content subtable +(ContentRow), we modify and prune (as before) a similar raw +table as before:

+
+more_analysis_fnc <- function(x) {
+  in_rows(
+    "median" = median(x),
+    "mean" = mean(x),
+    .formats = "xx.x"
+  )
+}
+
+raw_lyt <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  split_rows_by(
+    "RACE",
+    split_fun = drop_and_remove_levels("WHITE") # dropping WHITE levels
+  ) %>%
+  summarize_row_groups() %>%
+  split_rows_by("STRATA1") %>%
+  summarize_row_groups() %>%
+  analyze("AGE", afun = more_analysis_fnc)
+
+tbl <- build_table(raw_lyt, DM) %>%
+  prune_table() %>%
+  print()
+
#                             A: Drug X    B: Placebo   C: Combination
+# ————————————————————————————————————————————————————————————————————
+# ASIAN                       79 (65.3%)   68 (64.2%)     84 (65.1%)  
+#   A                         27 (22.3%)   20 (18.9%)     31 (24.0%)  
+#     median                     30.0         33.0           36.0     
+#     mean                       32.2         33.9           36.8     
+#   B                         24 (19.8%)   29 (27.4%)     22 (17.1%)  
+#     median                     32.5         32.0           34.0     
+#     mean                       34.1         31.6           34.7     
+#   C                         28 (23.1%)   19 (17.9%)     31 (24.0%)  
+#     median                     36.5         34.0           33.0     
+#     mean                       36.2         33.0           32.4     
+# BLACK OR AFRICAN AMERICAN   28 (23.1%)   24 (22.6%)     27 (20.9%)  
+#   A                          6 (5.0%)     7 (6.6%)       8 (6.2%)   
+#     median                     32.0         29.0           32.5     
+#     mean                       31.5         28.6           33.6     
+#   B                         10 (8.3%)     6 (5.7%)      12 (9.3%)   
+#     median                     33.0         30.0           33.5     
+#     mean                       35.6         30.8           33.7     
+#   C                         12 (9.9%)    11 (10.4%)      7 (5.4%)   
+#     median                     33.0         36.0           32.0     
+#     mean                       35.5         34.2           35.0
+

What should we do now if we want to sort each median and mean in each +of the strata variables? We need to write a custom score function as the +ready-made ones at the moment work only with content nodes +(content_table() access function for +cont_n_allcols() and cont_n_onecol(), of which +we will talk in a moment). But before that, we need to think about what +are we ordering, i.e. we need to specify the right path. We suggest +looking at the structure first with table_structure() or +row_paths_summary().

+
+table_structure(tbl) # Direct inspection into the tree-like structure of rtables
+
# [TableTree] RACE
+#  [TableTree] ASIAN [cont: 1 x 3]
+#   [TableTree] STRATA1
+#    [TableTree] A [cont: 1 x 3]
+#     [ElementaryTable] AGE (2 x 3)
+#    [TableTree] B [cont: 1 x 3]
+#     [ElementaryTable] AGE (2 x 3)
+#    [TableTree] C [cont: 1 x 3]
+#     [ElementaryTable] AGE (2 x 3)
+#  [TableTree] BLACK OR AFRICAN AMERICAN [cont: 1 x 3]
+#   [TableTree] STRATA1
+#    [TableTree] A [cont: 1 x 3]
+#     [ElementaryTable] AGE (2 x 3)
+#    [TableTree] B [cont: 1 x 3]
+#     [ElementaryTable] AGE (2 x 3)
+#    [TableTree] C [cont: 1 x 3]
+#     [ElementaryTable] AGE (2 x 3)
+

We see that to order all of the AGE nodes we need to get +there with something like this: +RACE, ASIAN, STRATA1, A, AGE and no more as the next level +is what we need to sort. But we see now that this path would sort only +the first group. We need wildcards: +RACE, *, STRATA1, *, AGE.

+

Now, we have found a way to select relevant paths that we want to +sort. We want to construct a scoring function that works on the median +and mean and sort them. To do so, we may want to enter our scoring +function with browser() to see what is fed to it and try to +retrieve the single value that is to be returned to do the sorting. We +allow the user to experiment with this, while here we show a possible +solution that considers summing all the column values that are retrieved +with row_values(tt) from the subtable that is fed to the +function itself. Note that any score function should be defined as +having a subtable tt as a unique input parameter and a +single numeric value as output.

+
+scorefun <- function(tt) {
+  # Here we could use browser()
+  sum(unlist(row_values(tt)))
+}
+sort_at_path(tbl, c("RACE", "*", "STRATA1", "*", "AGE"), scorefun)
+
#                             A: Drug X    B: Placebo   C: Combination
+# ————————————————————————————————————————————————————————————————————
+# ASIAN                       79 (65.3%)   68 (64.2%)     84 (65.1%)  
+#   A                         27 (22.3%)   20 (18.9%)     31 (24.0%)  
+#     mean                       32.2         33.9           36.8     
+#     median                     30.0         33.0           36.0     
+#   B                         24 (19.8%)   29 (27.4%)     22 (17.1%)  
+#     mean                       34.1         31.6           34.7     
+#     median                     32.5         32.0           34.0     
+#   C                         28 (23.1%)   19 (17.9%)     31 (24.0%)  
+#     median                     36.5         34.0           33.0     
+#     mean                       36.2         33.0           32.4     
+# BLACK OR AFRICAN AMERICAN   28 (23.1%)   24 (22.6%)     27 (20.9%)  
+#   A                          6 (5.0%)     7 (6.6%)       8 (6.2%)   
+#     mean                       31.5         28.6           33.6     
+#     median                     32.0         29.0           32.5     
+#   B                         10 (8.3%)     6 (5.7%)      12 (9.3%)   
+#     mean                       35.6         30.8           33.7     
+#     median                     33.0         30.0           33.5     
+#   C                         12 (9.9%)    11 (10.4%)      7 (5.4%)   
+#     mean                       35.5         34.2           35.0     
+#     median                     33.0         36.0           32.0
+

To help the user visualize what is happening in the score function we +show here an example of its exploration from the debugging:

+
> sort_at_path(tbl, c("RACE", "*", "STRATA1", "*", "AGE"), scorefun)
+Called from: scorefun(x)
+Browse[1]> tt ### THIS IS THE LEAF LEVEL -> DataRow ###
+[DataRow indent_mod 0]: median   30.0   33.0   36.0
+Browse[1]> row_values(tt) ### Extraction of values -> It will be a named list! ###
+$`A: Drug X`
+[1] 30
+
+$`B: Placebo`
+[1] 33
+
+$`C: Combination`
+[1] 36
+
+Browse[1]> sum(unlist(row_values(tt))) ### Final value we want to give back to sort_at_path ###
+[1] 99
+

We can see how powerful and pragmatic it might be to change the +sorting principles from within the custom scoring function. We show this +by selecting a specific column to sort. Looking at the pre-defined +function cont_n_onecol() gives us an insight into how to +proceed.

+
+cont_n_onecol
+
# function (j) 
+# {
+#     function(tt) {
+#         ctab <- content_table(tt)
+#         if (NROW(ctab) == 0) {
+#             stop("cont_n_allcols score function used at subtable [", 
+#                 obj_name(tt), "] that has no content table.")
+#         }
+#         row_values(tree_children(ctab)[[1]])[[j]][1]
+#     }
+# }
+# <bytecode: 0x55b02d7d9480>
+# <environment: namespace:rtables>
+

We see that a similar function to cont_n_allcols() is +wrapped by one that allows a parameter j to be used to +select a specific column. We will do the same here for selecting which +column we want to sort.

+
+scorefun_onecol <- function(colpath) {
+  function(tt) {
+    # Here we could use browser()
+    unlist(cell_values(tt, colpath = colpath), use.names = FALSE)[1] # Modified to lose the list names
+  }
+}
+sort_at_path(tbl, c("RACE", "*", "STRATA1", "*", "AGE"), scorefun_onecol(colpath = c("ARM", "A: Drug X")))
+
#                             A: Drug X    B: Placebo   C: Combination
+# ————————————————————————————————————————————————————————————————————
+# ASIAN                       79 (65.3%)   68 (64.2%)     84 (65.1%)  
+#   A                         27 (22.3%)   20 (18.9%)     31 (24.0%)  
+#     mean                       32.2         33.9           36.8     
+#     median                     30.0         33.0           36.0     
+#   B                         24 (19.8%)   29 (27.4%)     22 (17.1%)  
+#     mean                       34.1         31.6           34.7     
+#     median                     32.5         32.0           34.0     
+#   C                         28 (23.1%)   19 (17.9%)     31 (24.0%)  
+#     median                     36.5         34.0           33.0     
+#     mean                       36.2         33.0           32.4     
+# BLACK OR AFRICAN AMERICAN   28 (23.1%)   24 (22.6%)     27 (20.9%)  
+#   A                          6 (5.0%)     7 (6.6%)       8 (6.2%)   
+#     median                     32.0         29.0           32.5     
+#     mean                       31.5         28.6           33.6     
+#   B                         10 (8.3%)     6 (5.7%)      12 (9.3%)   
+#     mean                       35.6         30.8           33.7     
+#     median                     33.0         30.0           33.5     
+#   C                         12 (9.9%)    11 (10.4%)      7 (5.4%)   
+#     mean                       35.5         34.2           35.0     
+#     median                     33.0         36.0           32.0
+

In the above table we see that the mean and median rows are reordered +by their values in the first column, compared to the raw table, as +desired.

+

With this function we can also do the same for columns that are +nested within larger splits:

+
+# Simpler table
+tbl <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  split_cols_by("SEX",
+    split_fun = drop_and_remove_levels(c("U", "UNDIFFERENTIATED"))
+  ) %>%
+  analyze("AGE", afun = more_analysis_fnc) %>%
+  build_table(DM) %>%
+  prune_table() %>%
+  print()
+
#           A: Drug X      B: Placebo      C: Combination  
+#            F      M       F       M        F         M   
+# —————————————————————————————————————————————————————————
+# median   32.0    35.0   33.0    31.0     35.0      32.0  
+# mean     33.7    36.5   33.8    32.1     34.9      34.3
+
+sort_at_path(tbl, c("AGE"), scorefun_onecol(colpath = c("ARM", "B: Placebo", "SEX", "F")))
+
#           A: Drug X      B: Placebo      C: Combination  
+#            F      M       F       M        F         M   
+# —————————————————————————————————————————————————————————
+# mean     33.7    36.5   33.8    32.1     34.9      34.3  
+# median   32.0    35.0   33.0    31.0     35.0      32.0
+
+
+
+

Writing Custom Pruning Criteria and Scoring Functions +

+

Pruning criteria and scoring functions map TableTree or +TableRow objects to a Boolean value (for pruning criteria) +or a sortable scalar value (scoring functions). To do this we currently +need to interact with the structure of the objects more than usual. +Indeed, we showed already how sorting can be very complicated if the +concept of tree-like structure and pathing is not well understood. It is +important though to have in mind the following functions that can be +used in each pruning or sorting function to retrieve the relevant +information from the table.

+
+

Useful Functions and Accessors +

+
    +
  • +cell_values() - Retrieves a named list of a +TableRow or TableTree object’s values +
      +
    • accepts both rowpath and colpath to +restrict which cell values are returned
    • +
    +
  • +
  • +obj_name() - Retrieves the name of an object. Note this +can differ from the label that is displayed (if any is) when printing. +This will match the element in the path.
  • +
  • +obj_label() - Retrieves the display label of an object. +Note this can differ from the name that appears in the path.
  • +
  • +content_table() - Retrieves a TableTree +object’s content table (which contains its summary rows).
  • +
  • +tree_children() - Retrieves a TableTree +object’s direct children (either subtables, rows or possibly a mix +thereof, though that should not happen in practice)
  • +
+
+
+

Example Custom Scoring Functions +

+
+

Sort by a character “score” +

+

In this case, for convenience/simplicity, we use the name of the +table element but any logic which returns a single string could be used +here.

+

We sort the ethnicity by alphabetical order (in practice undoing our +previous sorting by ethnicity above).

+
+silly_name_scorer <- function(tt) {
+  nm <- obj_name(tt)
+  print(nm)
+  nm
+}
+
+sort_at_path(ethsort, "RACE", silly_name_scorer) # Now, it is sorted alphabetically!
+
# [1] "WHITE"
+# [1] "BLACK OR AFRICAN AMERICAN"
+# [1] "ASIAN"
+
#                                    A: Drug X                B: Placebo              C: Combination     
+#                                 F            M            F            M            F            M     
+# ———————————————————————————————————————————————————————————————————————————————————————————————————————
+# ASIAN                       44 (62.9%)   35 (68.6%)   37 (66.1%)   31 (62.0%)   40 (65.6%)   44 (64.7%)
+#   A                         15 (21.4%)   12 (23.5%)   14 (25.0%)   6 (12.0%)    15 (24.6%)   16 (23.5%)
+#     Mean                      30.40        34.42        35.43        30.33        37.40        36.25   
+#   B                         16 (22.9%)   8 (15.7%)    13 (23.2%)   16 (32.0%)   10 (16.4%)   12 (17.6%)
+#     Mean                      33.75        34.88        32.46        30.94        33.30        35.92   
+#   C                         13 (18.6%)   15 (29.4%)   10 (17.9%)   9 (18.0%)    15 (24.6%)   16 (23.5%)
+#     Mean                      36.92        35.60        34.00        31.89        33.47        31.38   
+# BLACK OR AFRICAN AMERICAN   18 (25.7%)   10 (19.6%)   12 (21.4%)   12 (24.0%)   13 (21.3%)   14 (20.6%)
+#   A                          5 (7.1%)     1 (2.0%)     5 (8.9%)     2 (4.0%)     4 (6.6%)     4 (5.9%) 
+#     Mean                      31.20        33.00        28.00        30.00        30.75        36.50   
+#   B                         7 (10.0%)     3 (5.9%)     3 (5.4%)     3 (6.0%)     6 (9.8%)     6 (8.8%) 
+#     Mean                      36.14        34.33        29.67        32.00        36.33        31.00   
+#   C                          6 (8.6%)    6 (11.8%)     4 (7.1%)    7 (14.0%)     3 (4.9%)     4 (5.9%) 
+#     Mean                      31.33        39.67        34.50        34.00        33.00        36.50   
+# WHITE                       8 (11.4%)    6 (11.8%)    7 (12.5%)    7 (14.0%)    8 (13.1%)    10 (14.7%)
+#   A                          2 (2.9%)     1 (2.0%)     3 (5.4%)     3 (6.0%)     1 (1.6%)     5 (7.4%) 
+#     Mean                      34.00        45.00        29.33        33.33        35.00        32.80   
+#   B                          4 (5.7%)     3 (5.9%)     1 (1.8%)     4 (8.0%)     3 (4.9%)     1 (1.5%) 
+#     Mean                      37.00        43.67        48.00        36.75        34.33        36.00   
+#   C                          2 (2.9%)     2 (3.9%)     3 (5.4%)     0 (0.0%)     4 (6.6%)     4 (5.9%) 
+#     Mean                      35.50        44.00        44.67          NA         38.50        35.00
+

NOTE: Generally this would be more appropriately +done using the reorder_split_levels() function within the +layout rather than as a sort post-processing step, but other character +scorers may or may not map as easily to layouting directives.

+
+
+

Sort by the Percent Difference in Counts Between Genders in Arm +C +

+

We need the F and M percents, only for Arm C (i.e. columns 5 and 6), +differenced.

+

We will sort the strata within each +ethnicity by the percent difference in counts between +males and females in arm C.

+

Note: this is not statistically meaningful at all, and is in fact a +terrible idea because it reorders the strata seemingly (but not) at +random within each race, but illustrates the various things we need to +do inside custom sorting functions.

+
+silly_gender_diffcount <- function(tt) {
+  ## (1st) content row has same name as object (STRATA1 level)
+  rpath <- c(obj_name(tt), "@content", obj_name(tt))
+  ## the [1] below is cause these are count (pct%) cells
+  ## and we only want the count part!
+  mcount <- unlist(cell_values(
+    tt,
+    rowpath = rpath,
+    colpath = c("ARM", "C: Combination", "SEX", "M")
+  ))[1]
+  fcount <- unlist(cell_values(
+    tt,
+    rowpath = rpath,
+    colpath = c("ARM", "C: Combination", "SEX", "F")
+  ))[1]
+  (mcount - fcount) / fcount
+}
+
+sort_at_path(pruned, c("RACE", "*", "STRATA1"), silly_gender_diffcount)
+
#                                    A: Drug X                B: Placebo              C: Combination     
+#                                 F            M            F            M            F            M     
+# ———————————————————————————————————————————————————————————————————————————————————————————————————————
+# ASIAN                       44 (62.9%)   35 (68.6%)   37 (66.1%)   31 (62.0%)   40 (65.6%)   44 (64.7%)
+#   B                         16 (22.9%)   8 (15.7%)    13 (23.2%)   16 (32.0%)   10 (16.4%)   12 (17.6%)
+#     Mean                      33.75        34.88        32.46        30.94        33.30        35.92   
+#   A                         15 (21.4%)   12 (23.5%)   14 (25.0%)   6 (12.0%)    15 (24.6%)   16 (23.5%)
+#     Mean                      30.40        34.42        35.43        30.33        37.40        36.25   
+#   C                         13 (18.6%)   15 (29.4%)   10 (17.9%)   9 (18.0%)    15 (24.6%)   16 (23.5%)
+#     Mean                      36.92        35.60        34.00        31.89        33.47        31.38   
+# BLACK OR AFRICAN AMERICAN   18 (25.7%)   10 (19.6%)   12 (21.4%)   12 (24.0%)   13 (21.3%)   14 (20.6%)
+#   C                          6 (8.6%)    6 (11.8%)     4 (7.1%)    7 (14.0%)     3 (4.9%)     4 (5.9%) 
+#     Mean                      31.33        39.67        34.50        34.00        33.00        36.50   
+#   A                          5 (7.1%)     1 (2.0%)     5 (8.9%)     2 (4.0%)     4 (6.6%)     4 (5.9%) 
+#     Mean                      31.20        33.00        28.00        30.00        30.75        36.50   
+#   B                         7 (10.0%)     3 (5.9%)     3 (5.4%)     3 (6.0%)     6 (9.8%)     6 (8.8%) 
+#     Mean                      36.14        34.33        29.67        32.00        36.33        31.00   
+# WHITE                       8 (11.4%)    6 (11.8%)    7 (12.5%)    7 (14.0%)    8 (13.1%)    10 (14.7%)
+#   A                          2 (2.9%)     1 (2.0%)     3 (5.4%)     3 (6.0%)     1 (1.6%)     5 (7.4%) 
+#     Mean                      34.00        45.00        29.33        33.33        35.00        32.80   
+#   C                          2 (2.9%)     2 (3.9%)     3 (5.4%)     0 (0.0%)     4 (6.6%)     4 (5.9%) 
+#     Mean                      35.50        44.00        44.67          NA         38.50        35.00   
+#   B                          4 (5.7%)     3 (5.9%)     1 (1.8%)     4 (8.0%)     3 (4.9%)     1 (1.5%) 
+#     Mean                      37.00        43.67        48.00        36.75        34.33        36.00
+
+
+
+
+
+ + + + +
+ + + + + + + diff --git a/v0.6.9/articles/split_functions.html b/v0.6.9/articles/split_functions.html new file mode 100644 index 000000000..1d7f59556 --- /dev/null +++ b/v0.6.9/articles/split_functions.html @@ -0,0 +1,750 @@ + + + + + + + + +Controlling Splitting Behavior • rtables + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

Controlling Facet Levels +

+
+

Provided Functions +

+

By default, split_*_by(varname, ...) generates a facet +for each level the variable varname takes in the +data - including unobserved ones in the factor case. This +behavior can be customized in various ways.

+

The most straightforward way to customize which facets are generated +by a split is with one of the split functions or split function families +provided by rtables.

+

These predefined split functions and function factories implement +commonly desired customization patterns of splitting behavior (i.e., +faceting behavior). They include:

+
    +
  • +remove_split_levels - remove specified levels from the +data for facet generation.
  • +
  • +keep_split_levels - keep only specified levels in the +data for facet generation (removing all others).
  • +
  • +drop_split_levels - drop levels that are unobserved +within the data being split, i.e., associated with the parent +facet.
  • +
  • +reorder_split_levels - reorder the levels (and thus the +generated facets) to the specified order.
  • +
  • +trim_levels_in_group - drop unobserved levels of +another variable independently within the data associated with each +facet generated by the current split.
  • +
  • +add_overall_level, add_combo_levels - add +additional “virtual” levels which combine two or more levels of the +variable being split. See the following section.
  • +
  • +trim_levels_to_map - trim the levels of multiple +variables to a pre-specified set of value combinations. See the +following section.
  • +
+

The first four of these are fairly self-describing and for brevity, +we refer our readers to ?split_funcs for details including +working examples.

+
+
+

Controlling Combinations of Levels Across Multiple Variables +

+

Often with nested splitting involving multiple variables, the values +of the variables in question are logically nested; meaning that +certain values of the inner variable are only coherent in combination +with a specific value or values of the outer variable.

+

As an example, suppose we have a variable vehicle_class, +which can take the values "automobile", and +"boat", and a variable vehicle_type, which can +take the values "car", "truck", +"suv","sailboat", and +"cruiseliner". The combination ("automobile", +"cruiseliner") does not make sense and will never occur in +any (correctly cleaned) data set; nor does the combination +("boat", "truck").

+

We will showcase strategies to deal with this in the next sections +using the following artificial data:

+
+set.seed(0)
+levs_type <- c("car", "truck", "suv", "sailboat", "cruiseliner")
+
+vclass <- sample(c("auto", "boat"), 1000, replace = TRUE)
+auto_inds <- which(vclass == "auto")
+vtype <- rep(NA_character_, 1000)
+vtype[auto_inds] <- sample(
+  c("car", "truck"), ## suv missing on purpose
+  length(auto_inds),
+  replace = TRUE
+)
+vtype[-auto_inds] <- sample(
+  c("sailboat", "cruiseliner"),
+  1000 - length(auto_inds),
+  replace = TRUE
+)
+
+vehic_data <- data.frame(
+  vehicle_class = factor(vclass),
+  vehicle_type = factor(vtype, levels = levs_type),
+  color = sample(
+    c("white", "black", "red"), 1000,
+    prob = c(1, 2, 1),
+    replace = TRUE
+  ),
+  cost = ifelse(
+    vclass == "boat",
+    rnorm(1000, 100000, sd = 5000),
+    rnorm(1000, 40000, sd = 5000)
+  )
+)
+head(vehic_data)
+
##   vehicle_class vehicle_type color      cost
+## 1          boat     sailboat black 100393.81
+## 2          auto          car white  38150.17
+## 3          boat     sailboat white  98696.13
+## 4          auto        truck white  37677.16
+## 5          auto        truck black  38489.27
+## 6          boat  cruiseliner black 108709.72
+
+

+trim_levels_in_group +

+

The trim_levels_in_group split function factory creates +split functions which deal with this issue empirically; any combination +which is observed in the data being tabulated will appear as +nested facets within the table, while those that do not, will not.

+

If we use default level-based faceting, we get several logically +incoherent cells within our table:

+
+library(rtables)
+
+lyt <- basic_table() %>%
+  split_cols_by("color") %>%
+  split_rows_by("vehicle_class") %>%
+  split_rows_by("vehicle_type") %>%
+  analyze("cost")
+
+build_table(lyt, vehic_data)
+
##                   black      white        red   
+## ————————————————————————————————————————————————
+## auto                                            
+##   car                                           
+##     Mean        40431.92    40518.92   38713.14 
+##   truck                                         
+##     Mean        40061.70    40635.74   40024.41 
+##   suv                                           
+##     Mean           NA          NA         NA    
+##   sailboat                                      
+##     Mean           NA          NA         NA    
+##   cruiseliner                                   
+##     Mean           NA          NA         NA    
+## boat                                            
+##   car                                           
+##     Mean           NA          NA         NA    
+##   truck                                         
+##     Mean           NA          NA         NA    
+##   suv                                           
+##     Mean           NA          NA         NA    
+##   sailboat                                      
+##     Mean        99349.69    99996.54   101865.73
+##   cruiseliner                                   
+##     Mean        100212.00   99340.25   100363.52
+

This is obviously not the table we want, as the majority of its space +is taken up by meaningless combinations. If we use +trim_levels_in_group to trim the levels of +vehicle_type separately within each level of +vehicle_class, we get a table which only has meaningful +combinations:

+
+lyt2 <- basic_table() %>%
+  split_cols_by("color") %>%
+  split_rows_by("vehicle_class", split_fun = trim_levels_in_group("vehicle_type")) %>%
+  split_rows_by("vehicle_type") %>%
+  analyze("cost")
+
+build_table(lyt2, vehic_data)
+
##                   black      white        red   
+## ————————————————————————————————————————————————
+## auto                                            
+##   car                                           
+##     Mean        40431.92    40518.92   38713.14 
+##   truck                                         
+##     Mean        40061.70    40635.74   40024.41 
+## boat                                            
+##   sailboat                                      
+##     Mean        99349.69    99996.54   101865.73
+##   cruiseliner                                   
+##     Mean        100212.00   99340.25   100363.52
+

Note, however, that it does not contain all meaningful +combinations, only those that were actually observed in our data; which +happens to not include the perfectly valid "auto", +"suv" combination.

+

To restrict level combinations to those which are valid +regardless of whether the combination was observed, we must use +trim_levels_to_map() instead.

+
+
+

+trim_levels_to_map +

+

trim_levels_to_map is similar to +trim_levels_in_group in that its purpose is to avoid +combinatorial explosion when nesting splitting with logically nested +variables. Unlike its sibling function, however, with +trim_levels_to_map we define the exact set of allowed +combinations a priori, and that exact set of combinations is +produced in the resulting table, regardless of whether they are observed +or not.

+
+library(tibble)
+map <- tribble(
+  ~vehicle_class, ~vehicle_type,
+  "auto",         "truck",
+  "auto",         "suv",
+  "auto",         "car",
+  "boat",         "sailboat",
+  "boat",         "cruiseliner"
+)
+
+lyt3 <- basic_table() %>%
+  split_cols_by("color") %>%
+  split_rows_by("vehicle_class", split_fun = trim_levels_to_map(map)) %>%
+  split_rows_by("vehicle_type") %>%
+  analyze("cost")
+
+build_table(lyt3, vehic_data)
+
##                   black      white        red   
+## ————————————————————————————————————————————————
+## auto                                            
+##   car                                           
+##     Mean        40431.92    40518.92   38713.14 
+##   truck                                         
+##     Mean        40061.70    40635.74   40024.41 
+##   suv                                           
+##     Mean           NA          NA         NA    
+## boat                                            
+##   sailboat                                      
+##     Mean        99349.69    99996.54   101865.73
+##   cruiseliner                                   
+##     Mean        100212.00   99340.25   100363.52
+

Now we see that the "auto", "suv" +combination is again present, even though it is populated with +NAs (because there is no data in that category), but the +logically invalid combinations are still absent.

+
+
+
+

Combining Levels +

+

Another very common manipulation of faceting in a table context is +the introduction of combination levels that are not explicitly modeled +in the data. Most often, this involves the addition of an “overall” +category, but in both principle and practice it can involve any +arbitrary combination of levels.

+

rtables explicitly supports this via the +add_overall_level (for the all case) and +add_combo_levels split function factories.

+
+

+add_overall_level +

+

add_overall_level accepts valname which is +the name of the new level, as well as label, and +first (whether it should come first, if TRUE, +or last, if FALSE, in the ordering).

+

Building further on our arbitrary vehicles table, we can use this to +create an “all colors” category:

+
+lyt4 <- basic_table(show_colcounts = TRUE) %>%
+  split_cols_by("color", split_fun = add_overall_level("allcolors", label = "All Colors")) %>%
+  split_rows_by("vehicle_class", split_fun = trim_levels_to_map(map)) %>%
+  split_rows_by("vehicle_type") %>%
+  analyze("cost")
+
+build_table(lyt4, vehic_data)
+
##                 All Colors     black      white        red   
+##                  (N=1000)     (N=521)    (N=251)     (N=228) 
+## —————————————————————————————————————————————————————————————
+## auto                                                         
+##   car                                                        
+##     Mean         40095.49    40431.92    40518.92   38713.14 
+##   truck                                                      
+##     Mean         40194.68    40061.70    40635.74   40024.41 
+##   suv                                                        
+##     Mean            NA          NA          NA         NA    
+## boat                                                         
+##   sailboat                                                   
+##     Mean        100133.22    99349.69    99996.54   101865.73
+##   cruiseliner                                                
+##     Mean        100036.76    100212.00   99340.25   100363.52
+

With the column counts turned on, we can see that the “All Colors” +column encompasses the full 1000 (completely fake) vehicles in our data +set.

+

To add more arbitrary combinations, we use +add_combo_levels.

+
+
+

+add_combo_levels +

+

add_combo_levels allows us to add one or more arbitrary +combination levels to the faceting structure of our table.

+

We do this by defining a combination data.frame which +describes the levels we want to add. A combination +data.frame has the following columns and one row for each +combination to add:

+
    +
  • +valname - string indicating the name of the value, +which will appear in paths.
  • +
  • +label - a string indicating the label which should be +displayed when rendering.
  • +
  • +levelcombo - character vector of the individual levels +to be combined in this combination level.
  • +
  • +exargs - a list (usually list()) of extra +arguments which should be passed to analysis and content functions when +tabulated within this column or row.
  • +
+

Suppose we wanted combinations levels for all non-white colors, and +for white and black colors. We do this like so:

+
+combodf <- tribble(
+  ~valname, ~label, ~levelcombo, ~exargs,
+  "non-white", "Non-White", c("black", "red"), list(),
+  "blackwhite", "Black or White", c("black", "white"), list()
+)
+
+
+lyt5 <- basic_table(show_colcounts = TRUE) %>%
+  split_cols_by("color", split_fun = add_combo_levels(combodf)) %>%
+  split_rows_by("vehicle_class", split_fun = trim_levels_to_map(map)) %>%
+  split_rows_by("vehicle_type") %>%
+  analyze("cost")
+
+build_table(lyt5, vehic_data)
+
##                   black      white        red      Non-White   Black or White
+##                  (N=521)    (N=251)     (N=228)     (N=749)       (N=772)    
+## —————————————————————————————————————————————————————————————————————————————
+## auto                                                                         
+##   car                                                                        
+##     Mean        40431.92    40518.92   38713.14    39944.93       40460.77   
+##   truck                                                                      
+##     Mean        40061.70    40635.74   40024.41    40050.66       40243.57   
+##   suv                                                                        
+##     Mean           NA          NA         NA          NA             NA      
+## boat                                                                         
+##   sailboat                                                                   
+##     Mean        99349.69    99996.54   101865.73   100179.72      99567.50   
+##   cruiseliner                                                                
+##     Mean        100212.00   99340.25   100363.52   100258.56      99937.47
+
+
+
+
+

Fully Customizing Split (Facet) Behavior +

+

Beyond the ability to select common splitting customizations from the +split functions and split function factories rtables +provides, we can also fully customize every aspect of splitting behavior +by creating our own split functions. While it is possible to do so by +hand, the primary way we do this is via the +make_split_fun() function, which accepts functions +implementing different component behaviors and combines them into a +split function which can be used in a layout.

+

Splitting, or faceting as it is done in rtables, can be +thought of as the combination of 3 steps:

+
    +
  1. preprocessing - transformation of the incoming data which will be +faceted
  2. +
+
    +
  • e.g., dropping unused factor levels, etc.
  • +
+
    +
  1. splitting - mapping the incoming data to a set of 1 or more subsets +representing individual facets.
  2. +
  3. postprocessing - operations on the facets - e.g., combining them, +removing them, etc.
  4. +
+

The make_split_fun() function allows us to specify +custom behaviors for each of these steps independently when defining +custom splitting behavior via the pre, +core_split, and post arguments, which dictate +the above steps, respectively.

+

The pre argument accepts zero or more pre-processing +functions, which must accept: df, spl, +vals, labels, and can optionally accept +.spl_context. They then manipulate df (the +incoming data for the split) and return a modified data.frame. This +modified data.frame must contain all columns present in the +incoming data.frame, but can add columns if necessary. Although, we note +that these new columns cannot be used in the layout as split or +analysis variables, because they will not be present when validity +checking is done.

+

The pre-processing component is useful for things such as +manipulating factor levels, e.g., to trim unobserved ones or to reorder +levels based on observed counts, etc.

+

For a more detailed discussion on what custom split functions do, and +an example of a custom split function not implemented via +make_split_fun(), see ?custom_split_funs.

+
+

An Example Custom Split Function +

+

Here we will implement an arbitrary, custom split function where we +specify both pre- and post-processing instructions. It is unusual for +users to need to override the core splitting logic - and, in fact, is +only supported in row space currently - so we leave this off of our +example here but will provide another narrow example of that usage +below.

+
+

An Illustrative Example of A Custom Split Function +

+

First, we define two aspects of ‘pre-processing step’ behavior:

+
    +
  1. A function which reverses the order of the levels of a variable +(while retaining which level is associated with which observation), +and
  2. +
  3. A function factory which creates a function that removes a level and +the data associated with it.
  4. +
+
+## reverse order of levels
+
+rev_lev <- function(df, spl, vals, labels, ...) {
+  ## in the split_rows_by() and split_cols_by() cases,
+  ## spl_variable() gives us the variable
+  var <- spl_variable(spl)
+  vec <- df[[var]]
+  levs <- if (is.character(vec)) unique(vec) else levels(vec)
+  df[[var]] <- factor(vec, levels = rev(levs))
+  df
+}
+
+rem_lev_facet <- function(torem) {
+  function(df, spl, vals, labels, ...) {
+    var <- spl_variable(spl)
+    vec <- df[[var]]
+    bad <- vec == torem
+    df <- df[!bad, ]
+    levs <- if (is.character(vec)) unique(vec) else levels(vec)
+    df[[var]] <- factor(as.character(vec[!bad]), levels = setdiff(levs, torem))
+    df
+  }
+}
+

Finally we implement our post-processing function. Here we will +reorder the facets based on the amount of data each of them +represents.

+
+sort_them_facets <- function(splret, spl, fulldf, ...) {
+  ord <- order(sapply(splret$datasplit, nrow))
+  make_split_result(
+    splret$values[ord],
+    splret$datasplit[ord],
+    splret$labels[ord]
+  )
+}
+

Finally, we construct our custom split function and use it to create +our table:

+
+silly_splfun1 <- make_split_fun(
+  pre = list(
+    rev_lev,
+    rem_lev_facet("white")
+  ),
+  post = list(sort_them_facets)
+)
+
+lyt6 <- basic_table(show_colcounts = TRUE) %>%
+  split_cols_by("color", split_fun = silly_splfun1) %>%
+  split_rows_by("vehicle_class", split_fun = trim_levels_to_map(map)) %>%
+  split_rows_by("vehicle_type") %>%
+  analyze("cost")
+
+build_table(lyt6, vehic_data)
+
##                    red        black  
+##                  (N=228)     (N=521) 
+## —————————————————————————————————————
+## auto                                 
+##   car                                
+##     Mean        38713.14    40431.92 
+##   truck                              
+##     Mean        40024.41    40061.70 
+##   suv                                
+##     Mean           NA          NA    
+## boat                                 
+##   sailboat                           
+##     Mean        101865.73   99349.69 
+##   cruiseliner                        
+##     Mean        100363.52   100212.00
+
+
+

Overriding the Core Split Function +

+

Currently, overriding core split behavior is only supported +in functions used for row splits.

+

Next, we write a custom core-splitting function which divides the +observations into 4 groups: the first 100, observations 101-500, +observations 501-900, and the last hundred. We could claim this was to +test for structural bias in the first and last observations, but really +its to simply illustrate overriding the core splitting machinery and has +no meaningful statistical purpose.

+
+silly_core_split <- function(spl, df, vals, labels, .spl_context) {
+  make_split_result(
+    c("first", "lowmid", "highmid", "last"),
+    datasplit = list(
+      df[1:100, ],
+      df[101:500, ],
+      df[501:900, ],
+      df[901:1000, ]
+    ),
+    labels = c(
+      "first 100",
+      "obs 101-500",
+      "obs 501-900",
+      "last 100"
+    )
+  )
+}
+

We can use this to construct a splitting function. This can be +combined with pre- and post-processing functions, as each of the stages +is performed independently, but in this case, we won’t, because our core +splitting behavior is such that pre- or post-processing do not make much +sense.

+
+even_sillier_splfun <- make_split_fun(core_split = silly_core_split)
+
+lyt7 <- basic_table(show_colcounts = TRUE) %>%
+  split_cols_by("color") %>%
+  split_rows_by("vehicle_class", split_fun = even_sillier_splfun) %>%
+  split_rows_by("vehicle_type") %>%
+  analyze("cost")
+
+build_table(lyt7, vehic_data)
+
##                   black       white        red   
+##                  (N=521)     (N=251)     (N=228) 
+## —————————————————————————————————————————————————
+## first 100                                        
+##   car                                            
+##     Mean        40496.05    37785.41    37623.17 
+##   truck                                          
+##     Mean        41094.17    40437.29    37866.81 
+##   suv                                            
+##     Mean           NA          NA          NA    
+##   sailboat                                       
+##     Mean        100560.80   102017.05   101185.96
+##   cruiseliner                                    
+##     Mean        100838.12   96952.27    100610.71
+## obs 101-500                                      
+##   car                                            
+##     Mean        39350.88    41185.98    37978.72 
+##   truck                                          
+##     Mean        40166.87    41385.32    39885.72 
+##   suv                                            
+##     Mean           NA          NA          NA    
+##   sailboat                                       
+##     Mean        98845.47    99563.02    101462.79
+##   cruiseliner                                    
+##     Mean        101558.62   99039.91    97335.05 
+## obs 501-900                                      
+##   car                                            
+##     Mean        40721.82    40379.48    38681.26 
+##   truck                                          
+##     Mean        39951.92    39846.89    39840.39 
+##   suv                                            
+##     Mean           NA          NA          NA    
+##   sailboat                                       
+##     Mean        99533.20    100347.18   102732.12
+##   cruiseliner                                    
+##     Mean        99140.43    100074.43   101994.99
+## last 100                                         
+##   car                                            
+##     Mean        45204.44    40626.95    41214.33 
+##   truck                                          
+##     Mean        38920.70    40620.47    42899.14 
+##   suv                                            
+##     Mean           NA          NA          NA    
+##   sailboat                                       
+##     Mean        99380.21    97644.77    101691.92
+##   cruiseliner                                    
+##     Mean        100017.53   99581.94    100751.30
+
+
+

Design of Pre- and Post-Processing Functions For Use in +make_split_fun +

+

Pre-processing and post-processing functions in the custom-splitting +context are best thought of as (and implemented as) independent, atomic +building blocks for the desired overall behavior. This allows them to be +reused in a flexible mix-and-match way.

+

rtables provides several behavior components implemented +as either functions or function factories:

+
    +
  • Pre-processing “behavior blocks” +
      +
    • +drop_facet_levels - drop unobserved levels in the +variable being split
    • +
    +
  • +
  • Post-processing “behavior blocks” +
      +
    • +trim_levels_in_facets - provides +trim_levels_in_group behavior
    • +
    • +add_overall_facet - add a combination facet for the +full data
    • +
    • +add_combo_facet - add a single combination facet (can +be used more than once in a single make_split_fun +call)
    • +
    +
  • +
+
+
+
+
+
+ + + + +
+ + + + + + + diff --git a/v0.6.9/articles/subsetting_tables.html b/v0.6.9/articles/subsetting_tables.html new file mode 100644 index 000000000..a7c83b873 --- /dev/null +++ b/v0.6.9/articles/subsetting_tables.html @@ -0,0 +1,613 @@ + + + + + + + + +Subsetting and Manipulating Table Contents • rtables + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + + +
+

Introduction +

+

TableTree objects are based on a tree data structure as +the name indicates. The package is written such that the user does not +need to walk trees for many basic table manipulations. Walking trees +will still be necessary for certain manipulation and will be the subject +of a different vignette.

+

In this vignette we show some methods to subset tables and to extract +cell values.

+

We will use the following table for illustrative purposes:

+
+library(rtables)
+library(dplyr)
+
+lyt <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  split_rows_by("SEX", split_fun = drop_split_levels) %>%
+  analyze(c("AGE", "STRATA1"))
+
+tbl <- build_table(lyt, ex_adsl %>% filter(SEX %in% c("M", "F")))
+tbl
+
#             A: Drug X   B: Placebo   C: Combination
+# ———————————————————————————————————————————————————
+# F                                                  
+#   AGE                                              
+#     Mean      32.76       34.12          35.20     
+#   STRATA1                                          
+#     A          21           24             18      
+#     B          25           27             21      
+#     C          33           26             27      
+# M                                                  
+#   AGE                                              
+#     Mean      35.57       37.44          35.38     
+#   STRATA1                                          
+#     A          16           19             20      
+#     B          21           17             21      
+#     C          14           19             19
+
+
+

Traditional Subsetting and modification with [ +

+

The [ and [<- accessor functions operate +largely the same as their data.frame cousins:

+
    +
  • +[ and [<- both treat Tables as +rectangular objects (rather than trees) +
      +
    • In particular this means label rows are treated as rows with empty +cell values, rather than rows without cells
    • +
    +
  • +
  • +[ accepts both column and row absolute position, and +missing arguments mean “all indexes in that dimension” +
      +
    • multiple values can be specified in both row and column +position
    • +
    • negative numeric positions are supported, though like +[.data.frame they cannot be mixed with positive ones
    • +
    +
  • +
  • +[ always returns the same class as the object being +subset unless drop = TRUE +
  • +
  • +[ , drop = TRUE returns the raw (possibly +multi-element) value associated with the cell.
  • +
+

Known Differences from [.data.frame - +absolute position cannot currently be used to reorder columns or rows. +Note in general the result of such an ordering is unlikely to be +structurally valid. To change the order of values, please read sorting +and pruning vignette or relevant function +(sort_at_path()). - character indices are +treated as paths, not vectors of names in both [ and +[<-

+

The [ accessor function always returns an +TableTree object if drop=TRUE is not set. The +first argument are the row indices and the second argument the column +indices. Alternatively logical subsetting can be used. The indices are +based on visible rows and not on the tree structure. So:

+
+tbl[1, 1]
+
#     A: Drug X
+# —————————————
+# F
+

is a table with an empty cell because the first row is a label row. +We need to access a cell with actual cell data:

+
+tbl[3, 1]
+
#        A: Drug X
+# ————————————————
+# Mean     32.76
+

To retrieve the value, we use drop = TRUE:

+
+tbl[3, 1, drop = TRUE]
+
# [1] 32.75949
+

One can access multiple rows and columns:

+
+tbl[1:3, 1:2]
+
#            A: Drug X   B: Placebo
+# —————————————————————————————————
+# F                                
+#   AGE                            
+#     Mean     32.76       34.12
+

Note that we do not repeat label rows for descending children, +e.g.

+
+tbl[2:4, ]
+
#           A: Drug X   B: Placebo   C: Combination
+# —————————————————————————————————————————————————
+# AGE                                              
+#   Mean      32.76       34.12          35.20     
+# STRATA1
+

does not show that the first row is derived from AGE. In +order to repeat content/label information, one should use the pagination +feature. Please read the related vignette.

+

Character indices are interpreted as paths (see below), NOT elements +to be matched against names(tbl):

+
+tbl[, c("ARM", "A: Drug X")]
+
# Note: method with signature 'VTableTree#missing#ANY' chosen for function '[',
+#  target signature 'TableTree#missing#character'.
+#  "VTableTree#ANY#character" would also be valid
+
#             A: Drug X
+# —————————————————————
+# F                    
+#   AGE                
+#     Mean      32.76  
+#   STRATA1            
+#     A          21    
+#     B          25    
+#     C          33    
+# M                    
+#   AGE                
+#     Mean      35.57  
+#   STRATA1            
+#     A          16    
+#     B          21    
+#     C          14
+
+

Dealing with titles, foot notes, and top left information +

+

As standard no additional information is kept after subsetting. Here, +we show with a more complete table how it is still possible to keep the +(possibly) relevant information.

+
+top_left(tbl) <- "SEX"
+main_title(tbl) <- "Table 1"
+subtitles(tbl) <- c("Authors:", " - Abcd Zabcd", " - Cde Zbcd")
+
+main_footer(tbl) <- "Please regard this table as an example of smart subsetting"
+prov_footer(tbl) <- "Do remember where you read this though"
+
+fnotes_at_path(tbl, rowpath = c("M", "AGE", "Mean"), colpath = c("ARM", "A: Drug X")) <- "Very important mean"
+

Normal subsetting loses all the information showed above.

+
+tbl[3, 3]
+
#        C: Combination
+# —————————————————————
+# Mean       35.20
+

If all the rows are kept, top left information is also kept. This can +be also imposed by adding keep_topleft = TRUE to the +subsetting as follows:

+
+tbl[, 2:3]
+
# SEX         B: Placebo   C: Combination
+# ———————————————————————————————————————
+# F                                      
+#   AGE                                  
+#     Mean      34.12          35.20     
+#   STRATA1                              
+#     A           24             18      
+#     B           27             21      
+#     C           26             27      
+# M                                      
+#   AGE                                  
+#     Mean      37.44          35.38     
+#   STRATA1                              
+#     A           19             20      
+#     B           17             21      
+#     C           19             19
+
+tbl[1:3, 3, keep_topleft = TRUE]
+
# SEX        C: Combination
+# —————————————————————————
+# F                        
+#   AGE                    
+#     Mean       35.20
+

If the referenced entry is present in the subsetting, also the +referential footnote will appear. Please consider reading relevant +vignette about referential +footnotes. In case of subsetting, the referential footnotes are by +default indexed again, as if the produced table is a new one.

+
+tbl[10, 1]
+
#        A: Drug X
+# ————————————————
+# Mean   35.57 {1}
+# ————————————————
+# 
+# {1} - Very important mean
+# ————————————————
+
+col_paths_summary(tbl) # Use these to find the right path to value or label
+
# label             path               
+# —————————————————————————————————————
+# A: Drug X         ARM, A: Drug X     
+# B: Placebo        ARM, B: Placebo    
+# C: Combination    ARM, C: Combination
+ +
# rowname      node_class    path              
+# —————————————————————————————————————————————
+# F            LabelRow      SEX, F            
+#   AGE        LabelRow      SEX, F, AGE       
+#     Mean     DataRow       SEX, F, AGE, Mean 
+#   STRATA1    LabelRow      SEX, F, STRATA1   
+#     A        DataRow       SEX, F, STRATA1, A
+#     B        DataRow       SEX, F, STRATA1, B
+#     C        DataRow       SEX, F, STRATA1, C
+# M            LabelRow      SEX, M            
+#   AGE        LabelRow      SEX, M, AGE       
+#     Mean     DataRow       SEX, M, AGE, Mean 
+#   STRATA1    LabelRow      SEX, M, STRATA1   
+#     A        DataRow       SEX, M, STRATA1, A
+#     B        DataRow       SEX, M, STRATA1, B
+#     C        DataRow       SEX, M, STRATA1, C
+
+# To select column value, use `NULL` for `rowpath`
+fnotes_at_path(tbl, rowpath = NULL, colpath = c("ARM", "A: Drug X")) <- "Interesting"
+tbl[3, 1]
+
#        A: Drug X {1}
+# ————————————————————
+# Mean       32.76    
+# ————————————————————
+# 
+# {1} - Interesting
+# ————————————————————
+
+# reindexing of {2} as {1}
+fnotes_at_path(tbl, rowpath = c("M", "AGE", "Mean"), colpath = NULL) <- "THIS mean"
+tbl # {1}, {2}, and {3} are present
+
# Table 1
+# Authors:
+#  - Abcd Zabcd
+#  - Cde Zbcd
+# 
+# ——————————————————————————————————————————————————————————
+# SEX            A: Drug X {1}   B: Placebo   C: Combination
+# ——————————————————————————————————————————————————————————
+# F                                                         
+#   AGE                                                     
+#     Mean           32.76         34.12          35.20     
+#   STRATA1                                                 
+#     A               21             24             18      
+#     B               25             27             21      
+#     C               33             26             27      
+# M                                                         
+#   AGE                                                     
+#     Mean {2}     35.57 {3}       37.44          35.38     
+#   STRATA1                                                 
+#     A               16             19             20      
+#     B               21             17             21      
+#     C               14             19             19      
+# ——————————————————————————————————————————————————————————
+# 
+# {1} - Interesting
+# {2} - THIS mean
+# {3} - Very important mean
+# ——————————————————————————————————————————————————————————
+# 
+# Please regard this table as an example of smart subsetting
+# 
+# Do remember where you read this though
+
+tbl[10, 2] # only {1} which was previously {2}
+
#            B: Placebo
+# —————————————————————
+# Mean {1}     37.44   
+# —————————————————————
+# 
+# {1} - THIS mean
+# —————————————————————
+

Similar to what we have used to keep top left information, we can +specify to keep more information from the original table. As a standard +the foot notes are always present if the titles are kept.

+
+tbl[1:3, 2:3, keep_titles = TRUE]
+
# Table 1
+# Authors:
+#  - Abcd Zabcd
+#  - Cde Zbcd
+# 
+# ——————————————————————————————————————
+#            B: Placebo   C: Combination
+# ——————————————————————————————————————
+# F                                     
+#   AGE                                 
+#     Mean     34.12          35.20     
+# ——————————————————————————————————————
+# 
+# Please regard this table as an example of smart subsetting
+# 
+# Do remember where you read this though
+
+tbl[1:3, 2:3, keep_titles = FALSE, keep_footers = TRUE]
+
#            B: Placebo   C: Combination
+# ——————————————————————————————————————
+# F                                     
+#   AGE                                 
+#     Mean     34.12          35.20     
+# ——————————————————————————————————————
+# 
+# Please regard this table as an example of smart subsetting
+# 
+# Do remember where you read this though
+
+# Referential footnotes are not influenced by `keep_footers = FALSE`
+tbl[1:3, keep_titles = TRUE, keep_footers = FALSE]
+
# Table 1
+# Authors:
+#  - Abcd Zabcd
+#  - Cde Zbcd
+# 
+# ——————————————————————————————————————————————————————
+#            A: Drug X {1}   B: Placebo   C: Combination
+# ——————————————————————————————————————————————————————
+# F                                                     
+#   AGE                                                 
+#     Mean       32.76         34.12          35.20     
+# ——————————————————————————————————————————————————————
+# 
+# {1} - Interesting
+# ——————————————————————————————————————————————————————
+
+
+
+

Path Based Cell Value Accessing: +

+

Tables can be subset or modified in a structurally aware manner via +pathing.

+

Paths define semantically meaningful positions within a constructed +table that correspond to the logic of the layout used to create it.

+

A path is an ordered set of split names, the names of subgroups +generated by the split, and the @content directive, which +steps into a position’s content (or row group summary) table.

+

We can see the row and column paths of an existing table via the +row_paths(), col_paths(), +row_paths_summary(), and col_paths_summary(), +functions, or as a portion of the more general +make_row_df() function output.

+
+lyt2 <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  split_cols_by("SEX", split_fun = drop_split_levels) %>%
+  split_rows_by("RACE", split_fun = drop_split_levels) %>%
+  summarize_row_groups() %>%
+  analyze(c("AGE", "STRATA1"))
+
+tbl2 <- build_table(lyt2, ex_adsl %>% filter(SEX %in% c("M", "F") & RACE %in% (levels(RACE)[1:3])))
+tbl2
+
#                                    A: Drug X                B: Placebo              C: Combination     
+#                                 F            M            F            M            F            M     
+# ———————————————————————————————————————————————————————————————————————————————————————————————————————
+# ASIAN                       41 (53.9%)   25 (54.3%)   36 (52.2%)   30 (60.0%)   39 (60.9%)   32 (57.1%)
+#   AGE                                                                                                  
+#     Mean                      31.22        34.60        35.06        38.63        36.44        37.66   
+#   STRATA1                                                                                              
+#     A                           11           10           14           10           11           7     
+#     B                           11           9            15           7            11           14    
+#     C                           19           6            7            13           17           11    
+# BLACK OR AFRICAN AMERICAN   18 (23.7%)   12 (26.1%)   16 (23.2%)   12 (24.0%)   14 (21.9%)   14 (25.0%)
+#   AGE                                                                                                  
+#     Mean                      34.06        34.58        33.88        36.33        33.21        34.21   
+#   STRATA1                                                                                              
+#     A                           5            2            5            6            3            7     
+#     B                           6            5            3            4            4            4     
+#     C                           7            5            8            2            7            3     
+# WHITE                       17 (22.4%)   9 (19.6%)    17 (24.6%)   8 (16.0%)    11 (17.2%)   10 (17.9%)
+#   AGE                                                                                                  
+#     Mean                      34.12        40.00        32.41        34.62        33.00        30.80   
+#   STRATA1                                                                                              
+#     A                           5            3            3            3            3            5     
+#     B                           5            4            8            4            5            2     
+#     C                           7            2            6            1            3            3
+

So the column paths are as follows:

+ +
# label             path                       
+# —————————————————————————————————————————————
+# A: Drug X         ARM, A: Drug X             
+#   F               ARM, A: Drug X, SEX, F     
+#   M               ARM, A: Drug X, SEX, M     
+# B: Placebo        ARM, B: Placebo            
+#   F               ARM, B: Placebo, SEX, F    
+#   M               ARM, B: Placebo, SEX, M    
+# C: Combination    ARM, C: Combination        
+#   F               ARM, C: Combination, SEX, F
+#   M               ARM, C: Combination, SEX, M
+

and the row paths are as follows:

+ +
# rowname                      node_class    path                                                                
+# ———————————————————————————————————————————————————————————————————————————————————————————————————————————————
+# ASIAN                        ContentRow    RACE, ASIAN, @content, ASIAN                                        
+#   AGE                        LabelRow      RACE, ASIAN, AGE                                                    
+#     Mean                     DataRow       RACE, ASIAN, AGE, Mean                                              
+#   STRATA1                    LabelRow      RACE, ASIAN, STRATA1                                                
+#     A                        DataRow       RACE, ASIAN, STRATA1, A                                             
+#     B                        DataRow       RACE, ASIAN, STRATA1, B                                             
+#     C                        DataRow       RACE, ASIAN, STRATA1, C                                             
+# BLACK OR AFRICAN AMERICAN    ContentRow    RACE, BLACK OR AFRICAN AMERICAN, @content, BLACK OR AFRICAN AMERICAN
+#   AGE                        LabelRow      RACE, BLACK OR AFRICAN AMERICAN, AGE                                
+#     Mean                     DataRow       RACE, BLACK OR AFRICAN AMERICAN, AGE, Mean                          
+#   STRATA1                    LabelRow      RACE, BLACK OR AFRICAN AMERICAN, STRATA1                            
+#     A                        DataRow       RACE, BLACK OR AFRICAN AMERICAN, STRATA1, A                         
+#     B                        DataRow       RACE, BLACK OR AFRICAN AMERICAN, STRATA1, B                         
+#     C                        DataRow       RACE, BLACK OR AFRICAN AMERICAN, STRATA1, C                         
+# WHITE                        ContentRow    RACE, WHITE, @content, WHITE                                        
+#   AGE                        LabelRow      RACE, WHITE, AGE                                                    
+#     Mean                     DataRow       RACE, WHITE, AGE, Mean                                              
+#   STRATA1                    LabelRow      RACE, WHITE, STRATA1                                                
+#     A                        DataRow       RACE, WHITE, STRATA1, A                                             
+#     B                        DataRow       RACE, WHITE, STRATA1, B                                             
+#     C                        DataRow       RACE, WHITE, STRATA1, C
+

To get a semantically meaningful subset of our table, then, we can +use [ (or tt_at_path() which underlies it)

+
+tbl2[c("RACE", "ASIAN"), c("ARM", "C: Combination")]
+
#                 C: Combination     
+#                 F            M     
+# ———————————————————————————————————
+# ASIAN       39 (60.9%)   32 (57.1%)
+#   AGE                              
+#     Mean      36.44        37.66   
+#   STRATA1                          
+#     A           11           7     
+#     B           11           14    
+#     C           17           11
+

We can also retrieve individual cell-values via the +value_at() convenience function, which takes a pair of row +and column paths which resolve together to an individual cell, +e.g. average age for Asian female patients in arm A:

+
+value_at(tbl2, c("RACE", "ASIAN", "AGE", "Mean"), c("ARM", "A: Drug X", "SEX", "F"))
+
# [1] 31.21951
+

You can also request information from non-cell specific paths with +the cell_values() function:

+
+cell_values(tbl2, c("RACE", "ASIAN", "AGE", "Mean"), c("ARM", "A: Drug X"))
+
# $`A: Drug X.F`
+# [1] 31.21951
+# 
+# $`A: Drug X.M`
+# [1] 34.6
+

Note the return value of cell_values() is always a list +even if you specify a path to a cell:

+
+cell_values(tbl2, c("RACE", "ASIAN", "AGE", "Mean"), c("ARM", "A: Drug X", "SEX", "F"))
+
# $`A: Drug X.F`
+# [1] 31.21951
+
+
+
+ + + + +
+ + + + + + + diff --git a/v0.6.9/articles/tabulation_concepts.html b/v0.6.9/articles/tabulation_concepts.html new file mode 100644 index 000000000..e543e16b1 --- /dev/null +++ b/v0.6.9/articles/tabulation_concepts.html @@ -0,0 +1,912 @@ + + + + + + + + +Tabulation Concepts • rtables + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + + +
+

Introduction +

+

In this vignette we will introduce some theory behind using layouts +for table creation. Much of the theory also holds true when using other +table packages. For this vignette we will use the following +packages:

+ +

The data we use is the following, created with random number +generators:

+
+add_subgroup <- function(x) paste0(tolower(x), sample(1:3, length(x), TRUE))
+
+set.seed(1)
+
+df <- tibble(
+  x = rnorm(100),
+  c1 = factor(sample(c("A", "B", "C"), 100, replace = TRUE), levels = c("A", "B", "C")),
+  r1 = factor(sample(c("U", "V", "W"), 100, replace = TRUE), levels = c("U", "V", "W"))
+) %>%
+  mutate(
+    c2 = add_subgroup(c1),
+    r2 = add_subgroup(r1),
+    y = as.numeric(2 * as.numeric(c1) - 3 * as.numeric(r1))
+  ) %>%
+  select(c1, c2, r1, r2, x, y)
+
+df
+
# # A tibble: 100 × 6
+#    c1    c2    r1    r2         x     y
+#    <fct> <chr> <fct> <chr>  <dbl> <dbl>
+#  1 B     b2    U     u3    -0.626     1
+#  2 A     a3    V     v2     0.184    -4
+#  3 B     b1    V     v2    -0.836    -2
+#  4 B     b3    V     v2     1.60     -2
+#  5 B     b1    U     u1     0.330     1
+#  6 C     c1    U     u3    -0.820     3
+#  7 A     a3    U     u3     0.487    -1
+#  8 B     b1    U     u3     0.738     1
+#  9 C     c3    V     v2     0.576     0
+# 10 C     c3    U     u2    -0.305     3
+# # ℹ 90 more rows
+
+
+

Building A Table Row By Row +

+

Let’s look at a table that has 3 columns and 3 rows. Each row +represents a different analysis (functions foo, +bar, zoo that return an rcell() +object):

+
                     A         B         C
+------------------------------------------------
+foo_label        foo(df_A)  foo(df_B)  foo(df_C)
+bar_label        bar(df_A)  bar(df_B)  bar(df_C)
+zoo_label        zoo(df_A)  zoo(df_B)  zoo(df_C)
+

The data passed to the analysis functions are a subset defined by the +respective column and:

+
+df_A <- df %>% filter(c1 == "A")
+df_B <- df %>% filter(c1 == "B")
+df_C <- df %>% filter(c1 == "C")
+

Let’s do this on the concrete data with analyze():

+
+foo <- prod
+bar <- sum
+zoo <- mean
+
+lyt <- basic_table() %>%
+  split_cols_by("c1") %>%
+  analyze("x", function(df) foo(df$x), var_labels = "foo label", format = "xx.xx") %>%
+  analyze("x", function(df) bar(df$x), var_labels = "bar label", format = "xx.xx") %>%
+  analyze("x", function(df) zoo(df$x), var_labels = "zoo label", format = "xx.xx")
+
+tbl <- build_table(lyt, df)
+
# Warning: Non-unique sibling analysis table names. Using Labels instead. Use the table_names argument to analyze to avoid this when analyzing the same variable multiple times.
+#   occured at (row) path: root
+
+tbl
+
#                A       B       C  
+# ——————————————————————————————————
+# foo label                         
+#   foo label   0.00   -0.00   -0.00
+# bar label                         
+#   bar label   1.87   4.37    4.64 
+# zoo label                         
+#   zoo label   0.05   0.13    0.18
+

or if we wanted the x variable instead of the data +frame:

+
                     A         B         C
+------------------------------------------------
+foo_label        foo(x_A)  foo(x_B)  foo(x_C)
+bar_label        bar(x_A)  bar(x_B)  bar(x_C)
+zoo_label        zoo(x_A)  zoo(x_B)  zoo(x_C)
+

where:

+
+x_A <- df_A$x
+x_B <- df_B$x
+x_C <- df_C$x
+

The function passed to afun is evaluated using argument +matching. If afun has an argument x the +analysis variable specified in vars in +analyze() is passed to the function, and if +afun has an argument df then a subset of the +dataset is passed to afun:

+
+lyt2 <- basic_table() %>%
+  split_cols_by("c1") %>%
+  analyze("x", foo, var_labels = "foo label", format = "xx.xx") %>%
+  analyze("x", bar, var_labels = "bar label", format = "xx.xx") %>%
+  analyze("x", zoo, var_labels = "zoo label", format = "xx.xx")
+
+tbl2 <- build_table(lyt2, df)
+
# Warning: Non-unique sibling analysis table names. Using Labels instead. Use the table_names argument to analyze to avoid this when analyzing the same variable multiple times.
+#   occured at (row) path: root
+
+tbl2
+
#              A       B       C  
+# ————————————————————————————————
+# foo label                       
+#   foo       0.00   -0.00   -0.00
+# bar label                       
+#   bar       1.87   4.37    4.64 
+# zoo label                       
+#   zoo       0.05   0.13    0.18
+

Note that it is also possible that a function returns multiple rows +with in_rows():

+
+lyt3 <- basic_table() %>%
+  split_cols_by("c1") %>%
+  analyze("x", function(x) {
+    in_rows(
+      "row 1" = rcell(mean(x), format = "xx.xx"),
+      "row 2" = rcell(sd(x), format = "xx.xxx")
+    )
+  }, var_labels = "foo label") %>%
+  analyze("x", function(x) {
+    in_rows(
+      "more rows 1" = rcell(median(x), format = "xx.x"),
+      "even more rows 1" = rcell(IQR(x), format = "xx.xx")
+    )
+  }, var_labels = "bar label", format = "xx.xx")
+
+tbl3 <- build_table(lyt3, df)
+
# Warning: Non-unique sibling analysis table names. Using Labels instead. Use the table_names argument to analyze to avoid this when analyzing the same variable multiple times.
+#   occured at (row) path: root
+
+tbl3
+
#                        A       B       C  
+# ——————————————————————————————————————————
+# foo label                                 
+#   row 1              0.05    0.13    0.18 
+#   row 2              0.985   0.815   0.890
+# bar label                                 
+#   more rows 1        -0.0     0.2     0.3 
+#   even more rows 1   1.20    1.15    1.16
+

This is how we recommend you specify the row names explicitly.

+
+
+

Tabulation With Row Structure +

+

Let’s say we would like to create the following table:

+
            A         B         C
+--------------------------------------
+U        foo(df_UA)  foo(df_UB)  foo(df_UC)
+V        foo(df_VA)  foo(df_VB)  foo(df_VC)
+W        foo(df_WA)  foo(df_WB)  foo(df_WC)
+

where df_* are subsets of df as +follows:

+
+df_UA <- df %>% filter(r1 == "U", c1 == "A")
+df_VA <- df %>% filter(r1 == "V", c1 == "A")
+df_WA <- df %>% filter(r1 == "W", c1 == "A")
+df_UB <- df %>% filter(r1 == "U", c1 == "B")
+df_VB <- df %>% filter(r1 == "V", c1 == "B")
+df_WB <- df %>% filter(r1 == "W", c1 == "C")
+df_UC <- df %>% filter(r1 == "U", c1 == "C")
+df_VC <- df %>% filter(r1 == "V", c1 == "C")
+df_WC <- df %>% filter(r1 == "W", c1 == "C")
+

further note that df_* are of the same class as +df, i.e. tibbles. Hence foo +aggregates the subset of our data to a cell value.

+

Given a function foo (ignore the ... for +now):

+
+foo <- function(df, labelstr = "", ...) {
+  paste(dim(df), collapse = " x ")
+}
+

we can start calculating the cell values individually:

+
+foo(df_UA)
+
# [1] "17 x 6"
+
+foo(df_VA)
+
# [1] "9 x 6"
+
+foo(df_WA)
+
# [1] "14 x 6"
+
+foo(df_UB)
+
# [1] "13 x 6"
+
+foo(df_VB)
+
# [1] "15 x 6"
+
+foo(df_WB)
+
# [1] "11 x 6"
+
+foo(df_UC)
+
# [1] "10 x 6"
+
+foo(df_VC)
+
# [1] "5 x 6"
+
+foo(df_WC)
+
# [1] "11 x 6"
+

Now we are still missing the table structure:

+
+matrix(
+  list(
+    foo(df_UA),
+    foo(df_VA),
+    foo(df_WA),
+    foo(df_UB),
+    foo(df_VB),
+    foo(df_WB),
+    foo(df_UC),
+    foo(df_VC),
+    foo(df_WC)
+  ),
+  byrow = FALSE, ncol = 3
+)
+
#      [,1]     [,2]     [,3]    
+# [1,] "17 x 6" "13 x 6" "10 x 6"
+# [2,] "9 x 6"  "15 x 6" "5 x 6" 
+# [3,] "14 x 6" "11 x 6" "11 x 6"
+

In rtables this type of tabulation is done with +layouts:

+
+lyt4 <- basic_table() %>%
+  split_cols_by("c1") %>%
+  split_rows_by("r1") %>%
+  analyze("x", foo)
+
+tbl4 <- build_table(lyt4, df)
+tbl4
+
#           A        B        C   
+# ————————————————————————————————
+# U                               
+#   foo   17 x 6   13 x 6   10 x 6
+# V                               
+#   foo   9 x 6    15 x 6   5 x 6 
+# W                               
+#   foo   14 x 6   6 x 6    11 x 6
+

or if we would not want to see the foo label we would +have to use:

+
+lyt5 <- basic_table() %>%
+  split_cols_by("c1") %>%
+  split_rows_by("r1") %>%
+  summarize_row_groups(cfun = foo, format = "xx")
+
+tbl5 <- build_table(lyt5, df)
+tbl5
+
#      A        B        C   
+# ———————————————————————————
+#    17 x 6   13 x 6   10 x 6
+#    9 x 6    15 x 6   5 x 6 
+#    14 x 6   6 x 6    11 x 6
+

but now the row labels have disappeared. This is because +cfun needs to define its row label. So let’s redefine +foo:

+
+foo <- function(df, labelstr) {
+  rcell(paste(dim(df), collapse = " x "), format = "xx", label = labelstr)
+}
+
+lyt6 <- basic_table() %>%
+  split_cols_by("c1") %>%
+  split_rows_by("r1") %>%
+  summarize_row_groups(cfun = foo)
+
+tbl6 <- build_table(lyt6, df)
+tbl6
+
#       A        B        C   
+# ————————————————————————————
+# U   17 x 6   13 x 6   10 x 6
+# V   9 x 6    15 x 6   5 x 6 
+# W   14 x 6   6 x 6    11 x 6
+
+

Calculating the Mean +

+

Now let’s calculate the mean of df$y for pattern I:

+
+foo <- function(df, labelstr) {
+  rcell(mean(df$y), label = labelstr, format = "xx.xx")
+}
+
+lyt7 <- basic_table() %>%
+  split_cols_by("c1") %>%
+  split_rows_by("r1") %>%
+  summarize_row_groups(cfun = foo)
+
+tbl7 <- build_table(lyt7, df)
+tbl7
+
#       A       B       C  
+# —————————————————————————
+# U   -1.00   1.00    3.00 
+# V   -4.00   -2.00   0.00 
+# W   -7.00   -5.00   -3.00
+

Note that foo has the variable information hard-encoded +in the function body. Let’s try some alternatives returning to +analyze():

+
+lyt8 <- basic_table() %>%
+  split_cols_by("c1") %>%
+  split_rows_by("r1") %>%
+  analyze("y", afun = mean)
+
+tbl8 <- build_table(lyt8, df)
+tbl8
+
#          A    B    C 
+# —————————————————————
+# U                    
+#   mean   -1   1    3 
+# V                    
+#   mean   -4   -2   0 
+# W                    
+#   mean   -7   -5   -3
+

Note that the subset of the y variable is passed as the +x argument to mean(). We could also get the +data.frame instead of the variable:

+
+lyt9 <- basic_table() %>%
+  split_cols_by("c1") %>%
+  split_rows_by("r1") %>%
+  analyze("y", afun = function(df) mean(df$y))
+
+tbl9 <- build_table(lyt9, df)
+tbl9
+
#       A    B    C 
+# ——————————————————
+# U                 
+#   y   -1   1    3 
+# V                 
+#   y   -4   -2   0 
+# W                 
+#   y   -7   -5   -3
+

which is in contrast to:

+
+lyt10 <- basic_table() %>%
+  split_cols_by("c1") %>%
+  split_rows_by("r1") %>%
+  analyze("y", afun = function(x) mean(x))
+
+tbl10 <- build_table(lyt10, df)
+tbl10
+
#       A    B    C 
+# ——————————————————
+# U                 
+#   y   -1   1    3 
+# V                 
+#   y   -4   -2   0 
+# W                 
+#   y   -7   -5   -3
+

where the function receives the subset of y.

+
+
+

Group Summaries +

+

Pattern I is an interesting one as we can add more row structure +(with further splits). Consider the following table:

+
            A         B         C
+--------------------------------------
+U
+  u1     foo(<>)  foo(<>)  foo(<>)
+  u2     foo(<>)  foo(<>)  foo(<>)
+  u3     foo(<>)  foo(<>)  foo(<>)
+V
+  v1     foo(<>)  foo(<>)  foo(<>)
+  v2     foo(<>)  foo(<>)  foo(<>)
+  v3     foo(<>)  foo(<>)  foo(<>)
+W
+  w1     foo(<>)  foo(<>)  foo(<>)
+  w2     foo(<>)  foo(<>)  foo(<>)
+  w3     foo(<>)  foo(<>)  foo(<>)
+

where <> represents the data that is represented +by the cell. So for the cell U > u1, A we would have the +subset:

+
+df %>%
+  filter(r1 == "U", r2 == "u1", c1 == "A")
+
# # A tibble: 2 × 6
+#   c1    c2    r1    r2        x     y
+#   <fct> <chr> <fct> <chr> <dbl> <dbl>
+# 1 A     a2    U     u1    1.12     -1
+# 2 A     a1    U     u1    0.594    -1
+

and so on. We can get this table as follows:

+
+lyt11 <- basic_table() %>%
+  split_cols_by("c1") %>%
+  split_rows_by("r1") %>%
+  split_rows_by("r2") %>%
+  summarize_row_groups(cfun = function(df, labelstr) {
+    rcell(mean(df$x), format = "xx.xx", label = paste("mean x for", labelstr))
+  })
+
+tbl11 <- build_table(lyt11, df)
+tbl11
+
#                     A       B       C  
+# ———————————————————————————————————————
+# U                                      
+#   mean x for u3   -0.04   0.36    -0.25
+#   mean x for u1   0.86    0.32     NA  
+#   mean x for u2   -0.28   0.38    0.08 
+# V                                      
+#   mean x for v2   0.01    0.55    0.60 
+#   mean x for v3   -0.03   -0.30   1.06 
+#   mean x for v1   0.56    -0.27   -0.54
+# W                                      
+#   mean x for w1   -0.58   0.42    0.67 
+#   mean x for w3   0.56    0.69    -0.39
+#   mean x for w2   -1.99   -0.10   0.53
+

or, if we wanted to calculate two summaries per row split:

+
+s_mean_sd <- function(x) {
+  in_rows("mean (sd)" = rcell(c(mean(x), sd(x)), format = "xx.xx (xx.xx)"))
+}
+
+s_range <- function(x) {
+  in_rows("range" = rcell(range(x), format = "xx.xx - xx.xx"))
+}
+
+lyt12 <- basic_table() %>%
+  split_cols_by("c1") %>%
+  split_rows_by("r1") %>%
+  split_rows_by("r2") %>%
+  analyze("x", s_mean_sd, show_labels = "hidden") %>%
+  analyze("x", s_range, show_labels = "hidden")
+
+tbl12 <- build_table(lyt12, df)
+
# Warning: Non-unique sibling analysis table names. Using Labels instead. Use the table_names argument to analyze to avoid this when analyzing the same variable multiple times.
+#   occured at (row) path: r1[U]->r2[u3]
+
# Warning in min(x): no non-missing arguments to min; returning Inf
+
# Warning in max(x): no non-missing arguments to max; returning -Inf
+
# Warning: Non-unique sibling analysis table names. Using Labels instead. Use the table_names argument to analyze to avoid this when analyzing the same variable multiple times.
+#   occured at (row) path: r1[U]->r2[u1]
+
# Warning: Non-unique sibling analysis table names. Using Labels instead. Use the table_names argument to analyze to avoid this when analyzing the same variable multiple times.
+#   occured at (row) path: r1[U]->r2[u2]
+
# Warning: Non-unique sibling analysis table names. Using Labels instead. Use the table_names argument to analyze to avoid this when analyzing the same variable multiple times.
+#   occured at (row) path: r1[V]->r2[v2]
+
# Warning: Non-unique sibling analysis table names. Using Labels instead. Use the table_names argument to analyze to avoid this when analyzing the same variable multiple times.
+#   occured at (row) path: r1[V]->r2[v3]
+
# Warning: Non-unique sibling analysis table names. Using Labels instead. Use the table_names argument to analyze to avoid this when analyzing the same variable multiple times.
+#   occured at (row) path: r1[V]->r2[v1]
+
# Warning: Non-unique sibling analysis table names. Using Labels instead. Use the table_names argument to analyze to avoid this when analyzing the same variable multiple times.
+#   occured at (row) path: r1[W]->r2[w1]
+
# Warning: Non-unique sibling analysis table names. Using Labels instead. Use the table_names argument to analyze to avoid this when analyzing the same variable multiple times.
+#   occured at (row) path: r1[W]->r2[w3]
+
# Warning: Non-unique sibling analysis table names. Using Labels instead. Use the table_names argument to analyze to avoid this when analyzing the same variable multiple times.
+#   occured at (row) path: r1[W]->r2[w2]
+
+tbl12
+
#                       A              B              C      
+# ———————————————————————————————————————————————————————————
+# U                                                          
+#   u3                                                       
+#     mean (sd)   -0.04 (1.18)    0.36 (1.41)    -0.25 (0.72)
+#     range       -1.80 - 1.47    -1.28 - 2.40   -0.82 - 0.56
+#   u1                                                       
+#     mean (sd)    0.86 (0.38)    0.32 (0.51)         NA     
+#     range        0.59 - 1.12    -0.48 - 0.94    Inf - -Inf 
+#   u2                                                       
+#     mean (sd)   -0.28 (0.96)    0.38 (0.67)    0.08 (0.91) 
+#     range       -1.52 - 1.43    -0.39 - 0.82   -0.93 - 1.51
+# V                                                          
+#   v2                                                       
+#     mean (sd)    0.01 (0.25)    0.55 (1.14)    0.60 (0.03) 
+#     range       -0.16 - 0.18    -0.84 - 1.60   0.58 - 0.62 
+#   v3                                                       
+#     mean (sd)   -0.03 (0.37)    -0.30 (0.36)    1.06 (NA)  
+#     range       -0.41 - 0.33    -0.62 - 0.03   1.06 - 1.06 
+#   v1                                                       
+#     mean (sd)    0.56 (1.10)    -0.27 (0.73)   -0.54 (1.18)
+#     range       -0.16 - 2.17    -1.22 - 0.59   -1.38 - 0.29
+# W                                                          
+#   w1                                                       
+#     mean (sd)   -0.58 (0.85)     0.42 (NA)     0.67 (0.39) 
+#     range       -1.25 - 0.61    0.42 - 0.42    0.37 - 1.21 
+#   w3                                                       
+#     mean (sd)    0.56 (0.85)     0.69 (NA)     -0.39 (1.68)
+#     range       -0.71 - 1.98    0.69 - 0.69    -2.21 - 1.10
+#   w2                                                       
+#     mean (sd)    -1.99 (NA)     -0.10 (0.47)   0.53 (0.60) 
+#     range       -1.99 - -1.99   -0.61 - 0.39   -0.10 - 1.16
+

Which has the following structure:

+
                   A              B              C
+---------------------------------------------------------
+U
+  u1
+     mean_sd  s_mean_sd(<>)  s_mean_sd(<>)  s_mean_sd(<>)
+     range    s_range(<>)    s_range(<>)    s_range(<>)
+  u2
+     mean_sd  s_mean_sd(<>)  s_mean_sd(<>)  s_mean_sd(<>)
+     range    s_range(<>)    s_range(<>)    s_range(<>)
+  u3
+     mean_sd  s_mean_sd(<>)  s_mean_sd(<>)  s_mean_sd(<>)
+     range    s_range(<>)    s_range(<>)    s_range(<>)
+V
+  v1
+     mean_sd  s_mean_sd(<>)  s_mean_sd(<>)  s_mean_sd(<>)
+     range    s_range(<>)    s_range(<>)    s_range(<>)
+  v2
+     mean_sd  s_mean_sd(<>)  s_mean_sd(<>)  s_mean_sd(<>)
+     range    s_range(<>)    s_range(<>)    s_range(<>)
+  v3
+     mean_sd  s_mean_sd(<>)  s_mean_sd(<>)  s_mean_sd(<>)
+     range    s_range(<>)    s_range(<>)    s_range(<>)
+W
+  w1
+     mean_sd  s_mean_sd(<>)  s_mean_sd(<>)  s_mean_sd(<>)
+     range    s_range(<>)    s_range(<>)    s_range(<>)
+  w2
+     mean_sd  s_mean_sd(<>)  s_mean_sd(<>)  s_mean_sd(<>)
+     range    s_range(<>)    s_range(<>)    s_range(<>)
+  w3
+     mean_sd  s_mean_sd(<>)  s_mean_sd(<>)  s_mean_sd(<>)
+     range    s_range(<>)    s_range(<>)    s_range(<>)
+

The rows U, u1, u2, …, +W, w1, w2, w3 are +label rows and the other rows (with mean_sd and +range) are data rows. Currently we do not have content rows +in the table. Content rows summarize the data defined by their splitting +(i.e. V > v1, B). So if we wanted to add content rows at +the r2 split level then we would get:

+
                   A              B              C
+---------------------------------------------------------
+U
+  u1          s_cfun_2(<>)   s_cfun_2(<>)   s_cfun_2(<>)
+     mean_sd  s_mean_sd(<>)  s_mean_sd(<>)  s_mean_sd(<>)
+     range    s_range(<>)    s_range(<>)    s_range(<>)
+  u2          s_cfun_2(<>)   s_cfun_2(<>)   s_cfun_2(<>)
+     mean_sd  s_mean_sd(<>)  s_mean_sd(<>)  s_mean_sd(<>)
+     range    s_range(<>)    s_range(<>)    s_range(<>)
+  u3          s_cfun_2(<>)   s_cfun_2(<>)   s_cfun_2(<>)
+     mean_sd  s_mean_sd(<>)  s_mean_sd(<>)  s_mean_sd(<>)
+     range    s_range(<>)    s_range(<>)    s_range(<>)
+V
+  v1          s_cfun_2(<>)   s_cfun_2(<>)   s_cfun_2(<>)
+     mean_sd  s_mean_sd(<>)  s_mean_sd(<>)  s_mean_sd(<>)
+     range    s_range(<>)    s_range(<>)    s_range(<>)
+  v2          s_cfun_2(<>)   s_cfun_2(<>)   s_cfun_2(<>)
+     mean_sd  s_mean_sd(<>)  s_mean_sd(<>)  s_mean_sd(<>)
+     range    s_range(<>)    s_range(<>)    s_range(<>)
+  v3          s_cfun_2(<>)   s_cfun_2(<>)   s_cfun_2(<>)
+     mean_sd  s_mean_sd(<>)  s_mean_sd(<>)  s_mean_sd(<>)
+     range    s_range(<>)    s_range(<>)    s_range(<>)
+W
+  w1          s_cfun_2(<>)   s_cfun_2(<>)   s_cfun_2(<>)
+     mean_sd  s_mean_sd(<>)  s_mean_sd(<>)  s_mean_sd(<>)
+     range    s_range(<>)    s_range(<>)    s_range(<>)
+  w2          s_cfun_2(<>)   s_cfun_2(<>)   s_cfun_2(<>)
+     mean_sd  s_mean_sd(<>)  s_mean_sd(<>)  s_mean_sd(<>)
+     range    s_range(<>)    s_range(<>)    s_range(<>)
+  w3          s_cfun_2(<>)   s_cfun_2(<>)   s_cfun_2(<>)
+     mean_sd  s_mean_sd(<>)  s_mean_sd(<>)  s_mean_sd(<>)
+     range    s_range(<>)    s_range(<>)    s_range(<>)
+

where s_cfun_2 is the content function and either +returns one row via rcell() or multiple rows via +in_rows(). The data represented by <> +for the content rows is same data as for it’s descendant, i.e. for the +U > u1, A content row cell it is +df %>% filter(r1 == "U", r2 == "u1", c1 == "A"). Note +that content functions cfun operate only on data frames and +not on vectors/variables so they must take the df argument. +Further, a cfun must also have the labelstr +argument which is the split level. This way, the cfun can +define its own row name. In order to get the table above we can use the +layout framework as follows:

+
+s_mean_sd <- function(x) {
+  in_rows("mean (sd)" = rcell(c(mean(x), sd(x)), format = "xx.xx (xx.xx)"))
+}
+
+s_range <- function(x) {
+  in_rows("range" = rcell(range(x), format = "xx.xx - xx.xx"))
+}
+
+s_cfun_2 <- function(df, labelstr) {
+  rcell(nrow(df), format = "xx", label = paste(labelstr, "(n)"))
+}
+
+lyt13 <- basic_table() %>%
+  split_cols_by("c1") %>%
+  split_rows_by("r1") %>%
+  split_rows_by("r2") %>%
+  summarize_row_groups(cfun = s_cfun_2) %>%
+  analyze("x", s_mean_sd, show_labels = "hidden") %>%
+  analyze("x", s_range, show_labels = "hidden")
+
+tbl13 <- build_table(lyt13, df)
+
# Warning: Non-unique sibling analysis table names. Using Labels instead. Use the table_names argument to analyze to avoid this when analyzing the same variable multiple times.
+#   occured at (row) path: r1[U]->r2[u3]
+
# Warning in min(x): no non-missing arguments to min; returning Inf
+
# Warning in max(x): no non-missing arguments to max; returning -Inf
+
# Warning: Non-unique sibling analysis table names. Using Labels instead. Use the table_names argument to analyze to avoid this when analyzing the same variable multiple times.
+#   occured at (row) path: r1[U]->r2[u1]
+
# Warning: Non-unique sibling analysis table names. Using Labels instead. Use the table_names argument to analyze to avoid this when analyzing the same variable multiple times.
+#   occured at (row) path: r1[U]->r2[u2]
+
# Warning: Non-unique sibling analysis table names. Using Labels instead. Use the table_names argument to analyze to avoid this when analyzing the same variable multiple times.
+#   occured at (row) path: r1[V]->r2[v2]
+
# Warning: Non-unique sibling analysis table names. Using Labels instead. Use the table_names argument to analyze to avoid this when analyzing the same variable multiple times.
+#   occured at (row) path: r1[V]->r2[v3]
+
# Warning: Non-unique sibling analysis table names. Using Labels instead. Use the table_names argument to analyze to avoid this when analyzing the same variable multiple times.
+#   occured at (row) path: r1[V]->r2[v1]
+
# Warning: Non-unique sibling analysis table names. Using Labels instead. Use the table_names argument to analyze to avoid this when analyzing the same variable multiple times.
+#   occured at (row) path: r1[W]->r2[w1]
+
# Warning: Non-unique sibling analysis table names. Using Labels instead. Use the table_names argument to analyze to avoid this when analyzing the same variable multiple times.
+#   occured at (row) path: r1[W]->r2[w3]
+
# Warning: Non-unique sibling analysis table names. Using Labels instead. Use the table_names argument to analyze to avoid this when analyzing the same variable multiple times.
+#   occured at (row) path: r1[W]->r2[w2]
+
+tbl13
+
#                       A              B              C      
+# ———————————————————————————————————————————————————————————
+# U                                                          
+#   u3 (n)              6              5              3      
+#     mean (sd)   -0.04 (1.18)    0.36 (1.41)    -0.25 (0.72)
+#     range       -1.80 - 1.47    -1.28 - 2.40   -0.82 - 0.56
+#   u1 (n)              2              5              0      
+#     mean (sd)    0.86 (0.38)    0.32 (0.51)         NA     
+#     range        0.59 - 1.12    -0.48 - 0.94    Inf - -Inf 
+#   u2 (n)              9              3              7      
+#     mean (sd)   -0.28 (0.96)    0.38 (0.67)    0.08 (0.91) 
+#     range       -1.52 - 1.43    -0.39 - 0.82   -0.93 - 1.51
+# V                                                          
+#   v2 (n)              2              4              2      
+#     mean (sd)    0.01 (0.25)    0.55 (1.14)    0.60 (0.03) 
+#     range       -0.16 - 0.18    -0.84 - 1.60   0.58 - 0.62 
+#   v3 (n)              3              4              1      
+#     mean (sd)   -0.03 (0.37)    -0.30 (0.36)    1.06 (NA)  
+#     range       -0.41 - 0.33    -0.62 - 0.03   1.06 - 1.06 
+#   v1 (n)              4              7              2      
+#     mean (sd)    0.56 (1.10)    -0.27 (0.73)   -0.54 (1.18)
+#     range       -0.16 - 2.17    -1.22 - 0.59   -1.38 - 0.29
+# W                                                          
+#   w1 (n)              4              1              4      
+#     mean (sd)   -0.58 (0.85)     0.42 (NA)     0.67 (0.39) 
+#     range       -1.25 - 0.61    0.42 - 0.42    0.37 - 1.21 
+#   w3 (n)              9              1              3      
+#     mean (sd)    0.56 (0.85)     0.69 (NA)     -0.39 (1.68)
+#     range       -0.71 - 1.98    0.69 - 0.69    -2.21 - 1.10
+#   w2 (n)              1              4              4      
+#     mean (sd)    -1.99 (NA)     -0.10 (0.47)   0.53 (0.60) 
+#     range       -1.99 - -1.99   -0.61 - 0.39   -0.10 - 1.16
+

In the same manner, if we want content rows for the r1 +split we can do it at as follows:

+
+lyt14 <- basic_table() %>%
+  split_cols_by("c1") %>%
+  split_rows_by("r1") %>%
+  summarize_row_groups(cfun = s_cfun_2) %>%
+  split_rows_by("r2") %>%
+  summarize_row_groups(cfun = s_cfun_2) %>%
+  analyze("x", s_mean_sd, show_labels = "hidden") %>%
+  analyze("x", s_range, show_labels = "hidden")
+
+tbl14 <- build_table(lyt14, df)
+
# Warning: Non-unique sibling analysis table names. Using Labels instead. Use the table_names argument to analyze to avoid this when analyzing the same variable multiple times.
+#   occured at (row) path: r1[U]->r2[u3]
+
# Warning in min(x): no non-missing arguments to min; returning Inf
+
# Warning in max(x): no non-missing arguments to max; returning -Inf
+
# Warning: Non-unique sibling analysis table names. Using Labels instead. Use the table_names argument to analyze to avoid this when analyzing the same variable multiple times.
+#   occured at (row) path: r1[U]->r2[u1]
+
# Warning: Non-unique sibling analysis table names. Using Labels instead. Use the table_names argument to analyze to avoid this when analyzing the same variable multiple times.
+#   occured at (row) path: r1[U]->r2[u2]
+
# Warning: Non-unique sibling analysis table names. Using Labels instead. Use the table_names argument to analyze to avoid this when analyzing the same variable multiple times.
+#   occured at (row) path: r1[V]->r2[v2]
+
# Warning: Non-unique sibling analysis table names. Using Labels instead. Use the table_names argument to analyze to avoid this when analyzing the same variable multiple times.
+#   occured at (row) path: r1[V]->r2[v3]
+
# Warning: Non-unique sibling analysis table names. Using Labels instead. Use the table_names argument to analyze to avoid this when analyzing the same variable multiple times.
+#   occured at (row) path: r1[V]->r2[v1]
+
# Warning: Non-unique sibling analysis table names. Using Labels instead. Use the table_names argument to analyze to avoid this when analyzing the same variable multiple times.
+#   occured at (row) path: r1[W]->r2[w1]
+
# Warning: Non-unique sibling analysis table names. Using Labels instead. Use the table_names argument to analyze to avoid this when analyzing the same variable multiple times.
+#   occured at (row) path: r1[W]->r2[w3]
+
# Warning: Non-unique sibling analysis table names. Using Labels instead. Use the table_names argument to analyze to avoid this when analyzing the same variable multiple times.
+#   occured at (row) path: r1[W]->r2[w2]
+
+tbl14
+
#                       A              B              C      
+# ———————————————————————————————————————————————————————————
+# U (n)                17              13             10     
+#   u3 (n)              6              5              3      
+#     mean (sd)   -0.04 (1.18)    0.36 (1.41)    -0.25 (0.72)
+#     range       -1.80 - 1.47    -1.28 - 2.40   -0.82 - 0.56
+#   u1 (n)              2              5              0      
+#     mean (sd)    0.86 (0.38)    0.32 (0.51)         NA     
+#     range        0.59 - 1.12    -0.48 - 0.94    Inf - -Inf 
+#   u2 (n)              9              3              7      
+#     mean (sd)   -0.28 (0.96)    0.38 (0.67)    0.08 (0.91) 
+#     range       -1.52 - 1.43    -0.39 - 0.82   -0.93 - 1.51
+# V (n)                 9              15             5      
+#   v2 (n)              2              4              2      
+#     mean (sd)    0.01 (0.25)    0.55 (1.14)    0.60 (0.03) 
+#     range       -0.16 - 0.18    -0.84 - 1.60   0.58 - 0.62 
+#   v3 (n)              3              4              1      
+#     mean (sd)   -0.03 (0.37)    -0.30 (0.36)    1.06 (NA)  
+#     range       -0.41 - 0.33    -0.62 - 0.03   1.06 - 1.06 
+#   v1 (n)              4              7              2      
+#     mean (sd)    0.56 (1.10)    -0.27 (0.73)   -0.54 (1.18)
+#     range       -0.16 - 2.17    -1.22 - 0.59   -1.38 - 0.29
+# W (n)                14              6              11     
+#   w1 (n)              4              1              4      
+#     mean (sd)   -0.58 (0.85)     0.42 (NA)     0.67 (0.39) 
+#     range       -1.25 - 0.61    0.42 - 0.42    0.37 - 1.21 
+#   w3 (n)              9              1              3      
+#     mean (sd)    0.56 (0.85)     0.69 (NA)     -0.39 (1.68)
+#     range       -0.71 - 1.98    0.69 - 0.69    -2.21 - 1.10
+#   w2 (n)              1              4              4      
+#     mean (sd)    -1.99 (NA)     -0.10 (0.47)   0.53 (0.60) 
+#     range       -1.99 - -1.99   -0.61 - 0.39   -0.10 - 1.16
+

In pagination, content rows and label rows get repeated if a page is +split in a descendant of a content row. So, for example, if we were to +split the following table at ***:

+
                   A              B              C
+---------------------------------------------------------
+U
+  u1 (n)      s_cfun_2(<>)   s_cfun_2(<>)   s_cfun_2(<>)
+     mean_sd  s_mean_sd(<>)  s_mean_sd(<>)  s_mean_sd(<>)
+***
+     range    s_range(<>)    s_range(<>)    s_range(<>)
+  u2 (n)      s_cfun_2(<>)   s_cfun_2(<>)   s_cfun_2(<>)
+     mean_sd  s_mean_sd(<>)  s_mean_sd(<>)  s_mean_sd(<>)
+     range    s_range(<>)    s_range(<>)    s_range(<>)
+

Then we would get the following two tables:

+
                   A              B              C
+---------------------------------------------------------
+U
+  u1 (n)      s_cfun_2(<>)   s_cfun_2(<>)   s_cfun_2(<>)
+     mean_sd  s_mean_sd(<>)  s_mean_sd(<>)  s_mean_sd(<>)
+

and

+
                   A              B              C
+---------------------------------------------------------
+U
+  u1 (n)      s_cfun_2(<>)   s_cfun_2(<>)   s_cfun_2(<>)
+     range    s_range(<>)    s_range(<>)    s_range(<>)
+  u2 (n)      s_cfun_2(<>)   s_cfun_2(<>)   s_cfun_2(<>)
+     mean_sd  s_mean_sd(<>)  s_mean_sd(<>)  s_mean_sd(<>)
+     range    s_range(<>)    s_range(<>)    s_range(<>)
+
+
+

Pattern III +

+

Let’s consider the following tabulation pattern:

+
                     A         B         C
+------------------------------------------------
+label 1        foo(x_A)  bar(x_B)  zoo(x_C)
+label 2        foo(x_A)  bar(x_B)  zoo(x_C)
+label 3        foo(x_A)  bar(x_B)  zoo(x_C)
+

We will discuss that in a future release of rtables.

+
+
+
+
+ + + + +
+ + + + + + + diff --git a/v0.6.9/articles/tabulation_dplyr.html b/v0.6.9/articles/tabulation_dplyr.html new file mode 100644 index 000000000..142334f77 --- /dev/null +++ b/v0.6.9/articles/tabulation_dplyr.html @@ -0,0 +1,486 @@ + + + + + + + + +Comparison with dplyr Tabulation • rtables + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + + +
+

Introduction +

+

In this vignette, we would like to discuss the similarities and +differences between dplyr and rtable.

+

Much of the rtables framework focuses on +tabulation/summarizing of data and then the visualization of the table. +In this vignette, we focus on summarizing data using dplyr +and contrast it to rtables. We won’t pay attention to the +table visualization/markup and just derive the cell content.

+

Using dplyr to summarize data and gt to +visualize the table is a good way if the tabulation is of a certain +nature or complexity. However, there are tables such as the table +created in the introduction +vignette that take some effort to create with dplyr. Part +of the effort is due to fact that when using dplyr the +table data is stored in data.frames or tibbles +which is not the most natural way to represent a table as we will show +in this vignette.

+

If you know a more elegant way of deriving the table content with +dplyr please let us know and we will update the +vignette.

+ +

Here is the table and data used in the introduction +vignette:

+
+n <- 400
+
+set.seed(1)
+
+df <- tibble(
+  arm = factor(sample(c("Arm A", "Arm B"), n, replace = TRUE), levels = c("Arm A", "Arm B")),
+  country = factor(sample(c("CAN", "USA"), n, replace = TRUE, prob = c(.55, .45)), levels = c("CAN", "USA")),
+  gender = factor(sample(c("Female", "Male"), n, replace = TRUE), levels = c("Female", "Male")),
+  handed = factor(sample(c("Left", "Right"), n, prob = c(.6, .4), replace = TRUE), levels = c("Left", "Right")),
+  age = rchisq(n, 30) + 10
+) %>% mutate(
+  weight = 35 * rnorm(n, sd = .5) + ifelse(gender == "Female", 140, 180)
+)
+
+lyt <- basic_table(show_colcounts = TRUE) %>%
+  split_cols_by("arm") %>%
+  split_cols_by("gender") %>%
+  split_rows_by("country") %>%
+  summarize_row_groups() %>%
+  split_rows_by("handed") %>%
+  summarize_row_groups() %>%
+  analyze("age", afun = mean, format = "xx.x")
+
+tbl <- build_table(lyt, df)
+tbl
+
#                     Arm A                     Arm B         
+#              Female        Male        Female        Male   
+#              (N=96)      (N=105)       (N=92)      (N=107)  
+# ————————————————————————————————————————————————————————————
+# CAN        45 (46.9%)   64 (61.0%)   46 (50.0%)   62 (57.9%)
+#   Left     32 (33.3%)   42 (40.0%)   26 (28.3%)   37 (34.6%)
+#     mean      38.9         40.4         40.3         37.7   
+#   Right    13 (13.5%)   22 (21.0%)   20 (21.7%)   25 (23.4%)
+#     mean      36.6         40.2         40.2         40.6   
+# USA        51 (53.1%)   41 (39.0%)   46 (50.0%)   45 (42.1%)
+#   Left     34 (35.4%)   19 (18.1%)   25 (27.2%)   25 (23.4%)
+#     mean      40.4         39.7         39.2         40.1   
+#   Right    17 (17.7%)   22 (21.0%)   21 (22.8%)   20 (18.7%)
+#     mean      36.9         39.8         38.5         39.0
+
+
+

Getting Started +

+

We will start by deriving the first data cell on row 3 (note, row 1 +and 2 have content cells, see the introduction +vignette). Cell 3,1 contains the mean age for left handed & female +Canadians in “Arm A”:

+
+mean(df$age[df$country == "CAN" & df$arm == "Arm A" & df$gender == "Female" & df$handed == "Left"])
+
# [1] 38.86979
+

or with dplyr:

+
+df %>%
+  filter(country == "CAN", arm == "Arm A", gender == "Female", handed == "Left") %>%
+  summarise(mean_age = mean(age))
+
# # A tibble: 1 × 1
+#   mean_age
+#      <dbl>
+# 1     38.9
+

Further, dplyr gives us other verbs to easily get the +average age of left handed Canadians for each group defined by the 4 +columns:

+
+df %>%
+  group_by(arm, gender) %>%
+  filter(country == "CAN", handed == "Left") %>%
+  summarise(mean_age = mean(age))
+
# `summarise()` has grouped output by 'arm'. You can override using the `.groups`
+# argument.
+
# # A tibble: 4 × 3
+# # Groups:   arm [2]
+#   arm   gender mean_age
+#   <fct> <fct>     <dbl>
+# 1 Arm A Female     38.9
+# 2 Arm A Male       40.4
+# 3 Arm B Female     40.3
+# 4 Arm B Male       37.7
+

We can further get to all the average age cell values with:

+
+average_age <- df %>%
+  group_by(arm, gender, country, handed) %>%
+  summarise(mean_age = mean(age))
+
# `summarise()` has grouped output by 'arm', 'gender', 'country'. You can
+# override using the `.groups` argument.
+
+average_age
+
# # A tibble: 16 × 5
+# # Groups:   arm, gender, country [8]
+#    arm   gender country handed mean_age
+#    <fct> <fct>  <fct>   <fct>     <dbl>
+#  1 Arm A Female CAN     Left       38.9
+#  2 Arm A Female CAN     Right      36.6
+#  3 Arm A Female USA     Left       40.4
+#  4 Arm A Female USA     Right      36.9
+#  5 Arm A Male   CAN     Left       40.4
+#  6 Arm A Male   CAN     Right      40.2
+#  7 Arm A Male   USA     Left       39.7
+#  8 Arm A Male   USA     Right      39.8
+#  9 Arm B Female CAN     Left       40.3
+# 10 Arm B Female CAN     Right      40.2
+# 11 Arm B Female USA     Left       39.2
+# 12 Arm B Female USA     Right      38.5
+# 13 Arm B Male   CAN     Left       37.7
+# 14 Arm B Male   CAN     Right      40.6
+# 15 Arm B Male   USA     Left       40.1
+# 16 Arm B Male   USA     Right      39.0
+

In rtable syntax, we need the following code to get to +the same content:

+
+lyt <- basic_table() %>%
+  split_cols_by("arm") %>%
+  split_cols_by("gender") %>%
+  split_rows_by("country") %>%
+  split_rows_by("handed") %>%
+  analyze("age", afun = mean, format = "xx.x")
+
+tbl <- build_table(lyt, df)
+tbl
+
#                Arm A           Arm B    
+#            Female   Male   Female   Male
+# ————————————————————————————————————————
+# CAN                                     
+#   Left                                  
+#     mean    38.9    40.4    40.3    37.7
+#   Right                                 
+#     mean    36.6    40.2    40.2    40.6
+# USA                                     
+#   Left                                  
+#     mean    40.4    39.7    39.2    40.1
+#   Right                                 
+#     mean    36.9    39.8    38.5    39.0
+

As mentioned in the introduction to this vignette, please ignore the +difference in arranging and formatting the data: it’s possible to +condense the rtable more and it is possible to make the +tibble look more like the reference table using the +gt R package.

+

In terms of tabulation for this example there was arguably not much +added by rtables over dplyr.

+
+
+

Content Information +

+

Unlike in rtables the different levels of summarization +are discrete computations in dplyr which we will then need +to combine

+

We first focus on the count and percentage information for handedness +within each country (for each arm-gender pair), along with the analysis +row mean values:

+
+c_h_df <- df %>%
+  group_by(arm, gender, country, handed) %>%
+  summarize(mean = mean(age), c_h_count = n()) %>%
+  ## we need the sum below to *not* be by country, so that we're dividing by the column counts
+  ungroup(country) %>%
+  # now the `handed` grouping has been removed, therefore we can calculate percent now:
+  mutate(n_col = sum(c_h_count), c_h_percent = c_h_count / n_col)
+
# `summarise()` has grouped output by 'arm', 'gender', 'country'. You can
+# override using the `.groups` argument.
+
+c_h_df
+
# # A tibble: 16 × 8
+# # Groups:   arm, gender [4]
+#    arm   gender country handed  mean c_h_count n_col c_h_percent
+#    <fct> <fct>  <fct>   <fct>  <dbl>     <int> <int>       <dbl>
+#  1 Arm A Female CAN     Left    38.9        32    96       0.333
+#  2 Arm A Female CAN     Right   36.6        13    96       0.135
+#  3 Arm A Female USA     Left    40.4        34    96       0.354
+#  4 Arm A Female USA     Right   36.9        17    96       0.177
+#  5 Arm A Male   CAN     Left    40.4        42   105       0.4  
+#  6 Arm A Male   CAN     Right   40.2        22   105       0.210
+#  7 Arm A Male   USA     Left    39.7        19   105       0.181
+#  8 Arm A Male   USA     Right   39.8        22   105       0.210
+#  9 Arm B Female CAN     Left    40.3        26    92       0.283
+# 10 Arm B Female CAN     Right   40.2        20    92       0.217
+# 11 Arm B Female USA     Left    39.2        25    92       0.272
+# 12 Arm B Female USA     Right   38.5        21    92       0.228
+# 13 Arm B Male   CAN     Left    37.7        37   107       0.346
+# 14 Arm B Male   CAN     Right   40.6        25   107       0.234
+# 15 Arm B Male   USA     Left    40.1        25   107       0.234
+# 16 Arm B Male   USA     Right   39.0        20   107       0.187
+

which has 16 rows (cells) like the average_age data +frame defined above. Next, we will derive the group information for +countries:

+
+c_df <- df %>%
+  group_by(arm, gender, country) %>%
+  summarize(c_count = n()) %>%
+  # now the `handed` grouping has been removed, therefore we can calculate percent now:
+  mutate(n_col = sum(c_count), c_percent = c_count / n_col)
+
# `summarise()` has grouped output by 'arm', 'gender'. You can override using the
+# `.groups` argument.
+
+c_df
+
# # A tibble: 8 × 6
+# # Groups:   arm, gender [4]
+#   arm   gender country c_count n_col c_percent
+#   <fct> <fct>  <fct>     <int> <int>     <dbl>
+# 1 Arm A Female CAN          45    96     0.469
+# 2 Arm A Female USA          51    96     0.531
+# 3 Arm A Male   CAN          64   105     0.610
+# 4 Arm A Male   USA          41   105     0.390
+# 5 Arm B Female CAN          46    92     0.5  
+# 6 Arm B Female USA          46    92     0.5  
+# 7 Arm B Male   CAN          62   107     0.579
+# 8 Arm B Male   USA          45   107     0.421
+

Finally, we left_join() the two levels of summary to get +a data.frame containing the full set of values which make up the body of +our table (note, however, they are not in the same order):

+
+full_dplyr <- left_join(c_h_df, c_df) %>% ungroup()
+
# Joining with `by = join_by(arm, gender, country, n_col)`
+

Alternatively, we could calculate only the counts in +c_h_df, and use mutate() after the +left_join() to divide the counts by the n_col +values which are more naturally calculated within c_df. +This would simplify c_h_df’s creation somewhat by not +requiring the explicit ungroup(), but it prevents each +level of summarization from being a self-contained set of +computations.

+

The rtables call in contrast is:

+
+lyt <- basic_table(show_colcounts = TRUE) %>%
+  split_cols_by("arm") %>%
+  split_cols_by("gender") %>%
+  split_rows_by("country") %>%
+  summarize_row_groups() %>%
+  split_rows_by("handed") %>%
+  summarize_row_groups() %>%
+  analyze("age", afun = mean, format = "xx.x")
+
+tbl <- build_table(lyt, df)
+tbl
+
#                     Arm A                     Arm B         
+#              Female        Male        Female        Male   
+#              (N=96)      (N=105)       (N=92)      (N=107)  
+# ————————————————————————————————————————————————————————————
+# CAN        45 (46.9%)   64 (61.0%)   46 (50.0%)   62 (57.9%)
+#   Left     32 (33.3%)   42 (40.0%)   26 (28.3%)   37 (34.6%)
+#     mean      38.9         40.4         40.3         37.7   
+#   Right    13 (13.5%)   22 (21.0%)   20 (21.7%)   25 (23.4%)
+#     mean      36.6         40.2         40.2         40.6   
+# USA        51 (53.1%)   41 (39.0%)   46 (50.0%)   45 (42.1%)
+#   Left     34 (35.4%)   19 (18.1%)   25 (27.2%)   25 (23.4%)
+#     mean      40.4         39.7         39.2         40.1   
+#   Right    17 (17.7%)   22 (21.0%)   21 (22.8%)   20 (18.7%)
+#     mean      36.9         39.8         38.5         39.0
+

We can now spot check that the values are the same

+
+frm_rtables_h <- cell_values(
+  tbl,
+  rowpath = c("country", "CAN", "handed", "Right", "@content"),
+  colpath = c("arm", "Arm B", "gender", "Female")
+)[[1]]
+frm_rtables_h
+
# [1] 20.0000000  0.2173913
+
+frm_dplyr_h <- full_dplyr %>%
+  filter(country == "CAN" & handed == "Right" & arm == "Arm B" & gender == "Female") %>%
+  select(c_h_count, c_h_percent)
+
+frm_dplyr_h
+
# # A tibble: 1 × 2
+#   c_h_count c_h_percent
+#       <int>       <dbl>
+# 1        20       0.217
+
+frm_rtables_c <- cell_values(
+  tbl,
+  rowpath = c("country", "CAN", "@content"),
+  colpath = c("arm", "Arm A", "gender", "Male")
+)[[1]]
+
+frm_rtables_c
+
# [1] 64.0000000  0.6095238
+
+frm_dplyr_c <- full_dplyr %>%
+  filter(country == "CAN" & arm == "Arm A" & gender == "Male") %>%
+  select(c_count, c_percent)
+
+frm_dplyr_c
+
# # A tibble: 2 × 2
+#   c_count c_percent
+#     <int>     <dbl>
+# 1      64     0.610
+# 2      64     0.610
+

Further, the rtable syntax has hopefully also become a +bit more straightforward to derive the cell values than with +dplyr for this particular table.

+
+
+

Summary +

+

In this vignette learned that:

+
    +
  • many tables are quite easily created with dplyr and +data.frame or tibble as data structure +
      +
    • +dplyr keeps simple things simple
    • +
    +
  • +
  • if tables have group summaries then repeating of information is +required
  • +
  • +rtables streamlines the construction of complex +tables
  • +
+

We recommend that you continue reading the clinical_trials +vignette where we create a number of more advanced tables using +layouts.

+
+
+
+ + + + +
+ + + + + + + diff --git a/v0.6.9/articles/title_footer.html b/v0.6.9/articles/title_footer.html new file mode 100644 index 000000000..58542ee17 --- /dev/null +++ b/v0.6.9/articles/title_footer.html @@ -0,0 +1,709 @@ + + + + + + + + +Titles, Footers, and Referential Footnotes • rtables + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + + +
+ +

An rtables table can be annotated with three types of +header (title) information, as well as three types of footer +information.

+

Header information comes in two forms that are specified directly +(main title and subtitles), as well as one that is populated +automatically as necessary (page title, which we will see in the next +section).

+

Similarly, footer materials come with two directly specified +components: main footer and provenance footer, in addition to one that +is computed when necessary: referential footnotes.

+

basic_table() accepts the values for each static title +and footer element during layout construction:

+
+library(rtables)
+library(dplyr)
+lyt <- basic_table(
+  title = "Study XXXXXXXX",
+  subtitles = c("subtitle YYYYYYYYYY", "subtitle2 ZZZZZZZZZ"),
+  main_footer = "Analysis was done using cool methods that are correct",
+  prov_footer = "file: /path/to/stuff/that/lives/there HASH:1ac41b242a"
+) %>%
+  split_cols_by("ARM") %>%
+  split_rows_by("SEX", split_fun = drop_split_levels) %>%
+  split_rows_by("STRATA1") %>%
+  analyze("AGE", mean, format = "xx.x")
+
+tbl <- build_table(lyt, DM)
+cat(export_as_txt(tbl, paginate = TRUE, page_break = "\n\n\n"))
+
# Study XXXXXXXX
+# subtitle YYYYYYYYYY
+# subtitle2 ZZZZZZZZZ
+# 
+# ——————————————————————————————————————————————————
+#            A: Drug X   B: Placebo   C: Combination
+# ——————————————————————————————————————————————————
+# F                                                 
+#   A                                               
+#     mean     30.9         32.9           36.0     
+#   B                                               
+#     mean     34.9         32.9           34.4     
+#   C                                               
+#     mean     35.2         36.0           34.3     
+# M                                                 
+#   A                                               
+#     mean     35.1         31.1           35.6     
+#   B                                               
+#     mean     36.6         32.1           34.4     
+#   C                                               
+#     mean     37.4         32.8           32.8     
+# ——————————————————————————————————————————————————
+# 
+# Analysis was done using cool methods that are correct
+# 
+# file: /path/to/stuff/that/lives/there HASH:1ac41b242a
+
+
+

Page-by splitting +

+

We often want to split tables based on the values of one or more +variables (e.g., lab measurement) and then paginate separately +within each of those table subsections. In rtables we +do this via page by row splits.

+

Row splits can be declared page by splits by setting +page_by = TRUE in the split_rows_by*() call, +as below.

+

When page by splits are present, page titles are generated +automatically by appending the split value (typically a factor level, +though it need not be), to the page_prefix, separated by a +:. By default, page_prefix is name of the +variable being split.

+
+lyt2 <- basic_table(
+  title = "Study XXXXXXXX",
+  subtitles = c("subtitle YYYYYYYYYY", "subtitle2 ZZZZZZZZZ"),
+  main_footer = "Analysis was done using cool methods that are correct",
+  prov_footer = "file: /path/to/stuff/that/lives/there HASH:1ac41b242a"
+) %>%
+  split_cols_by("ARM") %>%
+  split_rows_by("SEX", page_by = TRUE, page_prefix = "Patient Subset - Gender", split_fun = drop_split_levels) %>%
+  split_rows_by("STRATA1") %>%
+  analyze("AGE", mean, format = "xx.x")
+
+tbl2 <- build_table(lyt2, DM)
+cat(export_as_txt(tbl2, paginate = TRUE, page_break = "\n\n~~~~ Page Break ~~~~\n\n"))
+
# Study XXXXXXXX
+# subtitle YYYYYYYYYY
+# subtitle2 ZZZZZZZZZ
+# Patient Subset - Gender: F
+# 
+# ——————————————————————————————————————————————————
+#            A: Drug X   B: Placebo   C: Combination
+# ——————————————————————————————————————————————————
+# A                                                 
+#   mean       30.9         32.9           36.0     
+# B                                                 
+#   mean       34.9         32.9           34.4     
+# C                                                 
+#   mean       35.2         36.0           34.3     
+# ——————————————————————————————————————————————————
+# 
+# Analysis was done using cool methods that are correct
+# 
+# file: /path/to/stuff/that/lives/there HASH:1ac41b242a
+# 
+# 
+# ~~~~ Page Break ~~~~
+# 
+# Study XXXXXXXX
+# subtitle YYYYYYYYYY
+# subtitle2 ZZZZZZZZZ
+# Patient Subset - Gender: M
+# 
+# ——————————————————————————————————————————————————
+#            A: Drug X   B: Placebo   C: Combination
+# ——————————————————————————————————————————————————
+# A                                                 
+#   mean       35.1         31.1           35.6     
+# B                                                 
+#   mean       36.6         32.1           34.4     
+# C                                                 
+#   mean       37.4         32.8           32.8     
+# ——————————————————————————————————————————————————
+# 
+# Analysis was done using cool methods that are correct
+# 
+# file: /path/to/stuff/that/lives/there HASH:1ac41b242a
+

Page by row splits can be nested, but only within other page_by +splits, they cannot be nested within traditional row splits. In this +case, a page title for each page by split will be present on every +resulting page, as seen below:

+
+lyt3 <- basic_table(
+  title = "Study XXXXXXXX",
+  subtitles = c("subtitle YYYYYYYYYY", "subtitle2 ZZZZZZZZZ"),
+  main_footer = "Analysis was done using cool methods that are correct",
+  prov_footer = "file: /path/to/stuff/that/lives/there HASH:1ac41b242a"
+) %>%
+  split_cols_by("ARM") %>%
+  split_rows_by("SEX", page_by = TRUE, page_prefix = "Patient Subset - Gender", split_fun = drop_split_levels) %>%
+  split_rows_by("STRATA1", page_by = TRUE, page_prefix = "Stratification - Strata") %>%
+  analyze("AGE", mean, format = "xx.x")
+
+tbl3 <- build_table(lyt3, DM)
+cat(export_as_txt(tbl3, paginate = TRUE, page_break = "\n\n~~~~ Page Break ~~~~\n\n"))
+
# Study XXXXXXXX
+# subtitle YYYYYYYYYY
+# subtitle2 ZZZZZZZZZ
+# Patient Subset - Gender: F
+# Stratification - Strata: A
+# 
+# ——————————————————————————————————————————————————
+#            A: Drug X   B: Placebo   C: Combination
+# ——————————————————————————————————————————————————
+# mean         30.9         32.9           36.0     
+# ——————————————————————————————————————————————————
+# 
+# Analysis was done using cool methods that are correct
+# 
+# file: /path/to/stuff/that/lives/there HASH:1ac41b242a
+# 
+# 
+# ~~~~ Page Break ~~~~
+# 
+# Study XXXXXXXX
+# subtitle YYYYYYYYYY
+# subtitle2 ZZZZZZZZZ
+# Patient Subset - Gender: F
+# Stratification - Strata: B
+# 
+# ——————————————————————————————————————————————————
+#            A: Drug X   B: Placebo   C: Combination
+# ——————————————————————————————————————————————————
+# mean         34.9         32.9           34.4     
+# ——————————————————————————————————————————————————
+# 
+# Analysis was done using cool methods that are correct
+# 
+# file: /path/to/stuff/that/lives/there HASH:1ac41b242a
+# 
+# 
+# ~~~~ Page Break ~~~~
+# 
+# Study XXXXXXXX
+# subtitle YYYYYYYYYY
+# subtitle2 ZZZZZZZZZ
+# Patient Subset - Gender: F
+# Stratification - Strata: C
+# 
+# ——————————————————————————————————————————————————
+#            A: Drug X   B: Placebo   C: Combination
+# ——————————————————————————————————————————————————
+# mean         35.2         36.0           34.3     
+# ——————————————————————————————————————————————————
+# 
+# Analysis was done using cool methods that are correct
+# 
+# file: /path/to/stuff/that/lives/there HASH:1ac41b242a
+# 
+# 
+# ~~~~ Page Break ~~~~
+# 
+# Study XXXXXXXX
+# subtitle YYYYYYYYYY
+# subtitle2 ZZZZZZZZZ
+# Patient Subset - Gender: M
+# Stratification - Strata: A
+# 
+# ——————————————————————————————————————————————————
+#            A: Drug X   B: Placebo   C: Combination
+# ——————————————————————————————————————————————————
+# mean         35.1         31.1           35.6     
+# ——————————————————————————————————————————————————
+# 
+# Analysis was done using cool methods that are correct
+# 
+# file: /path/to/stuff/that/lives/there HASH:1ac41b242a
+# 
+# 
+# ~~~~ Page Break ~~~~
+# 
+# Study XXXXXXXX
+# subtitle YYYYYYYYYY
+# subtitle2 ZZZZZZZZZ
+# Patient Subset - Gender: M
+# Stratification - Strata: B
+# 
+# ——————————————————————————————————————————————————
+#            A: Drug X   B: Placebo   C: Combination
+# ——————————————————————————————————————————————————
+# mean         36.6         32.1           34.4     
+# ——————————————————————————————————————————————————
+# 
+# Analysis was done using cool methods that are correct
+# 
+# file: /path/to/stuff/that/lives/there HASH:1ac41b242a
+# 
+# 
+# ~~~~ Page Break ~~~~
+# 
+# Study XXXXXXXX
+# subtitle YYYYYYYYYY
+# subtitle2 ZZZZZZZZZ
+# Patient Subset - Gender: M
+# Stratification - Strata: C
+# 
+# ——————————————————————————————————————————————————
+#            A: Drug X   B: Placebo   C: Combination
+# ——————————————————————————————————————————————————
+# mean         37.4         32.8           32.8     
+# ——————————————————————————————————————————————————
+# 
+# Analysis was done using cool methods that are correct
+# 
+# file: /path/to/stuff/that/lives/there HASH:1ac41b242a
+
+
+

Referential Footnotes +

+

Referential footnotes are footnotes associated with a particular +component of a table: a column, a row, or a cell. They can be added +during tabulation via analysis functions, but they can also be added +post-hoc once a table is created.

+

They are rendered as a number within curly braces within the table +body, row, or column labels, followed by a message associated with that +number printed below the table during rendering.

+
+

Adding Cell- and Analysis-row Referential Footnotes At Tabulation +Time +

+
+afun <- function(df, .var, .spl_context) {
+  val <- .spl_context$value[NROW(.spl_context)]
+  rw_fnotes <- if (val == "C") list("This is strata level C for these patients") else list()
+  cl_fnotes <- if (val == "B" && df[1, "ARM", drop = TRUE] == "C: Combination") {
+    list("these Strata B patients got the drug combination")
+  } else {
+    list()
+  }
+
+  in_rows(
+    mean = mean(df[[.var]]),
+    .row_footnotes = rw_fnotes,
+    .cell_footnotes = cl_fnotes,
+    .formats = c(mean = "xx.x")
+  )
+}
+
+lyt <- basic_table(
+  title = "Study XXXXXXXX",
+  subtitles = c("subtitle YYYYYYYYYY", "subtitle2 ZZZZZZZZZ"),
+  main_footer = "Analysis was done using cool methods that are correct",
+  prov_footer = "file: /path/to/stuff/that/lives/there HASH:1ac41b242a"
+) %>%
+  split_cols_by("ARM") %>%
+  split_rows_by("SEX", page_by = TRUE, page_prefix = "Patient Subset - Gender", split_fun = drop_split_levels) %>%
+  split_rows_by("STRATA1") %>%
+  analyze("AGE", afun, format = "xx.x")
+
+tbl <- build_table(lyt, DM)
+cat(export_as_txt(tbl, paginate = TRUE, page_break = "\n\n\n"))
+
# Study XXXXXXXX
+# subtitle YYYYYYYYYY
+# subtitle2 ZZZZZZZZZ
+# Patient Subset - Gender: F
+# 
+# ——————————————————————————————————————————————————————
+#                A: Drug X   B: Placebo   C: Combination
+# ——————————————————————————————————————————————————————
+# A                                                     
+#   mean           30.9         32.9           36.0     
+# B                                                     
+#   mean           34.9         32.9         34.4 {1}   
+# C                                                     
+#   mean {2}       35.2         36.0           34.3     
+# ——————————————————————————————————————————————————————
+# 
+# {1} - these Strata B patients got the drug combination
+# {2} - This is strata level C for these patients
+# ——————————————————————————————————————————————————————
+# 
+# Analysis was done using cool methods that are correct
+# 
+# file: /path/to/stuff/that/lives/there HASH:1ac41b242a
+# 
+# 
+# 
+# Study XXXXXXXX
+# subtitle YYYYYYYYYY
+# subtitle2 ZZZZZZZZZ
+# Patient Subset - Gender: M
+# 
+# ——————————————————————————————————————————————————————
+#                A: Drug X   B: Placebo   C: Combination
+# ——————————————————————————————————————————————————————
+# A                                                     
+#   mean           35.1         31.1           35.6     
+# B                                                     
+#   mean           36.6         32.1         34.4 {1}   
+# C                                                     
+#   mean {2}       37.4         32.8           32.8     
+# ——————————————————————————————————————————————————————
+# 
+# {1} - these Strata B patients got the drug combination
+# {2} - This is strata level C for these patients
+# ——————————————————————————————————————————————————————
+# 
+# Analysis was done using cool methods that are correct
+# 
+# file: /path/to/stuff/that/lives/there HASH:1ac41b242a
+

We note that typically the type of footnote added within the analysis +function would be dependent on the computations done to calculate the +cell value(s), e.g., a model not converging. We simply use context +information as an illustrative proxy for that.

+

The procedure for adding footnotes to content (summary row) rows or +cells is identical to the above, when done within a content +function.

+
+
+

Annotating an Existing Table with Referential Footnotes +

+

In addition to inserting referential footnotes at tabulation time +within our analysis functions, we can also annotate our tables with them +post-hoc.

+

This is also the only way to add footnotes to column +labels, as those cannot be controlled within an analysis or content +function.

+
+## from ?tolower example slightly modified
+.simpleCap <- function(x) {
+  if (length(x) > 1) {
+    return(sapply(x, .simpleCap))
+  }
+  s <- strsplit(tolower(x), " ")[[1]]
+  paste(toupper(substring(s, 1, 1)), substring(s, 2), sep = "", collapse = " ")
+}
+
+adsl2 <- ex_adsl %>%
+  filter(SEX %in% c("M", "F") & RACE %in% (levels(RACE)[1:3])) %>%
+  ## we trim the level names here solely due to space considerations
+  mutate(ethnicity = .simpleCap(gsub("(.*)OR.*", "\\1", RACE)), RACE = factor(RACE))
+
+lyt2 <- basic_table() %>%
+  split_cols_by("ARM") %>%
+  split_cols_by("SEX", split_fun = drop_split_levels) %>%
+  split_rows_by("RACE", labels_var = "ethnicity", split_fun = drop_split_levels) %>%
+  summarize_row_groups() %>%
+  analyze(c("AGE", "STRATA1"))
+
+tbl2 <- build_table(lyt2, adsl2)
+tbl2
+
#                    A: Drug X                B: Placebo              C: Combination     
+#                 F            M            F            M            F            M     
+# ———————————————————————————————————————————————————————————————————————————————————————
+# Asian       41 (53.9%)   25 (54.3%)   36 (52.2%)   30 (60.0%)   39 (60.9%)   32 (57.1%)
+#   AGE                                                                                  
+#     Mean      31.22        34.60        35.06        38.63        36.44        37.66   
+#   STRATA1                                                                              
+#     A           11           10           14           10           11           7     
+#     B           11           9            15           7            11           14    
+#     C           19           6            7            13           17           11    
+# Black       18 (23.7%)   12 (26.1%)   16 (23.2%)   12 (24.0%)   14 (21.9%)   14 (25.0%)
+#   AGE                                                                                  
+#     Mean      34.06        34.58        33.88        36.33        33.21        34.21   
+#   STRATA1                                                                              
+#     A           5            2            5            6            3            7     
+#     B           6            5            3            4            4            4     
+#     C           7            5            8            2            7            3     
+# White       17 (22.4%)   9 (19.6%)    17 (24.6%)   8 (16.0%)    11 (17.2%)   10 (17.9%)
+#   AGE                                                                                  
+#     Mean      34.12        40.00        32.41        34.62        33.00        30.80   
+#   STRATA1                                                                              
+#     A           5            3            3            3            3            5     
+#     B           5            4            8            4            5            2     
+#     C           7            2            6            1            3            3
+

We do this with the fnotes_at_path<- function which +accepts a row path, a column path, and a value for the full set of +footnotes for the defined locations (NULL or a +character vector).

+

A non-NULL row path with a NULL column path +specifies the footnote(s) should be attached to the row, while +NULL row path with non-NULL column path +indicates they go with the column. Both being non-NULL +indicates a cell (and must resolve to an individual cell).

+
+fnotes_at_path(tbl2, c("RACE", "ASIAN")) <- c("hi", "there")
+tbl2
+
#                       A: Drug X                B: Placebo              C: Combination     
+#                    F            M            F            M            F            M     
+# ——————————————————————————————————————————————————————————————————————————————————————————
+# Asian {1, 2}   41 (53.9%)   25 (54.3%)   36 (52.2%)   30 (60.0%)   39 (60.9%)   32 (57.1%)
+#   AGE                                                                                     
+#     Mean         31.22        34.60        35.06        38.63        36.44        37.66   
+#   STRATA1                                                                                 
+#     A              11           10           14           10           11           7     
+#     B              11           9            15           7            11           14    
+#     C              19           6            7            13           17           11    
+# Black          18 (23.7%)   12 (26.1%)   16 (23.2%)   12 (24.0%)   14 (21.9%)   14 (25.0%)
+#   AGE                                                                                     
+#     Mean         34.06        34.58        33.88        36.33        33.21        34.21   
+#   STRATA1                                                                                 
+#     A              5            2            5            6            3            7     
+#     B              6            5            3            4            4            4     
+#     C              7            5            8            2            7            3     
+# White          17 (22.4%)   9 (19.6%)    17 (24.6%)   8 (16.0%)    11 (17.2%)   10 (17.9%)
+#   AGE                                                                                     
+#     Mean         34.12        40.00        32.41        34.62        33.00        30.80   
+#   STRATA1                                                                                 
+#     A              5            3            3            3            3            5     
+#     B              5            4            8            4            5            2     
+#     C              7            2            6            1            3            3     
+# ——————————————————————————————————————————————————————————————————————————————————————————
+# 
+# {1} - hi
+# {2} - there
+# ——————————————————————————————————————————————————————————————————————————————————————————
+
+fnotes_at_path(tbl2, rowpath = NULL, c("ARM", "B: Placebo")) <- c("this is a placebo")
+tbl2
+
#                       A: Drug X              B: Placebo {NA}           C: Combination     
+#                    F            M            F            M            F            M     
+# ——————————————————————————————————————————————————————————————————————————————————————————
+# Asian {1, 2}   41 (53.9%)   25 (54.3%)   36 (52.2%)   30 (60.0%)   39 (60.9%)   32 (57.1%)
+#   AGE                                                                                     
+#     Mean         31.22        34.60        35.06        38.63        36.44        37.66   
+#   STRATA1                                                                                 
+#     A              11           10           14           10           11           7     
+#     B              11           9            15           7            11           14    
+#     C              19           6            7            13           17           11    
+# Black          18 (23.7%)   12 (26.1%)   16 (23.2%)   12 (24.0%)   14 (21.9%)   14 (25.0%)
+#   AGE                                                                                     
+#     Mean         34.06        34.58        33.88        36.33        33.21        34.21   
+#   STRATA1                                                                                 
+#     A              5            2            5            6            3            7     
+#     B              6            5            3            4            4            4     
+#     C              7            5            8            2            7            3     
+# White          17 (22.4%)   9 (19.6%)    17 (24.6%)   8 (16.0%)    11 (17.2%)   10 (17.9%)
+#   AGE                                                                                     
+#     Mean         34.12        40.00        32.41        34.62        33.00        30.80   
+#   STRATA1                                                                                 
+#     A              5            3            3            3            3            5     
+#     B              5            4            8            4            5            2     
+#     C              7            2            6            1            3            3     
+# ——————————————————————————————————————————————————————————————————————————————————————————
+# 
+# {1} - hi
+# {2} - there
+# {NA} - this is a placebo
+# ——————————————————————————————————————————————————————————————————————————————————————————
+

Note to step into a content row we must add that to the path, even +though we didn’t need it to put a footnote on the full row.

+

Currently, content rows by default are named with the label +rather than name of the corresponding facet. This is reflected +in the output of, e.g., row_paths_summary.

+ +
# rowname      node_class    path                                            
+# ———————————————————————————————————————————————————————————————————————————
+# Asian        ContentRow    RACE, ASIAN, @content, Asian                    
+#   AGE        LabelRow      RACE, ASIAN, AGE                                
+#     Mean     DataRow       RACE, ASIAN, AGE, Mean                          
+#   STRATA1    LabelRow      RACE, ASIAN, STRATA1                            
+#     A        DataRow       RACE, ASIAN, STRATA1, A                         
+#     B        DataRow       RACE, ASIAN, STRATA1, B                         
+#     C        DataRow       RACE, ASIAN, STRATA1, C                         
+# Black        ContentRow    RACE, BLACK OR AFRICAN AMERICAN, @content, Black
+#   AGE        LabelRow      RACE, BLACK OR AFRICAN AMERICAN, AGE            
+#     Mean     DataRow       RACE, BLACK OR AFRICAN AMERICAN, AGE, Mean      
+#   STRATA1    LabelRow      RACE, BLACK OR AFRICAN AMERICAN, STRATA1        
+#     A        DataRow       RACE, BLACK OR AFRICAN AMERICAN, STRATA1, A     
+#     B        DataRow       RACE, BLACK OR AFRICAN AMERICAN, STRATA1, B     
+#     C        DataRow       RACE, BLACK OR AFRICAN AMERICAN, STRATA1, C     
+# White        ContentRow    RACE, WHITE, @content, White                    
+#   AGE        LabelRow      RACE, WHITE, AGE                                
+#     Mean     DataRow       RACE, WHITE, AGE, Mean                          
+#   STRATA1    LabelRow      RACE, WHITE, STRATA1                            
+#     A        DataRow       RACE, WHITE, STRATA1, A                         
+#     B        DataRow       RACE, WHITE, STRATA1, B                         
+#     C        DataRow       RACE, WHITE, STRATA1, C
+

So we can add our footnotes to the cell like so:

+
+fnotes_at_path(
+  tbl2,
+  rowpath = c("RACE", "ASIAN", "@content", "Asian"),
+  colpath = c("ARM", "B: Placebo", "SEX", "F")
+) <- "These asian women got placebo treatments"
+tbl2
+
#                       A: Drug X                B: Placebo {NA}             C: Combination     
+#                    F            M              F              M            F            M     
+# ——————————————————————————————————————————————————————————————————————————————————————————————
+# Asian {1, 2}   41 (53.9%)   25 (54.3%)   36 (52.2%) {3}   30 (60.0%)   39 (60.9%)   32 (57.1%)
+#   AGE                                                                                         
+#     Mean         31.22        34.60          35.06          38.63        36.44        37.66   
+#   STRATA1                                                                                     
+#     A              11           10             14             10           11           7     
+#     B              11           9              15             7            11           14    
+#     C              19           6              7              13           17           11    
+# Black          18 (23.7%)   12 (26.1%)     16 (23.2%)     12 (24.0%)   14 (21.9%)   14 (25.0%)
+#   AGE                                                                                         
+#     Mean         34.06        34.58          33.88          36.33        33.21        34.21   
+#   STRATA1                                                                                     
+#     A              5            2              5              6            3            7     
+#     B              6            5              3              4            4            4     
+#     C              7            5              8              2            7            3     
+# White          17 (22.4%)   9 (19.6%)      17 (24.6%)     8 (16.0%)    11 (17.2%)   10 (17.9%)
+#   AGE                                                                                         
+#     Mean         34.12        40.00          32.41          34.62        33.00        30.80   
+#   STRATA1                                                                                     
+#     A              5            3              3              3            3            5     
+#     B              5            4              8              4            5            2     
+#     C              7            2              6              1            3            3     
+# ——————————————————————————————————————————————————————————————————————————————————————————————
+# 
+# {1} - hi
+# {2} - there
+# {3} - These asian women got placebo treatments
+# {NA} - this is a placebo
+# ——————————————————————————————————————————————————————————————————————————————————————————————
+
+
+
+
+ + + + +
+ + + + + + + diff --git a/v0.6.9/authors.html b/v0.6.9/authors.html new file mode 100644 index 000000000..fe82abd84 --- /dev/null +++ b/v0.6.9/authors.html @@ -0,0 +1,156 @@ + +Authors and Citation • rtables + Skip to contents + + +
+
+
+ +
+

Authors

+

We define authors as those who are actively maintaining the code base, and contributors as those who made a significant contribution in the past. For all acknowledgements, see the eponymous section in the Home Page.

+
  • +

    Gabriel Becker. Author. +
    Original creator of the package

    +
  • +
  • +

    Adrian Waddell. Author. +

    +
  • +
  • +

    Daniel Sabanés Bové. Contributor. +

    +
  • +
  • +

    Maximilian Mordig. Contributor. +

    +
  • +
  • +

    Davide Garolini. Contributor. +

    +
  • +
  • +

    Emily de la Rua. Contributor. +

    +
  • +
  • +

    Abinaya Yogasekaram. Contributor. +

    +
  • +
  • +

    Joe Zhu. Contributor, maintainer. +

    +
  • +
  • +

    F. Hoffmann-La Roche AG. Copyright holder, funder. +

    +
  • +
+ +
+

Citation

+

Source: DESCRIPTION

+ +

Becker G, Waddell A (2024). +rtables: Reporting Tables. +R package version 0.6.9, +https://insightsengineering.github.io/rtables/, https://github.com/insightsengineering/rtables. +

+
@Manual{,
+  title = {rtables: Reporting Tables},
+  author = {Gabriel Becker and Adrian Waddell},
+  year = {2024},
+  note = {R package version 0.6.9, 
+https://insightsengineering.github.io/rtables/},
+  url = {https://github.com/insightsengineering/rtables},
+}
+
+
+ + +
+ + + + + + + diff --git a/v0.6.9/consent.css b/v0.6.9/consent.css new file mode 100644 index 000000000..e1396f6c1 --- /dev/null +++ b/v0.6.9/consent.css @@ -0,0 +1,28 @@ +.cookie-consent { + position: fixed; + bottom: 8px; + left: 20px; + width: 260px; + color: #fff; + line-height: 20px; + padding-block: 7px; + padding-left: 10px; + padding-right: 10px; + font-size: 14px; + background: #292929; + z-index: 120; + cursor: pointer; + border-radius: 3px; +} + +.cookie-button { + height: 20px; + width: 104px; + color: #fff; + font-size: 12px; + line-height: 10px; + border-radius: 3px; + border: 1px solid grey; + background-color: grey; + margin-inline: auto; +} diff --git a/v0.6.9/consent.js b/v0.6.9/consent.js new file mode 100644 index 000000000..4b8b9e196 --- /dev/null +++ b/v0.6.9/consent.js @@ -0,0 +1,95 @@ +(function ($) { + "use strict"; + $.fn.cookieWall = function (options) { + const params = $.extend({ + id: '', + cookie: { + name: 'nest-documentation', + days: 15, + path: '/' + }, + tag: { + cookiePrefix: '', + cookieDomain: '', + cookieExpires: '', + cookieUpdate: '' + } + }, options); + const tag_params = {} + for (const property in params.tag) { + if (params.tag[property] != '') { + tag_params[property.replace(/([A-Z])/g, "-$1").toLowerCase()] = params.tag[property]; + } + } + const tag = '' + + ''; + + const cookieNotification = ` + `; + + function init() { + if (params.id != '') { + let c = getCookie(); + if (c == null || (c != 0 && c != 1)) { + displayCookieNotification(); + } else if (c == 1) { + addTag(); + } + } else { + console.log('No ID defined in the cookieWall params.'); + } + } + + function displayCookieNotification() { + $('body').prepend(cookieNotification); + $('body').on('mousedown', '.cookie-consent .cookie-button', setChoice); + } + + function removeCookieNotification() { + $('body .cookie-consent').remove(); + } + + function setChoice(e) { + e.preventDefault(); + if (this.id == 'cookie_accept') { + setCookie(1); + addTag(); + } else { + setCookie(0); + } + removeCookieNotification(); + } + + function addTag() { + $('body').append(tag); + } + + function getCookie() { + let t = document.cookie.split('; '); + let f = t.find(row => row.startsWith(params.cookie.name + '=')); + if (typeof f != 'undefined') { + return f.split('=')[1]; + } + return null; + } + + function setCookie(value) { + let a = params.cookie.days * 86400; + document.cookie = params.cookie.name + '=' + value + ';max-age=' + a + ';path=' + params.cookie.path + ';SameSite=None;Secure'; + } + init(); + return this; + }; +})(jQuery); diff --git a/v0.6.9/cookie_policy.txt b/v0.6.9/cookie_policy.txt new file mode 100644 index 000000000..96f6f2629 --- /dev/null +++ b/v0.6.9/cookie_policy.txt @@ -0,0 +1,11 @@ +Cookie files are used to analyze website traffic by Google Analytics service. + +Information about your browsing and use of the website is transmitted and will be analyzed anonymously to improve services. The data will be transmitted to the United States and are subject to the Google privacy policy (https://policies.google.com/privacy?hl=en-US). + +List of cookies: + +- "_ga": Used to distinguish users (expires after 2 years) +- "_gid": Used to distinguish users (expires after 24 hours) +- "_gat": Used to limit request rate (expires after 1 minute) + +Your consent is kept for 15 days. You can reset your consent by deleting the nest-documentation cookie from your browser data. diff --git a/v0.6.9/deps/bootstrap-5.3.1/bootstrap.bundle.min.js b/v0.6.9/deps/bootstrap-5.3.1/bootstrap.bundle.min.js new file mode 100644 index 000000000..e8f21f703 --- /dev/null +++ b/v0.6.9/deps/bootstrap-5.3.1/bootstrap.bundle.min.js @@ -0,0 +1,7 @@ +/*! + * Bootstrap v5.3.1 (https://getbootstrap.com/) + * Copyright 2011-2023 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t=new Map,e={set(e,i,n){t.has(e)||t.set(e,new Map);const s=t.get(e);s.has(i)||0===s.size?s.set(i,n):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(s.keys())[0]}.`)},get:(e,i)=>t.has(e)&&t.get(e).get(i)||null,remove(e,i){if(!t.has(e))return;const n=t.get(e);n.delete(i),0===n.size&&t.delete(e)}},i="transitionend",n=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),s=t=>{t.dispatchEvent(new Event(i))},o=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),r=t=>o(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(n(t)):null,a=t=>{if(!o(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},l=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),c=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?c(t.parentNode):null},h=()=>{},d=t=>{t.offsetHeight},u=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,f=[],p=()=>"rtl"===document.documentElement.dir,m=t=>{var e;e=()=>{const e=u();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(f.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of f)t()})),f.push(e)):e()},g=(t,e=[],i=t)=>"function"==typeof t?t(...e):i,_=(t,e,n=!0)=>{if(!n)return void g(t);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let r=!1;const a=({target:n})=>{n===e&&(r=!0,e.removeEventListener(i,a),g(t))};e.addEventListener(i,a),setTimeout((()=>{r||s(e)}),o)},b=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},v=/[^.]*(?=\..*)\.|.*/,y=/\..*/,w=/::\d+$/,A={};let E=1;const T={mouseenter:"mouseover",mouseleave:"mouseout"},C=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function O(t,e){return e&&`${e}::${E++}`||t.uidEvent||E++}function x(t){const e=O(t);return t.uidEvent=e,A[e]=A[e]||{},A[e]}function k(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function L(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=I(t);return C.has(o)||(o=t),[n,s,o]}function S(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=L(e,i,n);if(e in T){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=x(t),c=l[a]||(l[a]={}),h=k(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=O(r,e.replace(v,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return P(s,{delegateTarget:r}),n.oneOff&&N.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return P(n,{delegateTarget:t}),i.oneOff&&N.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function D(t,e,i,n,s){const o=k(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function $(t,e,i,n){const s=e[i]||{};for(const[o,r]of Object.entries(s))o.includes(n)&&D(t,e,i,r.callable,r.delegationSelector)}function I(t){return t=t.replace(y,""),T[t]||t}const N={on(t,e,i,n){S(t,e,i,n,!1)},one(t,e,i,n){S(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=L(e,i,n),a=r!==e,l=x(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))$(t,l,i,e.slice(1));for(const[i,n]of Object.entries(c)){const s=i.replace(w,"");a&&!e.includes(s)||D(t,l,r,n.callable,n.delegationSelector)}}else{if(!Object.keys(c).length)return;D(t,l,r,o,s?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=u();let s=null,o=!0,r=!0,a=!1;e!==I(e)&&n&&(s=n.Event(e,i),n(t).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());const l=P(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}};function P(t,e={}){for(const[i,n]of Object.entries(e))try{t[i]=n}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>n})}return t}function M(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function j(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const F={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${j(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${j(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const n of i){let i=n.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1,i.length),e[i]=M(t.dataset[n])}return e},getDataAttribute:(t,e)=>M(t.getAttribute(`data-bs-${j(e)}`))};class H{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=o(e)?F.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...o(e)?F.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[n,s]of Object.entries(e)){const e=t[n],r=o(e)?"element":null==(i=e)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(s).test(r))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${r}" but expected type "${s}".`)}var i}}class W extends H{constructor(t,i){super(),(t=r(t))&&(this._element=t,this._config=this._getConfig(i),e.set(this._element,this.constructor.DATA_KEY,this))}dispose(){e.remove(this._element,this.constructor.DATA_KEY),N.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){_(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return e.get(r(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.3.1"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const B=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return n(e)},z={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode.closest(e);for(;n;)i.push(n),n=n.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(",");return this.find(e,t).filter((t=>!l(t)&&a(t)))},getSelectorFromElement(t){const e=B(t);return e&&z.findOne(e)?e:null},getElementFromSelector(t){const e=B(t);return e?z.findOne(e):null},getMultipleElementsFromSelector(t){const e=B(t);return e?z.find(e):[]}},R=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,n=t.NAME;N.on(document,i,`[data-bs-dismiss="${n}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),l(this))return;const s=z.getElementFromSelector(this)||this.closest(`.${n}`);t.getOrCreateInstance(s)[e]()}))},q=".bs.alert",V=`close${q}`,K=`closed${q}`;class Q extends W{static get NAME(){return"alert"}close(){if(N.trigger(this._element,V).defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),N.trigger(this._element,K),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=Q.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}R(Q,"close"),m(Q);const X='[data-bs-toggle="button"]';class Y extends W{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=Y.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}N.on(document,"click.bs.button.data-api",X,(t=>{t.preventDefault();const e=t.target.closest(X);Y.getOrCreateInstance(e).toggle()})),m(Y);const U=".bs.swipe",G=`touchstart${U}`,J=`touchmove${U}`,Z=`touchend${U}`,tt=`pointerdown${U}`,et=`pointerup${U}`,it={endCallback:null,leftCallback:null,rightCallback:null},nt={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class st extends H{constructor(t,e){super(),this._element=t,t&&st.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return it}static get DefaultType(){return nt}static get NAME(){return"swipe"}dispose(){N.off(this._element,U)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),g(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&g(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(N.on(this._element,tt,(t=>this._start(t))),N.on(this._element,et,(t=>this._end(t))),this._element.classList.add("pointer-event")):(N.on(this._element,G,(t=>this._start(t))),N.on(this._element,J,(t=>this._move(t))),N.on(this._element,Z,(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const ot=".bs.carousel",rt=".data-api",at="next",lt="prev",ct="left",ht="right",dt=`slide${ot}`,ut=`slid${ot}`,ft=`keydown${ot}`,pt=`mouseenter${ot}`,mt=`mouseleave${ot}`,gt=`dragstart${ot}`,_t=`load${ot}${rt}`,bt=`click${ot}${rt}`,vt="carousel",yt="active",wt=".active",At=".carousel-item",Et=wt+At,Tt={ArrowLeft:ht,ArrowRight:ct},Ct={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},Ot={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class xt extends W{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=z.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===vt&&this.cycle()}static get Default(){return Ct}static get DefaultType(){return Ot}static get NAME(){return"carousel"}next(){this._slide(at)}nextWhenVisible(){!document.hidden&&a(this._element)&&this.next()}prev(){this._slide(lt)}pause(){this._isSliding&&s(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?N.one(this._element,ut,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void N.one(this._element,ut,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const n=t>i?at:lt;this._slide(n,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&N.on(this._element,ft,(t=>this._keydown(t))),"hover"===this._config.pause&&(N.on(this._element,pt,(()=>this.pause())),N.on(this._element,mt,(()=>this._maybeEnableCycle()))),this._config.touch&&st.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of z.find(".carousel-item img",this._element))N.on(t,gt,(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(ct)),rightCallback:()=>this._slide(this._directionToOrder(ht)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new st(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=Tt[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=z.findOne(wt,this._indicatorsElement);e.classList.remove(yt),e.removeAttribute("aria-current");const i=z.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(yt),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),n=t===at,s=e||b(this._getItems(),i,n,this._config.wrap);if(s===i)return;const o=this._getItemIndex(s),r=e=>N.trigger(this._element,e,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(dt).defaultPrevented)return;if(!i||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=n?"carousel-item-start":"carousel-item-end",c=n?"carousel-item-next":"carousel-item-prev";s.classList.add(c),d(s),i.classList.add(l),s.classList.add(l),this._queueCallback((()=>{s.classList.remove(l,c),s.classList.add(yt),i.classList.remove(yt,c,l),this._isSliding=!1,r(ut)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return z.findOne(Et,this._element)}_getItems(){return z.find(At,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return p()?t===ct?lt:at:t===ct?at:lt}_orderToDirection(t){return p()?t===lt?ct:ht:t===lt?ht:ct}static jQueryInterface(t){return this.each((function(){const e=xt.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)}))}}N.on(document,bt,"[data-bs-slide], [data-bs-slide-to]",(function(t){const e=z.getElementFromSelector(this);if(!e||!e.classList.contains(vt))return;t.preventDefault();const i=xt.getOrCreateInstance(e),n=this.getAttribute("data-bs-slide-to");return n?(i.to(n),void i._maybeEnableCycle()):"next"===F.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),N.on(window,_t,(()=>{const t=z.find('[data-bs-ride="carousel"]');for(const e of t)xt.getOrCreateInstance(e)})),m(xt);const kt=".bs.collapse",Lt=`show${kt}`,St=`shown${kt}`,Dt=`hide${kt}`,$t=`hidden${kt}`,It=`click${kt}.data-api`,Nt="show",Pt="collapse",Mt="collapsing",jt=`:scope .${Pt} .${Pt}`,Ft='[data-bs-toggle="collapse"]',Ht={parent:null,toggle:!0},Wt={parent:"(null|element)",toggle:"boolean"};class Bt extends W{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=z.find(Ft);for(const t of i){const e=z.getSelectorFromElement(t),i=z.find(e).filter((t=>t===this._element));null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return Ht}static get DefaultType(){return Wt}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((t=>t!==this._element)).map((t=>Bt.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(N.trigger(this._element,Lt).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(Pt),this._element.classList.add(Mt),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(Mt),this._element.classList.add(Pt,Nt),this._element.style[e]="",N.trigger(this._element,St)}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(N.trigger(this._element,Dt).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,d(this._element),this._element.classList.add(Mt),this._element.classList.remove(Pt,Nt);for(const t of this._triggerArray){const e=z.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(Mt),this._element.classList.add(Pt),N.trigger(this._element,$t)}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(Nt)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=r(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(Ft);for(const e of t){const t=z.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=z.find(jt,this._config.parent);return z.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=Bt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}N.on(document,It,Ft,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of z.getMultipleElementsFromSelector(this))Bt.getOrCreateInstance(t,{toggle:!1}).toggle()})),m(Bt);var zt="top",Rt="bottom",qt="right",Vt="left",Kt="auto",Qt=[zt,Rt,qt,Vt],Xt="start",Yt="end",Ut="clippingParents",Gt="viewport",Jt="popper",Zt="reference",te=Qt.reduce((function(t,e){return t.concat([e+"-"+Xt,e+"-"+Yt])}),[]),ee=[].concat(Qt,[Kt]).reduce((function(t,e){return t.concat([e,e+"-"+Xt,e+"-"+Yt])}),[]),ie="beforeRead",ne="read",se="afterRead",oe="beforeMain",re="main",ae="afterMain",le="beforeWrite",ce="write",he="afterWrite",de=[ie,ne,se,oe,re,ae,le,ce,he];function ue(t){return t?(t.nodeName||"").toLowerCase():null}function fe(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function pe(t){return t instanceof fe(t).Element||t instanceof Element}function me(t){return t instanceof fe(t).HTMLElement||t instanceof HTMLElement}function ge(t){return"undefined"!=typeof ShadowRoot&&(t instanceof fe(t).ShadowRoot||t instanceof ShadowRoot)}const _e={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];me(s)&&ue(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});me(n)&&ue(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function be(t){return t.split("-")[0]}var ve=Math.max,ye=Math.min,we=Math.round;function Ae(){var t=navigator.userAgentData;return null!=t&&t.brands&&Array.isArray(t.brands)?t.brands.map((function(t){return t.brand+"/"+t.version})).join(" "):navigator.userAgent}function Ee(){return!/^((?!chrome|android).)*safari/i.test(Ae())}function Te(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),s=1,o=1;e&&me(t)&&(s=t.offsetWidth>0&&we(n.width)/t.offsetWidth||1,o=t.offsetHeight>0&&we(n.height)/t.offsetHeight||1);var r=(pe(t)?fe(t):window).visualViewport,a=!Ee()&&i,l=(n.left+(a&&r?r.offsetLeft:0))/s,c=(n.top+(a&&r?r.offsetTop:0))/o,h=n.width/s,d=n.height/o;return{width:h,height:d,top:c,right:l+h,bottom:c+d,left:l,x:l,y:c}}function Ce(t){var e=Te(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function Oe(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&ge(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function xe(t){return fe(t).getComputedStyle(t)}function ke(t){return["table","td","th"].indexOf(ue(t))>=0}function Le(t){return((pe(t)?t.ownerDocument:t.document)||window.document).documentElement}function Se(t){return"html"===ue(t)?t:t.assignedSlot||t.parentNode||(ge(t)?t.host:null)||Le(t)}function De(t){return me(t)&&"fixed"!==xe(t).position?t.offsetParent:null}function $e(t){for(var e=fe(t),i=De(t);i&&ke(i)&&"static"===xe(i).position;)i=De(i);return i&&("html"===ue(i)||"body"===ue(i)&&"static"===xe(i).position)?e:i||function(t){var e=/firefox/i.test(Ae());if(/Trident/i.test(Ae())&&me(t)&&"fixed"===xe(t).position)return null;var i=Se(t);for(ge(i)&&(i=i.host);me(i)&&["html","body"].indexOf(ue(i))<0;){var n=xe(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function Ie(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function Ne(t,e,i){return ve(t,ye(e,i))}function Pe(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function Me(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const je={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=be(i.placement),l=Ie(a),c=[Vt,qt].indexOf(a)>=0?"height":"width";if(o&&r){var h=function(t,e){return Pe("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:Me(t,Qt))}(s.padding,i),d=Ce(o),u="y"===l?zt:Vt,f="y"===l?Rt:qt,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],m=r[l]-i.rects.reference[l],g=$e(o),_=g?"y"===l?g.clientHeight||0:g.clientWidth||0:0,b=p/2-m/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,A=Ne(v,w,y),E=l;i.modifiersData[n]=((e={})[E]=A,e.centerOffset=A-w,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&Oe(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Fe(t){return t.split("-")[1]}var He={top:"auto",right:"auto",bottom:"auto",left:"auto"};function We(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.variation,r=t.offsets,a=t.position,l=t.gpuAcceleration,c=t.adaptive,h=t.roundOffsets,d=t.isFixed,u=r.x,f=void 0===u?0:u,p=r.y,m=void 0===p?0:p,g="function"==typeof h?h({x:f,y:m}):{x:f,y:m};f=g.x,m=g.y;var _=r.hasOwnProperty("x"),b=r.hasOwnProperty("y"),v=Vt,y=zt,w=window;if(c){var A=$e(i),E="clientHeight",T="clientWidth";A===fe(i)&&"static"!==xe(A=Le(i)).position&&"absolute"===a&&(E="scrollHeight",T="scrollWidth"),(s===zt||(s===Vt||s===qt)&&o===Yt)&&(y=Rt,m-=(d&&A===w&&w.visualViewport?w.visualViewport.height:A[E])-n.height,m*=l?1:-1),s!==Vt&&(s!==zt&&s!==Rt||o!==Yt)||(v=qt,f-=(d&&A===w&&w.visualViewport?w.visualViewport.width:A[T])-n.width,f*=l?1:-1)}var C,O=Object.assign({position:a},c&&He),x=!0===h?function(t,e){var i=t.x,n=t.y,s=e.devicePixelRatio||1;return{x:we(i*s)/s||0,y:we(n*s)/s||0}}({x:f,y:m},fe(i)):{x:f,y:m};return f=x.x,m=x.y,l?Object.assign({},O,((C={})[y]=b?"0":"",C[v]=_?"0":"",C.transform=(w.devicePixelRatio||1)<=1?"translate("+f+"px, "+m+"px)":"translate3d("+f+"px, "+m+"px, 0)",C)):Object.assign({},O,((e={})[y]=b?m+"px":"",e[v]=_?f+"px":"",e.transform="",e))}const Be={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:be(e.placement),variation:Fe(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,We(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,We(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var ze={passive:!0};const Re={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=fe(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,ze)})),a&&l.addEventListener("resize",i.update,ze),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,ze)})),a&&l.removeEventListener("resize",i.update,ze)}},data:{}};var qe={left:"right",right:"left",bottom:"top",top:"bottom"};function Ve(t){return t.replace(/left|right|bottom|top/g,(function(t){return qe[t]}))}var Ke={start:"end",end:"start"};function Qe(t){return t.replace(/start|end/g,(function(t){return Ke[t]}))}function Xe(t){var e=fe(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function Ye(t){return Te(Le(t)).left+Xe(t).scrollLeft}function Ue(t){var e=xe(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function Ge(t){return["html","body","#document"].indexOf(ue(t))>=0?t.ownerDocument.body:me(t)&&Ue(t)?t:Ge(Se(t))}function Je(t,e){var i;void 0===e&&(e=[]);var n=Ge(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=fe(n),r=s?[o].concat(o.visualViewport||[],Ue(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(Je(Se(r)))}function Ze(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function ti(t,e,i){return e===Gt?Ze(function(t,e){var i=fe(t),n=Le(t),s=i.visualViewport,o=n.clientWidth,r=n.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=Ee();(c||!c&&"fixed"===e)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+Ye(t),y:l}}(t,i)):pe(e)?function(t,e){var i=Te(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):Ze(function(t){var e,i=Le(t),n=Xe(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=ve(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=ve(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+Ye(t),l=-n.scrollTop;return"rtl"===xe(s||i).direction&&(a+=ve(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(Le(t)))}function ei(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?be(s):null,r=s?Fe(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case zt:e={x:a,y:i.y-n.height};break;case Rt:e={x:a,y:i.y+i.height};break;case qt:e={x:i.x+i.width,y:l};break;case Vt:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?Ie(o):null;if(null!=c){var h="y"===c?"height":"width";switch(r){case Xt:e[c]=e[c]-(i[h]/2-n[h]/2);break;case Yt:e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function ii(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.strategy,r=void 0===o?t.strategy:o,a=i.boundary,l=void 0===a?Ut:a,c=i.rootBoundary,h=void 0===c?Gt:c,d=i.elementContext,u=void 0===d?Jt:d,f=i.altBoundary,p=void 0!==f&&f,m=i.padding,g=void 0===m?0:m,_=Pe("number"!=typeof g?g:Me(g,Qt)),b=u===Jt?Zt:Jt,v=t.rects.popper,y=t.elements[p?b:u],w=function(t,e,i,n){var s="clippingParents"===e?function(t){var e=Je(Se(t)),i=["absolute","fixed"].indexOf(xe(t).position)>=0&&me(t)?$e(t):t;return pe(i)?e.filter((function(t){return pe(t)&&Oe(t,i)&&"body"!==ue(t)})):[]}(t):[].concat(e),o=[].concat(s,[i]),r=o[0],a=o.reduce((function(e,i){var s=ti(t,i,n);return e.top=ve(s.top,e.top),e.right=ye(s.right,e.right),e.bottom=ye(s.bottom,e.bottom),e.left=ve(s.left,e.left),e}),ti(t,r,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(pe(y)?y:y.contextElement||Le(t.elements.popper),l,h,r),A=Te(t.elements.reference),E=ei({reference:A,element:v,strategy:"absolute",placement:s}),T=Ze(Object.assign({},v,E)),C=u===Jt?T:A,O={top:w.top-C.top+_.top,bottom:C.bottom-w.bottom+_.bottom,left:w.left-C.left+_.left,right:C.right-w.right+_.right},x=t.modifiersData.offset;if(u===Jt&&x){var k=x[s];Object.keys(O).forEach((function(t){var e=[qt,Rt].indexOf(t)>=0?1:-1,i=[zt,Rt].indexOf(t)>=0?"y":"x";O[t]+=k[i]*e}))}return O}function ni(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?ee:l,h=Fe(n),d=h?a?te:te.filter((function(t){return Fe(t)===h})):Qt,u=d.filter((function(t){return c.indexOf(t)>=0}));0===u.length&&(u=d);var f=u.reduce((function(e,i){return e[i]=ii(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[be(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}const si={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,m=i.allowedAutoPlacements,g=e.options.placement,_=be(g),b=l||(_!==g&&p?function(t){if(be(t)===Kt)return[];var e=Ve(t);return[Qe(t),e,Qe(e)]}(g):[Ve(g)]),v=[g].concat(b).reduce((function(t,i){return t.concat(be(i)===Kt?ni(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:m}):i)}),[]),y=e.rects.reference,w=e.rects.popper,A=new Map,E=!0,T=v[0],C=0;C=0,S=L?"width":"height",D=ii(e,{placement:O,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),$=L?k?qt:Vt:k?Rt:zt;y[S]>w[S]&&($=Ve($));var I=Ve($),N=[];if(o&&N.push(D[x]<=0),a&&N.push(D[$]<=0,D[I]<=0),N.every((function(t){return t}))){T=O,E=!1;break}A.set(O,N)}if(E)for(var P=function(t){var e=v.find((function(e){var i=A.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return T=e,"break"},M=p?3:1;M>0&&"break"!==P(M);M--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function oi(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function ri(t){return[zt,qt,Rt,Vt].some((function(e){return t[e]>=0}))}const ai={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=ii(e,{elementContext:"reference"}),a=ii(e,{altBoundary:!0}),l=oi(r,n),c=oi(a,s,o),h=ri(l),d=ri(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},li={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=ee.reduce((function(t,i){return t[i]=function(t,e,i){var n=be(t),s=[Vt,zt].indexOf(n)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[Vt,qt].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},ci={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=ei({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},hi={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,m=void 0===p?0:p,g=ii(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:h}),_=be(e.placement),b=Fe(e.placement),v=!b,y=Ie(_),w="x"===y?"y":"x",A=e.modifiersData.popperOffsets,E=e.rects.reference,T=e.rects.popper,C="function"==typeof m?m(Object.assign({},e.rects,{placement:e.placement})):m,O="number"==typeof C?{mainAxis:C,altAxis:C}:Object.assign({mainAxis:0,altAxis:0},C),x=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,k={x:0,y:0};if(A){if(o){var L,S="y"===y?zt:Vt,D="y"===y?Rt:qt,$="y"===y?"height":"width",I=A[y],N=I+g[S],P=I-g[D],M=f?-T[$]/2:0,j=b===Xt?E[$]:T[$],F=b===Xt?-T[$]:-E[$],H=e.elements.arrow,W=f&&H?Ce(H):{width:0,height:0},B=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},z=B[S],R=B[D],q=Ne(0,E[$],W[$]),V=v?E[$]/2-M-q-z-O.mainAxis:j-q-z-O.mainAxis,K=v?-E[$]/2+M+q+R+O.mainAxis:F+q+R+O.mainAxis,Q=e.elements.arrow&&$e(e.elements.arrow),X=Q?"y"===y?Q.clientTop||0:Q.clientLeft||0:0,Y=null!=(L=null==x?void 0:x[y])?L:0,U=I+K-Y,G=Ne(f?ye(N,I+V-Y-X):N,I,f?ve(P,U):P);A[y]=G,k[y]=G-I}if(a){var J,Z="x"===y?zt:Vt,tt="x"===y?Rt:qt,et=A[w],it="y"===w?"height":"width",nt=et+g[Z],st=et-g[tt],ot=-1!==[zt,Vt].indexOf(_),rt=null!=(J=null==x?void 0:x[w])?J:0,at=ot?nt:et-E[it]-T[it]-rt+O.altAxis,lt=ot?et+E[it]+T[it]-rt-O.altAxis:st,ct=f&&ot?function(t,e,i){var n=Ne(t,e,i);return n>i?i:n}(at,et,lt):Ne(f?at:nt,et,f?lt:st);A[w]=ct,k[w]=ct-et}e.modifiersData[n]=k}},requiresIfExists:["offset"]};function di(t,e,i){void 0===i&&(i=!1);var n,s,o=me(e),r=me(e)&&function(t){var e=t.getBoundingClientRect(),i=we(e.width)/t.offsetWidth||1,n=we(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=Le(e),l=Te(t,r,i),c={scrollLeft:0,scrollTop:0},h={x:0,y:0};return(o||!o&&!i)&&(("body"!==ue(e)||Ue(a))&&(c=(n=e)!==fe(n)&&me(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:Xe(n)),me(e)?((h=Te(e,!0)).x+=e.clientLeft,h.y+=e.clientTop):a&&(h.x=Ye(a))),{x:l.left+c.scrollLeft-h.x,y:l.top+c.scrollTop-h.y,width:l.width,height:l.height}}function ui(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var fi={placement:"bottom",modifiers:[],strategy:"absolute"};function pi(){for(var t=arguments.length,e=new Array(t),i=0;iNumber.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(F.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,...g(this._config.popperConfig,[t])}}_selectMenuItem({key:t,target:e}){const i=z.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>a(t)));i.length&&b(i,e,t===Ti,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=qi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=z.find(Ni);for(const i of e){const e=qi.getInstance(i);if(!e||!1===e._config.autoClose)continue;const n=t.composedPath(),s=n.includes(e._menu);if(n.includes(e._element)||"inside"===e._config.autoClose&&!s||"outside"===e._config.autoClose&&s)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,n=[Ei,Ti].includes(t.key);if(!n&&!i)return;if(e&&!i)return;t.preventDefault();const s=this.matches(Ii)?this:z.prev(this,Ii)[0]||z.next(this,Ii)[0]||z.findOne(Ii,t.delegateTarget.parentNode),o=qi.getOrCreateInstance(s);if(n)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),s.focus())}}N.on(document,Si,Ii,qi.dataApiKeydownHandler),N.on(document,Si,Pi,qi.dataApiKeydownHandler),N.on(document,Li,qi.clearMenus),N.on(document,Di,qi.clearMenus),N.on(document,Li,Ii,(function(t){t.preventDefault(),qi.getOrCreateInstance(this).toggle()})),m(qi);const Vi="backdrop",Ki="show",Qi=`mousedown.bs.${Vi}`,Xi={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Yi={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Ui extends H{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return Xi}static get DefaultType(){return Yi}static get NAME(){return Vi}show(t){if(!this._config.isVisible)return void g(t);this._append();const e=this._getElement();this._config.isAnimated&&d(e),e.classList.add(Ki),this._emulateAnimation((()=>{g(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(Ki),this._emulateAnimation((()=>{this.dispose(),g(t)}))):g(t)}dispose(){this._isAppended&&(N.off(this._element,Qi),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=r(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),N.on(t,Qi,(()=>{g(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){_(t,this._getElement(),this._config.isAnimated)}}const Gi=".bs.focustrap",Ji=`focusin${Gi}`,Zi=`keydown.tab${Gi}`,tn="backward",en={autofocus:!0,trapElement:null},nn={autofocus:"boolean",trapElement:"element"};class sn extends H{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return en}static get DefaultType(){return nn}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),N.off(document,Gi),N.on(document,Ji,(t=>this._handleFocusin(t))),N.on(document,Zi,(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,N.off(document,Gi))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=z.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===tn?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?tn:"forward")}}const on=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",rn=".sticky-top",an="padding-right",ln="margin-right";class cn{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,an,(e=>e+t)),this._setElementAttributes(on,an,(e=>e+t)),this._setElementAttributes(rn,ln,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,an),this._resetElementAttributes(on,an),this._resetElementAttributes(rn,ln)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(s))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&F.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=F.getDataAttribute(t,e);null!==i?(F.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(o(t))e(t);else for(const i of z.find(t,this._element))e(i)}}const hn=".bs.modal",dn=`hide${hn}`,un=`hidePrevented${hn}`,fn=`hidden${hn}`,pn=`show${hn}`,mn=`shown${hn}`,gn=`resize${hn}`,_n=`click.dismiss${hn}`,bn=`mousedown.dismiss${hn}`,vn=`keydown.dismiss${hn}`,yn=`click${hn}.data-api`,wn="modal-open",An="show",En="modal-static",Tn={backdrop:!0,focus:!0,keyboard:!0},Cn={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class On extends W{constructor(t,e){super(t,e),this._dialog=z.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new cn,this._addEventListeners()}static get Default(){return Tn}static get DefaultType(){return Cn}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||N.trigger(this._element,pn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(wn),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(N.trigger(this._element,dn).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(An),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){N.off(window,hn),N.off(this._dialog,hn),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Ui({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new sn({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=z.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),d(this._element),this._element.classList.add(An),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,N.trigger(this._element,mn,{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){N.on(this._element,vn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())})),N.on(window,gn,(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),N.on(this._element,bn,(t=>{N.one(this._element,_n,(e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(wn),this._resetAdjustments(),this._scrollBar.reset(),N.trigger(this._element,fn)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(N.trigger(this._element,un).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(En)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(En),this._queueCallback((()=>{this._element.classList.remove(En),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=p()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=p()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=On.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}N.on(document,yn,'[data-bs-toggle="modal"]',(function(t){const e=z.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),N.one(e,pn,(t=>{t.defaultPrevented||N.one(e,fn,(()=>{a(this)&&this.focus()}))}));const i=z.findOne(".modal.show");i&&On.getInstance(i).hide(),On.getOrCreateInstance(e).toggle(this)})),R(On),m(On);const xn=".bs.offcanvas",kn=".data-api",Ln=`load${xn}${kn}`,Sn="show",Dn="showing",$n="hiding",In=".offcanvas.show",Nn=`show${xn}`,Pn=`shown${xn}`,Mn=`hide${xn}`,jn=`hidePrevented${xn}`,Fn=`hidden${xn}`,Hn=`resize${xn}`,Wn=`click${xn}${kn}`,Bn=`keydown.dismiss${xn}`,zn={backdrop:!0,keyboard:!0,scroll:!1},Rn={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class qn extends W{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return zn}static get DefaultType(){return Rn}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||N.trigger(this._element,Nn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new cn).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(Dn),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(Sn),this._element.classList.remove(Dn),N.trigger(this._element,Pn,{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(N.trigger(this._element,Mn).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add($n),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove(Sn,$n),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new cn).reset(),N.trigger(this._element,Fn)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new Ui({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():N.trigger(this._element,jn)}:null})}_initializeFocusTrap(){return new sn({trapElement:this._element})}_addEventListeners(){N.on(this._element,Bn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():N.trigger(this._element,jn))}))}static jQueryInterface(t){return this.each((function(){const e=qn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}N.on(document,Wn,'[data-bs-toggle="offcanvas"]',(function(t){const e=z.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this))return;N.one(e,Fn,(()=>{a(this)&&this.focus()}));const i=z.findOne(In);i&&i!==e&&qn.getInstance(i).hide(),qn.getOrCreateInstance(e).toggle(this)})),N.on(window,Ln,(()=>{for(const t of z.find(In))qn.getOrCreateInstance(t).show()})),N.on(window,Hn,(()=>{for(const t of z.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&qn.getOrCreateInstance(t).hide()})),R(qn),m(qn);const Vn={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Kn=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Qn=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,Xn=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!Kn.has(i)||Boolean(Qn.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},Yn={allowList:Vn,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
"},Un={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},Gn={entry:"(string|element|function|null)",selector:"(string|element)"};class Jn extends H{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return Yn}static get DefaultType(){return Un}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},Gn)}_setContent(t,e,i){const n=z.findOne(i,t);n&&((e=this._resolvePossibleFunction(e))?o(e)?this._putElementInTemplate(r(e),n):this._config.html?n.innerHTML=this._maybeSanitize(e):n.textContent=e:n.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const n=(new window.DOMParser).parseFromString(t,"text/html"),s=[].concat(...n.body.querySelectorAll("*"));for(const t of s){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const n=[].concat(...t.attributes),s=[].concat(e["*"]||[],e[i]||[]);for(const e of n)Xn(e,s)||t.removeAttribute(e.nodeName)}return n.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return g(t,[this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const Zn=new Set(["sanitize","allowList","sanitizeFn"]),ts="fade",es="show",is=".modal",ns="hide.bs.modal",ss="hover",os="focus",rs={AUTO:"auto",TOP:"top",RIGHT:p()?"left":"right",BOTTOM:"bottom",LEFT:p()?"right":"left"},as={allowList:Vn,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},ls={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class cs extends W{constructor(t,e){if(void 0===vi)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t,e),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return as}static get DefaultType(){return ls}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._activeTrigger.click=!this._activeTrigger.click,this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),N.off(this._element.closest(is),ns,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=N.trigger(this._element,this.constructor.eventName("show")),e=(c(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),N.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add(es),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))N.on(t,"mouseover",h);this._queueCallback((()=>{N.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(this._isShown()&&!N.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove(es),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))N.off(t,"mouseover",h);this._activeTrigger.click=!1,this._activeTrigger[os]=!1,this._activeTrigger[ss]=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),N.trigger(this._element,this.constructor.eventName("hidden")))}),this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(ts,es),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(ts),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new Jn({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{".tooltip-inner":this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(ts)}_isShown(){return this.tip&&this.tip.classList.contains(es)}_createPopper(t){const e=g(this._config.placement,[this,t,this._element]),i=rs[e.toUpperCase()];return bi(this._element,t,this._getPopperConfig(i))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return g(t,[this._element])}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,...g(this._config.popperConfig,[e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)N.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>{this._initializeOnDelegatedTarget(t).toggle()}));else if("manual"!==e){const t=e===ss?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===ss?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");N.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?os:ss]=!0,e._enter()})),N.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?os:ss]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},N.on(this._element.closest(is),ns,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=F.getDataAttributes(this._element);for(const t of Object.keys(e))Zn.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:r(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each((function(){const e=cs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m(cs);const hs={...cs.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},ds={...cs.DefaultType,content:"(null|string|element|function)"};class us extends cs{static get Default(){return hs}static get DefaultType(){return ds}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{".popover-header":this._getTitle(),".popover-body":this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each((function(){const e=us.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m(us);const fs=".bs.scrollspy",ps=`activate${fs}`,ms=`click${fs}`,gs=`load${fs}.data-api`,_s="active",bs="[href]",vs=".nav-link",ys=`${vs}, .nav-item > ${vs}, .list-group-item`,ws={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},As={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Es extends W{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return ws}static get DefaultType(){return As}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=r(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map((t=>Number.parseFloat(t)))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(N.off(this._config.target,ms),N.on(this._config.target,ms,bs,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,n=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:n,behavior:"smooth"});i.scrollTop=n}})))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver((t=>this._observerCallback(t)),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},n=(this._rootElement||document.documentElement).scrollTop,s=n>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=n;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(s&&t){if(i(o),!n)return}else s||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=z.find(bs,this._config.target);for(const e of t){if(!e.hash||l(e))continue;const t=z.findOne(decodeURI(e.hash),this._element);a(t)&&(this._targetLinks.set(decodeURI(e.hash),e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(_s),this._activateParents(t),N.trigger(this._element,ps,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))z.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(_s);else for(const e of z.parents(t,".nav, .list-group"))for(const t of z.prev(e,ys))t.classList.add(_s)}_clearActiveClass(t){t.classList.remove(_s);const e=z.find(`${bs}.${_s}`,t);for(const t of e)t.classList.remove(_s)}static jQueryInterface(t){return this.each((function(){const e=Es.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(window,gs,(()=>{for(const t of z.find('[data-bs-spy="scroll"]'))Es.getOrCreateInstance(t)})),m(Es);const Ts=".bs.tab",Cs=`hide${Ts}`,Os=`hidden${Ts}`,xs=`show${Ts}`,ks=`shown${Ts}`,Ls=`click${Ts}`,Ss=`keydown${Ts}`,Ds=`load${Ts}`,$s="ArrowLeft",Is="ArrowRight",Ns="ArrowUp",Ps="ArrowDown",Ms="Home",js="End",Fs="active",Hs="fade",Ws="show",Bs=":not(.dropdown-toggle)",zs='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',Rs=`.nav-link${Bs}, .list-group-item${Bs}, [role="tab"]${Bs}, ${zs}`,qs=`.${Fs}[data-bs-toggle="tab"], .${Fs}[data-bs-toggle="pill"], .${Fs}[data-bs-toggle="list"]`;class Vs extends W{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),N.on(this._element,Ss,(t=>this._keydown(t))))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?N.trigger(e,Cs,{relatedTarget:t}):null;N.trigger(t,xs,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(Fs),this._activate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),N.trigger(t,ks,{relatedTarget:e})):t.classList.add(Ws)}),t,t.classList.contains(Hs)))}_deactivate(t,e){t&&(t.classList.remove(Fs),t.blur(),this._deactivate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),N.trigger(t,Os,{relatedTarget:e})):t.classList.remove(Ws)}),t,t.classList.contains(Hs)))}_keydown(t){if(![$s,Is,Ns,Ps,Ms,js].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=this._getChildren().filter((t=>!l(t)));let i;if([Ms,js].includes(t.key))i=e[t.key===Ms?0:e.length-1];else{const n=[Is,Ps].includes(t.key);i=b(e,t.target,n,!0)}i&&(i.focus({preventScroll:!0}),Vs.getOrCreateInstance(i).show())}_getChildren(){return z.find(Rs,this._parent)}_getActiveElem(){return this._getChildren().find((t=>this._elemIsActive(t)))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=z.getElementFromSelector(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const n=(t,n)=>{const s=z.findOne(t,i);s&&s.classList.toggle(n,e)};n(".dropdown-toggle",Fs),n(".dropdown-menu",Ws),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(Fs)}_getInnerElement(t){return t.matches(Rs)?t:z.findOne(Rs,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each((function(){const e=Vs.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(document,Ls,zs,(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this)||Vs.getOrCreateInstance(this).show()})),N.on(window,Ds,(()=>{for(const t of z.find(qs))Vs.getOrCreateInstance(t)})),m(Vs);const Ks=".bs.toast",Qs=`mouseover${Ks}`,Xs=`mouseout${Ks}`,Ys=`focusin${Ks}`,Us=`focusout${Ks}`,Gs=`hide${Ks}`,Js=`hidden${Ks}`,Zs=`show${Ks}`,to=`shown${Ks}`,eo="hide",io="show",no="showing",so={animation:"boolean",autohide:"boolean",delay:"number"},oo={animation:!0,autohide:!0,delay:5e3};class ro extends W{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return oo}static get DefaultType(){return so}static get NAME(){return"toast"}show(){N.trigger(this._element,Zs).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(eo),d(this._element),this._element.classList.add(io,no),this._queueCallback((()=>{this._element.classList.remove(no),N.trigger(this._element,to),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&(N.trigger(this._element,Gs).defaultPrevented||(this._element.classList.add(no),this._queueCallback((()=>{this._element.classList.add(eo),this._element.classList.remove(no,io),N.trigger(this._element,Js)}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(io),super.dispose()}isShown(){return this._element.classList.contains(io)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){N.on(this._element,Qs,(t=>this._onInteraction(t,!0))),N.on(this._element,Xs,(t=>this._onInteraction(t,!1))),N.on(this._element,Ys,(t=>this._onInteraction(t,!0))),N.on(this._element,Us,(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=ro.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return R(ro),m(ro),{Alert:Q,Button:Y,Carousel:xt,Collapse:Bt,Dropdown:qi,Modal:On,Offcanvas:qn,Popover:us,ScrollSpy:Es,Tab:Vs,Toast:ro,Tooltip:cs}})); +//# sourceMappingURL=bootstrap.bundle.min.js.map \ No newline at end of file diff --git a/v0.6.9/deps/bootstrap-5.3.1/bootstrap.bundle.min.js.map b/v0.6.9/deps/bootstrap-5.3.1/bootstrap.bundle.min.js.map new file mode 100644 index 000000000..3863da8b7 --- /dev/null +++ b/v0.6.9/deps/bootstrap-5.3.1/bootstrap.bundle.min.js.map @@ -0,0 +1 @@ +{"version":3,"names":["elementMap","Map","Data","set","element","key","instance","has","instanceMap","get","size","console","error","Array","from","keys","remove","delete","TRANSITION_END","parseSelector","selector","window","CSS","escape","replace","match","id","triggerTransitionEnd","dispatchEvent","Event","isElement","object","jquery","nodeType","getElement","length","document","querySelector","isVisible","getClientRects","elementIsVisible","getComputedStyle","getPropertyValue","closedDetails","closest","summary","parentNode","isDisabled","Node","ELEMENT_NODE","classList","contains","disabled","hasAttribute","getAttribute","findShadowRoot","documentElement","attachShadow","getRootNode","root","ShadowRoot","noop","reflow","offsetHeight","getjQuery","jQuery","body","DOMContentLoadedCallbacks","isRTL","dir","defineJQueryPlugin","plugin","callback","$","name","NAME","JQUERY_NO_CONFLICT","fn","jQueryInterface","Constructor","noConflict","readyState","addEventListener","push","execute","possibleCallback","args","defaultValue","executeAfterTransition","transitionElement","waitForTransition","emulatedDuration","transitionDuration","transitionDelay","floatTransitionDuration","Number","parseFloat","floatTransitionDelay","split","getTransitionDurationFromElement","called","handler","target","removeEventListener","setTimeout","getNextActiveElement","list","activeElement","shouldGetNext","isCycleAllowed","listLength","index","indexOf","Math","max","min","namespaceRegex","stripNameRegex","stripUidRegex","eventRegistry","uidEvent","customEvents","mouseenter","mouseleave","nativeEvents","Set","makeEventUid","uid","getElementEvents","findHandler","events","callable","delegationSelector","Object","values","find","event","normalizeParameters","originalTypeEvent","delegationFunction","isDelegated","typeEvent","getTypeEvent","addHandler","oneOff","wrapFunction","relatedTarget","delegateTarget","call","this","handlers","previousFunction","domElements","querySelectorAll","domElement","hydrateObj","EventHandler","off","type","apply","bootstrapDelegationHandler","bootstrapHandler","removeHandler","Boolean","removeNamespacedHandlers","namespace","storeElementEvent","handlerKey","entries","includes","on","one","inNamespace","isNamespace","startsWith","elementEvent","slice","keyHandlers","trigger","jQueryEvent","bubbles","nativeDispatch","defaultPrevented","isPropagationStopped","isImmediatePropagationStopped","isDefaultPrevented","evt","cancelable","preventDefault","obj","meta","value","_unused","defineProperty","configurable","normalizeData","toString","JSON","parse","decodeURIComponent","normalizeDataKey","chr","toLowerCase","Manipulator","setDataAttribute","setAttribute","removeDataAttribute","removeAttribute","getDataAttributes","attributes","bsKeys","dataset","filter","pureKey","charAt","getDataAttribute","Config","Default","DefaultType","Error","_getConfig","config","_mergeConfigObj","_configAfterMerge","_typeCheckConfig","jsonConfig","constructor","configTypes","property","expectedTypes","valueType","prototype","RegExp","test","TypeError","toUpperCase","BaseComponent","super","_element","_config","DATA_KEY","dispose","EVENT_KEY","propertyName","getOwnPropertyNames","_queueCallback","isAnimated","getInstance","getOrCreateInstance","VERSION","eventName","getSelector","hrefAttribute","trim","SelectorEngine","concat","Element","findOne","children","child","matches","parents","ancestor","prev","previous","previousElementSibling","next","nextElementSibling","focusableChildren","focusables","map","join","el","getSelectorFromElement","getElementFromSelector","getMultipleElementsFromSelector","enableDismissTrigger","component","method","clickEvent","tagName","EVENT_CLOSE","EVENT_CLOSED","Alert","close","_destroyElement","each","data","undefined","SELECTOR_DATA_TOGGLE","Button","toggle","button","EVENT_TOUCHSTART","EVENT_TOUCHMOVE","EVENT_TOUCHEND","EVENT_POINTERDOWN","EVENT_POINTERUP","endCallback","leftCallback","rightCallback","Swipe","isSupported","_deltaX","_supportPointerEvents","PointerEvent","_initEvents","_start","_eventIsPointerPenTouch","clientX","touches","_end","_handleSwipe","_move","absDeltaX","abs","direction","add","pointerType","navigator","maxTouchPoints","DATA_API_KEY","ORDER_NEXT","ORDER_PREV","DIRECTION_LEFT","DIRECTION_RIGHT","EVENT_SLIDE","EVENT_SLID","EVENT_KEYDOWN","EVENT_MOUSEENTER","EVENT_MOUSELEAVE","EVENT_DRAG_START","EVENT_LOAD_DATA_API","EVENT_CLICK_DATA_API","CLASS_NAME_CAROUSEL","CLASS_NAME_ACTIVE","SELECTOR_ACTIVE","SELECTOR_ITEM","SELECTOR_ACTIVE_ITEM","KEY_TO_DIRECTION","ArrowLeft","ArrowRight","interval","keyboard","pause","ride","touch","wrap","Carousel","_interval","_activeElement","_isSliding","touchTimeout","_swipeHelper","_indicatorsElement","_addEventListeners","cycle","_slide","nextWhenVisible","hidden","_clearInterval","_updateInterval","setInterval","_maybeEnableCycle","to","items","_getItems","activeIndex","_getItemIndex","_getActive","order","defaultInterval","_keydown","_addTouchEventListeners","img","swipeConfig","_directionToOrder","endCallBack","clearTimeout","_setActiveIndicatorElement","activeIndicator","newActiveIndicator","elementInterval","parseInt","isNext","nextElement","nextElementIndex","triggerEvent","_orderToDirection","isCycling","directionalClassName","orderClassName","completeCallBack","_isAnimated","clearInterval","carousel","slideIndex","carousels","EVENT_SHOW","EVENT_SHOWN","EVENT_HIDE","EVENT_HIDDEN","CLASS_NAME_SHOW","CLASS_NAME_COLLAPSE","CLASS_NAME_COLLAPSING","CLASS_NAME_DEEPER_CHILDREN","parent","Collapse","_isTransitioning","_triggerArray","toggleList","elem","filterElement","foundElement","_initializeChildren","_addAriaAndCollapsedClass","_isShown","hide","show","activeChildren","_getFirstLevelChildren","activeInstance","dimension","_getDimension","style","scrollSize","complete","getBoundingClientRect","selected","triggerArray","isOpen","top","bottom","right","left","auto","basePlacements","start","end","clippingParents","viewport","popper","reference","variationPlacements","reduce","acc","placement","placements","beforeRead","read","afterRead","beforeMain","main","afterMain","beforeWrite","write","afterWrite","modifierPhases","getNodeName","nodeName","getWindow","node","ownerDocument","defaultView","isHTMLElement","HTMLElement","isShadowRoot","applyStyles$1","enabled","phase","_ref","state","elements","forEach","styles","assign","effect","_ref2","initialStyles","position","options","strategy","margin","arrow","hasOwnProperty","attribute","requires","getBasePlacement","round","getUAString","uaData","userAgentData","brands","isArray","item","brand","version","userAgent","isLayoutViewport","includeScale","isFixedStrategy","clientRect","scaleX","scaleY","offsetWidth","width","height","visualViewport","addVisualOffsets","x","offsetLeft","y","offsetTop","getLayoutRect","rootNode","isSameNode","host","isTableElement","getDocumentElement","getParentNode","assignedSlot","getTrueOffsetParent","offsetParent","getOffsetParent","isFirefox","currentNode","css","transform","perspective","contain","willChange","getContainingBlock","getMainAxisFromPlacement","within","mathMax","mathMin","mergePaddingObject","paddingObject","expandToHashMap","hashMap","arrow$1","_state$modifiersData$","arrowElement","popperOffsets","modifiersData","basePlacement","axis","len","padding","rects","toPaddingObject","arrowRect","minProp","maxProp","endDiff","startDiff","arrowOffsetParent","clientSize","clientHeight","clientWidth","centerToReference","center","offset","axisProp","centerOffset","_options$element","requiresIfExists","getVariation","unsetSides","mapToStyles","_Object$assign2","popperRect","variation","offsets","gpuAcceleration","adaptive","roundOffsets","isFixed","_offsets$x","_offsets$y","_ref3","hasX","hasY","sideX","sideY","win","heightProp","widthProp","_Object$assign","commonStyles","_ref4","dpr","devicePixelRatio","roundOffsetsByDPR","computeStyles$1","_ref5","_options$gpuAccelerat","_options$adaptive","_options$roundOffsets","passive","eventListeners","_options$scroll","scroll","_options$resize","resize","scrollParents","scrollParent","update","hash","getOppositePlacement","matched","getOppositeVariationPlacement","getWindowScroll","scrollLeft","pageXOffset","scrollTop","pageYOffset","getWindowScrollBarX","isScrollParent","_getComputedStyle","overflow","overflowX","overflowY","getScrollParent","listScrollParents","_element$ownerDocumen","isBody","updatedList","rectToClientRect","rect","getClientRectFromMixedType","clippingParent","html","layoutViewport","getViewportRect","clientTop","clientLeft","getInnerBoundingClientRect","winScroll","scrollWidth","scrollHeight","getDocumentRect","computeOffsets","commonX","commonY","mainAxis","detectOverflow","_options","_options$placement","_options$strategy","_options$boundary","boundary","_options$rootBoundary","rootBoundary","_options$elementConte","elementContext","_options$altBoundary","altBoundary","_options$padding","altContext","clippingClientRect","mainClippingParents","clipperElement","getClippingParents","firstClippingParent","clippingRect","accRect","getClippingRect","contextElement","referenceClientRect","popperClientRect","elementClientRect","overflowOffsets","offsetData","multiply","computeAutoPlacement","flipVariations","_options$allowedAutoP","allowedAutoPlacements","allPlacements","allowedPlacements","overflows","sort","a","b","flip$1","_skip","_options$mainAxis","checkMainAxis","_options$altAxis","altAxis","checkAltAxis","specifiedFallbackPlacements","fallbackPlacements","_options$flipVariatio","preferredPlacement","oppositePlacement","getExpandedFallbackPlacements","referenceRect","checksMap","makeFallbackChecks","firstFittingPlacement","i","_basePlacement","isStartVariation","isVertical","mainVariationSide","altVariationSide","checks","every","check","_loop","_i","fittingPlacement","reset","getSideOffsets","preventedOffsets","isAnySideFullyClipped","some","side","hide$1","preventOverflow","referenceOverflow","popperAltOverflow","referenceClippingOffsets","popperEscapeOffsets","isReferenceHidden","hasPopperEscaped","offset$1","_options$offset","invertDistance","skidding","distance","distanceAndSkiddingToXY","_data$state$placement","popperOffsets$1","preventOverflow$1","_options$tether","tether","_options$tetherOffset","tetherOffset","isBasePlacement","tetherOffsetValue","normalizedTetherOffsetValue","offsetModifierState","_offsetModifierState$","mainSide","altSide","additive","minLen","maxLen","arrowPaddingObject","arrowPaddingMin","arrowPaddingMax","arrowLen","minOffset","maxOffset","clientOffset","offsetModifierValue","tetherMax","preventedOffset","_offsetModifierState$2","_mainSide","_altSide","_offset","_len","_min","_max","isOriginSide","_offsetModifierValue","_tetherMin","_tetherMax","_preventedOffset","v","withinMaxClamp","getCompositeRect","elementOrVirtualElement","isOffsetParentAnElement","offsetParentIsScaled","isElementScaled","modifiers","visited","result","modifier","dep","depModifier","DEFAULT_OPTIONS","areValidElements","arguments","_key","popperGenerator","generatorOptions","_generatorOptions","_generatorOptions$def","defaultModifiers","_generatorOptions$def2","defaultOptions","pending","orderedModifiers","effectCleanupFns","isDestroyed","setOptions","setOptionsAction","cleanupModifierEffects","merged","orderModifiers","current","existing","m","_ref$options","cleanupFn","forceUpdate","_state$elements","_state$orderedModifie","_state$orderedModifie2","Promise","resolve","then","destroy","onFirstUpdate","createPopper","computeStyles","applyStyles","flip","ARROW_UP_KEY","ARROW_DOWN_KEY","EVENT_KEYDOWN_DATA_API","EVENT_KEYUP_DATA_API","SELECTOR_DATA_TOGGLE_SHOWN","SELECTOR_MENU","PLACEMENT_TOP","PLACEMENT_TOPEND","PLACEMENT_BOTTOM","PLACEMENT_BOTTOMEND","PLACEMENT_RIGHT","PLACEMENT_LEFT","autoClose","display","popperConfig","Dropdown","_popper","_parent","_menu","_inNavbar","_detectNavbar","_createPopper","focus","_completeHide","Popper","referenceElement","_getPopperConfig","_getPlacement","parentDropdown","isEnd","_getOffset","popperData","defaultBsPopperConfig","_selectMenuItem","clearMenus","openToggles","context","composedPath","isMenuTarget","dataApiKeydownHandler","isInput","isEscapeEvent","isUpOrDownEvent","getToggleButton","stopPropagation","EVENT_MOUSEDOWN","className","clickCallback","rootElement","Backdrop","_isAppended","_append","_getElement","_emulateAnimation","backdrop","createElement","append","EVENT_FOCUSIN","EVENT_KEYDOWN_TAB","TAB_NAV_BACKWARD","autofocus","trapElement","FocusTrap","_isActive","_lastTabNavDirection","activate","_handleFocusin","_handleKeydown","deactivate","shiftKey","SELECTOR_FIXED_CONTENT","SELECTOR_STICKY_CONTENT","PROPERTY_PADDING","PROPERTY_MARGIN","ScrollBarHelper","getWidth","documentWidth","innerWidth","_disableOverFlow","_setElementAttributes","calculatedValue","_resetElementAttributes","isOverflowing","_saveInitialAttribute","styleProperty","scrollbarWidth","_applyManipulationCallback","setProperty","actualValue","removeProperty","callBack","sel","EVENT_HIDE_PREVENTED","EVENT_RESIZE","EVENT_CLICK_DISMISS","EVENT_MOUSEDOWN_DISMISS","EVENT_KEYDOWN_DISMISS","CLASS_NAME_OPEN","CLASS_NAME_STATIC","Modal","_dialog","_backdrop","_initializeBackDrop","_focustrap","_initializeFocusTrap","_scrollBar","_adjustDialog","_showElement","_hideModal","handleUpdate","modalBody","transitionComplete","_triggerBackdropTransition","event2","_resetAdjustments","isModalOverflowing","initialOverflowY","isBodyOverflowing","paddingLeft","paddingRight","showEvent","alreadyOpen","CLASS_NAME_SHOWING","CLASS_NAME_HIDING","OPEN_SELECTOR","Offcanvas","blur","completeCallback","DefaultAllowlist","area","br","col","code","div","em","hr","h1","h2","h3","h4","h5","h6","li","ol","p","pre","s","small","span","sub","sup","strong","u","ul","uriAttributes","SAFE_URL_PATTERN","allowedAttribute","allowedAttributeList","attributeName","nodeValue","attributeRegex","regex","allowList","content","extraClass","sanitize","sanitizeFn","template","DefaultContentType","entry","TemplateFactory","getContent","_resolvePossibleFunction","hasContent","changeContent","_checkContent","toHtml","templateWrapper","innerHTML","_maybeSanitize","text","_setContent","arg","templateElement","_putElementInTemplate","textContent","unsafeHtml","sanitizeFunction","createdDocument","DOMParser","parseFromString","elementName","attributeList","allowedAttributes","sanitizeHtml","DISALLOWED_ATTRIBUTES","CLASS_NAME_FADE","SELECTOR_MODAL","EVENT_MODAL_HIDE","TRIGGER_HOVER","TRIGGER_FOCUS","AttachmentMap","AUTO","TOP","RIGHT","BOTTOM","LEFT","animation","container","customClass","delay","title","Tooltip","_isEnabled","_timeout","_isHovered","_activeTrigger","_templateFactory","_newContent","tip","_setListeners","_fixTitle","enable","disable","toggleEnabled","click","_leave","_enter","_hideModalHandler","_disposePopper","_isWithContent","isInTheDom","_getTipElement","_isWithActiveTrigger","_getTitle","_createTipElement","_getContentForTemplate","_getTemplateFactory","tipId","prefix","floor","random","getElementById","getUID","setContent","_initializeOnDelegatedTarget","_getDelegateConfig","attachment","triggers","eventIn","eventOut","_setTimeout","timeout","dataAttributes","dataAttribute","Popover","_getContent","EVENT_ACTIVATE","EVENT_CLICK","SELECTOR_TARGET_LINKS","SELECTOR_NAV_LINKS","SELECTOR_LINK_ITEMS","rootMargin","smoothScroll","threshold","ScrollSpy","_targetLinks","_observableSections","_rootElement","_activeTarget","_observer","_previousScrollData","visibleEntryTop","parentScrollTop","refresh","_initializeTargetsAndObservables","_maybeEnableSmoothScroll","disconnect","_getNewObserver","section","observe","observableSection","scrollTo","behavior","IntersectionObserver","_observerCallback","targetElement","_process","userScrollsDown","isIntersecting","_clearActiveClass","entryIsLowerThanPrevious","targetLinks","anchor","decodeURI","_activateParents","listGroup","activeNodes","spy","ARROW_LEFT_KEY","ARROW_RIGHT_KEY","HOME_KEY","END_KEY","NOT_SELECTOR_DROPDOWN_TOGGLE","SELECTOR_INNER_ELEM","SELECTOR_DATA_TOGGLE_ACTIVE","Tab","_setInitialAttributes","_getChildren","innerElem","_elemIsActive","active","_getActiveElem","hideEvent","_deactivate","_activate","relatedElem","_toggleDropDown","nextActiveElement","preventScroll","_setAttributeIfNotExists","_setInitialAttributesOnChild","_getInnerElement","isActive","outerElem","_getOuterElement","_setInitialAttributesOnTargetPanel","open","EVENT_MOUSEOVER","EVENT_MOUSEOUT","EVENT_FOCUSOUT","CLASS_NAME_HIDE","autohide","Toast","_hasMouseInteraction","_hasKeyboardInteraction","_clearTimeout","_maybeScheduleHide","isShown","_onInteraction","isInteracting"],"sources":["../../js/src/dom/data.js","../../js/src/util/index.js","../../js/src/dom/event-handler.js","../../js/src/dom/manipulator.js","../../js/src/util/config.js","../../js/src/base-component.js","../../js/src/dom/selector-engine.js","../../js/src/util/component-functions.js","../../js/src/alert.js","../../js/src/button.js","../../js/src/util/swipe.js","../../js/src/carousel.js","../../js/src/collapse.js","../../node_modules/@popperjs/core/lib/enums.js","../../node_modules/@popperjs/core/lib/dom-utils/getNodeName.js","../../node_modules/@popperjs/core/lib/dom-utils/getWindow.js","../../node_modules/@popperjs/core/lib/dom-utils/instanceOf.js","../../node_modules/@popperjs/core/lib/modifiers/applyStyles.js","../../node_modules/@popperjs/core/lib/utils/getBasePlacement.js","../../node_modules/@popperjs/core/lib/utils/math.js","../../node_modules/@popperjs/core/lib/utils/userAgent.js","../../node_modules/@popperjs/core/lib/dom-utils/isLayoutViewport.js","../../node_modules/@popperjs/core/lib/dom-utils/getBoundingClientRect.js","../../node_modules/@popperjs/core/lib/dom-utils/getLayoutRect.js","../../node_modules/@popperjs/core/lib/dom-utils/contains.js","../../node_modules/@popperjs/core/lib/dom-utils/getComputedStyle.js","../../node_modules/@popperjs/core/lib/dom-utils/isTableElement.js","../../node_modules/@popperjs/core/lib/dom-utils/getDocumentElement.js","../../node_modules/@popperjs/core/lib/dom-utils/getParentNode.js","../../node_modules/@popperjs/core/lib/dom-utils/getOffsetParent.js","../../node_modules/@popperjs/core/lib/utils/getMainAxisFromPlacement.js","../../node_modules/@popperjs/core/lib/utils/within.js","../../node_modules/@popperjs/core/lib/utils/mergePaddingObject.js","../../node_modules/@popperjs/core/lib/utils/getFreshSideObject.js","../../node_modules/@popperjs/core/lib/utils/expandToHashMap.js","../../node_modules/@popperjs/core/lib/modifiers/arrow.js","../../node_modules/@popperjs/core/lib/utils/getVariation.js","../../node_modules/@popperjs/core/lib/modifiers/computeStyles.js","../../node_modules/@popperjs/core/lib/modifiers/eventListeners.js","../../node_modules/@popperjs/core/lib/utils/getOppositePlacement.js","../../node_modules/@popperjs/core/lib/utils/getOppositeVariationPlacement.js","../../node_modules/@popperjs/core/lib/dom-utils/getWindowScroll.js","../../node_modules/@popperjs/core/lib/dom-utils/getWindowScrollBarX.js","../../node_modules/@popperjs/core/lib/dom-utils/isScrollParent.js","../../node_modules/@popperjs/core/lib/dom-utils/getScrollParent.js","../../node_modules/@popperjs/core/lib/dom-utils/listScrollParents.js","../../node_modules/@popperjs/core/lib/utils/rectToClientRect.js","../../node_modules/@popperjs/core/lib/dom-utils/getClippingRect.js","../../node_modules/@popperjs/core/lib/dom-utils/getViewportRect.js","../../node_modules/@popperjs/core/lib/dom-utils/getDocumentRect.js","../../node_modules/@popperjs/core/lib/utils/computeOffsets.js","../../node_modules/@popperjs/core/lib/utils/detectOverflow.js","../../node_modules/@popperjs/core/lib/utils/computeAutoPlacement.js","../../node_modules/@popperjs/core/lib/modifiers/flip.js","../../node_modules/@popperjs/core/lib/modifiers/hide.js","../../node_modules/@popperjs/core/lib/modifiers/offset.js","../../node_modules/@popperjs/core/lib/modifiers/popperOffsets.js","../../node_modules/@popperjs/core/lib/modifiers/preventOverflow.js","../../node_modules/@popperjs/core/lib/utils/getAltAxis.js","../../node_modules/@popperjs/core/lib/dom-utils/getCompositeRect.js","../../node_modules/@popperjs/core/lib/dom-utils/getNodeScroll.js","../../node_modules/@popperjs/core/lib/dom-utils/getHTMLElementScroll.js","../../node_modules/@popperjs/core/lib/utils/orderModifiers.js","../../node_modules/@popperjs/core/lib/createPopper.js","../../node_modules/@popperjs/core/lib/utils/debounce.js","../../node_modules/@popperjs/core/lib/utils/mergeByName.js","../../node_modules/@popperjs/core/lib/popper-lite.js","../../node_modules/@popperjs/core/lib/popper.js","../../js/src/dropdown.js","../../js/src/util/backdrop.js","../../js/src/util/focustrap.js","../../js/src/util/scrollbar.js","../../js/src/modal.js","../../js/src/offcanvas.js","../../js/src/util/sanitizer.js","../../js/src/util/template-factory.js","../../js/src/tooltip.js","../../js/src/popover.js","../../js/src/scrollspy.js","../../js/src/tab.js","../../js/src/toast.js","../../js/index.umd.js"],"sourcesContent":["/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/data.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n/**\n * Constants\n */\n\nconst elementMap = new Map()\n\nexport default {\n set(element, key, instance) {\n if (!elementMap.has(element)) {\n elementMap.set(element, new Map())\n }\n\n const instanceMap = elementMap.get(element)\n\n // make it clear we only want one instance per element\n // can be removed later when multiple key/instances are fine to be used\n if (!instanceMap.has(key) && instanceMap.size !== 0) {\n // eslint-disable-next-line no-console\n console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(instanceMap.keys())[0]}.`)\n return\n }\n\n instanceMap.set(key, instance)\n },\n\n get(element, key) {\n if (elementMap.has(element)) {\n return elementMap.get(element).get(key) || null\n }\n\n return null\n },\n\n remove(element, key) {\n if (!elementMap.has(element)) {\n return\n }\n\n const instanceMap = elementMap.get(element)\n\n instanceMap.delete(key)\n\n // free up element references if there are no instances left for an element\n if (instanceMap.size === 0) {\n elementMap.delete(element)\n }\n }\n}\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/index.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst MAX_UID = 1_000_000\nconst MILLISECONDS_MULTIPLIER = 1000\nconst TRANSITION_END = 'transitionend'\n\n/**\n * Properly escape IDs selectors to handle weird IDs\n * @param {string} selector\n * @returns {string}\n */\nconst parseSelector = selector => {\n if (selector && window.CSS && window.CSS.escape) {\n // document.querySelector needs escaping to handle IDs (html5+) containing for instance /\n selector = selector.replace(/#([^\\s\"#']+)/g, (match, id) => `#${CSS.escape(id)}`)\n }\n\n return selector\n}\n\n// Shout-out Angus Croll (https://goo.gl/pxwQGp)\nconst toType = object => {\n if (object === null || object === undefined) {\n return `${object}`\n }\n\n return Object.prototype.toString.call(object).match(/\\s([a-z]+)/i)[1].toLowerCase()\n}\n\n/**\n * Public Util API\n */\n\nconst getUID = prefix => {\n do {\n prefix += Math.floor(Math.random() * MAX_UID)\n } while (document.getElementById(prefix))\n\n return prefix\n}\n\nconst getTransitionDurationFromElement = element => {\n if (!element) {\n return 0\n }\n\n // Get transition-duration of the element\n let { transitionDuration, transitionDelay } = window.getComputedStyle(element)\n\n const floatTransitionDuration = Number.parseFloat(transitionDuration)\n const floatTransitionDelay = Number.parseFloat(transitionDelay)\n\n // Return 0 if element or transition duration is not found\n if (!floatTransitionDuration && !floatTransitionDelay) {\n return 0\n }\n\n // If multiple durations are defined, take the first\n transitionDuration = transitionDuration.split(',')[0]\n transitionDelay = transitionDelay.split(',')[0]\n\n return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER\n}\n\nconst triggerTransitionEnd = element => {\n element.dispatchEvent(new Event(TRANSITION_END))\n}\n\nconst isElement = object => {\n if (!object || typeof object !== 'object') {\n return false\n }\n\n if (typeof object.jquery !== 'undefined') {\n object = object[0]\n }\n\n return typeof object.nodeType !== 'undefined'\n}\n\nconst getElement = object => {\n // it's a jQuery object or a node element\n if (isElement(object)) {\n return object.jquery ? object[0] : object\n }\n\n if (typeof object === 'string' && object.length > 0) {\n return document.querySelector(parseSelector(object))\n }\n\n return null\n}\n\nconst isVisible = element => {\n if (!isElement(element) || element.getClientRects().length === 0) {\n return false\n }\n\n const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible'\n // Handle `details` element as its content may falsie appear visible when it is closed\n const closedDetails = element.closest('details:not([open])')\n\n if (!closedDetails) {\n return elementIsVisible\n }\n\n if (closedDetails !== element) {\n const summary = element.closest('summary')\n if (summary && summary.parentNode !== closedDetails) {\n return false\n }\n\n if (summary === null) {\n return false\n }\n }\n\n return elementIsVisible\n}\n\nconst isDisabled = element => {\n if (!element || element.nodeType !== Node.ELEMENT_NODE) {\n return true\n }\n\n if (element.classList.contains('disabled')) {\n return true\n }\n\n if (typeof element.disabled !== 'undefined') {\n return element.disabled\n }\n\n return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false'\n}\n\nconst findShadowRoot = element => {\n if (!document.documentElement.attachShadow) {\n return null\n }\n\n // Can find the shadow root otherwise it'll return the document\n if (typeof element.getRootNode === 'function') {\n const root = element.getRootNode()\n return root instanceof ShadowRoot ? root : null\n }\n\n if (element instanceof ShadowRoot) {\n return element\n }\n\n // when we don't find a shadow root\n if (!element.parentNode) {\n return null\n }\n\n return findShadowRoot(element.parentNode)\n}\n\nconst noop = () => {}\n\n/**\n * Trick to restart an element's animation\n *\n * @param {HTMLElement} element\n * @return void\n *\n * @see https://www.charistheo.io/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation\n */\nconst reflow = element => {\n element.offsetHeight // eslint-disable-line no-unused-expressions\n}\n\nconst getjQuery = () => {\n if (window.jQuery && !document.body.hasAttribute('data-bs-no-jquery')) {\n return window.jQuery\n }\n\n return null\n}\n\nconst DOMContentLoadedCallbacks = []\n\nconst onDOMContentLoaded = callback => {\n if (document.readyState === 'loading') {\n // add listener on the first call when the document is in loading state\n if (!DOMContentLoadedCallbacks.length) {\n document.addEventListener('DOMContentLoaded', () => {\n for (const callback of DOMContentLoadedCallbacks) {\n callback()\n }\n })\n }\n\n DOMContentLoadedCallbacks.push(callback)\n } else {\n callback()\n }\n}\n\nconst isRTL = () => document.documentElement.dir === 'rtl'\n\nconst defineJQueryPlugin = plugin => {\n onDOMContentLoaded(() => {\n const $ = getjQuery()\n /* istanbul ignore if */\n if ($) {\n const name = plugin.NAME\n const JQUERY_NO_CONFLICT = $.fn[name]\n $.fn[name] = plugin.jQueryInterface\n $.fn[name].Constructor = plugin\n $.fn[name].noConflict = () => {\n $.fn[name] = JQUERY_NO_CONFLICT\n return plugin.jQueryInterface\n }\n }\n })\n}\n\nconst execute = (possibleCallback, args = [], defaultValue = possibleCallback) => {\n return typeof possibleCallback === 'function' ? possibleCallback(...args) : defaultValue\n}\n\nconst executeAfterTransition = (callback, transitionElement, waitForTransition = true) => {\n if (!waitForTransition) {\n execute(callback)\n return\n }\n\n const durationPadding = 5\n const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding\n\n let called = false\n\n const handler = ({ target }) => {\n if (target !== transitionElement) {\n return\n }\n\n called = true\n transitionElement.removeEventListener(TRANSITION_END, handler)\n execute(callback)\n }\n\n transitionElement.addEventListener(TRANSITION_END, handler)\n setTimeout(() => {\n if (!called) {\n triggerTransitionEnd(transitionElement)\n }\n }, emulatedDuration)\n}\n\n/**\n * Return the previous/next element of a list.\n *\n * @param {array} list The list of elements\n * @param activeElement The active element\n * @param shouldGetNext Choose to get next or previous element\n * @param isCycleAllowed\n * @return {Element|elem} The proper element\n */\nconst getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => {\n const listLength = list.length\n let index = list.indexOf(activeElement)\n\n // if the element does not exist in the list return an element\n // depending on the direction and if cycle is allowed\n if (index === -1) {\n return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0]\n }\n\n index += shouldGetNext ? 1 : -1\n\n if (isCycleAllowed) {\n index = (index + listLength) % listLength\n }\n\n return list[Math.max(0, Math.min(index, listLength - 1))]\n}\n\nexport {\n defineJQueryPlugin,\n execute,\n executeAfterTransition,\n findShadowRoot,\n getElement,\n getjQuery,\n getNextActiveElement,\n getTransitionDurationFromElement,\n getUID,\n isDisabled,\n isElement,\n isRTL,\n isVisible,\n noop,\n onDOMContentLoaded,\n parseSelector,\n reflow,\n triggerTransitionEnd,\n toType\n}\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/event-handler.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport { getjQuery } from '../util/index.js'\n\n/**\n * Constants\n */\n\nconst namespaceRegex = /[^.]*(?=\\..*)\\.|.*/\nconst stripNameRegex = /\\..*/\nconst stripUidRegex = /::\\d+$/\nconst eventRegistry = {} // Events storage\nlet uidEvent = 1\nconst customEvents = {\n mouseenter: 'mouseover',\n mouseleave: 'mouseout'\n}\n\nconst nativeEvents = new Set([\n 'click',\n 'dblclick',\n 'mouseup',\n 'mousedown',\n 'contextmenu',\n 'mousewheel',\n 'DOMMouseScroll',\n 'mouseover',\n 'mouseout',\n 'mousemove',\n 'selectstart',\n 'selectend',\n 'keydown',\n 'keypress',\n 'keyup',\n 'orientationchange',\n 'touchstart',\n 'touchmove',\n 'touchend',\n 'touchcancel',\n 'pointerdown',\n 'pointermove',\n 'pointerup',\n 'pointerleave',\n 'pointercancel',\n 'gesturestart',\n 'gesturechange',\n 'gestureend',\n 'focus',\n 'blur',\n 'change',\n 'reset',\n 'select',\n 'submit',\n 'focusin',\n 'focusout',\n 'load',\n 'unload',\n 'beforeunload',\n 'resize',\n 'move',\n 'DOMContentLoaded',\n 'readystatechange',\n 'error',\n 'abort',\n 'scroll'\n])\n\n/**\n * Private methods\n */\n\nfunction makeEventUid(element, uid) {\n return (uid && `${uid}::${uidEvent++}`) || element.uidEvent || uidEvent++\n}\n\nfunction getElementEvents(element) {\n const uid = makeEventUid(element)\n\n element.uidEvent = uid\n eventRegistry[uid] = eventRegistry[uid] || {}\n\n return eventRegistry[uid]\n}\n\nfunction bootstrapHandler(element, fn) {\n return function handler(event) {\n hydrateObj(event, { delegateTarget: element })\n\n if (handler.oneOff) {\n EventHandler.off(element, event.type, fn)\n }\n\n return fn.apply(element, [event])\n }\n}\n\nfunction bootstrapDelegationHandler(element, selector, fn) {\n return function handler(event) {\n const domElements = element.querySelectorAll(selector)\n\n for (let { target } = event; target && target !== this; target = target.parentNode) {\n for (const domElement of domElements) {\n if (domElement !== target) {\n continue\n }\n\n hydrateObj(event, { delegateTarget: target })\n\n if (handler.oneOff) {\n EventHandler.off(element, event.type, selector, fn)\n }\n\n return fn.apply(target, [event])\n }\n }\n }\n}\n\nfunction findHandler(events, callable, delegationSelector = null) {\n return Object.values(events)\n .find(event => event.callable === callable && event.delegationSelector === delegationSelector)\n}\n\nfunction normalizeParameters(originalTypeEvent, handler, delegationFunction) {\n const isDelegated = typeof handler === 'string'\n // TODO: tooltip passes `false` instead of selector, so we need to check\n const callable = isDelegated ? delegationFunction : (handler || delegationFunction)\n let typeEvent = getTypeEvent(originalTypeEvent)\n\n if (!nativeEvents.has(typeEvent)) {\n typeEvent = originalTypeEvent\n }\n\n return [isDelegated, callable, typeEvent]\n}\n\nfunction addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) {\n if (typeof originalTypeEvent !== 'string' || !element) {\n return\n }\n\n let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction)\n\n // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position\n // this prevents the handler from being dispatched the same way as mouseover or mouseout does\n if (originalTypeEvent in customEvents) {\n const wrapFunction = fn => {\n return function (event) {\n if (!event.relatedTarget || (event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget))) {\n return fn.call(this, event)\n }\n }\n }\n\n callable = wrapFunction(callable)\n }\n\n const events = getElementEvents(element)\n const handlers = events[typeEvent] || (events[typeEvent] = {})\n const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null)\n\n if (previousFunction) {\n previousFunction.oneOff = previousFunction.oneOff && oneOff\n\n return\n }\n\n const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, ''))\n const fn = isDelegated ?\n bootstrapDelegationHandler(element, handler, callable) :\n bootstrapHandler(element, callable)\n\n fn.delegationSelector = isDelegated ? handler : null\n fn.callable = callable\n fn.oneOff = oneOff\n fn.uidEvent = uid\n handlers[uid] = fn\n\n element.addEventListener(typeEvent, fn, isDelegated)\n}\n\nfunction removeHandler(element, events, typeEvent, handler, delegationSelector) {\n const fn = findHandler(events[typeEvent], handler, delegationSelector)\n\n if (!fn) {\n return\n }\n\n element.removeEventListener(typeEvent, fn, Boolean(delegationSelector))\n delete events[typeEvent][fn.uidEvent]\n}\n\nfunction removeNamespacedHandlers(element, events, typeEvent, namespace) {\n const storeElementEvent = events[typeEvent] || {}\n\n for (const [handlerKey, event] of Object.entries(storeElementEvent)) {\n if (handlerKey.includes(namespace)) {\n removeHandler(element, events, typeEvent, event.callable, event.delegationSelector)\n }\n }\n}\n\nfunction getTypeEvent(event) {\n // allow to get the native events from namespaced events ('click.bs.button' --> 'click')\n event = event.replace(stripNameRegex, '')\n return customEvents[event] || event\n}\n\nconst EventHandler = {\n on(element, event, handler, delegationFunction) {\n addHandler(element, event, handler, delegationFunction, false)\n },\n\n one(element, event, handler, delegationFunction) {\n addHandler(element, event, handler, delegationFunction, true)\n },\n\n off(element, originalTypeEvent, handler, delegationFunction) {\n if (typeof originalTypeEvent !== 'string' || !element) {\n return\n }\n\n const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction)\n const inNamespace = typeEvent !== originalTypeEvent\n const events = getElementEvents(element)\n const storeElementEvent = events[typeEvent] || {}\n const isNamespace = originalTypeEvent.startsWith('.')\n\n if (typeof callable !== 'undefined') {\n // Simplest case: handler is passed, remove that listener ONLY.\n if (!Object.keys(storeElementEvent).length) {\n return\n }\n\n removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null)\n return\n }\n\n if (isNamespace) {\n for (const elementEvent of Object.keys(events)) {\n removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1))\n }\n }\n\n for (const [keyHandlers, event] of Object.entries(storeElementEvent)) {\n const handlerKey = keyHandlers.replace(stripUidRegex, '')\n\n if (!inNamespace || originalTypeEvent.includes(handlerKey)) {\n removeHandler(element, events, typeEvent, event.callable, event.delegationSelector)\n }\n }\n },\n\n trigger(element, event, args) {\n if (typeof event !== 'string' || !element) {\n return null\n }\n\n const $ = getjQuery()\n const typeEvent = getTypeEvent(event)\n const inNamespace = event !== typeEvent\n\n let jQueryEvent = null\n let bubbles = true\n let nativeDispatch = true\n let defaultPrevented = false\n\n if (inNamespace && $) {\n jQueryEvent = $.Event(event, args)\n\n $(element).trigger(jQueryEvent)\n bubbles = !jQueryEvent.isPropagationStopped()\n nativeDispatch = !jQueryEvent.isImmediatePropagationStopped()\n defaultPrevented = jQueryEvent.isDefaultPrevented()\n }\n\n const evt = hydrateObj(new Event(event, { bubbles, cancelable: true }), args)\n\n if (defaultPrevented) {\n evt.preventDefault()\n }\n\n if (nativeDispatch) {\n element.dispatchEvent(evt)\n }\n\n if (evt.defaultPrevented && jQueryEvent) {\n jQueryEvent.preventDefault()\n }\n\n return evt\n }\n}\n\nfunction hydrateObj(obj, meta = {}) {\n for (const [key, value] of Object.entries(meta)) {\n try {\n obj[key] = value\n } catch {\n Object.defineProperty(obj, key, {\n configurable: true,\n get() {\n return value\n }\n })\n }\n }\n\n return obj\n}\n\nexport default EventHandler\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/manipulator.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nfunction normalizeData(value) {\n if (value === 'true') {\n return true\n }\n\n if (value === 'false') {\n return false\n }\n\n if (value === Number(value).toString()) {\n return Number(value)\n }\n\n if (value === '' || value === 'null') {\n return null\n }\n\n if (typeof value !== 'string') {\n return value\n }\n\n try {\n return JSON.parse(decodeURIComponent(value))\n } catch {\n return value\n }\n}\n\nfunction normalizeDataKey(key) {\n return key.replace(/[A-Z]/g, chr => `-${chr.toLowerCase()}`)\n}\n\nconst Manipulator = {\n setDataAttribute(element, key, value) {\n element.setAttribute(`data-bs-${normalizeDataKey(key)}`, value)\n },\n\n removeDataAttribute(element, key) {\n element.removeAttribute(`data-bs-${normalizeDataKey(key)}`)\n },\n\n getDataAttributes(element) {\n if (!element) {\n return {}\n }\n\n const attributes = {}\n const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig'))\n\n for (const key of bsKeys) {\n let pureKey = key.replace(/^bs/, '')\n pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1, pureKey.length)\n attributes[pureKey] = normalizeData(element.dataset[key])\n }\n\n return attributes\n },\n\n getDataAttribute(element, key) {\n return normalizeData(element.getAttribute(`data-bs-${normalizeDataKey(key)}`))\n }\n}\n\nexport default Manipulator\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/config.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport Manipulator from '../dom/manipulator.js'\nimport { isElement, toType } from './index.js'\n\n/**\n * Class definition\n */\n\nclass Config {\n // Getters\n static get Default() {\n return {}\n }\n\n static get DefaultType() {\n return {}\n }\n\n static get NAME() {\n throw new Error('You have to implement the static method \"NAME\", for each component!')\n }\n\n _getConfig(config) {\n config = this._mergeConfigObj(config)\n config = this._configAfterMerge(config)\n this._typeCheckConfig(config)\n return config\n }\n\n _configAfterMerge(config) {\n return config\n }\n\n _mergeConfigObj(config, element) {\n const jsonConfig = isElement(element) ? Manipulator.getDataAttribute(element, 'config') : {} // try to parse\n\n return {\n ...this.constructor.Default,\n ...(typeof jsonConfig === 'object' ? jsonConfig : {}),\n ...(isElement(element) ? Manipulator.getDataAttributes(element) : {}),\n ...(typeof config === 'object' ? config : {})\n }\n }\n\n _typeCheckConfig(config, configTypes = this.constructor.DefaultType) {\n for (const [property, expectedTypes] of Object.entries(configTypes)) {\n const value = config[property]\n const valueType = isElement(value) ? 'element' : toType(value)\n\n if (!new RegExp(expectedTypes).test(valueType)) {\n throw new TypeError(\n `${this.constructor.NAME.toUpperCase()}: Option \"${property}\" provided type \"${valueType}\" but expected type \"${expectedTypes}\".`\n )\n }\n }\n }\n}\n\nexport default Config\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap base-component.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport Data from './dom/data.js'\nimport EventHandler from './dom/event-handler.js'\nimport Config from './util/config.js'\nimport { executeAfterTransition, getElement } from './util/index.js'\n\n/**\n * Constants\n */\n\nconst VERSION = '5.3.1'\n\n/**\n * Class definition\n */\n\nclass BaseComponent extends Config {\n constructor(element, config) {\n super()\n\n element = getElement(element)\n if (!element) {\n return\n }\n\n this._element = element\n this._config = this._getConfig(config)\n\n Data.set(this._element, this.constructor.DATA_KEY, this)\n }\n\n // Public\n dispose() {\n Data.remove(this._element, this.constructor.DATA_KEY)\n EventHandler.off(this._element, this.constructor.EVENT_KEY)\n\n for (const propertyName of Object.getOwnPropertyNames(this)) {\n this[propertyName] = null\n }\n }\n\n _queueCallback(callback, element, isAnimated = true) {\n executeAfterTransition(callback, element, isAnimated)\n }\n\n _getConfig(config) {\n config = this._mergeConfigObj(config, this._element)\n config = this._configAfterMerge(config)\n this._typeCheckConfig(config)\n return config\n }\n\n // Static\n static getInstance(element) {\n return Data.get(getElement(element), this.DATA_KEY)\n }\n\n static getOrCreateInstance(element, config = {}) {\n return this.getInstance(element) || new this(element, typeof config === 'object' ? config : null)\n }\n\n static get VERSION() {\n return VERSION\n }\n\n static get DATA_KEY() {\n return `bs.${this.NAME}`\n }\n\n static get EVENT_KEY() {\n return `.${this.DATA_KEY}`\n }\n\n static eventName(name) {\n return `${name}${this.EVENT_KEY}`\n }\n}\n\nexport default BaseComponent\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/selector-engine.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport { isDisabled, isVisible, parseSelector } from '../util/index.js'\n\nconst getSelector = element => {\n let selector = element.getAttribute('data-bs-target')\n\n if (!selector || selector === '#') {\n let hrefAttribute = element.getAttribute('href')\n\n // The only valid content that could double as a selector are IDs or classes,\n // so everything starting with `#` or `.`. If a \"real\" URL is used as the selector,\n // `document.querySelector` will rightfully complain it is invalid.\n // See https://github.com/twbs/bootstrap/issues/32273\n if (!hrefAttribute || (!hrefAttribute.includes('#') && !hrefAttribute.startsWith('.'))) {\n return null\n }\n\n // Just in case some CMS puts out a full URL with the anchor appended\n if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) {\n hrefAttribute = `#${hrefAttribute.split('#')[1]}`\n }\n\n selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null\n }\n\n return parseSelector(selector)\n}\n\nconst SelectorEngine = {\n find(selector, element = document.documentElement) {\n return [].concat(...Element.prototype.querySelectorAll.call(element, selector))\n },\n\n findOne(selector, element = document.documentElement) {\n return Element.prototype.querySelector.call(element, selector)\n },\n\n children(element, selector) {\n return [].concat(...element.children).filter(child => child.matches(selector))\n },\n\n parents(element, selector) {\n const parents = []\n let ancestor = element.parentNode.closest(selector)\n\n while (ancestor) {\n parents.push(ancestor)\n ancestor = ancestor.parentNode.closest(selector)\n }\n\n return parents\n },\n\n prev(element, selector) {\n let previous = element.previousElementSibling\n\n while (previous) {\n if (previous.matches(selector)) {\n return [previous]\n }\n\n previous = previous.previousElementSibling\n }\n\n return []\n },\n // TODO: this is now unused; remove later along with prev()\n next(element, selector) {\n let next = element.nextElementSibling\n\n while (next) {\n if (next.matches(selector)) {\n return [next]\n }\n\n next = next.nextElementSibling\n }\n\n return []\n },\n\n focusableChildren(element) {\n const focusables = [\n 'a',\n 'button',\n 'input',\n 'textarea',\n 'select',\n 'details',\n '[tabindex]',\n '[contenteditable=\"true\"]'\n ].map(selector => `${selector}:not([tabindex^=\"-\"])`).join(',')\n\n return this.find(focusables, element).filter(el => !isDisabled(el) && isVisible(el))\n },\n\n getSelectorFromElement(element) {\n const selector = getSelector(element)\n\n if (selector) {\n return SelectorEngine.findOne(selector) ? selector : null\n }\n\n return null\n },\n\n getElementFromSelector(element) {\n const selector = getSelector(element)\n\n return selector ? SelectorEngine.findOne(selector) : null\n },\n\n getMultipleElementsFromSelector(element) {\n const selector = getSelector(element)\n\n return selector ? SelectorEngine.find(selector) : []\n }\n}\n\nexport default SelectorEngine\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/component-functions.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport EventHandler from '../dom/event-handler.js'\nimport SelectorEngine from '../dom/selector-engine.js'\nimport { isDisabled } from './index.js'\n\nconst enableDismissTrigger = (component, method = 'hide') => {\n const clickEvent = `click.dismiss${component.EVENT_KEY}`\n const name = component.NAME\n\n EventHandler.on(document, clickEvent, `[data-bs-dismiss=\"${name}\"]`, function (event) {\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault()\n }\n\n if (isDisabled(this)) {\n return\n }\n\n const target = SelectorEngine.getElementFromSelector(this) || this.closest(`.${name}`)\n const instance = component.getOrCreateInstance(target)\n\n // Method argument is left, for Alert and only, as it doesn't implement the 'hide' method\n instance[method]()\n })\n}\n\nexport {\n enableDismissTrigger\n}\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap alert.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport { enableDismissTrigger } from './util/component-functions.js'\nimport { defineJQueryPlugin } from './util/index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'alert'\nconst DATA_KEY = 'bs.alert'\nconst EVENT_KEY = `.${DATA_KEY}`\n\nconst EVENT_CLOSE = `close${EVENT_KEY}`\nconst EVENT_CLOSED = `closed${EVENT_KEY}`\nconst CLASS_NAME_FADE = 'fade'\nconst CLASS_NAME_SHOW = 'show'\n\n/**\n * Class definition\n */\n\nclass Alert extends BaseComponent {\n // Getters\n static get NAME() {\n return NAME\n }\n\n // Public\n close() {\n const closeEvent = EventHandler.trigger(this._element, EVENT_CLOSE)\n\n if (closeEvent.defaultPrevented) {\n return\n }\n\n this._element.classList.remove(CLASS_NAME_SHOW)\n\n const isAnimated = this._element.classList.contains(CLASS_NAME_FADE)\n this._queueCallback(() => this._destroyElement(), this._element, isAnimated)\n }\n\n // Private\n _destroyElement() {\n this._element.remove()\n EventHandler.trigger(this._element, EVENT_CLOSED)\n this.dispose()\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Alert.getOrCreateInstance(this)\n\n if (typeof config !== 'string') {\n return\n }\n\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config](this)\n })\n }\n}\n\n/**\n * Data API implementation\n */\n\nenableDismissTrigger(Alert, 'close')\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Alert)\n\nexport default Alert\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap button.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport { defineJQueryPlugin } from './util/index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'button'\nconst DATA_KEY = 'bs.button'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\n\nconst CLASS_NAME_ACTIVE = 'active'\nconst SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"button\"]'\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\n\n/**\n * Class definition\n */\n\nclass Button extends BaseComponent {\n // Getters\n static get NAME() {\n return NAME\n }\n\n // Public\n toggle() {\n // Toggle class and sync the `aria-pressed` attribute with the return value of the `.toggle()` method\n this._element.setAttribute('aria-pressed', this._element.classList.toggle(CLASS_NAME_ACTIVE))\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Button.getOrCreateInstance(this)\n\n if (config === 'toggle') {\n data[config]()\n }\n })\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, event => {\n event.preventDefault()\n\n const button = event.target.closest(SELECTOR_DATA_TOGGLE)\n const data = Button.getOrCreateInstance(button)\n\n data.toggle()\n})\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Button)\n\nexport default Button\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/swipe.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport EventHandler from '../dom/event-handler.js'\nimport Config from './config.js'\nimport { execute } from './index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'swipe'\nconst EVENT_KEY = '.bs.swipe'\nconst EVENT_TOUCHSTART = `touchstart${EVENT_KEY}`\nconst EVENT_TOUCHMOVE = `touchmove${EVENT_KEY}`\nconst EVENT_TOUCHEND = `touchend${EVENT_KEY}`\nconst EVENT_POINTERDOWN = `pointerdown${EVENT_KEY}`\nconst EVENT_POINTERUP = `pointerup${EVENT_KEY}`\nconst POINTER_TYPE_TOUCH = 'touch'\nconst POINTER_TYPE_PEN = 'pen'\nconst CLASS_NAME_POINTER_EVENT = 'pointer-event'\nconst SWIPE_THRESHOLD = 40\n\nconst Default = {\n endCallback: null,\n leftCallback: null,\n rightCallback: null\n}\n\nconst DefaultType = {\n endCallback: '(function|null)',\n leftCallback: '(function|null)',\n rightCallback: '(function|null)'\n}\n\n/**\n * Class definition\n */\n\nclass Swipe extends Config {\n constructor(element, config) {\n super()\n this._element = element\n\n if (!element || !Swipe.isSupported()) {\n return\n }\n\n this._config = this._getConfig(config)\n this._deltaX = 0\n this._supportPointerEvents = Boolean(window.PointerEvent)\n this._initEvents()\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n dispose() {\n EventHandler.off(this._element, EVENT_KEY)\n }\n\n // Private\n _start(event) {\n if (!this._supportPointerEvents) {\n this._deltaX = event.touches[0].clientX\n\n return\n }\n\n if (this._eventIsPointerPenTouch(event)) {\n this._deltaX = event.clientX\n }\n }\n\n _end(event) {\n if (this._eventIsPointerPenTouch(event)) {\n this._deltaX = event.clientX - this._deltaX\n }\n\n this._handleSwipe()\n execute(this._config.endCallback)\n }\n\n _move(event) {\n this._deltaX = event.touches && event.touches.length > 1 ?\n 0 :\n event.touches[0].clientX - this._deltaX\n }\n\n _handleSwipe() {\n const absDeltaX = Math.abs(this._deltaX)\n\n if (absDeltaX <= SWIPE_THRESHOLD) {\n return\n }\n\n const direction = absDeltaX / this._deltaX\n\n this._deltaX = 0\n\n if (!direction) {\n return\n }\n\n execute(direction > 0 ? this._config.rightCallback : this._config.leftCallback)\n }\n\n _initEvents() {\n if (this._supportPointerEvents) {\n EventHandler.on(this._element, EVENT_POINTERDOWN, event => this._start(event))\n EventHandler.on(this._element, EVENT_POINTERUP, event => this._end(event))\n\n this._element.classList.add(CLASS_NAME_POINTER_EVENT)\n } else {\n EventHandler.on(this._element, EVENT_TOUCHSTART, event => this._start(event))\n EventHandler.on(this._element, EVENT_TOUCHMOVE, event => this._move(event))\n EventHandler.on(this._element, EVENT_TOUCHEND, event => this._end(event))\n }\n }\n\n _eventIsPointerPenTouch(event) {\n return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH)\n }\n\n // Static\n static isSupported() {\n return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0\n }\n}\n\nexport default Swipe\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap carousel.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport Manipulator from './dom/manipulator.js'\nimport SelectorEngine from './dom/selector-engine.js'\nimport {\n defineJQueryPlugin,\n getNextActiveElement,\n isRTL,\n isVisible,\n reflow,\n triggerTransitionEnd\n} from './util/index.js'\nimport Swipe from './util/swipe.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'carousel'\nconst DATA_KEY = 'bs.carousel'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\n\nconst ARROW_LEFT_KEY = 'ArrowLeft'\nconst ARROW_RIGHT_KEY = 'ArrowRight'\nconst TOUCHEVENT_COMPAT_WAIT = 500 // Time for mouse compat events to fire after touch\n\nconst ORDER_NEXT = 'next'\nconst ORDER_PREV = 'prev'\nconst DIRECTION_LEFT = 'left'\nconst DIRECTION_RIGHT = 'right'\n\nconst EVENT_SLIDE = `slide${EVENT_KEY}`\nconst EVENT_SLID = `slid${EVENT_KEY}`\nconst EVENT_KEYDOWN = `keydown${EVENT_KEY}`\nconst EVENT_MOUSEENTER = `mouseenter${EVENT_KEY}`\nconst EVENT_MOUSELEAVE = `mouseleave${EVENT_KEY}`\nconst EVENT_DRAG_START = `dragstart${EVENT_KEY}`\nconst EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_CAROUSEL = 'carousel'\nconst CLASS_NAME_ACTIVE = 'active'\nconst CLASS_NAME_SLIDE = 'slide'\nconst CLASS_NAME_END = 'carousel-item-end'\nconst CLASS_NAME_START = 'carousel-item-start'\nconst CLASS_NAME_NEXT = 'carousel-item-next'\nconst CLASS_NAME_PREV = 'carousel-item-prev'\n\nconst SELECTOR_ACTIVE = '.active'\nconst SELECTOR_ITEM = '.carousel-item'\nconst SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM\nconst SELECTOR_ITEM_IMG = '.carousel-item img'\nconst SELECTOR_INDICATORS = '.carousel-indicators'\nconst SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]'\nconst SELECTOR_DATA_RIDE = '[data-bs-ride=\"carousel\"]'\n\nconst KEY_TO_DIRECTION = {\n [ARROW_LEFT_KEY]: DIRECTION_RIGHT,\n [ARROW_RIGHT_KEY]: DIRECTION_LEFT\n}\n\nconst Default = {\n interval: 5000,\n keyboard: true,\n pause: 'hover',\n ride: false,\n touch: true,\n wrap: true\n}\n\nconst DefaultType = {\n interval: '(number|boolean)', // TODO:v6 remove boolean support\n keyboard: 'boolean',\n pause: '(string|boolean)',\n ride: '(boolean|string)',\n touch: 'boolean',\n wrap: 'boolean'\n}\n\n/**\n * Class definition\n */\n\nclass Carousel extends BaseComponent {\n constructor(element, config) {\n super(element, config)\n\n this._interval = null\n this._activeElement = null\n this._isSliding = false\n this.touchTimeout = null\n this._swipeHelper = null\n\n this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element)\n this._addEventListeners()\n\n if (this._config.ride === CLASS_NAME_CAROUSEL) {\n this.cycle()\n }\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n next() {\n this._slide(ORDER_NEXT)\n }\n\n nextWhenVisible() {\n // FIXME TODO use `document.visibilityState`\n // Don't call next when the page isn't visible\n // or the carousel or its parent isn't visible\n if (!document.hidden && isVisible(this._element)) {\n this.next()\n }\n }\n\n prev() {\n this._slide(ORDER_PREV)\n }\n\n pause() {\n if (this._isSliding) {\n triggerTransitionEnd(this._element)\n }\n\n this._clearInterval()\n }\n\n cycle() {\n this._clearInterval()\n this._updateInterval()\n\n this._interval = setInterval(() => this.nextWhenVisible(), this._config.interval)\n }\n\n _maybeEnableCycle() {\n if (!this._config.ride) {\n return\n }\n\n if (this._isSliding) {\n EventHandler.one(this._element, EVENT_SLID, () => this.cycle())\n return\n }\n\n this.cycle()\n }\n\n to(index) {\n const items = this._getItems()\n if (index > items.length - 1 || index < 0) {\n return\n }\n\n if (this._isSliding) {\n EventHandler.one(this._element, EVENT_SLID, () => this.to(index))\n return\n }\n\n const activeIndex = this._getItemIndex(this._getActive())\n if (activeIndex === index) {\n return\n }\n\n const order = index > activeIndex ? ORDER_NEXT : ORDER_PREV\n\n this._slide(order, items[index])\n }\n\n dispose() {\n if (this._swipeHelper) {\n this._swipeHelper.dispose()\n }\n\n super.dispose()\n }\n\n // Private\n _configAfterMerge(config) {\n config.defaultInterval = config.interval\n return config\n }\n\n _addEventListeners() {\n if (this._config.keyboard) {\n EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event))\n }\n\n if (this._config.pause === 'hover') {\n EventHandler.on(this._element, EVENT_MOUSEENTER, () => this.pause())\n EventHandler.on(this._element, EVENT_MOUSELEAVE, () => this._maybeEnableCycle())\n }\n\n if (this._config.touch && Swipe.isSupported()) {\n this._addTouchEventListeners()\n }\n }\n\n _addTouchEventListeners() {\n for (const img of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) {\n EventHandler.on(img, EVENT_DRAG_START, event => event.preventDefault())\n }\n\n const endCallBack = () => {\n if (this._config.pause !== 'hover') {\n return\n }\n\n // If it's a touch-enabled device, mouseenter/leave are fired as\n // part of the mouse compatibility events on first tap - the carousel\n // would stop cycling until user tapped out of it;\n // here, we listen for touchend, explicitly pause the carousel\n // (as if it's the second time we tap on it, mouseenter compat event\n // is NOT fired) and after a timeout (to allow for mouse compatibility\n // events to fire) we explicitly restart cycling\n\n this.pause()\n if (this.touchTimeout) {\n clearTimeout(this.touchTimeout)\n }\n\n this.touchTimeout = setTimeout(() => this._maybeEnableCycle(), TOUCHEVENT_COMPAT_WAIT + this._config.interval)\n }\n\n const swipeConfig = {\n leftCallback: () => this._slide(this._directionToOrder(DIRECTION_LEFT)),\n rightCallback: () => this._slide(this._directionToOrder(DIRECTION_RIGHT)),\n endCallback: endCallBack\n }\n\n this._swipeHelper = new Swipe(this._element, swipeConfig)\n }\n\n _keydown(event) {\n if (/input|textarea/i.test(event.target.tagName)) {\n return\n }\n\n const direction = KEY_TO_DIRECTION[event.key]\n if (direction) {\n event.preventDefault()\n this._slide(this._directionToOrder(direction))\n }\n }\n\n _getItemIndex(element) {\n return this._getItems().indexOf(element)\n }\n\n _setActiveIndicatorElement(index) {\n if (!this._indicatorsElement) {\n return\n }\n\n const activeIndicator = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement)\n\n activeIndicator.classList.remove(CLASS_NAME_ACTIVE)\n activeIndicator.removeAttribute('aria-current')\n\n const newActiveIndicator = SelectorEngine.findOne(`[data-bs-slide-to=\"${index}\"]`, this._indicatorsElement)\n\n if (newActiveIndicator) {\n newActiveIndicator.classList.add(CLASS_NAME_ACTIVE)\n newActiveIndicator.setAttribute('aria-current', 'true')\n }\n }\n\n _updateInterval() {\n const element = this._activeElement || this._getActive()\n\n if (!element) {\n return\n }\n\n const elementInterval = Number.parseInt(element.getAttribute('data-bs-interval'), 10)\n\n this._config.interval = elementInterval || this._config.defaultInterval\n }\n\n _slide(order, element = null) {\n if (this._isSliding) {\n return\n }\n\n const activeElement = this._getActive()\n const isNext = order === ORDER_NEXT\n const nextElement = element || getNextActiveElement(this._getItems(), activeElement, isNext, this._config.wrap)\n\n if (nextElement === activeElement) {\n return\n }\n\n const nextElementIndex = this._getItemIndex(nextElement)\n\n const triggerEvent = eventName => {\n return EventHandler.trigger(this._element, eventName, {\n relatedTarget: nextElement,\n direction: this._orderToDirection(order),\n from: this._getItemIndex(activeElement),\n to: nextElementIndex\n })\n }\n\n const slideEvent = triggerEvent(EVENT_SLIDE)\n\n if (slideEvent.defaultPrevented) {\n return\n }\n\n if (!activeElement || !nextElement) {\n // Some weirdness is happening, so we bail\n // TODO: change tests that use empty divs to avoid this check\n return\n }\n\n const isCycling = Boolean(this._interval)\n this.pause()\n\n this._isSliding = true\n\n this._setActiveIndicatorElement(nextElementIndex)\n this._activeElement = nextElement\n\n const directionalClassName = isNext ? CLASS_NAME_START : CLASS_NAME_END\n const orderClassName = isNext ? CLASS_NAME_NEXT : CLASS_NAME_PREV\n\n nextElement.classList.add(orderClassName)\n\n reflow(nextElement)\n\n activeElement.classList.add(directionalClassName)\n nextElement.classList.add(directionalClassName)\n\n const completeCallBack = () => {\n nextElement.classList.remove(directionalClassName, orderClassName)\n nextElement.classList.add(CLASS_NAME_ACTIVE)\n\n activeElement.classList.remove(CLASS_NAME_ACTIVE, orderClassName, directionalClassName)\n\n this._isSliding = false\n\n triggerEvent(EVENT_SLID)\n }\n\n this._queueCallback(completeCallBack, activeElement, this._isAnimated())\n\n if (isCycling) {\n this.cycle()\n }\n }\n\n _isAnimated() {\n return this._element.classList.contains(CLASS_NAME_SLIDE)\n }\n\n _getActive() {\n return SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element)\n }\n\n _getItems() {\n return SelectorEngine.find(SELECTOR_ITEM, this._element)\n }\n\n _clearInterval() {\n if (this._interval) {\n clearInterval(this._interval)\n this._interval = null\n }\n }\n\n _directionToOrder(direction) {\n if (isRTL()) {\n return direction === DIRECTION_LEFT ? ORDER_PREV : ORDER_NEXT\n }\n\n return direction === DIRECTION_LEFT ? ORDER_NEXT : ORDER_PREV\n }\n\n _orderToDirection(order) {\n if (isRTL()) {\n return order === ORDER_PREV ? DIRECTION_LEFT : DIRECTION_RIGHT\n }\n\n return order === ORDER_PREV ? DIRECTION_RIGHT : DIRECTION_LEFT\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Carousel.getOrCreateInstance(this, config)\n\n if (typeof config === 'number') {\n data.to(config)\n return\n }\n\n if (typeof config === 'string') {\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config]()\n }\n })\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_SLIDE, function (event) {\n const target = SelectorEngine.getElementFromSelector(this)\n\n if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) {\n return\n }\n\n event.preventDefault()\n\n const carousel = Carousel.getOrCreateInstance(target)\n const slideIndex = this.getAttribute('data-bs-slide-to')\n\n if (slideIndex) {\n carousel.to(slideIndex)\n carousel._maybeEnableCycle()\n return\n }\n\n if (Manipulator.getDataAttribute(this, 'slide') === 'next') {\n carousel.next()\n carousel._maybeEnableCycle()\n return\n }\n\n carousel.prev()\n carousel._maybeEnableCycle()\n})\n\nEventHandler.on(window, EVENT_LOAD_DATA_API, () => {\n const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE)\n\n for (const carousel of carousels) {\n Carousel.getOrCreateInstance(carousel)\n }\n})\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Carousel)\n\nexport default Carousel\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap collapse.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport SelectorEngine from './dom/selector-engine.js'\nimport {\n defineJQueryPlugin,\n getElement,\n reflow\n} from './util/index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'collapse'\nconst DATA_KEY = 'bs.collapse'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\n\nconst EVENT_SHOW = `show${EVENT_KEY}`\nconst EVENT_SHOWN = `shown${EVENT_KEY}`\nconst EVENT_HIDE = `hide${EVENT_KEY}`\nconst EVENT_HIDDEN = `hidden${EVENT_KEY}`\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_SHOW = 'show'\nconst CLASS_NAME_COLLAPSE = 'collapse'\nconst CLASS_NAME_COLLAPSING = 'collapsing'\nconst CLASS_NAME_COLLAPSED = 'collapsed'\nconst CLASS_NAME_DEEPER_CHILDREN = `:scope .${CLASS_NAME_COLLAPSE} .${CLASS_NAME_COLLAPSE}`\nconst CLASS_NAME_HORIZONTAL = 'collapse-horizontal'\n\nconst WIDTH = 'width'\nconst HEIGHT = 'height'\n\nconst SELECTOR_ACTIVES = '.collapse.show, .collapse.collapsing'\nconst SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"collapse\"]'\n\nconst Default = {\n parent: null,\n toggle: true\n}\n\nconst DefaultType = {\n parent: '(null|element)',\n toggle: 'boolean'\n}\n\n/**\n * Class definition\n */\n\nclass Collapse extends BaseComponent {\n constructor(element, config) {\n super(element, config)\n\n this._isTransitioning = false\n this._triggerArray = []\n\n const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE)\n\n for (const elem of toggleList) {\n const selector = SelectorEngine.getSelectorFromElement(elem)\n const filterElement = SelectorEngine.find(selector)\n .filter(foundElement => foundElement === this._element)\n\n if (selector !== null && filterElement.length) {\n this._triggerArray.push(elem)\n }\n }\n\n this._initializeChildren()\n\n if (!this._config.parent) {\n this._addAriaAndCollapsedClass(this._triggerArray, this._isShown())\n }\n\n if (this._config.toggle) {\n this.toggle()\n }\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n toggle() {\n if (this._isShown()) {\n this.hide()\n } else {\n this.show()\n }\n }\n\n show() {\n if (this._isTransitioning || this._isShown()) {\n return\n }\n\n let activeChildren = []\n\n // find active children\n if (this._config.parent) {\n activeChildren = this._getFirstLevelChildren(SELECTOR_ACTIVES)\n .filter(element => element !== this._element)\n .map(element => Collapse.getOrCreateInstance(element, { toggle: false }))\n }\n\n if (activeChildren.length && activeChildren[0]._isTransitioning) {\n return\n }\n\n const startEvent = EventHandler.trigger(this._element, EVENT_SHOW)\n if (startEvent.defaultPrevented) {\n return\n }\n\n for (const activeInstance of activeChildren) {\n activeInstance.hide()\n }\n\n const dimension = this._getDimension()\n\n this._element.classList.remove(CLASS_NAME_COLLAPSE)\n this._element.classList.add(CLASS_NAME_COLLAPSING)\n\n this._element.style[dimension] = 0\n\n this._addAriaAndCollapsedClass(this._triggerArray, true)\n this._isTransitioning = true\n\n const complete = () => {\n this._isTransitioning = false\n\n this._element.classList.remove(CLASS_NAME_COLLAPSING)\n this._element.classList.add(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW)\n\n this._element.style[dimension] = ''\n\n EventHandler.trigger(this._element, EVENT_SHOWN)\n }\n\n const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1)\n const scrollSize = `scroll${capitalizedDimension}`\n\n this._queueCallback(complete, this._element, true)\n this._element.style[dimension] = `${this._element[scrollSize]}px`\n }\n\n hide() {\n if (this._isTransitioning || !this._isShown()) {\n return\n }\n\n const startEvent = EventHandler.trigger(this._element, EVENT_HIDE)\n if (startEvent.defaultPrevented) {\n return\n }\n\n const dimension = this._getDimension()\n\n this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`\n\n reflow(this._element)\n\n this._element.classList.add(CLASS_NAME_COLLAPSING)\n this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW)\n\n for (const trigger of this._triggerArray) {\n const element = SelectorEngine.getElementFromSelector(trigger)\n\n if (element && !this._isShown(element)) {\n this._addAriaAndCollapsedClass([trigger], false)\n }\n }\n\n this._isTransitioning = true\n\n const complete = () => {\n this._isTransitioning = false\n this._element.classList.remove(CLASS_NAME_COLLAPSING)\n this._element.classList.add(CLASS_NAME_COLLAPSE)\n EventHandler.trigger(this._element, EVENT_HIDDEN)\n }\n\n this._element.style[dimension] = ''\n\n this._queueCallback(complete, this._element, true)\n }\n\n _isShown(element = this._element) {\n return element.classList.contains(CLASS_NAME_SHOW)\n }\n\n // Private\n _configAfterMerge(config) {\n config.toggle = Boolean(config.toggle) // Coerce string values\n config.parent = getElement(config.parent)\n return config\n }\n\n _getDimension() {\n return this._element.classList.contains(CLASS_NAME_HORIZONTAL) ? WIDTH : HEIGHT\n }\n\n _initializeChildren() {\n if (!this._config.parent) {\n return\n }\n\n const children = this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE)\n\n for (const element of children) {\n const selected = SelectorEngine.getElementFromSelector(element)\n\n if (selected) {\n this._addAriaAndCollapsedClass([element], this._isShown(selected))\n }\n }\n }\n\n _getFirstLevelChildren(selector) {\n const children = SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN, this._config.parent)\n // remove children if greater depth\n return SelectorEngine.find(selector, this._config.parent).filter(element => !children.includes(element))\n }\n\n _addAriaAndCollapsedClass(triggerArray, isOpen) {\n if (!triggerArray.length) {\n return\n }\n\n for (const element of triggerArray) {\n element.classList.toggle(CLASS_NAME_COLLAPSED, !isOpen)\n element.setAttribute('aria-expanded', isOpen)\n }\n }\n\n // Static\n static jQueryInterface(config) {\n const _config = {}\n if (typeof config === 'string' && /show|hide/.test(config)) {\n _config.toggle = false\n }\n\n return this.each(function () {\n const data = Collapse.getOrCreateInstance(this, _config)\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config]()\n }\n })\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n // preventDefault only for elements (which change the URL) not inside the collapsible element\n if (event.target.tagName === 'A' || (event.delegateTarget && event.delegateTarget.tagName === 'A')) {\n event.preventDefault()\n }\n\n for (const element of SelectorEngine.getMultipleElementsFromSelector(this)) {\n Collapse.getOrCreateInstance(element, { toggle: false }).toggle()\n }\n})\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Collapse)\n\nexport default Collapse\n","export var top = 'top';\nexport var bottom = 'bottom';\nexport var right = 'right';\nexport var left = 'left';\nexport var auto = 'auto';\nexport var basePlacements = [top, bottom, right, left];\nexport var start = 'start';\nexport var end = 'end';\nexport var clippingParents = 'clippingParents';\nexport var viewport = 'viewport';\nexport var popper = 'popper';\nexport var reference = 'reference';\nexport var variationPlacements = /*#__PURE__*/basePlacements.reduce(function (acc, placement) {\n return acc.concat([placement + \"-\" + start, placement + \"-\" + end]);\n}, []);\nexport var placements = /*#__PURE__*/[].concat(basePlacements, [auto]).reduce(function (acc, placement) {\n return acc.concat([placement, placement + \"-\" + start, placement + \"-\" + end]);\n}, []); // modifiers that need to read the DOM\n\nexport var beforeRead = 'beforeRead';\nexport var read = 'read';\nexport var afterRead = 'afterRead'; // pure-logic modifiers\n\nexport var beforeMain = 'beforeMain';\nexport var main = 'main';\nexport var afterMain = 'afterMain'; // modifier with the purpose to write to the DOM (or write into a framework state)\n\nexport var beforeWrite = 'beforeWrite';\nexport var write = 'write';\nexport var afterWrite = 'afterWrite';\nexport var modifierPhases = [beforeRead, read, afterRead, beforeMain, main, afterMain, beforeWrite, write, afterWrite];","export default function getNodeName(element) {\n return element ? (element.nodeName || '').toLowerCase() : null;\n}","export default function getWindow(node) {\n if (node == null) {\n return window;\n }\n\n if (node.toString() !== '[object Window]') {\n var ownerDocument = node.ownerDocument;\n return ownerDocument ? ownerDocument.defaultView || window : window;\n }\n\n return node;\n}","import getWindow from \"./getWindow.js\";\n\nfunction isElement(node) {\n var OwnElement = getWindow(node).Element;\n return node instanceof OwnElement || node instanceof Element;\n}\n\nfunction isHTMLElement(node) {\n var OwnElement = getWindow(node).HTMLElement;\n return node instanceof OwnElement || node instanceof HTMLElement;\n}\n\nfunction isShadowRoot(node) {\n // IE 11 has no ShadowRoot\n if (typeof ShadowRoot === 'undefined') {\n return false;\n }\n\n var OwnElement = getWindow(node).ShadowRoot;\n return node instanceof OwnElement || node instanceof ShadowRoot;\n}\n\nexport { isElement, isHTMLElement, isShadowRoot };","import getNodeName from \"../dom-utils/getNodeName.js\";\nimport { isHTMLElement } from \"../dom-utils/instanceOf.js\"; // This modifier takes the styles prepared by the `computeStyles` modifier\n// and applies them to the HTMLElements such as popper and arrow\n\nfunction applyStyles(_ref) {\n var state = _ref.state;\n Object.keys(state.elements).forEach(function (name) {\n var style = state.styles[name] || {};\n var attributes = state.attributes[name] || {};\n var element = state.elements[name]; // arrow is optional + virtual elements\n\n if (!isHTMLElement(element) || !getNodeName(element)) {\n return;\n } // Flow doesn't support to extend this property, but it's the most\n // effective way to apply styles to an HTMLElement\n // $FlowFixMe[cannot-write]\n\n\n Object.assign(element.style, style);\n Object.keys(attributes).forEach(function (name) {\n var value = attributes[name];\n\n if (value === false) {\n element.removeAttribute(name);\n } else {\n element.setAttribute(name, value === true ? '' : value);\n }\n });\n });\n}\n\nfunction effect(_ref2) {\n var state = _ref2.state;\n var initialStyles = {\n popper: {\n position: state.options.strategy,\n left: '0',\n top: '0',\n margin: '0'\n },\n arrow: {\n position: 'absolute'\n },\n reference: {}\n };\n Object.assign(state.elements.popper.style, initialStyles.popper);\n state.styles = initialStyles;\n\n if (state.elements.arrow) {\n Object.assign(state.elements.arrow.style, initialStyles.arrow);\n }\n\n return function () {\n Object.keys(state.elements).forEach(function (name) {\n var element = state.elements[name];\n var attributes = state.attributes[name] || {};\n var styleProperties = Object.keys(state.styles.hasOwnProperty(name) ? state.styles[name] : initialStyles[name]); // Set all values to an empty string to unset them\n\n var style = styleProperties.reduce(function (style, property) {\n style[property] = '';\n return style;\n }, {}); // arrow is optional + virtual elements\n\n if (!isHTMLElement(element) || !getNodeName(element)) {\n return;\n }\n\n Object.assign(element.style, style);\n Object.keys(attributes).forEach(function (attribute) {\n element.removeAttribute(attribute);\n });\n });\n };\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'applyStyles',\n enabled: true,\n phase: 'write',\n fn: applyStyles,\n effect: effect,\n requires: ['computeStyles']\n};","import { auto } from \"../enums.js\";\nexport default function getBasePlacement(placement) {\n return placement.split('-')[0];\n}","export var max = Math.max;\nexport var min = Math.min;\nexport var round = Math.round;","export default function getUAString() {\n var uaData = navigator.userAgentData;\n\n if (uaData != null && uaData.brands && Array.isArray(uaData.brands)) {\n return uaData.brands.map(function (item) {\n return item.brand + \"/\" + item.version;\n }).join(' ');\n }\n\n return navigator.userAgent;\n}","import getUAString from \"../utils/userAgent.js\";\nexport default function isLayoutViewport() {\n return !/^((?!chrome|android).)*safari/i.test(getUAString());\n}","import { isElement, isHTMLElement } from \"./instanceOf.js\";\nimport { round } from \"../utils/math.js\";\nimport getWindow from \"./getWindow.js\";\nimport isLayoutViewport from \"./isLayoutViewport.js\";\nexport default function getBoundingClientRect(element, includeScale, isFixedStrategy) {\n if (includeScale === void 0) {\n includeScale = false;\n }\n\n if (isFixedStrategy === void 0) {\n isFixedStrategy = false;\n }\n\n var clientRect = element.getBoundingClientRect();\n var scaleX = 1;\n var scaleY = 1;\n\n if (includeScale && isHTMLElement(element)) {\n scaleX = element.offsetWidth > 0 ? round(clientRect.width) / element.offsetWidth || 1 : 1;\n scaleY = element.offsetHeight > 0 ? round(clientRect.height) / element.offsetHeight || 1 : 1;\n }\n\n var _ref = isElement(element) ? getWindow(element) : window,\n visualViewport = _ref.visualViewport;\n\n var addVisualOffsets = !isLayoutViewport() && isFixedStrategy;\n var x = (clientRect.left + (addVisualOffsets && visualViewport ? visualViewport.offsetLeft : 0)) / scaleX;\n var y = (clientRect.top + (addVisualOffsets && visualViewport ? visualViewport.offsetTop : 0)) / scaleY;\n var width = clientRect.width / scaleX;\n var height = clientRect.height / scaleY;\n return {\n width: width,\n height: height,\n top: y,\n right: x + width,\n bottom: y + height,\n left: x,\n x: x,\n y: y\n };\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\"; // Returns the layout rect of an element relative to its offsetParent. Layout\n// means it doesn't take into account transforms.\n\nexport default function getLayoutRect(element) {\n var clientRect = getBoundingClientRect(element); // Use the clientRect sizes if it's not been transformed.\n // Fixes https://github.com/popperjs/popper-core/issues/1223\n\n var width = element.offsetWidth;\n var height = element.offsetHeight;\n\n if (Math.abs(clientRect.width - width) <= 1) {\n width = clientRect.width;\n }\n\n if (Math.abs(clientRect.height - height) <= 1) {\n height = clientRect.height;\n }\n\n return {\n x: element.offsetLeft,\n y: element.offsetTop,\n width: width,\n height: height\n };\n}","import { isShadowRoot } from \"./instanceOf.js\";\nexport default function contains(parent, child) {\n var rootNode = child.getRootNode && child.getRootNode(); // First, attempt with faster native method\n\n if (parent.contains(child)) {\n return true;\n } // then fallback to custom implementation with Shadow DOM support\n else if (rootNode && isShadowRoot(rootNode)) {\n var next = child;\n\n do {\n if (next && parent.isSameNode(next)) {\n return true;\n } // $FlowFixMe[prop-missing]: need a better way to handle this...\n\n\n next = next.parentNode || next.host;\n } while (next);\n } // Give up, the result is false\n\n\n return false;\n}","import getWindow from \"./getWindow.js\";\nexport default function getComputedStyle(element) {\n return getWindow(element).getComputedStyle(element);\n}","import getNodeName from \"./getNodeName.js\";\nexport default function isTableElement(element) {\n return ['table', 'td', 'th'].indexOf(getNodeName(element)) >= 0;\n}","import { isElement } from \"./instanceOf.js\";\nexport default function getDocumentElement(element) {\n // $FlowFixMe[incompatible-return]: assume body is always available\n return ((isElement(element) ? element.ownerDocument : // $FlowFixMe[prop-missing]\n element.document) || window.document).documentElement;\n}","import getNodeName from \"./getNodeName.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport { isShadowRoot } from \"./instanceOf.js\";\nexport default function getParentNode(element) {\n if (getNodeName(element) === 'html') {\n return element;\n }\n\n return (// this is a quicker (but less type safe) way to save quite some bytes from the bundle\n // $FlowFixMe[incompatible-return]\n // $FlowFixMe[prop-missing]\n element.assignedSlot || // step into the shadow DOM of the parent of a slotted node\n element.parentNode || ( // DOM Element detected\n isShadowRoot(element) ? element.host : null) || // ShadowRoot detected\n // $FlowFixMe[incompatible-call]: HTMLElement is a Node\n getDocumentElement(element) // fallback\n\n );\n}","import getWindow from \"./getWindow.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport { isHTMLElement, isShadowRoot } from \"./instanceOf.js\";\nimport isTableElement from \"./isTableElement.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport getUAString from \"../utils/userAgent.js\";\n\nfunction getTrueOffsetParent(element) {\n if (!isHTMLElement(element) || // https://github.com/popperjs/popper-core/issues/837\n getComputedStyle(element).position === 'fixed') {\n return null;\n }\n\n return element.offsetParent;\n} // `.offsetParent` reports `null` for fixed elements, while absolute elements\n// return the containing block\n\n\nfunction getContainingBlock(element) {\n var isFirefox = /firefox/i.test(getUAString());\n var isIE = /Trident/i.test(getUAString());\n\n if (isIE && isHTMLElement(element)) {\n // In IE 9, 10 and 11 fixed elements containing block is always established by the viewport\n var elementCss = getComputedStyle(element);\n\n if (elementCss.position === 'fixed') {\n return null;\n }\n }\n\n var currentNode = getParentNode(element);\n\n if (isShadowRoot(currentNode)) {\n currentNode = currentNode.host;\n }\n\n while (isHTMLElement(currentNode) && ['html', 'body'].indexOf(getNodeName(currentNode)) < 0) {\n var css = getComputedStyle(currentNode); // This is non-exhaustive but covers the most common CSS properties that\n // create a containing block.\n // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block\n\n if (css.transform !== 'none' || css.perspective !== 'none' || css.contain === 'paint' || ['transform', 'perspective'].indexOf(css.willChange) !== -1 || isFirefox && css.willChange === 'filter' || isFirefox && css.filter && css.filter !== 'none') {\n return currentNode;\n } else {\n currentNode = currentNode.parentNode;\n }\n }\n\n return null;\n} // Gets the closest ancestor positioned element. Handles some edge cases,\n// such as table ancestors and cross browser bugs.\n\n\nexport default function getOffsetParent(element) {\n var window = getWindow(element);\n var offsetParent = getTrueOffsetParent(element);\n\n while (offsetParent && isTableElement(offsetParent) && getComputedStyle(offsetParent).position === 'static') {\n offsetParent = getTrueOffsetParent(offsetParent);\n }\n\n if (offsetParent && (getNodeName(offsetParent) === 'html' || getNodeName(offsetParent) === 'body' && getComputedStyle(offsetParent).position === 'static')) {\n return window;\n }\n\n return offsetParent || getContainingBlock(element) || window;\n}","export default function getMainAxisFromPlacement(placement) {\n return ['top', 'bottom'].indexOf(placement) >= 0 ? 'x' : 'y';\n}","import { max as mathMax, min as mathMin } from \"./math.js\";\nexport function within(min, value, max) {\n return mathMax(min, mathMin(value, max));\n}\nexport function withinMaxClamp(min, value, max) {\n var v = within(min, value, max);\n return v > max ? max : v;\n}","import getFreshSideObject from \"./getFreshSideObject.js\";\nexport default function mergePaddingObject(paddingObject) {\n return Object.assign({}, getFreshSideObject(), paddingObject);\n}","export default function getFreshSideObject() {\n return {\n top: 0,\n right: 0,\n bottom: 0,\n left: 0\n };\n}","export default function expandToHashMap(value, keys) {\n return keys.reduce(function (hashMap, key) {\n hashMap[key] = value;\n return hashMap;\n }, {});\n}","import getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getLayoutRect from \"../dom-utils/getLayoutRect.js\";\nimport contains from \"../dom-utils/contains.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport getMainAxisFromPlacement from \"../utils/getMainAxisFromPlacement.js\";\nimport { within } from \"../utils/within.js\";\nimport mergePaddingObject from \"../utils/mergePaddingObject.js\";\nimport expandToHashMap from \"../utils/expandToHashMap.js\";\nimport { left, right, basePlacements, top, bottom } from \"../enums.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar toPaddingObject = function toPaddingObject(padding, state) {\n padding = typeof padding === 'function' ? padding(Object.assign({}, state.rects, {\n placement: state.placement\n })) : padding;\n return mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n};\n\nfunction arrow(_ref) {\n var _state$modifiersData$;\n\n var state = _ref.state,\n name = _ref.name,\n options = _ref.options;\n var arrowElement = state.elements.arrow;\n var popperOffsets = state.modifiersData.popperOffsets;\n var basePlacement = getBasePlacement(state.placement);\n var axis = getMainAxisFromPlacement(basePlacement);\n var isVertical = [left, right].indexOf(basePlacement) >= 0;\n var len = isVertical ? 'height' : 'width';\n\n if (!arrowElement || !popperOffsets) {\n return;\n }\n\n var paddingObject = toPaddingObject(options.padding, state);\n var arrowRect = getLayoutRect(arrowElement);\n var minProp = axis === 'y' ? top : left;\n var maxProp = axis === 'y' ? bottom : right;\n var endDiff = state.rects.reference[len] + state.rects.reference[axis] - popperOffsets[axis] - state.rects.popper[len];\n var startDiff = popperOffsets[axis] - state.rects.reference[axis];\n var arrowOffsetParent = getOffsetParent(arrowElement);\n var clientSize = arrowOffsetParent ? axis === 'y' ? arrowOffsetParent.clientHeight || 0 : arrowOffsetParent.clientWidth || 0 : 0;\n var centerToReference = endDiff / 2 - startDiff / 2; // Make sure the arrow doesn't overflow the popper if the center point is\n // outside of the popper bounds\n\n var min = paddingObject[minProp];\n var max = clientSize - arrowRect[len] - paddingObject[maxProp];\n var center = clientSize / 2 - arrowRect[len] / 2 + centerToReference;\n var offset = within(min, center, max); // Prevents breaking syntax highlighting...\n\n var axisProp = axis;\n state.modifiersData[name] = (_state$modifiersData$ = {}, _state$modifiersData$[axisProp] = offset, _state$modifiersData$.centerOffset = offset - center, _state$modifiersData$);\n}\n\nfunction effect(_ref2) {\n var state = _ref2.state,\n options = _ref2.options;\n var _options$element = options.element,\n arrowElement = _options$element === void 0 ? '[data-popper-arrow]' : _options$element;\n\n if (arrowElement == null) {\n return;\n } // CSS selector\n\n\n if (typeof arrowElement === 'string') {\n arrowElement = state.elements.popper.querySelector(arrowElement);\n\n if (!arrowElement) {\n return;\n }\n }\n\n if (!contains(state.elements.popper, arrowElement)) {\n return;\n }\n\n state.elements.arrow = arrowElement;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'arrow',\n enabled: true,\n phase: 'main',\n fn: arrow,\n effect: effect,\n requires: ['popperOffsets'],\n requiresIfExists: ['preventOverflow']\n};","export default function getVariation(placement) {\n return placement.split('-')[1];\n}","import { top, left, right, bottom, end } from \"../enums.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport getWindow from \"../dom-utils/getWindow.js\";\nimport getDocumentElement from \"../dom-utils/getDocumentElement.js\";\nimport getComputedStyle from \"../dom-utils/getComputedStyle.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getVariation from \"../utils/getVariation.js\";\nimport { round } from \"../utils/math.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar unsetSides = {\n top: 'auto',\n right: 'auto',\n bottom: 'auto',\n left: 'auto'\n}; // Round the offsets to the nearest suitable subpixel based on the DPR.\n// Zooming can change the DPR, but it seems to report a value that will\n// cleanly divide the values into the appropriate subpixels.\n\nfunction roundOffsetsByDPR(_ref, win) {\n var x = _ref.x,\n y = _ref.y;\n var dpr = win.devicePixelRatio || 1;\n return {\n x: round(x * dpr) / dpr || 0,\n y: round(y * dpr) / dpr || 0\n };\n}\n\nexport function mapToStyles(_ref2) {\n var _Object$assign2;\n\n var popper = _ref2.popper,\n popperRect = _ref2.popperRect,\n placement = _ref2.placement,\n variation = _ref2.variation,\n offsets = _ref2.offsets,\n position = _ref2.position,\n gpuAcceleration = _ref2.gpuAcceleration,\n adaptive = _ref2.adaptive,\n roundOffsets = _ref2.roundOffsets,\n isFixed = _ref2.isFixed;\n var _offsets$x = offsets.x,\n x = _offsets$x === void 0 ? 0 : _offsets$x,\n _offsets$y = offsets.y,\n y = _offsets$y === void 0 ? 0 : _offsets$y;\n\n var _ref3 = typeof roundOffsets === 'function' ? roundOffsets({\n x: x,\n y: y\n }) : {\n x: x,\n y: y\n };\n\n x = _ref3.x;\n y = _ref3.y;\n var hasX = offsets.hasOwnProperty('x');\n var hasY = offsets.hasOwnProperty('y');\n var sideX = left;\n var sideY = top;\n var win = window;\n\n if (adaptive) {\n var offsetParent = getOffsetParent(popper);\n var heightProp = 'clientHeight';\n var widthProp = 'clientWidth';\n\n if (offsetParent === getWindow(popper)) {\n offsetParent = getDocumentElement(popper);\n\n if (getComputedStyle(offsetParent).position !== 'static' && position === 'absolute') {\n heightProp = 'scrollHeight';\n widthProp = 'scrollWidth';\n }\n } // $FlowFixMe[incompatible-cast]: force type refinement, we compare offsetParent with window above, but Flow doesn't detect it\n\n\n offsetParent = offsetParent;\n\n if (placement === top || (placement === left || placement === right) && variation === end) {\n sideY = bottom;\n var offsetY = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.height : // $FlowFixMe[prop-missing]\n offsetParent[heightProp];\n y -= offsetY - popperRect.height;\n y *= gpuAcceleration ? 1 : -1;\n }\n\n if (placement === left || (placement === top || placement === bottom) && variation === end) {\n sideX = right;\n var offsetX = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.width : // $FlowFixMe[prop-missing]\n offsetParent[widthProp];\n x -= offsetX - popperRect.width;\n x *= gpuAcceleration ? 1 : -1;\n }\n }\n\n var commonStyles = Object.assign({\n position: position\n }, adaptive && unsetSides);\n\n var _ref4 = roundOffsets === true ? roundOffsetsByDPR({\n x: x,\n y: y\n }, getWindow(popper)) : {\n x: x,\n y: y\n };\n\n x = _ref4.x;\n y = _ref4.y;\n\n if (gpuAcceleration) {\n var _Object$assign;\n\n return Object.assign({}, commonStyles, (_Object$assign = {}, _Object$assign[sideY] = hasY ? '0' : '', _Object$assign[sideX] = hasX ? '0' : '', _Object$assign.transform = (win.devicePixelRatio || 1) <= 1 ? \"translate(\" + x + \"px, \" + y + \"px)\" : \"translate3d(\" + x + \"px, \" + y + \"px, 0)\", _Object$assign));\n }\n\n return Object.assign({}, commonStyles, (_Object$assign2 = {}, _Object$assign2[sideY] = hasY ? y + \"px\" : '', _Object$assign2[sideX] = hasX ? x + \"px\" : '', _Object$assign2.transform = '', _Object$assign2));\n}\n\nfunction computeStyles(_ref5) {\n var state = _ref5.state,\n options = _ref5.options;\n var _options$gpuAccelerat = options.gpuAcceleration,\n gpuAcceleration = _options$gpuAccelerat === void 0 ? true : _options$gpuAccelerat,\n _options$adaptive = options.adaptive,\n adaptive = _options$adaptive === void 0 ? true : _options$adaptive,\n _options$roundOffsets = options.roundOffsets,\n roundOffsets = _options$roundOffsets === void 0 ? true : _options$roundOffsets;\n var commonStyles = {\n placement: getBasePlacement(state.placement),\n variation: getVariation(state.placement),\n popper: state.elements.popper,\n popperRect: state.rects.popper,\n gpuAcceleration: gpuAcceleration,\n isFixed: state.options.strategy === 'fixed'\n };\n\n if (state.modifiersData.popperOffsets != null) {\n state.styles.popper = Object.assign({}, state.styles.popper, mapToStyles(Object.assign({}, commonStyles, {\n offsets: state.modifiersData.popperOffsets,\n position: state.options.strategy,\n adaptive: adaptive,\n roundOffsets: roundOffsets\n })));\n }\n\n if (state.modifiersData.arrow != null) {\n state.styles.arrow = Object.assign({}, state.styles.arrow, mapToStyles(Object.assign({}, commonStyles, {\n offsets: state.modifiersData.arrow,\n position: 'absolute',\n adaptive: false,\n roundOffsets: roundOffsets\n })));\n }\n\n state.attributes.popper = Object.assign({}, state.attributes.popper, {\n 'data-popper-placement': state.placement\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'computeStyles',\n enabled: true,\n phase: 'beforeWrite',\n fn: computeStyles,\n data: {}\n};","import getWindow from \"../dom-utils/getWindow.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar passive = {\n passive: true\n};\n\nfunction effect(_ref) {\n var state = _ref.state,\n instance = _ref.instance,\n options = _ref.options;\n var _options$scroll = options.scroll,\n scroll = _options$scroll === void 0 ? true : _options$scroll,\n _options$resize = options.resize,\n resize = _options$resize === void 0 ? true : _options$resize;\n var window = getWindow(state.elements.popper);\n var scrollParents = [].concat(state.scrollParents.reference, state.scrollParents.popper);\n\n if (scroll) {\n scrollParents.forEach(function (scrollParent) {\n scrollParent.addEventListener('scroll', instance.update, passive);\n });\n }\n\n if (resize) {\n window.addEventListener('resize', instance.update, passive);\n }\n\n return function () {\n if (scroll) {\n scrollParents.forEach(function (scrollParent) {\n scrollParent.removeEventListener('scroll', instance.update, passive);\n });\n }\n\n if (resize) {\n window.removeEventListener('resize', instance.update, passive);\n }\n };\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'eventListeners',\n enabled: true,\n phase: 'write',\n fn: function fn() {},\n effect: effect,\n data: {}\n};","var hash = {\n left: 'right',\n right: 'left',\n bottom: 'top',\n top: 'bottom'\n};\nexport default function getOppositePlacement(placement) {\n return placement.replace(/left|right|bottom|top/g, function (matched) {\n return hash[matched];\n });\n}","var hash = {\n start: 'end',\n end: 'start'\n};\nexport default function getOppositeVariationPlacement(placement) {\n return placement.replace(/start|end/g, function (matched) {\n return hash[matched];\n });\n}","import getWindow from \"./getWindow.js\";\nexport default function getWindowScroll(node) {\n var win = getWindow(node);\n var scrollLeft = win.pageXOffset;\n var scrollTop = win.pageYOffset;\n return {\n scrollLeft: scrollLeft,\n scrollTop: scrollTop\n };\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getWindowScroll from \"./getWindowScroll.js\";\nexport default function getWindowScrollBarX(element) {\n // If has a CSS width greater than the viewport, then this will be\n // incorrect for RTL.\n // Popper 1 is broken in this case and never had a bug report so let's assume\n // it's not an issue. I don't think anyone ever specifies width on \n // anyway.\n // Browsers where the left scrollbar doesn't cause an issue report `0` for\n // this (e.g. Edge 2019, IE11, Safari)\n return getBoundingClientRect(getDocumentElement(element)).left + getWindowScroll(element).scrollLeft;\n}","import getComputedStyle from \"./getComputedStyle.js\";\nexport default function isScrollParent(element) {\n // Firefox wants us to check `-x` and `-y` variations as well\n var _getComputedStyle = getComputedStyle(element),\n overflow = _getComputedStyle.overflow,\n overflowX = _getComputedStyle.overflowX,\n overflowY = _getComputedStyle.overflowY;\n\n return /auto|scroll|overlay|hidden/.test(overflow + overflowY + overflowX);\n}","import getParentNode from \"./getParentNode.js\";\nimport isScrollParent from \"./isScrollParent.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nexport default function getScrollParent(node) {\n if (['html', 'body', '#document'].indexOf(getNodeName(node)) >= 0) {\n // $FlowFixMe[incompatible-return]: assume body is always available\n return node.ownerDocument.body;\n }\n\n if (isHTMLElement(node) && isScrollParent(node)) {\n return node;\n }\n\n return getScrollParent(getParentNode(node));\n}","import getScrollParent from \"./getScrollParent.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport getWindow from \"./getWindow.js\";\nimport isScrollParent from \"./isScrollParent.js\";\n/*\ngiven a DOM element, return the list of all scroll parents, up the list of ancesors\nuntil we get to the top window object. This list is what we attach scroll listeners\nto, because if any of these parent elements scroll, we'll need to re-calculate the\nreference element's position.\n*/\n\nexport default function listScrollParents(element, list) {\n var _element$ownerDocumen;\n\n if (list === void 0) {\n list = [];\n }\n\n var scrollParent = getScrollParent(element);\n var isBody = scrollParent === ((_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body);\n var win = getWindow(scrollParent);\n var target = isBody ? [win].concat(win.visualViewport || [], isScrollParent(scrollParent) ? scrollParent : []) : scrollParent;\n var updatedList = list.concat(target);\n return isBody ? updatedList : // $FlowFixMe[incompatible-call]: isBody tells us target will be an HTMLElement here\n updatedList.concat(listScrollParents(getParentNode(target)));\n}","export default function rectToClientRect(rect) {\n return Object.assign({}, rect, {\n left: rect.x,\n top: rect.y,\n right: rect.x + rect.width,\n bottom: rect.y + rect.height\n });\n}","import { viewport } from \"../enums.js\";\nimport getViewportRect from \"./getViewportRect.js\";\nimport getDocumentRect from \"./getDocumentRect.js\";\nimport listScrollParents from \"./listScrollParents.js\";\nimport getOffsetParent from \"./getOffsetParent.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport { isElement, isHTMLElement } from \"./instanceOf.js\";\nimport getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport contains from \"./contains.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport rectToClientRect from \"../utils/rectToClientRect.js\";\nimport { max, min } from \"../utils/math.js\";\n\nfunction getInnerBoundingClientRect(element, strategy) {\n var rect = getBoundingClientRect(element, false, strategy === 'fixed');\n rect.top = rect.top + element.clientTop;\n rect.left = rect.left + element.clientLeft;\n rect.bottom = rect.top + element.clientHeight;\n rect.right = rect.left + element.clientWidth;\n rect.width = element.clientWidth;\n rect.height = element.clientHeight;\n rect.x = rect.left;\n rect.y = rect.top;\n return rect;\n}\n\nfunction getClientRectFromMixedType(element, clippingParent, strategy) {\n return clippingParent === viewport ? rectToClientRect(getViewportRect(element, strategy)) : isElement(clippingParent) ? getInnerBoundingClientRect(clippingParent, strategy) : rectToClientRect(getDocumentRect(getDocumentElement(element)));\n} // A \"clipping parent\" is an overflowable container with the characteristic of\n// clipping (or hiding) overflowing elements with a position different from\n// `initial`\n\n\nfunction getClippingParents(element) {\n var clippingParents = listScrollParents(getParentNode(element));\n var canEscapeClipping = ['absolute', 'fixed'].indexOf(getComputedStyle(element).position) >= 0;\n var clipperElement = canEscapeClipping && isHTMLElement(element) ? getOffsetParent(element) : element;\n\n if (!isElement(clipperElement)) {\n return [];\n } // $FlowFixMe[incompatible-return]: https://github.com/facebook/flow/issues/1414\n\n\n return clippingParents.filter(function (clippingParent) {\n return isElement(clippingParent) && contains(clippingParent, clipperElement) && getNodeName(clippingParent) !== 'body';\n });\n} // Gets the maximum area that the element is visible in due to any number of\n// clipping parents\n\n\nexport default function getClippingRect(element, boundary, rootBoundary, strategy) {\n var mainClippingParents = boundary === 'clippingParents' ? getClippingParents(element) : [].concat(boundary);\n var clippingParents = [].concat(mainClippingParents, [rootBoundary]);\n var firstClippingParent = clippingParents[0];\n var clippingRect = clippingParents.reduce(function (accRect, clippingParent) {\n var rect = getClientRectFromMixedType(element, clippingParent, strategy);\n accRect.top = max(rect.top, accRect.top);\n accRect.right = min(rect.right, accRect.right);\n accRect.bottom = min(rect.bottom, accRect.bottom);\n accRect.left = max(rect.left, accRect.left);\n return accRect;\n }, getClientRectFromMixedType(element, firstClippingParent, strategy));\n clippingRect.width = clippingRect.right - clippingRect.left;\n clippingRect.height = clippingRect.bottom - clippingRect.top;\n clippingRect.x = clippingRect.left;\n clippingRect.y = clippingRect.top;\n return clippingRect;\n}","import getWindow from \"./getWindow.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport isLayoutViewport from \"./isLayoutViewport.js\";\nexport default function getViewportRect(element, strategy) {\n var win = getWindow(element);\n var html = getDocumentElement(element);\n var visualViewport = win.visualViewport;\n var width = html.clientWidth;\n var height = html.clientHeight;\n var x = 0;\n var y = 0;\n\n if (visualViewport) {\n width = visualViewport.width;\n height = visualViewport.height;\n var layoutViewport = isLayoutViewport();\n\n if (layoutViewport || !layoutViewport && strategy === 'fixed') {\n x = visualViewport.offsetLeft;\n y = visualViewport.offsetTop;\n }\n }\n\n return {\n width: width,\n height: height,\n x: x + getWindowScrollBarX(element),\n y: y\n };\n}","import getDocumentElement from \"./getDocumentElement.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport getWindowScroll from \"./getWindowScroll.js\";\nimport { max } from \"../utils/math.js\"; // Gets the entire size of the scrollable document area, even extending outside\n// of the `` and `` rect bounds if horizontally scrollable\n\nexport default function getDocumentRect(element) {\n var _element$ownerDocumen;\n\n var html = getDocumentElement(element);\n var winScroll = getWindowScroll(element);\n var body = (_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body;\n var width = max(html.scrollWidth, html.clientWidth, body ? body.scrollWidth : 0, body ? body.clientWidth : 0);\n var height = max(html.scrollHeight, html.clientHeight, body ? body.scrollHeight : 0, body ? body.clientHeight : 0);\n var x = -winScroll.scrollLeft + getWindowScrollBarX(element);\n var y = -winScroll.scrollTop;\n\n if (getComputedStyle(body || html).direction === 'rtl') {\n x += max(html.clientWidth, body ? body.clientWidth : 0) - width;\n }\n\n return {\n width: width,\n height: height,\n x: x,\n y: y\n };\n}","import getBasePlacement from \"./getBasePlacement.js\";\nimport getVariation from \"./getVariation.js\";\nimport getMainAxisFromPlacement from \"./getMainAxisFromPlacement.js\";\nimport { top, right, bottom, left, start, end } from \"../enums.js\";\nexport default function computeOffsets(_ref) {\n var reference = _ref.reference,\n element = _ref.element,\n placement = _ref.placement;\n var basePlacement = placement ? getBasePlacement(placement) : null;\n var variation = placement ? getVariation(placement) : null;\n var commonX = reference.x + reference.width / 2 - element.width / 2;\n var commonY = reference.y + reference.height / 2 - element.height / 2;\n var offsets;\n\n switch (basePlacement) {\n case top:\n offsets = {\n x: commonX,\n y: reference.y - element.height\n };\n break;\n\n case bottom:\n offsets = {\n x: commonX,\n y: reference.y + reference.height\n };\n break;\n\n case right:\n offsets = {\n x: reference.x + reference.width,\n y: commonY\n };\n break;\n\n case left:\n offsets = {\n x: reference.x - element.width,\n y: commonY\n };\n break;\n\n default:\n offsets = {\n x: reference.x,\n y: reference.y\n };\n }\n\n var mainAxis = basePlacement ? getMainAxisFromPlacement(basePlacement) : null;\n\n if (mainAxis != null) {\n var len = mainAxis === 'y' ? 'height' : 'width';\n\n switch (variation) {\n case start:\n offsets[mainAxis] = offsets[mainAxis] - (reference[len] / 2 - element[len] / 2);\n break;\n\n case end:\n offsets[mainAxis] = offsets[mainAxis] + (reference[len] / 2 - element[len] / 2);\n break;\n\n default:\n }\n }\n\n return offsets;\n}","import getClippingRect from \"../dom-utils/getClippingRect.js\";\nimport getDocumentElement from \"../dom-utils/getDocumentElement.js\";\nimport getBoundingClientRect from \"../dom-utils/getBoundingClientRect.js\";\nimport computeOffsets from \"./computeOffsets.js\";\nimport rectToClientRect from \"./rectToClientRect.js\";\nimport { clippingParents, reference, popper, bottom, top, right, basePlacements, viewport } from \"../enums.js\";\nimport { isElement } from \"../dom-utils/instanceOf.js\";\nimport mergePaddingObject from \"./mergePaddingObject.js\";\nimport expandToHashMap from \"./expandToHashMap.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport default function detectOverflow(state, options) {\n if (options === void 0) {\n options = {};\n }\n\n var _options = options,\n _options$placement = _options.placement,\n placement = _options$placement === void 0 ? state.placement : _options$placement,\n _options$strategy = _options.strategy,\n strategy = _options$strategy === void 0 ? state.strategy : _options$strategy,\n _options$boundary = _options.boundary,\n boundary = _options$boundary === void 0 ? clippingParents : _options$boundary,\n _options$rootBoundary = _options.rootBoundary,\n rootBoundary = _options$rootBoundary === void 0 ? viewport : _options$rootBoundary,\n _options$elementConte = _options.elementContext,\n elementContext = _options$elementConte === void 0 ? popper : _options$elementConte,\n _options$altBoundary = _options.altBoundary,\n altBoundary = _options$altBoundary === void 0 ? false : _options$altBoundary,\n _options$padding = _options.padding,\n padding = _options$padding === void 0 ? 0 : _options$padding;\n var paddingObject = mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n var altContext = elementContext === popper ? reference : popper;\n var popperRect = state.rects.popper;\n var element = state.elements[altBoundary ? altContext : elementContext];\n var clippingClientRect = getClippingRect(isElement(element) ? element : element.contextElement || getDocumentElement(state.elements.popper), boundary, rootBoundary, strategy);\n var referenceClientRect = getBoundingClientRect(state.elements.reference);\n var popperOffsets = computeOffsets({\n reference: referenceClientRect,\n element: popperRect,\n strategy: 'absolute',\n placement: placement\n });\n var popperClientRect = rectToClientRect(Object.assign({}, popperRect, popperOffsets));\n var elementClientRect = elementContext === popper ? popperClientRect : referenceClientRect; // positive = overflowing the clipping rect\n // 0 or negative = within the clipping rect\n\n var overflowOffsets = {\n top: clippingClientRect.top - elementClientRect.top + paddingObject.top,\n bottom: elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom,\n left: clippingClientRect.left - elementClientRect.left + paddingObject.left,\n right: elementClientRect.right - clippingClientRect.right + paddingObject.right\n };\n var offsetData = state.modifiersData.offset; // Offsets can be applied only to the popper element\n\n if (elementContext === popper && offsetData) {\n var offset = offsetData[placement];\n Object.keys(overflowOffsets).forEach(function (key) {\n var multiply = [right, bottom].indexOf(key) >= 0 ? 1 : -1;\n var axis = [top, bottom].indexOf(key) >= 0 ? 'y' : 'x';\n overflowOffsets[key] += offset[axis] * multiply;\n });\n }\n\n return overflowOffsets;\n}","import getVariation from \"./getVariation.js\";\nimport { variationPlacements, basePlacements, placements as allPlacements } from \"../enums.js\";\nimport detectOverflow from \"./detectOverflow.js\";\nimport getBasePlacement from \"./getBasePlacement.js\";\nexport default function computeAutoPlacement(state, options) {\n if (options === void 0) {\n options = {};\n }\n\n var _options = options,\n placement = _options.placement,\n boundary = _options.boundary,\n rootBoundary = _options.rootBoundary,\n padding = _options.padding,\n flipVariations = _options.flipVariations,\n _options$allowedAutoP = _options.allowedAutoPlacements,\n allowedAutoPlacements = _options$allowedAutoP === void 0 ? allPlacements : _options$allowedAutoP;\n var variation = getVariation(placement);\n var placements = variation ? flipVariations ? variationPlacements : variationPlacements.filter(function (placement) {\n return getVariation(placement) === variation;\n }) : basePlacements;\n var allowedPlacements = placements.filter(function (placement) {\n return allowedAutoPlacements.indexOf(placement) >= 0;\n });\n\n if (allowedPlacements.length === 0) {\n allowedPlacements = placements;\n } // $FlowFixMe[incompatible-type]: Flow seems to have problems with two array unions...\n\n\n var overflows = allowedPlacements.reduce(function (acc, placement) {\n acc[placement] = detectOverflow(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding\n })[getBasePlacement(placement)];\n return acc;\n }, {});\n return Object.keys(overflows).sort(function (a, b) {\n return overflows[a] - overflows[b];\n });\n}","import getOppositePlacement from \"../utils/getOppositePlacement.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getOppositeVariationPlacement from \"../utils/getOppositeVariationPlacement.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\nimport computeAutoPlacement from \"../utils/computeAutoPlacement.js\";\nimport { bottom, top, start, right, left, auto } from \"../enums.js\";\nimport getVariation from \"../utils/getVariation.js\"; // eslint-disable-next-line import/no-unused-modules\n\nfunction getExpandedFallbackPlacements(placement) {\n if (getBasePlacement(placement) === auto) {\n return [];\n }\n\n var oppositePlacement = getOppositePlacement(placement);\n return [getOppositeVariationPlacement(placement), oppositePlacement, getOppositeVariationPlacement(oppositePlacement)];\n}\n\nfunction flip(_ref) {\n var state = _ref.state,\n options = _ref.options,\n name = _ref.name;\n\n if (state.modifiersData[name]._skip) {\n return;\n }\n\n var _options$mainAxis = options.mainAxis,\n checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n _options$altAxis = options.altAxis,\n checkAltAxis = _options$altAxis === void 0 ? true : _options$altAxis,\n specifiedFallbackPlacements = options.fallbackPlacements,\n padding = options.padding,\n boundary = options.boundary,\n rootBoundary = options.rootBoundary,\n altBoundary = options.altBoundary,\n _options$flipVariatio = options.flipVariations,\n flipVariations = _options$flipVariatio === void 0 ? true : _options$flipVariatio,\n allowedAutoPlacements = options.allowedAutoPlacements;\n var preferredPlacement = state.options.placement;\n var basePlacement = getBasePlacement(preferredPlacement);\n var isBasePlacement = basePlacement === preferredPlacement;\n var fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipVariations ? [getOppositePlacement(preferredPlacement)] : getExpandedFallbackPlacements(preferredPlacement));\n var placements = [preferredPlacement].concat(fallbackPlacements).reduce(function (acc, placement) {\n return acc.concat(getBasePlacement(placement) === auto ? computeAutoPlacement(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding,\n flipVariations: flipVariations,\n allowedAutoPlacements: allowedAutoPlacements\n }) : placement);\n }, []);\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var checksMap = new Map();\n var makeFallbackChecks = true;\n var firstFittingPlacement = placements[0];\n\n for (var i = 0; i < placements.length; i++) {\n var placement = placements[i];\n\n var _basePlacement = getBasePlacement(placement);\n\n var isStartVariation = getVariation(placement) === start;\n var isVertical = [top, bottom].indexOf(_basePlacement) >= 0;\n var len = isVertical ? 'width' : 'height';\n var overflow = detectOverflow(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n altBoundary: altBoundary,\n padding: padding\n });\n var mainVariationSide = isVertical ? isStartVariation ? right : left : isStartVariation ? bottom : top;\n\n if (referenceRect[len] > popperRect[len]) {\n mainVariationSide = getOppositePlacement(mainVariationSide);\n }\n\n var altVariationSide = getOppositePlacement(mainVariationSide);\n var checks = [];\n\n if (checkMainAxis) {\n checks.push(overflow[_basePlacement] <= 0);\n }\n\n if (checkAltAxis) {\n checks.push(overflow[mainVariationSide] <= 0, overflow[altVariationSide] <= 0);\n }\n\n if (checks.every(function (check) {\n return check;\n })) {\n firstFittingPlacement = placement;\n makeFallbackChecks = false;\n break;\n }\n\n checksMap.set(placement, checks);\n }\n\n if (makeFallbackChecks) {\n // `2` may be desired in some cases – research later\n var numberOfChecks = flipVariations ? 3 : 1;\n\n var _loop = function _loop(_i) {\n var fittingPlacement = placements.find(function (placement) {\n var checks = checksMap.get(placement);\n\n if (checks) {\n return checks.slice(0, _i).every(function (check) {\n return check;\n });\n }\n });\n\n if (fittingPlacement) {\n firstFittingPlacement = fittingPlacement;\n return \"break\";\n }\n };\n\n for (var _i = numberOfChecks; _i > 0; _i--) {\n var _ret = _loop(_i);\n\n if (_ret === \"break\") break;\n }\n }\n\n if (state.placement !== firstFittingPlacement) {\n state.modifiersData[name]._skip = true;\n state.placement = firstFittingPlacement;\n state.reset = true;\n }\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'flip',\n enabled: true,\n phase: 'main',\n fn: flip,\n requiresIfExists: ['offset'],\n data: {\n _skip: false\n }\n};","import { top, bottom, left, right } from \"../enums.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\n\nfunction getSideOffsets(overflow, rect, preventedOffsets) {\n if (preventedOffsets === void 0) {\n preventedOffsets = {\n x: 0,\n y: 0\n };\n }\n\n return {\n top: overflow.top - rect.height - preventedOffsets.y,\n right: overflow.right - rect.width + preventedOffsets.x,\n bottom: overflow.bottom - rect.height + preventedOffsets.y,\n left: overflow.left - rect.width - preventedOffsets.x\n };\n}\n\nfunction isAnySideFullyClipped(overflow) {\n return [top, right, bottom, left].some(function (side) {\n return overflow[side] >= 0;\n });\n}\n\nfunction hide(_ref) {\n var state = _ref.state,\n name = _ref.name;\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var preventedOffsets = state.modifiersData.preventOverflow;\n var referenceOverflow = detectOverflow(state, {\n elementContext: 'reference'\n });\n var popperAltOverflow = detectOverflow(state, {\n altBoundary: true\n });\n var referenceClippingOffsets = getSideOffsets(referenceOverflow, referenceRect);\n var popperEscapeOffsets = getSideOffsets(popperAltOverflow, popperRect, preventedOffsets);\n var isReferenceHidden = isAnySideFullyClipped(referenceClippingOffsets);\n var hasPopperEscaped = isAnySideFullyClipped(popperEscapeOffsets);\n state.modifiersData[name] = {\n referenceClippingOffsets: referenceClippingOffsets,\n popperEscapeOffsets: popperEscapeOffsets,\n isReferenceHidden: isReferenceHidden,\n hasPopperEscaped: hasPopperEscaped\n };\n state.attributes.popper = Object.assign({}, state.attributes.popper, {\n 'data-popper-reference-hidden': isReferenceHidden,\n 'data-popper-escaped': hasPopperEscaped\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'hide',\n enabled: true,\n phase: 'main',\n requiresIfExists: ['preventOverflow'],\n fn: hide\n};","import getBasePlacement from \"../utils/getBasePlacement.js\";\nimport { top, left, right, placements } from \"../enums.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport function distanceAndSkiddingToXY(placement, rects, offset) {\n var basePlacement = getBasePlacement(placement);\n var invertDistance = [left, top].indexOf(basePlacement) >= 0 ? -1 : 1;\n\n var _ref = typeof offset === 'function' ? offset(Object.assign({}, rects, {\n placement: placement\n })) : offset,\n skidding = _ref[0],\n distance = _ref[1];\n\n skidding = skidding || 0;\n distance = (distance || 0) * invertDistance;\n return [left, right].indexOf(basePlacement) >= 0 ? {\n x: distance,\n y: skidding\n } : {\n x: skidding,\n y: distance\n };\n}\n\nfunction offset(_ref2) {\n var state = _ref2.state,\n options = _ref2.options,\n name = _ref2.name;\n var _options$offset = options.offset,\n offset = _options$offset === void 0 ? [0, 0] : _options$offset;\n var data = placements.reduce(function (acc, placement) {\n acc[placement] = distanceAndSkiddingToXY(placement, state.rects, offset);\n return acc;\n }, {});\n var _data$state$placement = data[state.placement],\n x = _data$state$placement.x,\n y = _data$state$placement.y;\n\n if (state.modifiersData.popperOffsets != null) {\n state.modifiersData.popperOffsets.x += x;\n state.modifiersData.popperOffsets.y += y;\n }\n\n state.modifiersData[name] = data;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'offset',\n enabled: true,\n phase: 'main',\n requires: ['popperOffsets'],\n fn: offset\n};","import computeOffsets from \"../utils/computeOffsets.js\";\n\nfunction popperOffsets(_ref) {\n var state = _ref.state,\n name = _ref.name;\n // Offsets are the actual position the popper needs to have to be\n // properly positioned near its reference element\n // This is the most basic placement, and will be adjusted by\n // the modifiers in the next step\n state.modifiersData[name] = computeOffsets({\n reference: state.rects.reference,\n element: state.rects.popper,\n strategy: 'absolute',\n placement: state.placement\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'popperOffsets',\n enabled: true,\n phase: 'read',\n fn: popperOffsets,\n data: {}\n};","import { top, left, right, bottom, start } from \"../enums.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getMainAxisFromPlacement from \"../utils/getMainAxisFromPlacement.js\";\nimport getAltAxis from \"../utils/getAltAxis.js\";\nimport { within, withinMaxClamp } from \"../utils/within.js\";\nimport getLayoutRect from \"../dom-utils/getLayoutRect.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\nimport getVariation from \"../utils/getVariation.js\";\nimport getFreshSideObject from \"../utils/getFreshSideObject.js\";\nimport { min as mathMin, max as mathMax } from \"../utils/math.js\";\n\nfunction preventOverflow(_ref) {\n var state = _ref.state,\n options = _ref.options,\n name = _ref.name;\n var _options$mainAxis = options.mainAxis,\n checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n _options$altAxis = options.altAxis,\n checkAltAxis = _options$altAxis === void 0 ? false : _options$altAxis,\n boundary = options.boundary,\n rootBoundary = options.rootBoundary,\n altBoundary = options.altBoundary,\n padding = options.padding,\n _options$tether = options.tether,\n tether = _options$tether === void 0 ? true : _options$tether,\n _options$tetherOffset = options.tetherOffset,\n tetherOffset = _options$tetherOffset === void 0 ? 0 : _options$tetherOffset;\n var overflow = detectOverflow(state, {\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding,\n altBoundary: altBoundary\n });\n var basePlacement = getBasePlacement(state.placement);\n var variation = getVariation(state.placement);\n var isBasePlacement = !variation;\n var mainAxis = getMainAxisFromPlacement(basePlacement);\n var altAxis = getAltAxis(mainAxis);\n var popperOffsets = state.modifiersData.popperOffsets;\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var tetherOffsetValue = typeof tetherOffset === 'function' ? tetherOffset(Object.assign({}, state.rects, {\n placement: state.placement\n })) : tetherOffset;\n var normalizedTetherOffsetValue = typeof tetherOffsetValue === 'number' ? {\n mainAxis: tetherOffsetValue,\n altAxis: tetherOffsetValue\n } : Object.assign({\n mainAxis: 0,\n altAxis: 0\n }, tetherOffsetValue);\n var offsetModifierState = state.modifiersData.offset ? state.modifiersData.offset[state.placement] : null;\n var data = {\n x: 0,\n y: 0\n };\n\n if (!popperOffsets) {\n return;\n }\n\n if (checkMainAxis) {\n var _offsetModifierState$;\n\n var mainSide = mainAxis === 'y' ? top : left;\n var altSide = mainAxis === 'y' ? bottom : right;\n var len = mainAxis === 'y' ? 'height' : 'width';\n var offset = popperOffsets[mainAxis];\n var min = offset + overflow[mainSide];\n var max = offset - overflow[altSide];\n var additive = tether ? -popperRect[len] / 2 : 0;\n var minLen = variation === start ? referenceRect[len] : popperRect[len];\n var maxLen = variation === start ? -popperRect[len] : -referenceRect[len]; // We need to include the arrow in the calculation so the arrow doesn't go\n // outside the reference bounds\n\n var arrowElement = state.elements.arrow;\n var arrowRect = tether && arrowElement ? getLayoutRect(arrowElement) : {\n width: 0,\n height: 0\n };\n var arrowPaddingObject = state.modifiersData['arrow#persistent'] ? state.modifiersData['arrow#persistent'].padding : getFreshSideObject();\n var arrowPaddingMin = arrowPaddingObject[mainSide];\n var arrowPaddingMax = arrowPaddingObject[altSide]; // If the reference length is smaller than the arrow length, we don't want\n // to include its full size in the calculation. If the reference is small\n // and near the edge of a boundary, the popper can overflow even if the\n // reference is not overflowing as well (e.g. virtual elements with no\n // width or height)\n\n var arrowLen = within(0, referenceRect[len], arrowRect[len]);\n var minOffset = isBasePlacement ? referenceRect[len] / 2 - additive - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis : minLen - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis;\n var maxOffset = isBasePlacement ? -referenceRect[len] / 2 + additive + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis : maxLen + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis;\n var arrowOffsetParent = state.elements.arrow && getOffsetParent(state.elements.arrow);\n var clientOffset = arrowOffsetParent ? mainAxis === 'y' ? arrowOffsetParent.clientTop || 0 : arrowOffsetParent.clientLeft || 0 : 0;\n var offsetModifierValue = (_offsetModifierState$ = offsetModifierState == null ? void 0 : offsetModifierState[mainAxis]) != null ? _offsetModifierState$ : 0;\n var tetherMin = offset + minOffset - offsetModifierValue - clientOffset;\n var tetherMax = offset + maxOffset - offsetModifierValue;\n var preventedOffset = within(tether ? mathMin(min, tetherMin) : min, offset, tether ? mathMax(max, tetherMax) : max);\n popperOffsets[mainAxis] = preventedOffset;\n data[mainAxis] = preventedOffset - offset;\n }\n\n if (checkAltAxis) {\n var _offsetModifierState$2;\n\n var _mainSide = mainAxis === 'x' ? top : left;\n\n var _altSide = mainAxis === 'x' ? bottom : right;\n\n var _offset = popperOffsets[altAxis];\n\n var _len = altAxis === 'y' ? 'height' : 'width';\n\n var _min = _offset + overflow[_mainSide];\n\n var _max = _offset - overflow[_altSide];\n\n var isOriginSide = [top, left].indexOf(basePlacement) !== -1;\n\n var _offsetModifierValue = (_offsetModifierState$2 = offsetModifierState == null ? void 0 : offsetModifierState[altAxis]) != null ? _offsetModifierState$2 : 0;\n\n var _tetherMin = isOriginSide ? _min : _offset - referenceRect[_len] - popperRect[_len] - _offsetModifierValue + normalizedTetherOffsetValue.altAxis;\n\n var _tetherMax = isOriginSide ? _offset + referenceRect[_len] + popperRect[_len] - _offsetModifierValue - normalizedTetherOffsetValue.altAxis : _max;\n\n var _preventedOffset = tether && isOriginSide ? withinMaxClamp(_tetherMin, _offset, _tetherMax) : within(tether ? _tetherMin : _min, _offset, tether ? _tetherMax : _max);\n\n popperOffsets[altAxis] = _preventedOffset;\n data[altAxis] = _preventedOffset - _offset;\n }\n\n state.modifiersData[name] = data;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'preventOverflow',\n enabled: true,\n phase: 'main',\n fn: preventOverflow,\n requiresIfExists: ['offset']\n};","export default function getAltAxis(axis) {\n return axis === 'x' ? 'y' : 'x';\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getNodeScroll from \"./getNodeScroll.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport isScrollParent from \"./isScrollParent.js\";\nimport { round } from \"../utils/math.js\";\n\nfunction isElementScaled(element) {\n var rect = element.getBoundingClientRect();\n var scaleX = round(rect.width) / element.offsetWidth || 1;\n var scaleY = round(rect.height) / element.offsetHeight || 1;\n return scaleX !== 1 || scaleY !== 1;\n} // Returns the composite rect of an element relative to its offsetParent.\n// Composite means it takes into account transforms as well as layout.\n\n\nexport default function getCompositeRect(elementOrVirtualElement, offsetParent, isFixed) {\n if (isFixed === void 0) {\n isFixed = false;\n }\n\n var isOffsetParentAnElement = isHTMLElement(offsetParent);\n var offsetParentIsScaled = isHTMLElement(offsetParent) && isElementScaled(offsetParent);\n var documentElement = getDocumentElement(offsetParent);\n var rect = getBoundingClientRect(elementOrVirtualElement, offsetParentIsScaled, isFixed);\n var scroll = {\n scrollLeft: 0,\n scrollTop: 0\n };\n var offsets = {\n x: 0,\n y: 0\n };\n\n if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) {\n if (getNodeName(offsetParent) !== 'body' || // https://github.com/popperjs/popper-core/issues/1078\n isScrollParent(documentElement)) {\n scroll = getNodeScroll(offsetParent);\n }\n\n if (isHTMLElement(offsetParent)) {\n offsets = getBoundingClientRect(offsetParent, true);\n offsets.x += offsetParent.clientLeft;\n offsets.y += offsetParent.clientTop;\n } else if (documentElement) {\n offsets.x = getWindowScrollBarX(documentElement);\n }\n }\n\n return {\n x: rect.left + scroll.scrollLeft - offsets.x,\n y: rect.top + scroll.scrollTop - offsets.y,\n width: rect.width,\n height: rect.height\n };\n}","import getWindowScroll from \"./getWindowScroll.js\";\nimport getWindow from \"./getWindow.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nimport getHTMLElementScroll from \"./getHTMLElementScroll.js\";\nexport default function getNodeScroll(node) {\n if (node === getWindow(node) || !isHTMLElement(node)) {\n return getWindowScroll(node);\n } else {\n return getHTMLElementScroll(node);\n }\n}","export default function getHTMLElementScroll(element) {\n return {\n scrollLeft: element.scrollLeft,\n scrollTop: element.scrollTop\n };\n}","import { modifierPhases } from \"../enums.js\"; // source: https://stackoverflow.com/questions/49875255\n\nfunction order(modifiers) {\n var map = new Map();\n var visited = new Set();\n var result = [];\n modifiers.forEach(function (modifier) {\n map.set(modifier.name, modifier);\n }); // On visiting object, check for its dependencies and visit them recursively\n\n function sort(modifier) {\n visited.add(modifier.name);\n var requires = [].concat(modifier.requires || [], modifier.requiresIfExists || []);\n requires.forEach(function (dep) {\n if (!visited.has(dep)) {\n var depModifier = map.get(dep);\n\n if (depModifier) {\n sort(depModifier);\n }\n }\n });\n result.push(modifier);\n }\n\n modifiers.forEach(function (modifier) {\n if (!visited.has(modifier.name)) {\n // check for visited object\n sort(modifier);\n }\n });\n return result;\n}\n\nexport default function orderModifiers(modifiers) {\n // order based on dependencies\n var orderedModifiers = order(modifiers); // order based on phase\n\n return modifierPhases.reduce(function (acc, phase) {\n return acc.concat(orderedModifiers.filter(function (modifier) {\n return modifier.phase === phase;\n }));\n }, []);\n}","import getCompositeRect from \"./dom-utils/getCompositeRect.js\";\nimport getLayoutRect from \"./dom-utils/getLayoutRect.js\";\nimport listScrollParents from \"./dom-utils/listScrollParents.js\";\nimport getOffsetParent from \"./dom-utils/getOffsetParent.js\";\nimport orderModifiers from \"./utils/orderModifiers.js\";\nimport debounce from \"./utils/debounce.js\";\nimport mergeByName from \"./utils/mergeByName.js\";\nimport detectOverflow from \"./utils/detectOverflow.js\";\nimport { isElement } from \"./dom-utils/instanceOf.js\";\nvar DEFAULT_OPTIONS = {\n placement: 'bottom',\n modifiers: [],\n strategy: 'absolute'\n};\n\nfunction areValidElements() {\n for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {\n args[_key] = arguments[_key];\n }\n\n return !args.some(function (element) {\n return !(element && typeof element.getBoundingClientRect === 'function');\n });\n}\n\nexport function popperGenerator(generatorOptions) {\n if (generatorOptions === void 0) {\n generatorOptions = {};\n }\n\n var _generatorOptions = generatorOptions,\n _generatorOptions$def = _generatorOptions.defaultModifiers,\n defaultModifiers = _generatorOptions$def === void 0 ? [] : _generatorOptions$def,\n _generatorOptions$def2 = _generatorOptions.defaultOptions,\n defaultOptions = _generatorOptions$def2 === void 0 ? DEFAULT_OPTIONS : _generatorOptions$def2;\n return function createPopper(reference, popper, options) {\n if (options === void 0) {\n options = defaultOptions;\n }\n\n var state = {\n placement: 'bottom',\n orderedModifiers: [],\n options: Object.assign({}, DEFAULT_OPTIONS, defaultOptions),\n modifiersData: {},\n elements: {\n reference: reference,\n popper: popper\n },\n attributes: {},\n styles: {}\n };\n var effectCleanupFns = [];\n var isDestroyed = false;\n var instance = {\n state: state,\n setOptions: function setOptions(setOptionsAction) {\n var options = typeof setOptionsAction === 'function' ? setOptionsAction(state.options) : setOptionsAction;\n cleanupModifierEffects();\n state.options = Object.assign({}, defaultOptions, state.options, options);\n state.scrollParents = {\n reference: isElement(reference) ? listScrollParents(reference) : reference.contextElement ? listScrollParents(reference.contextElement) : [],\n popper: listScrollParents(popper)\n }; // Orders the modifiers based on their dependencies and `phase`\n // properties\n\n var orderedModifiers = orderModifiers(mergeByName([].concat(defaultModifiers, state.options.modifiers))); // Strip out disabled modifiers\n\n state.orderedModifiers = orderedModifiers.filter(function (m) {\n return m.enabled;\n });\n runModifierEffects();\n return instance.update();\n },\n // Sync update – it will always be executed, even if not necessary. This\n // is useful for low frequency updates where sync behavior simplifies the\n // logic.\n // For high frequency updates (e.g. `resize` and `scroll` events), always\n // prefer the async Popper#update method\n forceUpdate: function forceUpdate() {\n if (isDestroyed) {\n return;\n }\n\n var _state$elements = state.elements,\n reference = _state$elements.reference,\n popper = _state$elements.popper; // Don't proceed if `reference` or `popper` are not valid elements\n // anymore\n\n if (!areValidElements(reference, popper)) {\n return;\n } // Store the reference and popper rects to be read by modifiers\n\n\n state.rects = {\n reference: getCompositeRect(reference, getOffsetParent(popper), state.options.strategy === 'fixed'),\n popper: getLayoutRect(popper)\n }; // Modifiers have the ability to reset the current update cycle. The\n // most common use case for this is the `flip` modifier changing the\n // placement, which then needs to re-run all the modifiers, because the\n // logic was previously ran for the previous placement and is therefore\n // stale/incorrect\n\n state.reset = false;\n state.placement = state.options.placement; // On each update cycle, the `modifiersData` property for each modifier\n // is filled with the initial data specified by the modifier. This means\n // it doesn't persist and is fresh on each update.\n // To ensure persistent data, use `${name}#persistent`\n\n state.orderedModifiers.forEach(function (modifier) {\n return state.modifiersData[modifier.name] = Object.assign({}, modifier.data);\n });\n\n for (var index = 0; index < state.orderedModifiers.length; index++) {\n if (state.reset === true) {\n state.reset = false;\n index = -1;\n continue;\n }\n\n var _state$orderedModifie = state.orderedModifiers[index],\n fn = _state$orderedModifie.fn,\n _state$orderedModifie2 = _state$orderedModifie.options,\n _options = _state$orderedModifie2 === void 0 ? {} : _state$orderedModifie2,\n name = _state$orderedModifie.name;\n\n if (typeof fn === 'function') {\n state = fn({\n state: state,\n options: _options,\n name: name,\n instance: instance\n }) || state;\n }\n }\n },\n // Async and optimistically optimized update – it will not be executed if\n // not necessary (debounced to run at most once-per-tick)\n update: debounce(function () {\n return new Promise(function (resolve) {\n instance.forceUpdate();\n resolve(state);\n });\n }),\n destroy: function destroy() {\n cleanupModifierEffects();\n isDestroyed = true;\n }\n };\n\n if (!areValidElements(reference, popper)) {\n return instance;\n }\n\n instance.setOptions(options).then(function (state) {\n if (!isDestroyed && options.onFirstUpdate) {\n options.onFirstUpdate(state);\n }\n }); // Modifiers have the ability to execute arbitrary code before the first\n // update cycle runs. They will be executed in the same order as the update\n // cycle. This is useful when a modifier adds some persistent data that\n // other modifiers need to use, but the modifier is run after the dependent\n // one.\n\n function runModifierEffects() {\n state.orderedModifiers.forEach(function (_ref) {\n var name = _ref.name,\n _ref$options = _ref.options,\n options = _ref$options === void 0 ? {} : _ref$options,\n effect = _ref.effect;\n\n if (typeof effect === 'function') {\n var cleanupFn = effect({\n state: state,\n name: name,\n instance: instance,\n options: options\n });\n\n var noopFn = function noopFn() {};\n\n effectCleanupFns.push(cleanupFn || noopFn);\n }\n });\n }\n\n function cleanupModifierEffects() {\n effectCleanupFns.forEach(function (fn) {\n return fn();\n });\n effectCleanupFns = [];\n }\n\n return instance;\n };\n}\nexport var createPopper = /*#__PURE__*/popperGenerator(); // eslint-disable-next-line import/no-unused-modules\n\nexport { detectOverflow };","export default function debounce(fn) {\n var pending;\n return function () {\n if (!pending) {\n pending = new Promise(function (resolve) {\n Promise.resolve().then(function () {\n pending = undefined;\n resolve(fn());\n });\n });\n }\n\n return pending;\n };\n}","export default function mergeByName(modifiers) {\n var merged = modifiers.reduce(function (merged, current) {\n var existing = merged[current.name];\n merged[current.name] = existing ? Object.assign({}, existing, current, {\n options: Object.assign({}, existing.options, current.options),\n data: Object.assign({}, existing.data, current.data)\n }) : current;\n return merged;\n }, {}); // IE11 does not support Object.values\n\n return Object.keys(merged).map(function (key) {\n return merged[key];\n });\n}","import { popperGenerator, detectOverflow } from \"./createPopper.js\";\nimport eventListeners from \"./modifiers/eventListeners.js\";\nimport popperOffsets from \"./modifiers/popperOffsets.js\";\nimport computeStyles from \"./modifiers/computeStyles.js\";\nimport applyStyles from \"./modifiers/applyStyles.js\";\nvar defaultModifiers = [eventListeners, popperOffsets, computeStyles, applyStyles];\nvar createPopper = /*#__PURE__*/popperGenerator({\n defaultModifiers: defaultModifiers\n}); // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper, popperGenerator, defaultModifiers, detectOverflow };","import { popperGenerator, detectOverflow } from \"./createPopper.js\";\nimport eventListeners from \"./modifiers/eventListeners.js\";\nimport popperOffsets from \"./modifiers/popperOffsets.js\";\nimport computeStyles from \"./modifiers/computeStyles.js\";\nimport applyStyles from \"./modifiers/applyStyles.js\";\nimport offset from \"./modifiers/offset.js\";\nimport flip from \"./modifiers/flip.js\";\nimport preventOverflow from \"./modifiers/preventOverflow.js\";\nimport arrow from \"./modifiers/arrow.js\";\nimport hide from \"./modifiers/hide.js\";\nvar defaultModifiers = [eventListeners, popperOffsets, computeStyles, applyStyles, offset, flip, preventOverflow, arrow, hide];\nvar createPopper = /*#__PURE__*/popperGenerator({\n defaultModifiers: defaultModifiers\n}); // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper, popperGenerator, defaultModifiers, detectOverflow }; // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper as createPopperLite } from \"./popper-lite.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport * from \"./modifiers/index.js\";","/**\n * --------------------------------------------------------------------------\n * Bootstrap dropdown.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport * as Popper from '@popperjs/core'\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport Manipulator from './dom/manipulator.js'\nimport SelectorEngine from './dom/selector-engine.js'\nimport {\n defineJQueryPlugin,\n execute,\n getElement,\n getNextActiveElement,\n isDisabled,\n isElement,\n isRTL,\n isVisible,\n noop\n} from './util/index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'dropdown'\nconst DATA_KEY = 'bs.dropdown'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\n\nconst ESCAPE_KEY = 'Escape'\nconst TAB_KEY = 'Tab'\nconst ARROW_UP_KEY = 'ArrowUp'\nconst ARROW_DOWN_KEY = 'ArrowDown'\nconst RIGHT_MOUSE_BUTTON = 2 // MouseEvent.button value for the secondary button, usually the right button\n\nconst EVENT_HIDE = `hide${EVENT_KEY}`\nconst EVENT_HIDDEN = `hidden${EVENT_KEY}`\nconst EVENT_SHOW = `show${EVENT_KEY}`\nconst EVENT_SHOWN = `shown${EVENT_KEY}`\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\nconst EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY}${DATA_API_KEY}`\nconst EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_SHOW = 'show'\nconst CLASS_NAME_DROPUP = 'dropup'\nconst CLASS_NAME_DROPEND = 'dropend'\nconst CLASS_NAME_DROPSTART = 'dropstart'\nconst CLASS_NAME_DROPUP_CENTER = 'dropup-center'\nconst CLASS_NAME_DROPDOWN_CENTER = 'dropdown-center'\n\nconst SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"dropdown\"]:not(.disabled):not(:disabled)'\nconst SELECTOR_DATA_TOGGLE_SHOWN = `${SELECTOR_DATA_TOGGLE}.${CLASS_NAME_SHOW}`\nconst SELECTOR_MENU = '.dropdown-menu'\nconst SELECTOR_NAVBAR = '.navbar'\nconst SELECTOR_NAVBAR_NAV = '.navbar-nav'\nconst SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'\n\nconst PLACEMENT_TOP = isRTL() ? 'top-end' : 'top-start'\nconst PLACEMENT_TOPEND = isRTL() ? 'top-start' : 'top-end'\nconst PLACEMENT_BOTTOM = isRTL() ? 'bottom-end' : 'bottom-start'\nconst PLACEMENT_BOTTOMEND = isRTL() ? 'bottom-start' : 'bottom-end'\nconst PLACEMENT_RIGHT = isRTL() ? 'left-start' : 'right-start'\nconst PLACEMENT_LEFT = isRTL() ? 'right-start' : 'left-start'\nconst PLACEMENT_TOPCENTER = 'top'\nconst PLACEMENT_BOTTOMCENTER = 'bottom'\n\nconst Default = {\n autoClose: true,\n boundary: 'clippingParents',\n display: 'dynamic',\n offset: [0, 2],\n popperConfig: null,\n reference: 'toggle'\n}\n\nconst DefaultType = {\n autoClose: '(boolean|string)',\n boundary: '(string|element)',\n display: 'string',\n offset: '(array|string|function)',\n popperConfig: '(null|object|function)',\n reference: '(string|element|object)'\n}\n\n/**\n * Class definition\n */\n\nclass Dropdown extends BaseComponent {\n constructor(element, config) {\n super(element, config)\n\n this._popper = null\n this._parent = this._element.parentNode // dropdown wrapper\n // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/\n this._menu = SelectorEngine.next(this._element, SELECTOR_MENU)[0] ||\n SelectorEngine.prev(this._element, SELECTOR_MENU)[0] ||\n SelectorEngine.findOne(SELECTOR_MENU, this._parent)\n this._inNavbar = this._detectNavbar()\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n toggle() {\n return this._isShown() ? this.hide() : this.show()\n }\n\n show() {\n if (isDisabled(this._element) || this._isShown()) {\n return\n }\n\n const relatedTarget = {\n relatedTarget: this._element\n }\n\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, relatedTarget)\n\n if (showEvent.defaultPrevented) {\n return\n }\n\n this._createPopper()\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.on(element, 'mouseover', noop)\n }\n }\n\n this._element.focus()\n this._element.setAttribute('aria-expanded', true)\n\n this._menu.classList.add(CLASS_NAME_SHOW)\n this._element.classList.add(CLASS_NAME_SHOW)\n EventHandler.trigger(this._element, EVENT_SHOWN, relatedTarget)\n }\n\n hide() {\n if (isDisabled(this._element) || !this._isShown()) {\n return\n }\n\n const relatedTarget = {\n relatedTarget: this._element\n }\n\n this._completeHide(relatedTarget)\n }\n\n dispose() {\n if (this._popper) {\n this._popper.destroy()\n }\n\n super.dispose()\n }\n\n update() {\n this._inNavbar = this._detectNavbar()\n if (this._popper) {\n this._popper.update()\n }\n }\n\n // Private\n _completeHide(relatedTarget) {\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE, relatedTarget)\n if (hideEvent.defaultPrevented) {\n return\n }\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.off(element, 'mouseover', noop)\n }\n }\n\n if (this._popper) {\n this._popper.destroy()\n }\n\n this._menu.classList.remove(CLASS_NAME_SHOW)\n this._element.classList.remove(CLASS_NAME_SHOW)\n this._element.setAttribute('aria-expanded', 'false')\n Manipulator.removeDataAttribute(this._menu, 'popper')\n EventHandler.trigger(this._element, EVENT_HIDDEN, relatedTarget)\n }\n\n _getConfig(config) {\n config = super._getConfig(config)\n\n if (typeof config.reference === 'object' && !isElement(config.reference) &&\n typeof config.reference.getBoundingClientRect !== 'function'\n ) {\n // Popper virtual elements require a getBoundingClientRect method\n throw new TypeError(`${NAME.toUpperCase()}: Option \"reference\" provided type \"object\" without a required \"getBoundingClientRect\" method.`)\n }\n\n return config\n }\n\n _createPopper() {\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s dropdowns require Popper (https://popper.js.org)')\n }\n\n let referenceElement = this._element\n\n if (this._config.reference === 'parent') {\n referenceElement = this._parent\n } else if (isElement(this._config.reference)) {\n referenceElement = getElement(this._config.reference)\n } else if (typeof this._config.reference === 'object') {\n referenceElement = this._config.reference\n }\n\n const popperConfig = this._getPopperConfig()\n this._popper = Popper.createPopper(referenceElement, this._menu, popperConfig)\n }\n\n _isShown() {\n return this._menu.classList.contains(CLASS_NAME_SHOW)\n }\n\n _getPlacement() {\n const parentDropdown = this._parent\n\n if (parentDropdown.classList.contains(CLASS_NAME_DROPEND)) {\n return PLACEMENT_RIGHT\n }\n\n if (parentDropdown.classList.contains(CLASS_NAME_DROPSTART)) {\n return PLACEMENT_LEFT\n }\n\n if (parentDropdown.classList.contains(CLASS_NAME_DROPUP_CENTER)) {\n return PLACEMENT_TOPCENTER\n }\n\n if (parentDropdown.classList.contains(CLASS_NAME_DROPDOWN_CENTER)) {\n return PLACEMENT_BOTTOMCENTER\n }\n\n // We need to trim the value because custom properties can also include spaces\n const isEnd = getComputedStyle(this._menu).getPropertyValue('--bs-position').trim() === 'end'\n\n if (parentDropdown.classList.contains(CLASS_NAME_DROPUP)) {\n return isEnd ? PLACEMENT_TOPEND : PLACEMENT_TOP\n }\n\n return isEnd ? PLACEMENT_BOTTOMEND : PLACEMENT_BOTTOM\n }\n\n _detectNavbar() {\n return this._element.closest(SELECTOR_NAVBAR) !== null\n }\n\n _getOffset() {\n const { offset } = this._config\n\n if (typeof offset === 'string') {\n return offset.split(',').map(value => Number.parseInt(value, 10))\n }\n\n if (typeof offset === 'function') {\n return popperData => offset(popperData, this._element)\n }\n\n return offset\n }\n\n _getPopperConfig() {\n const defaultBsPopperConfig = {\n placement: this._getPlacement(),\n modifiers: [{\n name: 'preventOverflow',\n options: {\n boundary: this._config.boundary\n }\n },\n {\n name: 'offset',\n options: {\n offset: this._getOffset()\n }\n }]\n }\n\n // Disable Popper if we have a static display or Dropdown is in Navbar\n if (this._inNavbar || this._config.display === 'static') {\n Manipulator.setDataAttribute(this._menu, 'popper', 'static') // TODO: v6 remove\n defaultBsPopperConfig.modifiers = [{\n name: 'applyStyles',\n enabled: false\n }]\n }\n\n return {\n ...defaultBsPopperConfig,\n ...execute(this._config.popperConfig, [defaultBsPopperConfig])\n }\n }\n\n _selectMenuItem({ key, target }) {\n const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(element => isVisible(element))\n\n if (!items.length) {\n return\n }\n\n // if target isn't included in items (e.g. when expanding the dropdown)\n // allow cycling to get the last item in case key equals ARROW_UP_KEY\n getNextActiveElement(items, target, key === ARROW_DOWN_KEY, !items.includes(target)).focus()\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Dropdown.getOrCreateInstance(this, config)\n\n if (typeof config !== 'string') {\n return\n }\n\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config]()\n })\n }\n\n static clearMenus(event) {\n if (event.button === RIGHT_MOUSE_BUTTON || (event.type === 'keyup' && event.key !== TAB_KEY)) {\n return\n }\n\n const openToggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE_SHOWN)\n\n for (const toggle of openToggles) {\n const context = Dropdown.getInstance(toggle)\n if (!context || context._config.autoClose === false) {\n continue\n }\n\n const composedPath = event.composedPath()\n const isMenuTarget = composedPath.includes(context._menu)\n if (\n composedPath.includes(context._element) ||\n (context._config.autoClose === 'inside' && !isMenuTarget) ||\n (context._config.autoClose === 'outside' && isMenuTarget)\n ) {\n continue\n }\n\n // Tab navigation through the dropdown menu or events from contained inputs shouldn't close the menu\n if (context._menu.contains(event.target) && ((event.type === 'keyup' && event.key === TAB_KEY) || /input|select|option|textarea|form/i.test(event.target.tagName))) {\n continue\n }\n\n const relatedTarget = { relatedTarget: context._element }\n\n if (event.type === 'click') {\n relatedTarget.clickEvent = event\n }\n\n context._completeHide(relatedTarget)\n }\n }\n\n static dataApiKeydownHandler(event) {\n // If not an UP | DOWN | ESCAPE key => not a dropdown command\n // If input/textarea && if key is other than ESCAPE => not a dropdown command\n\n const isInput = /input|textarea/i.test(event.target.tagName)\n const isEscapeEvent = event.key === ESCAPE_KEY\n const isUpOrDownEvent = [ARROW_UP_KEY, ARROW_DOWN_KEY].includes(event.key)\n\n if (!isUpOrDownEvent && !isEscapeEvent) {\n return\n }\n\n if (isInput && !isEscapeEvent) {\n return\n }\n\n event.preventDefault()\n\n // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/\n const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE) ?\n this :\n (SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0] ||\n SelectorEngine.next(this, SELECTOR_DATA_TOGGLE)[0] ||\n SelectorEngine.findOne(SELECTOR_DATA_TOGGLE, event.delegateTarget.parentNode))\n\n const instance = Dropdown.getOrCreateInstance(getToggleButton)\n\n if (isUpOrDownEvent) {\n event.stopPropagation()\n instance.show()\n instance._selectMenuItem(event)\n return\n }\n\n if (instance._isShown()) { // else is escape and we check if it is shown\n event.stopPropagation()\n instance.hide()\n getToggleButton.focus()\n }\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE, Dropdown.dataApiKeydownHandler)\nEventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Dropdown.dataApiKeydownHandler)\nEventHandler.on(document, EVENT_CLICK_DATA_API, Dropdown.clearMenus)\nEventHandler.on(document, EVENT_KEYUP_DATA_API, Dropdown.clearMenus)\nEventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n event.preventDefault()\n Dropdown.getOrCreateInstance(this).toggle()\n})\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Dropdown)\n\nexport default Dropdown\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/backdrop.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport EventHandler from '../dom/event-handler.js'\nimport Config from './config.js'\nimport { execute, executeAfterTransition, getElement, reflow } from './index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'backdrop'\nconst CLASS_NAME_FADE = 'fade'\nconst CLASS_NAME_SHOW = 'show'\nconst EVENT_MOUSEDOWN = `mousedown.bs.${NAME}`\n\nconst Default = {\n className: 'modal-backdrop',\n clickCallback: null,\n isAnimated: false,\n isVisible: true, // if false, we use the backdrop helper without adding any element to the dom\n rootElement: 'body' // give the choice to place backdrop under different elements\n}\n\nconst DefaultType = {\n className: 'string',\n clickCallback: '(function|null)',\n isAnimated: 'boolean',\n isVisible: 'boolean',\n rootElement: '(element|string)'\n}\n\n/**\n * Class definition\n */\n\nclass Backdrop extends Config {\n constructor(config) {\n super()\n this._config = this._getConfig(config)\n this._isAppended = false\n this._element = null\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n show(callback) {\n if (!this._config.isVisible) {\n execute(callback)\n return\n }\n\n this._append()\n\n const element = this._getElement()\n if (this._config.isAnimated) {\n reflow(element)\n }\n\n element.classList.add(CLASS_NAME_SHOW)\n\n this._emulateAnimation(() => {\n execute(callback)\n })\n }\n\n hide(callback) {\n if (!this._config.isVisible) {\n execute(callback)\n return\n }\n\n this._getElement().classList.remove(CLASS_NAME_SHOW)\n\n this._emulateAnimation(() => {\n this.dispose()\n execute(callback)\n })\n }\n\n dispose() {\n if (!this._isAppended) {\n return\n }\n\n EventHandler.off(this._element, EVENT_MOUSEDOWN)\n\n this._element.remove()\n this._isAppended = false\n }\n\n // Private\n _getElement() {\n if (!this._element) {\n const backdrop = document.createElement('div')\n backdrop.className = this._config.className\n if (this._config.isAnimated) {\n backdrop.classList.add(CLASS_NAME_FADE)\n }\n\n this._element = backdrop\n }\n\n return this._element\n }\n\n _configAfterMerge(config) {\n // use getElement() with the default \"body\" to get a fresh Element on each instantiation\n config.rootElement = getElement(config.rootElement)\n return config\n }\n\n _append() {\n if (this._isAppended) {\n return\n }\n\n const element = this._getElement()\n this._config.rootElement.append(element)\n\n EventHandler.on(element, EVENT_MOUSEDOWN, () => {\n execute(this._config.clickCallback)\n })\n\n this._isAppended = true\n }\n\n _emulateAnimation(callback) {\n executeAfterTransition(callback, this._getElement(), this._config.isAnimated)\n }\n}\n\nexport default Backdrop\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/focustrap.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport EventHandler from '../dom/event-handler.js'\nimport SelectorEngine from '../dom/selector-engine.js'\nimport Config from './config.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'focustrap'\nconst DATA_KEY = 'bs.focustrap'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst EVENT_FOCUSIN = `focusin${EVENT_KEY}`\nconst EVENT_KEYDOWN_TAB = `keydown.tab${EVENT_KEY}`\n\nconst TAB_KEY = 'Tab'\nconst TAB_NAV_FORWARD = 'forward'\nconst TAB_NAV_BACKWARD = 'backward'\n\nconst Default = {\n autofocus: true,\n trapElement: null // The element to trap focus inside of\n}\n\nconst DefaultType = {\n autofocus: 'boolean',\n trapElement: 'element'\n}\n\n/**\n * Class definition\n */\n\nclass FocusTrap extends Config {\n constructor(config) {\n super()\n this._config = this._getConfig(config)\n this._isActive = false\n this._lastTabNavDirection = null\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n activate() {\n if (this._isActive) {\n return\n }\n\n if (this._config.autofocus) {\n this._config.trapElement.focus()\n }\n\n EventHandler.off(document, EVENT_KEY) // guard against infinite focus loop\n EventHandler.on(document, EVENT_FOCUSIN, event => this._handleFocusin(event))\n EventHandler.on(document, EVENT_KEYDOWN_TAB, event => this._handleKeydown(event))\n\n this._isActive = true\n }\n\n deactivate() {\n if (!this._isActive) {\n return\n }\n\n this._isActive = false\n EventHandler.off(document, EVENT_KEY)\n }\n\n // Private\n _handleFocusin(event) {\n const { trapElement } = this._config\n\n if (event.target === document || event.target === trapElement || trapElement.contains(event.target)) {\n return\n }\n\n const elements = SelectorEngine.focusableChildren(trapElement)\n\n if (elements.length === 0) {\n trapElement.focus()\n } else if (this._lastTabNavDirection === TAB_NAV_BACKWARD) {\n elements[elements.length - 1].focus()\n } else {\n elements[0].focus()\n }\n }\n\n _handleKeydown(event) {\n if (event.key !== TAB_KEY) {\n return\n }\n\n this._lastTabNavDirection = event.shiftKey ? TAB_NAV_BACKWARD : TAB_NAV_FORWARD\n }\n}\n\nexport default FocusTrap\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/scrollBar.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport Manipulator from '../dom/manipulator.js'\nimport SelectorEngine from '../dom/selector-engine.js'\nimport { isElement } from './index.js'\n\n/**\n * Constants\n */\n\nconst SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top'\nconst SELECTOR_STICKY_CONTENT = '.sticky-top'\nconst PROPERTY_PADDING = 'padding-right'\nconst PROPERTY_MARGIN = 'margin-right'\n\n/**\n * Class definition\n */\n\nclass ScrollBarHelper {\n constructor() {\n this._element = document.body\n }\n\n // Public\n getWidth() {\n // https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes\n const documentWidth = document.documentElement.clientWidth\n return Math.abs(window.innerWidth - documentWidth)\n }\n\n hide() {\n const width = this.getWidth()\n this._disableOverFlow()\n // give padding to element to balance the hidden scrollbar width\n this._setElementAttributes(this._element, PROPERTY_PADDING, calculatedValue => calculatedValue + width)\n // trick: We adjust positive paddingRight and negative marginRight to sticky-top elements to keep showing fullwidth\n this._setElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING, calculatedValue => calculatedValue + width)\n this._setElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN, calculatedValue => calculatedValue - width)\n }\n\n reset() {\n this._resetElementAttributes(this._element, 'overflow')\n this._resetElementAttributes(this._element, PROPERTY_PADDING)\n this._resetElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING)\n this._resetElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN)\n }\n\n isOverflowing() {\n return this.getWidth() > 0\n }\n\n // Private\n _disableOverFlow() {\n this._saveInitialAttribute(this._element, 'overflow')\n this._element.style.overflow = 'hidden'\n }\n\n _setElementAttributes(selector, styleProperty, callback) {\n const scrollbarWidth = this.getWidth()\n const manipulationCallBack = element => {\n if (element !== this._element && window.innerWidth > element.clientWidth + scrollbarWidth) {\n return\n }\n\n this._saveInitialAttribute(element, styleProperty)\n const calculatedValue = window.getComputedStyle(element).getPropertyValue(styleProperty)\n element.style.setProperty(styleProperty, `${callback(Number.parseFloat(calculatedValue))}px`)\n }\n\n this._applyManipulationCallback(selector, manipulationCallBack)\n }\n\n _saveInitialAttribute(element, styleProperty) {\n const actualValue = element.style.getPropertyValue(styleProperty)\n if (actualValue) {\n Manipulator.setDataAttribute(element, styleProperty, actualValue)\n }\n }\n\n _resetElementAttributes(selector, styleProperty) {\n const manipulationCallBack = element => {\n const value = Manipulator.getDataAttribute(element, styleProperty)\n // We only want to remove the property if the value is `null`; the value can also be zero\n if (value === null) {\n element.style.removeProperty(styleProperty)\n return\n }\n\n Manipulator.removeDataAttribute(element, styleProperty)\n element.style.setProperty(styleProperty, value)\n }\n\n this._applyManipulationCallback(selector, manipulationCallBack)\n }\n\n _applyManipulationCallback(selector, callBack) {\n if (isElement(selector)) {\n callBack(selector)\n return\n }\n\n for (const sel of SelectorEngine.find(selector, this._element)) {\n callBack(sel)\n }\n }\n}\n\nexport default ScrollBarHelper\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap modal.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport SelectorEngine from './dom/selector-engine.js'\nimport Backdrop from './util/backdrop.js'\nimport { enableDismissTrigger } from './util/component-functions.js'\nimport FocusTrap from './util/focustrap.js'\nimport { defineJQueryPlugin, isRTL, isVisible, reflow } from './util/index.js'\nimport ScrollBarHelper from './util/scrollbar.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'modal'\nconst DATA_KEY = 'bs.modal'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst ESCAPE_KEY = 'Escape'\n\nconst EVENT_HIDE = `hide${EVENT_KEY}`\nconst EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}`\nconst EVENT_HIDDEN = `hidden${EVENT_KEY}`\nconst EVENT_SHOW = `show${EVENT_KEY}`\nconst EVENT_SHOWN = `shown${EVENT_KEY}`\nconst EVENT_RESIZE = `resize${EVENT_KEY}`\nconst EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY}`\nconst EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY}`\nconst EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_OPEN = 'modal-open'\nconst CLASS_NAME_FADE = 'fade'\nconst CLASS_NAME_SHOW = 'show'\nconst CLASS_NAME_STATIC = 'modal-static'\n\nconst OPEN_SELECTOR = '.modal.show'\nconst SELECTOR_DIALOG = '.modal-dialog'\nconst SELECTOR_MODAL_BODY = '.modal-body'\nconst SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"modal\"]'\n\nconst Default = {\n backdrop: true,\n focus: true,\n keyboard: true\n}\n\nconst DefaultType = {\n backdrop: '(boolean|string)',\n focus: 'boolean',\n keyboard: 'boolean'\n}\n\n/**\n * Class definition\n */\n\nclass Modal extends BaseComponent {\n constructor(element, config) {\n super(element, config)\n\n this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element)\n this._backdrop = this._initializeBackDrop()\n this._focustrap = this._initializeFocusTrap()\n this._isShown = false\n this._isTransitioning = false\n this._scrollBar = new ScrollBarHelper()\n\n this._addEventListeners()\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget)\n }\n\n show(relatedTarget) {\n if (this._isShown || this._isTransitioning) {\n return\n }\n\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, {\n relatedTarget\n })\n\n if (showEvent.defaultPrevented) {\n return\n }\n\n this._isShown = true\n this._isTransitioning = true\n\n this._scrollBar.hide()\n\n document.body.classList.add(CLASS_NAME_OPEN)\n\n this._adjustDialog()\n\n this._backdrop.show(() => this._showElement(relatedTarget))\n }\n\n hide() {\n if (!this._isShown || this._isTransitioning) {\n return\n }\n\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE)\n\n if (hideEvent.defaultPrevented) {\n return\n }\n\n this._isShown = false\n this._isTransitioning = true\n this._focustrap.deactivate()\n\n this._element.classList.remove(CLASS_NAME_SHOW)\n\n this._queueCallback(() => this._hideModal(), this._element, this._isAnimated())\n }\n\n dispose() {\n EventHandler.off(window, EVENT_KEY)\n EventHandler.off(this._dialog, EVENT_KEY)\n\n this._backdrop.dispose()\n this._focustrap.deactivate()\n\n super.dispose()\n }\n\n handleUpdate() {\n this._adjustDialog()\n }\n\n // Private\n _initializeBackDrop() {\n return new Backdrop({\n isVisible: Boolean(this._config.backdrop), // 'static' option will be translated to true, and booleans will keep their value,\n isAnimated: this._isAnimated()\n })\n }\n\n _initializeFocusTrap() {\n return new FocusTrap({\n trapElement: this._element\n })\n }\n\n _showElement(relatedTarget) {\n // try to append dynamic modal\n if (!document.body.contains(this._element)) {\n document.body.append(this._element)\n }\n\n this._element.style.display = 'block'\n this._element.removeAttribute('aria-hidden')\n this._element.setAttribute('aria-modal', true)\n this._element.setAttribute('role', 'dialog')\n this._element.scrollTop = 0\n\n const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog)\n if (modalBody) {\n modalBody.scrollTop = 0\n }\n\n reflow(this._element)\n\n this._element.classList.add(CLASS_NAME_SHOW)\n\n const transitionComplete = () => {\n if (this._config.focus) {\n this._focustrap.activate()\n }\n\n this._isTransitioning = false\n EventHandler.trigger(this._element, EVENT_SHOWN, {\n relatedTarget\n })\n }\n\n this._queueCallback(transitionComplete, this._dialog, this._isAnimated())\n }\n\n _addEventListeners() {\n EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {\n if (event.key !== ESCAPE_KEY) {\n return\n }\n\n if (this._config.keyboard) {\n this.hide()\n return\n }\n\n this._triggerBackdropTransition()\n })\n\n EventHandler.on(window, EVENT_RESIZE, () => {\n if (this._isShown && !this._isTransitioning) {\n this._adjustDialog()\n }\n })\n\n EventHandler.on(this._element, EVENT_MOUSEDOWN_DISMISS, event => {\n // a bad trick to segregate clicks that may start inside dialog but end outside, and avoid listen to scrollbar clicks\n EventHandler.one(this._element, EVENT_CLICK_DISMISS, event2 => {\n if (this._element !== event.target || this._element !== event2.target) {\n return\n }\n\n if (this._config.backdrop === 'static') {\n this._triggerBackdropTransition()\n return\n }\n\n if (this._config.backdrop) {\n this.hide()\n }\n })\n })\n }\n\n _hideModal() {\n this._element.style.display = 'none'\n this._element.setAttribute('aria-hidden', true)\n this._element.removeAttribute('aria-modal')\n this._element.removeAttribute('role')\n this._isTransitioning = false\n\n this._backdrop.hide(() => {\n document.body.classList.remove(CLASS_NAME_OPEN)\n this._resetAdjustments()\n this._scrollBar.reset()\n EventHandler.trigger(this._element, EVENT_HIDDEN)\n })\n }\n\n _isAnimated() {\n return this._element.classList.contains(CLASS_NAME_FADE)\n }\n\n _triggerBackdropTransition() {\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED)\n if (hideEvent.defaultPrevented) {\n return\n }\n\n const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight\n const initialOverflowY = this._element.style.overflowY\n // return if the following background transition hasn't yet completed\n if (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) {\n return\n }\n\n if (!isModalOverflowing) {\n this._element.style.overflowY = 'hidden'\n }\n\n this._element.classList.add(CLASS_NAME_STATIC)\n this._queueCallback(() => {\n this._element.classList.remove(CLASS_NAME_STATIC)\n this._queueCallback(() => {\n this._element.style.overflowY = initialOverflowY\n }, this._dialog)\n }, this._dialog)\n\n this._element.focus()\n }\n\n /**\n * The following methods are used to handle overflowing modals\n */\n\n _adjustDialog() {\n const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight\n const scrollbarWidth = this._scrollBar.getWidth()\n const isBodyOverflowing = scrollbarWidth > 0\n\n if (isBodyOverflowing && !isModalOverflowing) {\n const property = isRTL() ? 'paddingLeft' : 'paddingRight'\n this._element.style[property] = `${scrollbarWidth}px`\n }\n\n if (!isBodyOverflowing && isModalOverflowing) {\n const property = isRTL() ? 'paddingRight' : 'paddingLeft'\n this._element.style[property] = `${scrollbarWidth}px`\n }\n }\n\n _resetAdjustments() {\n this._element.style.paddingLeft = ''\n this._element.style.paddingRight = ''\n }\n\n // Static\n static jQueryInterface(config, relatedTarget) {\n return this.each(function () {\n const data = Modal.getOrCreateInstance(this, config)\n\n if (typeof config !== 'string') {\n return\n }\n\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config](relatedTarget)\n })\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n const target = SelectorEngine.getElementFromSelector(this)\n\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault()\n }\n\n EventHandler.one(target, EVENT_SHOW, showEvent => {\n if (showEvent.defaultPrevented) {\n // only register focus restorer if modal will actually get shown\n return\n }\n\n EventHandler.one(target, EVENT_HIDDEN, () => {\n if (isVisible(this)) {\n this.focus()\n }\n })\n })\n\n // avoid conflict when clicking modal toggler while another one is open\n const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR)\n if (alreadyOpen) {\n Modal.getInstance(alreadyOpen).hide()\n }\n\n const data = Modal.getOrCreateInstance(target)\n\n data.toggle(this)\n})\n\nenableDismissTrigger(Modal)\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Modal)\n\nexport default Modal\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap offcanvas.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport SelectorEngine from './dom/selector-engine.js'\nimport Backdrop from './util/backdrop.js'\nimport { enableDismissTrigger } from './util/component-functions.js'\nimport FocusTrap from './util/focustrap.js'\nimport {\n defineJQueryPlugin,\n isDisabled,\n isVisible\n} from './util/index.js'\nimport ScrollBarHelper from './util/scrollbar.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'offcanvas'\nconst DATA_KEY = 'bs.offcanvas'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`\nconst ESCAPE_KEY = 'Escape'\n\nconst CLASS_NAME_SHOW = 'show'\nconst CLASS_NAME_SHOWING = 'showing'\nconst CLASS_NAME_HIDING = 'hiding'\nconst CLASS_NAME_BACKDROP = 'offcanvas-backdrop'\nconst OPEN_SELECTOR = '.offcanvas.show'\n\nconst EVENT_SHOW = `show${EVENT_KEY}`\nconst EVENT_SHOWN = `shown${EVENT_KEY}`\nconst EVENT_HIDE = `hide${EVENT_KEY}`\nconst EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}`\nconst EVENT_HIDDEN = `hidden${EVENT_KEY}`\nconst EVENT_RESIZE = `resize${EVENT_KEY}`\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\nconst EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`\n\nconst SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"offcanvas\"]'\n\nconst Default = {\n backdrop: true,\n keyboard: true,\n scroll: false\n}\n\nconst DefaultType = {\n backdrop: '(boolean|string)',\n keyboard: 'boolean',\n scroll: 'boolean'\n}\n\n/**\n * Class definition\n */\n\nclass Offcanvas extends BaseComponent {\n constructor(element, config) {\n super(element, config)\n\n this._isShown = false\n this._backdrop = this._initializeBackDrop()\n this._focustrap = this._initializeFocusTrap()\n this._addEventListeners()\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget)\n }\n\n show(relatedTarget) {\n if (this._isShown) {\n return\n }\n\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, { relatedTarget })\n\n if (showEvent.defaultPrevented) {\n return\n }\n\n this._isShown = true\n this._backdrop.show()\n\n if (!this._config.scroll) {\n new ScrollBarHelper().hide()\n }\n\n this._element.setAttribute('aria-modal', true)\n this._element.setAttribute('role', 'dialog')\n this._element.classList.add(CLASS_NAME_SHOWING)\n\n const completeCallBack = () => {\n if (!this._config.scroll || this._config.backdrop) {\n this._focustrap.activate()\n }\n\n this._element.classList.add(CLASS_NAME_SHOW)\n this._element.classList.remove(CLASS_NAME_SHOWING)\n EventHandler.trigger(this._element, EVENT_SHOWN, { relatedTarget })\n }\n\n this._queueCallback(completeCallBack, this._element, true)\n }\n\n hide() {\n if (!this._isShown) {\n return\n }\n\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE)\n\n if (hideEvent.defaultPrevented) {\n return\n }\n\n this._focustrap.deactivate()\n this._element.blur()\n this._isShown = false\n this._element.classList.add(CLASS_NAME_HIDING)\n this._backdrop.hide()\n\n const completeCallback = () => {\n this._element.classList.remove(CLASS_NAME_SHOW, CLASS_NAME_HIDING)\n this._element.removeAttribute('aria-modal')\n this._element.removeAttribute('role')\n\n if (!this._config.scroll) {\n new ScrollBarHelper().reset()\n }\n\n EventHandler.trigger(this._element, EVENT_HIDDEN)\n }\n\n this._queueCallback(completeCallback, this._element, true)\n }\n\n dispose() {\n this._backdrop.dispose()\n this._focustrap.deactivate()\n super.dispose()\n }\n\n // Private\n _initializeBackDrop() {\n const clickCallback = () => {\n if (this._config.backdrop === 'static') {\n EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED)\n return\n }\n\n this.hide()\n }\n\n // 'static' option will be translated to true, and booleans will keep their value\n const isVisible = Boolean(this._config.backdrop)\n\n return new Backdrop({\n className: CLASS_NAME_BACKDROP,\n isVisible,\n isAnimated: true,\n rootElement: this._element.parentNode,\n clickCallback: isVisible ? clickCallback : null\n })\n }\n\n _initializeFocusTrap() {\n return new FocusTrap({\n trapElement: this._element\n })\n }\n\n _addEventListeners() {\n EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {\n if (event.key !== ESCAPE_KEY) {\n return\n }\n\n if (this._config.keyboard) {\n this.hide()\n return\n }\n\n EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED)\n })\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Offcanvas.getOrCreateInstance(this, config)\n\n if (typeof config !== 'string') {\n return\n }\n\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config](this)\n })\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n const target = SelectorEngine.getElementFromSelector(this)\n\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault()\n }\n\n if (isDisabled(this)) {\n return\n }\n\n EventHandler.one(target, EVENT_HIDDEN, () => {\n // focus on trigger when it is closed\n if (isVisible(this)) {\n this.focus()\n }\n })\n\n // avoid conflict when clicking a toggler of an offcanvas, while another is open\n const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR)\n if (alreadyOpen && alreadyOpen !== target) {\n Offcanvas.getInstance(alreadyOpen).hide()\n }\n\n const data = Offcanvas.getOrCreateInstance(target)\n data.toggle(this)\n})\n\nEventHandler.on(window, EVENT_LOAD_DATA_API, () => {\n for (const selector of SelectorEngine.find(OPEN_SELECTOR)) {\n Offcanvas.getOrCreateInstance(selector).show()\n }\n})\n\nEventHandler.on(window, EVENT_RESIZE, () => {\n for (const element of SelectorEngine.find('[aria-modal][class*=show][class*=offcanvas-]')) {\n if (getComputedStyle(element).position !== 'fixed') {\n Offcanvas.getOrCreateInstance(element).hide()\n }\n }\n})\n\nenableDismissTrigger(Offcanvas)\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Offcanvas)\n\nexport default Offcanvas\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/sanitizer.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n// js-docs-start allow-list\nconst ARIA_ATTRIBUTE_PATTERN = /^aria-[\\w-]*$/i\n\nexport const DefaultAllowlist = {\n // Global attributes allowed on any supplied element below.\n '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],\n a: ['target', 'href', 'title', 'rel'],\n area: [],\n b: [],\n br: [],\n col: [],\n code: [],\n div: [],\n em: [],\n hr: [],\n h1: [],\n h2: [],\n h3: [],\n h4: [],\n h5: [],\n h6: [],\n i: [],\n img: ['src', 'srcset', 'alt', 'title', 'width', 'height'],\n li: [],\n ol: [],\n p: [],\n pre: [],\n s: [],\n small: [],\n span: [],\n sub: [],\n sup: [],\n strong: [],\n u: [],\n ul: []\n}\n// js-docs-end allow-list\n\nconst uriAttributes = new Set([\n 'background',\n 'cite',\n 'href',\n 'itemtype',\n 'longdesc',\n 'poster',\n 'src',\n 'xlink:href'\n])\n\n/**\n * A pattern that recognizes URLs that are safe wrt. XSS in URL navigation\n * contexts.\n *\n * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38\n */\n// eslint-disable-next-line unicorn/better-regex\nconst SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i\n\nconst allowedAttribute = (attribute, allowedAttributeList) => {\n const attributeName = attribute.nodeName.toLowerCase()\n\n if (allowedAttributeList.includes(attributeName)) {\n if (uriAttributes.has(attributeName)) {\n return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue))\n }\n\n return true\n }\n\n // Check if a regular expression validates the attribute.\n return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp)\n .some(regex => regex.test(attributeName))\n}\n\nexport function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) {\n if (!unsafeHtml.length) {\n return unsafeHtml\n }\n\n if (sanitizeFunction && typeof sanitizeFunction === 'function') {\n return sanitizeFunction(unsafeHtml)\n }\n\n const domParser = new window.DOMParser()\n const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html')\n const elements = [].concat(...createdDocument.body.querySelectorAll('*'))\n\n for (const element of elements) {\n const elementName = element.nodeName.toLowerCase()\n\n if (!Object.keys(allowList).includes(elementName)) {\n element.remove()\n continue\n }\n\n const attributeList = [].concat(...element.attributes)\n const allowedAttributes = [].concat(allowList['*'] || [], allowList[elementName] || [])\n\n for (const attribute of attributeList) {\n if (!allowedAttribute(attribute, allowedAttributes)) {\n element.removeAttribute(attribute.nodeName)\n }\n }\n }\n\n return createdDocument.body.innerHTML\n}\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/template-factory.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport SelectorEngine from '../dom/selector-engine.js'\nimport Config from './config.js'\nimport { DefaultAllowlist, sanitizeHtml } from './sanitizer.js'\nimport { execute, getElement, isElement } from './index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'TemplateFactory'\n\nconst Default = {\n allowList: DefaultAllowlist,\n content: {}, // { selector : text , selector2 : text2 , }\n extraClass: '',\n html: false,\n sanitize: true,\n sanitizeFn: null,\n template: '
'\n}\n\nconst DefaultType = {\n allowList: 'object',\n content: 'object',\n extraClass: '(string|function)',\n html: 'boolean',\n sanitize: 'boolean',\n sanitizeFn: '(null|function)',\n template: 'string'\n}\n\nconst DefaultContentType = {\n entry: '(string|element|function|null)',\n selector: '(string|element)'\n}\n\n/**\n * Class definition\n */\n\nclass TemplateFactory extends Config {\n constructor(config) {\n super()\n this._config = this._getConfig(config)\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n getContent() {\n return Object.values(this._config.content)\n .map(config => this._resolvePossibleFunction(config))\n .filter(Boolean)\n }\n\n hasContent() {\n return this.getContent().length > 0\n }\n\n changeContent(content) {\n this._checkContent(content)\n this._config.content = { ...this._config.content, ...content }\n return this\n }\n\n toHtml() {\n const templateWrapper = document.createElement('div')\n templateWrapper.innerHTML = this._maybeSanitize(this._config.template)\n\n for (const [selector, text] of Object.entries(this._config.content)) {\n this._setContent(templateWrapper, text, selector)\n }\n\n const template = templateWrapper.children[0]\n const extraClass = this._resolvePossibleFunction(this._config.extraClass)\n\n if (extraClass) {\n template.classList.add(...extraClass.split(' '))\n }\n\n return template\n }\n\n // Private\n _typeCheckConfig(config) {\n super._typeCheckConfig(config)\n this._checkContent(config.content)\n }\n\n _checkContent(arg) {\n for (const [selector, content] of Object.entries(arg)) {\n super._typeCheckConfig({ selector, entry: content }, DefaultContentType)\n }\n }\n\n _setContent(template, content, selector) {\n const templateElement = SelectorEngine.findOne(selector, template)\n\n if (!templateElement) {\n return\n }\n\n content = this._resolvePossibleFunction(content)\n\n if (!content) {\n templateElement.remove()\n return\n }\n\n if (isElement(content)) {\n this._putElementInTemplate(getElement(content), templateElement)\n return\n }\n\n if (this._config.html) {\n templateElement.innerHTML = this._maybeSanitize(content)\n return\n }\n\n templateElement.textContent = content\n }\n\n _maybeSanitize(arg) {\n return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg\n }\n\n _resolvePossibleFunction(arg) {\n return execute(arg, [this])\n }\n\n _putElementInTemplate(element, templateElement) {\n if (this._config.html) {\n templateElement.innerHTML = ''\n templateElement.append(element)\n return\n }\n\n templateElement.textContent = element.textContent\n }\n}\n\nexport default TemplateFactory\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap tooltip.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport * as Popper from '@popperjs/core'\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport Manipulator from './dom/manipulator.js'\nimport { defineJQueryPlugin, execute, findShadowRoot, getElement, getUID, isRTL, noop } from './util/index.js'\nimport { DefaultAllowlist } from './util/sanitizer.js'\nimport TemplateFactory from './util/template-factory.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'tooltip'\nconst DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn'])\n\nconst CLASS_NAME_FADE = 'fade'\nconst CLASS_NAME_MODAL = 'modal'\nconst CLASS_NAME_SHOW = 'show'\n\nconst SELECTOR_TOOLTIP_INNER = '.tooltip-inner'\nconst SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`\n\nconst EVENT_MODAL_HIDE = 'hide.bs.modal'\n\nconst TRIGGER_HOVER = 'hover'\nconst TRIGGER_FOCUS = 'focus'\nconst TRIGGER_CLICK = 'click'\nconst TRIGGER_MANUAL = 'manual'\n\nconst EVENT_HIDE = 'hide'\nconst EVENT_HIDDEN = 'hidden'\nconst EVENT_SHOW = 'show'\nconst EVENT_SHOWN = 'shown'\nconst EVENT_INSERTED = 'inserted'\nconst EVENT_CLICK = 'click'\nconst EVENT_FOCUSIN = 'focusin'\nconst EVENT_FOCUSOUT = 'focusout'\nconst EVENT_MOUSEENTER = 'mouseenter'\nconst EVENT_MOUSELEAVE = 'mouseleave'\n\nconst AttachmentMap = {\n AUTO: 'auto',\n TOP: 'top',\n RIGHT: isRTL() ? 'left' : 'right',\n BOTTOM: 'bottom',\n LEFT: isRTL() ? 'right' : 'left'\n}\n\nconst Default = {\n allowList: DefaultAllowlist,\n animation: true,\n boundary: 'clippingParents',\n container: false,\n customClass: '',\n delay: 0,\n fallbackPlacements: ['top', 'right', 'bottom', 'left'],\n html: false,\n offset: [0, 6],\n placement: 'top',\n popperConfig: null,\n sanitize: true,\n sanitizeFn: null,\n selector: false,\n template: '
' +\n '
' +\n '
' +\n '
',\n title: '',\n trigger: 'hover focus'\n}\n\nconst DefaultType = {\n allowList: 'object',\n animation: 'boolean',\n boundary: '(string|element)',\n container: '(string|element|boolean)',\n customClass: '(string|function)',\n delay: '(number|object)',\n fallbackPlacements: 'array',\n html: 'boolean',\n offset: '(array|string|function)',\n placement: '(string|function)',\n popperConfig: '(null|object|function)',\n sanitize: 'boolean',\n sanitizeFn: '(null|function)',\n selector: '(string|boolean)',\n template: 'string',\n title: '(string|element|function)',\n trigger: 'string'\n}\n\n/**\n * Class definition\n */\n\nclass Tooltip extends BaseComponent {\n constructor(element, config) {\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s tooltips require Popper (https://popper.js.org)')\n }\n\n super(element, config)\n\n // Private\n this._isEnabled = true\n this._timeout = 0\n this._isHovered = null\n this._activeTrigger = {}\n this._popper = null\n this._templateFactory = null\n this._newContent = null\n\n // Protected\n this.tip = null\n\n this._setListeners()\n\n if (!this._config.selector) {\n this._fixTitle()\n }\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n enable() {\n this._isEnabled = true\n }\n\n disable() {\n this._isEnabled = false\n }\n\n toggleEnabled() {\n this._isEnabled = !this._isEnabled\n }\n\n toggle() {\n if (!this._isEnabled) {\n return\n }\n\n this._activeTrigger.click = !this._activeTrigger.click\n if (this._isShown()) {\n this._leave()\n return\n }\n\n this._enter()\n }\n\n dispose() {\n clearTimeout(this._timeout)\n\n EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler)\n\n if (this._element.getAttribute('data-bs-original-title')) {\n this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title'))\n }\n\n this._disposePopper()\n super.dispose()\n }\n\n show() {\n if (this._element.style.display === 'none') {\n throw new Error('Please use show on visible elements')\n }\n\n if (!(this._isWithContent() && this._isEnabled)) {\n return\n }\n\n const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW))\n const shadowRoot = findShadowRoot(this._element)\n const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element)\n\n if (showEvent.defaultPrevented || !isInTheDom) {\n return\n }\n\n // TODO: v6 remove this or make it optional\n this._disposePopper()\n\n const tip = this._getTipElement()\n\n this._element.setAttribute('aria-describedby', tip.getAttribute('id'))\n\n const { container } = this._config\n\n if (!this._element.ownerDocument.documentElement.contains(this.tip)) {\n container.append(tip)\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED))\n }\n\n this._popper = this._createPopper(tip)\n\n tip.classList.add(CLASS_NAME_SHOW)\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.on(element, 'mouseover', noop)\n }\n }\n\n const complete = () => {\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN))\n\n if (this._isHovered === false) {\n this._leave()\n }\n\n this._isHovered = false\n }\n\n this._queueCallback(complete, this.tip, this._isAnimated())\n }\n\n hide() {\n if (!this._isShown()) {\n return\n }\n\n const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE))\n if (hideEvent.defaultPrevented) {\n return\n }\n\n const tip = this._getTipElement()\n tip.classList.remove(CLASS_NAME_SHOW)\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.off(element, 'mouseover', noop)\n }\n }\n\n this._activeTrigger[TRIGGER_CLICK] = false\n this._activeTrigger[TRIGGER_FOCUS] = false\n this._activeTrigger[TRIGGER_HOVER] = false\n this._isHovered = null // it is a trick to support manual triggering\n\n const complete = () => {\n if (this._isWithActiveTrigger()) {\n return\n }\n\n if (!this._isHovered) {\n this._disposePopper()\n }\n\n this._element.removeAttribute('aria-describedby')\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN))\n }\n\n this._queueCallback(complete, this.tip, this._isAnimated())\n }\n\n update() {\n if (this._popper) {\n this._popper.update()\n }\n }\n\n // Protected\n _isWithContent() {\n return Boolean(this._getTitle())\n }\n\n _getTipElement() {\n if (!this.tip) {\n this.tip = this._createTipElement(this._newContent || this._getContentForTemplate())\n }\n\n return this.tip\n }\n\n _createTipElement(content) {\n const tip = this._getTemplateFactory(content).toHtml()\n\n // TODO: remove this check in v6\n if (!tip) {\n return null\n }\n\n tip.classList.remove(CLASS_NAME_FADE, CLASS_NAME_SHOW)\n // TODO: v6 the following can be achieved with CSS only\n tip.classList.add(`bs-${this.constructor.NAME}-auto`)\n\n const tipId = getUID(this.constructor.NAME).toString()\n\n tip.setAttribute('id', tipId)\n\n if (this._isAnimated()) {\n tip.classList.add(CLASS_NAME_FADE)\n }\n\n return tip\n }\n\n setContent(content) {\n this._newContent = content\n if (this._isShown()) {\n this._disposePopper()\n this.show()\n }\n }\n\n _getTemplateFactory(content) {\n if (this._templateFactory) {\n this._templateFactory.changeContent(content)\n } else {\n this._templateFactory = new TemplateFactory({\n ...this._config,\n // the `content` var has to be after `this._config`\n // to override config.content in case of popover\n content,\n extraClass: this._resolvePossibleFunction(this._config.customClass)\n })\n }\n\n return this._templateFactory\n }\n\n _getContentForTemplate() {\n return {\n [SELECTOR_TOOLTIP_INNER]: this._getTitle()\n }\n }\n\n _getTitle() {\n return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title')\n }\n\n // Private\n _initializeOnDelegatedTarget(event) {\n return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig())\n }\n\n _isAnimated() {\n return this._config.animation || (this.tip && this.tip.classList.contains(CLASS_NAME_FADE))\n }\n\n _isShown() {\n return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW)\n }\n\n _createPopper(tip) {\n const placement = execute(this._config.placement, [this, tip, this._element])\n const attachment = AttachmentMap[placement.toUpperCase()]\n return Popper.createPopper(this._element, tip, this._getPopperConfig(attachment))\n }\n\n _getOffset() {\n const { offset } = this._config\n\n if (typeof offset === 'string') {\n return offset.split(',').map(value => Number.parseInt(value, 10))\n }\n\n if (typeof offset === 'function') {\n return popperData => offset(popperData, this._element)\n }\n\n return offset\n }\n\n _resolvePossibleFunction(arg) {\n return execute(arg, [this._element])\n }\n\n _getPopperConfig(attachment) {\n const defaultBsPopperConfig = {\n placement: attachment,\n modifiers: [\n {\n name: 'flip',\n options: {\n fallbackPlacements: this._config.fallbackPlacements\n }\n },\n {\n name: 'offset',\n options: {\n offset: this._getOffset()\n }\n },\n {\n name: 'preventOverflow',\n options: {\n boundary: this._config.boundary\n }\n },\n {\n name: 'arrow',\n options: {\n element: `.${this.constructor.NAME}-arrow`\n }\n },\n {\n name: 'preSetPlacement',\n enabled: true,\n phase: 'beforeMain',\n fn: data => {\n // Pre-set Popper's placement attribute in order to read the arrow sizes properly.\n // Otherwise, Popper mixes up the width and height dimensions since the initial arrow style is for top placement\n this._getTipElement().setAttribute('data-popper-placement', data.state.placement)\n }\n }\n ]\n }\n\n return {\n ...defaultBsPopperConfig,\n ...execute(this._config.popperConfig, [defaultBsPopperConfig])\n }\n }\n\n _setListeners() {\n const triggers = this._config.trigger.split(' ')\n\n for (const trigger of triggers) {\n if (trigger === 'click') {\n EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK), this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event)\n context.toggle()\n })\n } else if (trigger !== TRIGGER_MANUAL) {\n const eventIn = trigger === TRIGGER_HOVER ?\n this.constructor.eventName(EVENT_MOUSEENTER) :\n this.constructor.eventName(EVENT_FOCUSIN)\n const eventOut = trigger === TRIGGER_HOVER ?\n this.constructor.eventName(EVENT_MOUSELEAVE) :\n this.constructor.eventName(EVENT_FOCUSOUT)\n\n EventHandler.on(this._element, eventIn, this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event)\n context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = true\n context._enter()\n })\n EventHandler.on(this._element, eventOut, this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event)\n context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] =\n context._element.contains(event.relatedTarget)\n\n context._leave()\n })\n }\n }\n\n this._hideModalHandler = () => {\n if (this._element) {\n this.hide()\n }\n }\n\n EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler)\n }\n\n _fixTitle() {\n const title = this._element.getAttribute('title')\n\n if (!title) {\n return\n }\n\n if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) {\n this._element.setAttribute('aria-label', title)\n }\n\n this._element.setAttribute('data-bs-original-title', title) // DO NOT USE IT. Is only for backwards compatibility\n this._element.removeAttribute('title')\n }\n\n _enter() {\n if (this._isShown() || this._isHovered) {\n this._isHovered = true\n return\n }\n\n this._isHovered = true\n\n this._setTimeout(() => {\n if (this._isHovered) {\n this.show()\n }\n }, this._config.delay.show)\n }\n\n _leave() {\n if (this._isWithActiveTrigger()) {\n return\n }\n\n this._isHovered = false\n\n this._setTimeout(() => {\n if (!this._isHovered) {\n this.hide()\n }\n }, this._config.delay.hide)\n }\n\n _setTimeout(handler, timeout) {\n clearTimeout(this._timeout)\n this._timeout = setTimeout(handler, timeout)\n }\n\n _isWithActiveTrigger() {\n return Object.values(this._activeTrigger).includes(true)\n }\n\n _getConfig(config) {\n const dataAttributes = Manipulator.getDataAttributes(this._element)\n\n for (const dataAttribute of Object.keys(dataAttributes)) {\n if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) {\n delete dataAttributes[dataAttribute]\n }\n }\n\n config = {\n ...dataAttributes,\n ...(typeof config === 'object' && config ? config : {})\n }\n config = this._mergeConfigObj(config)\n config = this._configAfterMerge(config)\n this._typeCheckConfig(config)\n return config\n }\n\n _configAfterMerge(config) {\n config.container = config.container === false ? document.body : getElement(config.container)\n\n if (typeof config.delay === 'number') {\n config.delay = {\n show: config.delay,\n hide: config.delay\n }\n }\n\n if (typeof config.title === 'number') {\n config.title = config.title.toString()\n }\n\n if (typeof config.content === 'number') {\n config.content = config.content.toString()\n }\n\n return config\n }\n\n _getDelegateConfig() {\n const config = {}\n\n for (const [key, value] of Object.entries(this._config)) {\n if (this.constructor.Default[key] !== value) {\n config[key] = value\n }\n }\n\n config.selector = false\n config.trigger = 'manual'\n\n // In the future can be replaced with:\n // const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]])\n // `Object.fromEntries(keysWithDifferentValues)`\n return config\n }\n\n _disposePopper() {\n if (this._popper) {\n this._popper.destroy()\n this._popper = null\n }\n\n if (this.tip) {\n this.tip.remove()\n this.tip = null\n }\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Tooltip.getOrCreateInstance(this, config)\n\n if (typeof config !== 'string') {\n return\n }\n\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config]()\n })\n }\n}\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Tooltip)\n\nexport default Tooltip\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap popover.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport Tooltip from './tooltip.js'\nimport { defineJQueryPlugin } from './util/index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'popover'\n\nconst SELECTOR_TITLE = '.popover-header'\nconst SELECTOR_CONTENT = '.popover-body'\n\nconst Default = {\n ...Tooltip.Default,\n content: '',\n offset: [0, 8],\n placement: 'right',\n template: '
' +\n '
' +\n '

' +\n '
' +\n '
',\n trigger: 'click'\n}\n\nconst DefaultType = {\n ...Tooltip.DefaultType,\n content: '(null|string|element|function)'\n}\n\n/**\n * Class definition\n */\n\nclass Popover extends Tooltip {\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Overrides\n _isWithContent() {\n return this._getTitle() || this._getContent()\n }\n\n // Private\n _getContentForTemplate() {\n return {\n [SELECTOR_TITLE]: this._getTitle(),\n [SELECTOR_CONTENT]: this._getContent()\n }\n }\n\n _getContent() {\n return this._resolvePossibleFunction(this._config.content)\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Popover.getOrCreateInstance(this, config)\n\n if (typeof config !== 'string') {\n return\n }\n\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config]()\n })\n }\n}\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Popover)\n\nexport default Popover\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap scrollspy.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport SelectorEngine from './dom/selector-engine.js'\nimport { defineJQueryPlugin, getElement, isDisabled, isVisible } from './util/index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'scrollspy'\nconst DATA_KEY = 'bs.scrollspy'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\n\nconst EVENT_ACTIVATE = `activate${EVENT_KEY}`\nconst EVENT_CLICK = `click${EVENT_KEY}`\nconst EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item'\nconst CLASS_NAME_ACTIVE = 'active'\n\nconst SELECTOR_DATA_SPY = '[data-bs-spy=\"scroll\"]'\nconst SELECTOR_TARGET_LINKS = '[href]'\nconst SELECTOR_NAV_LIST_GROUP = '.nav, .list-group'\nconst SELECTOR_NAV_LINKS = '.nav-link'\nconst SELECTOR_NAV_ITEMS = '.nav-item'\nconst SELECTOR_LIST_ITEMS = '.list-group-item'\nconst SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`\nconst SELECTOR_DROPDOWN = '.dropdown'\nconst SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle'\n\nconst Default = {\n offset: null, // TODO: v6 @deprecated, keep it for backwards compatibility reasons\n rootMargin: '0px 0px -25%',\n smoothScroll: false,\n target: null,\n threshold: [0.1, 0.5, 1]\n}\n\nconst DefaultType = {\n offset: '(number|null)', // TODO v6 @deprecated, keep it for backwards compatibility reasons\n rootMargin: 'string',\n smoothScroll: 'boolean',\n target: 'element',\n threshold: 'array'\n}\n\n/**\n * Class definition\n */\n\nclass ScrollSpy extends BaseComponent {\n constructor(element, config) {\n super(element, config)\n\n // this._element is the observablesContainer and config.target the menu links wrapper\n this._targetLinks = new Map()\n this._observableSections = new Map()\n this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element\n this._activeTarget = null\n this._observer = null\n this._previousScrollData = {\n visibleEntryTop: 0,\n parentScrollTop: 0\n }\n this.refresh() // initialize\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n refresh() {\n this._initializeTargetsAndObservables()\n this._maybeEnableSmoothScroll()\n\n if (this._observer) {\n this._observer.disconnect()\n } else {\n this._observer = this._getNewObserver()\n }\n\n for (const section of this._observableSections.values()) {\n this._observer.observe(section)\n }\n }\n\n dispose() {\n this._observer.disconnect()\n super.dispose()\n }\n\n // Private\n _configAfterMerge(config) {\n // TODO: on v6 target should be given explicitly & remove the {target: 'ss-target'} case\n config.target = getElement(config.target) || document.body\n\n // TODO: v6 Only for backwards compatibility reasons. Use rootMargin only\n config.rootMargin = config.offset ? `${config.offset}px 0px -30%` : config.rootMargin\n\n if (typeof config.threshold === 'string') {\n config.threshold = config.threshold.split(',').map(value => Number.parseFloat(value))\n }\n\n return config\n }\n\n _maybeEnableSmoothScroll() {\n if (!this._config.smoothScroll) {\n return\n }\n\n // unregister any previous listeners\n EventHandler.off(this._config.target, EVENT_CLICK)\n\n EventHandler.on(this._config.target, EVENT_CLICK, SELECTOR_TARGET_LINKS, event => {\n const observableSection = this._observableSections.get(event.target.hash)\n if (observableSection) {\n event.preventDefault()\n const root = this._rootElement || window\n const height = observableSection.offsetTop - this._element.offsetTop\n if (root.scrollTo) {\n root.scrollTo({ top: height, behavior: 'smooth' })\n return\n }\n\n // Chrome 60 doesn't support `scrollTo`\n root.scrollTop = height\n }\n })\n }\n\n _getNewObserver() {\n const options = {\n root: this._rootElement,\n threshold: this._config.threshold,\n rootMargin: this._config.rootMargin\n }\n\n return new IntersectionObserver(entries => this._observerCallback(entries), options)\n }\n\n // The logic of selection\n _observerCallback(entries) {\n const targetElement = entry => this._targetLinks.get(`#${entry.target.id}`)\n const activate = entry => {\n this._previousScrollData.visibleEntryTop = entry.target.offsetTop\n this._process(targetElement(entry))\n }\n\n const parentScrollTop = (this._rootElement || document.documentElement).scrollTop\n const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop\n this._previousScrollData.parentScrollTop = parentScrollTop\n\n for (const entry of entries) {\n if (!entry.isIntersecting) {\n this._activeTarget = null\n this._clearActiveClass(targetElement(entry))\n\n continue\n }\n\n const entryIsLowerThanPrevious = entry.target.offsetTop >= this._previousScrollData.visibleEntryTop\n // if we are scrolling down, pick the bigger offsetTop\n if (userScrollsDown && entryIsLowerThanPrevious) {\n activate(entry)\n // if parent isn't scrolled, let's keep the first visible item, breaking the iteration\n if (!parentScrollTop) {\n return\n }\n\n continue\n }\n\n // if we are scrolling up, pick the smallest offsetTop\n if (!userScrollsDown && !entryIsLowerThanPrevious) {\n activate(entry)\n }\n }\n }\n\n _initializeTargetsAndObservables() {\n this._targetLinks = new Map()\n this._observableSections = new Map()\n\n const targetLinks = SelectorEngine.find(SELECTOR_TARGET_LINKS, this._config.target)\n\n for (const anchor of targetLinks) {\n // ensure that the anchor has an id and is not disabled\n if (!anchor.hash || isDisabled(anchor)) {\n continue\n }\n\n const observableSection = SelectorEngine.findOne(decodeURI(anchor.hash), this._element)\n\n // ensure that the observableSection exists & is visible\n if (isVisible(observableSection)) {\n this._targetLinks.set(decodeURI(anchor.hash), anchor)\n this._observableSections.set(anchor.hash, observableSection)\n }\n }\n }\n\n _process(target) {\n if (this._activeTarget === target) {\n return\n }\n\n this._clearActiveClass(this._config.target)\n this._activeTarget = target\n target.classList.add(CLASS_NAME_ACTIVE)\n this._activateParents(target)\n\n EventHandler.trigger(this._element, EVENT_ACTIVATE, { relatedTarget: target })\n }\n\n _activateParents(target) {\n // Activate dropdown parents\n if (target.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) {\n SelectorEngine.findOne(SELECTOR_DROPDOWN_TOGGLE, target.closest(SELECTOR_DROPDOWN))\n .classList.add(CLASS_NAME_ACTIVE)\n return\n }\n\n for (const listGroup of SelectorEngine.parents(target, SELECTOR_NAV_LIST_GROUP)) {\n // Set triggered links parents as active\n // With both
    and