├── .Rbuildignore ├── .Rprofile ├── .github ├── .gitignore └── workflows │ ├── check-standard.yaml │ ├── eslint.yml │ ├── node.js.yml │ ├── pkgdown.yaml │ ├── pylint.yml │ └── python-package-conda.yml ├── .gitignore ├── .pylintrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── DESCRIPTION ├── LICENSE ├── LICENSE.md ├── NAMESPACE ├── PythonSettings.json ├── R ├── README.md ├── app_server.R ├── app_ui.R ├── cat_gifs.R ├── data.R ├── datamation_sanddance.R ├── datamation_tibble.R ├── datamations-package.R ├── dmta_group_by.R ├── dmta_summarize.R ├── dmta_ungroup.R ├── generate_mapping.R ├── generate_mapping_from_plot.R ├── make_coords.R ├── map_coords.R ├── mod_data_tabs.R ├── mod_datamation_sanddance.R ├── mod_inputs.R ├── mod_pipeline.R ├── parse_functions.R ├── parse_pipeline.R ├── prep_specs_count.R ├── prep_specs_data.R ├── prep_specs_filter.R ├── prep_specs_group_by.R ├── prep_specs_mutate.R ├── prep_specs_summarize.R ├── prep_specs_tally.R ├── prep_specs_utils.R ├── run_app.R ├── snake.R ├── theme_zilch.R ├── utils-pipe.R └── zzz.R ├── README.Rmd ├── README.md ├── SECURITY.md ├── _pkgdown.yml ├── app.R ├── azure-pipelines.yml ├── data-raw ├── .DS_Store ├── jeter.R ├── penguins.csv ├── products.csv ├── small_salary.R └── small_salary.csv ├── data ├── jeter_justice.rda └── small_salary.rda ├── datamations.Rproj ├── datamations ├── README.md ├── __init__.py ├── datamation_frame.py ├── datamation_groupby.py ├── pytest.ini ├── small_salary.py ├── tests │ ├── __init__.py │ ├── test_datamation_frame.py │ ├── test_datamation_groupby.py │ └── test_small_salary.py └── utils.py ├── demo-app ├── app.R ├── githublink.html └── www │ └── style.css ├── dist ├── datamations-1.0-py2.7.egg └── datamations-1.0-py3.9.egg ├── inst ├── README.md ├── app │ └── www │ │ └── style.css ├── export-gif │ ├── exported.gif │ ├── index.js │ ├── package-lock.json │ └── package.json ├── golem-config.yml ├── htmlwidgets │ ├── README.md │ ├── css │ │ ├── datamationSandDance.css │ │ └── fa-all.min.css │ ├── d3 │ │ ├── LICENSE │ │ └── d3.js │ ├── datamationSandDance.js │ ├── datamationSandDance.yaml │ ├── dev-app │ │ └── index.html │ ├── gemini │ │ ├── LICENSE │ │ └── gemini.web.js │ ├── js │ │ ├── .eslintrc.js │ │ ├── README.md │ │ ├── docs │ │ │ ├── app.js.md │ │ │ ├── custom-animations.js.md │ │ │ ├── generate.sh │ │ │ ├── hack-facet-view.js.md │ │ │ ├── layout.js.md │ │ │ └── utils.js.md │ │ ├── download2.js │ │ ├── gifshot.min.js │ │ ├── html2canvas.min.js │ │ └── src │ │ │ ├── .babelrc │ │ │ ├── dist │ │ │ ├── cjs │ │ │ │ ├── index.js │ │ │ │ └── index.js.map │ │ │ ├── datamations.min.js │ │ │ ├── datamations.min.js.map │ │ │ └── esm │ │ │ │ ├── index.js │ │ │ │ └── index.js.map │ │ │ ├── index.js │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ ├── rollup.config.js │ │ │ ├── scripts │ │ │ ├── app.js │ │ │ ├── config.js │ │ │ ├── custom-animations.js │ │ │ ├── datamation-sanddance.js │ │ │ ├── hack-facet-view.js │ │ │ ├── layout.js │ │ │ └── utils.js │ │ │ └── test │ │ │ └── test.js │ ├── vega-embed │ │ ├── LICENSE │ │ └── vega-embed.js │ ├── vega-lite │ │ ├── LICENSE │ │ └── vega-lite.js │ ├── vega-util │ │ └── vega-util.js │ ├── vega │ │ ├── LICENSE │ │ └── vega.js │ └── webfonts │ │ ├── fa-brands-400.eot │ │ ├── fa-brands-400.svg │ │ ├── fa-brands-400.ttf │ │ ├── fa-brands-400.woff │ │ ├── fa-brands-400.woff2 │ │ ├── fa-regular-400.eot │ │ ├── fa-regular-400.svg │ │ ├── fa-regular-400.ttf │ │ ├── fa-regular-400.woff │ │ ├── fa-regular-400.woff2 │ │ ├── fa-solid-900.eot │ │ ├── fa-solid-900.svg │ │ ├── fa-solid-900.ttf │ │ ├── fa-solid-900.woff │ │ └── fa-solid-900.woff2 └── specs │ ├── count_specs_one_column.json │ ├── count_specs_two_columns.json │ ├── groupby_degree_work.json │ ├── groupby_work.json │ ├── groupby_work_degree.json │ ├── max_specs_two_columns.json │ ├── min_specs_two_columns.json │ ├── prod_specs.json │ ├── prod_specs_two_columns.json │ ├── product_spec.json │ ├── quantile_specs_two_columns.json │ ├── raw_spec.json │ ├── sum_specs.json │ └── sum_specs_two_columns.json ├── man ├── datamationSandDance-shiny.Rd ├── datamation_sanddance.Rd ├── datamation_tibble.Rd ├── figures │ ├── README-ggplot2-existing-plot-1.png │ ├── README-mean_salary_group_by_degree-table.gif │ ├── README-mean_salary_group_by_degree.gif │ ├── README-mean_salary_group_by_degree_work.gif │ ├── README-mean_salary_group_by_degree_work_ggplot.gif │ ├── README-mean_salary_grouped_degree.gif │ └── README-mtcars_group_cyl.gif ├── jeter_justice.Rd ├── pipe.Rd ├── run_app.Rd └── small_salary.Rd ├── notebooks └── Datamations.ipynb ├── package-lock.json ├── renv.lock ├── renv ├── .gitignore ├── activate.R └── settings.dcf ├── sandbox ├── custom_animations │ ├── custom-animations-binary-R.json │ ├── custom-animations-count-R.json │ ├── custom-animations-count-manual.json │ ├── custom-animations-max-R.json │ ├── custom-animations-max-facet.json │ ├── custom-animations-max-manual.json │ ├── custom-animations-mean-R.json │ ├── custom-animations-mean-facet.json │ ├── custom-animations-mean-manual.json │ ├── custom-animations-median-R.json │ ├── custom-animations-median-facet.json │ ├── custom-animations-median-faceted-R.json │ ├── custom-animations-median-manual.json │ ├── custom-animations-min-R.json │ ├── custom-animations-min-facet.json │ ├── custom-animations-min-manual.json │ ├── custom-animations-quantile-R.json │ ├── custom-animations-quantile-manual.json │ └── custom-animations-sum-manual.json ├── labeling │ ├── long-label-specs-R.json │ └── long-label-specs-split-R.json ├── mutations │ ├── Mutation examples.html │ ├── grouped_mutation_specs-R.json │ ├── mutation_specs_multiple_variables-R.json │ ├── ungrouped_mutation_specs-2-R.json │ ├── ungrouped_mutation_specs-R.json │ └── ungrouped_mutation_specs-binary-R.json ├── penguins_median_specs.json ├── penguins_three_groups.json ├── products_mean_rating.json └── simpsons_paradox │ ├── group_by_player.json │ ├── group_by_player_year.json │ └── simpsons_paradox_specs.R ├── setup.py ├── tests ├── testthat.R └── testthat │ ├── helper.R │ ├── python_specs │ ├── groupby_work.json │ ├── product_spec.json │ ├── raw_spec.json │ └── specs │ │ ├── groupby_work.json │ │ ├── product_spec.json │ │ └── raw_spec.json │ ├── setup.R │ ├── test-datamation_sanddance.R │ ├── test-generate_mapping_from_plot.R │ ├── test-mod_data_tabs.R │ ├── test-parse_functions.R │ ├── test-parse_pipeline.R │ ├── test-prep_specs_count.R │ ├── test-prep_specs_data.R │ ├── test-prep_specs_filter.R │ ├── test-prep_specs_group_by.R │ ├── test-prep_specs_summarize.R │ └── test-snake.R └── vignettes ├── .gitignore ├── Examples.Rmd ├── details.Rmd ├── finer_control.Rmd ├── groupby_api.Rmd ├── mutations.Rmd ├── simpsons_paradox.Rmd ├── summarizing.Rmd └── variable_types.Rmd /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^renv$ 2 | ^renv\.lock$ 3 | ^.*\.Rproj$ 4 | ^\.Rproj\.user$ 5 | start-up.R 6 | ^README\.Rmd$ 7 | ^\.github$ 8 | ^renv$ 9 | ^README_cache$ 10 | ^data-raw$ 11 | ^app\.R$ 12 | ^rsconnect$ 13 | ^sandbox$ 14 | azure-pipelines.yml 15 | ^_pkgdown\.yml$ 16 | ^R/README\.md$ 17 | ^LICENSE\.md$ 18 | ^inst/htmlwidgets/test$ 19 | ^datamations$ 20 | ^scratch$ 21 | ^setup\.py$ 22 | ^notebooks$ 23 | ^PythonSettings\.json$ 24 | ^demo-app$ -------------------------------------------------------------------------------- /.Rprofile: -------------------------------------------------------------------------------- 1 | source("renv/activate.R") 2 | -------------------------------------------------------------------------------- /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /.github/workflows/check-standard.yaml: -------------------------------------------------------------------------------- 1 | # For help debugging build failures open an issue on the RStudio community with the 'github-actions' tag. 2 | # https://community.rstudio.com/new-topic?category=Package%20development&tags=github-actions 3 | name: R-CMD-check 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | 10 | pull_request: 11 | branches: 12 | - main 13 | 14 | jobs: 15 | R-CMD-check: 16 | runs-on: ${{ matrix.config.os }} 17 | 18 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | config: 24 | - {os: windows-latest, r: 'release'} 25 | - {os: macOS-latest, r: 'release'} 26 | # - {os: ubuntu-20.04, r: 'release', rspm: "https://packagemanager.rstudio.com/cran/__linux__/focal/latest"} 27 | # - {os: ubuntu-20.04, r: 'devel', rspm: "https://packagemanager.rstudio.com/cran/__linux__/focal/latest"} 28 | 29 | env: 30 | R_REMOTES_NO_ERRORS_FROM_WARNINGS: true 31 | RSPM: ${{ matrix.config.rspm }} 32 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 33 | 34 | steps: 35 | - uses: actions/checkout@v2 36 | 37 | - uses: r-lib/actions/setup-r@v2 38 | with: 39 | r-version: ${{ matrix.config.r }} 40 | 41 | - uses: r-lib/actions/setup-pandoc@v1 42 | 43 | - name: Query dependencies 44 | run: | 45 | install.packages('remotes') 46 | saveRDS(remotes::dev_package_deps(dependencies = TRUE), ".github/depends.Rds", version = 2) 47 | writeLines(sprintf("R-%i.%i", getRversion()$major, getRversion()$minor), ".github/R-version") 48 | shell: Rscript {0} 49 | 50 | - name: Restore R package cache 51 | if: runner.os != 'Windows' 52 | uses: actions/cache@v2 53 | with: 54 | path: ${{ env.R_LIBS_USER }} 55 | key: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1-${{ hashFiles('.github/depends.Rds') }} 56 | restore-keys: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1- 57 | 58 | - name: Install system dependencies 59 | if: runner.os == 'Linux' 60 | run: | 61 | while read -r cmd 62 | do 63 | eval sudo $cmd 64 | done < <(Rscript -e 'writeLines(remotes::system_requirements("ubuntu", "20.04"))') 65 | 66 | - name: Install dependencies 67 | run: | 68 | remotes::install_deps(dependencies = TRUE) 69 | remotes::install_cran("rcmdcheck") 70 | shell: Rscript {0} 71 | 72 | - name: Check 73 | env: 74 | _R_CHECK_CRAN_INCOMING_REMOTE_: false 75 | run: | 76 | options(crayon.enabled = TRUE) 77 | rcmdcheck::rcmdcheck(args = c("--no-manual", "--as-cran", "--no-build-vignettes"), build_args = c("--no-build-vignettes"), error_on = "error", check_dir = "check") 78 | shell: Rscript {0} 79 | 80 | - name: Upload check results 81 | if: failure() 82 | uses: actions/upload-artifact@main 83 | with: 84 | name: ${{ runner.os }}-r${{ matrix.config.r }}-results 85 | path: check 86 | -------------------------------------------------------------------------------- /.github/workflows/eslint.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # ESLint is a tool for identifying and reporting on patterns 6 | # found in ECMAScript/JavaScript code. 7 | # More details at https://github.com/eslint/eslint 8 | # and https://eslint.org 9 | 10 | name: ESLint 11 | 12 | on: [push] 13 | 14 | jobs: 15 | eslint: 16 | name: Run eslint scanning 17 | runs-on: ubuntu-latest 18 | permissions: 19 | contents: read 20 | security-events: write 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v3 24 | 25 | - name: Install ESLint 26 | run: | 27 | npm install eslint-config-standard eslint@8.10.0 @microsoft/eslint-formatter-sarif@2.1.7 28 | 29 | - name: Run ESLint 30 | run: npx eslint inst/htmlwidgets/js/src/scripts/**/*.js 31 | --config inst/htmlwidgets/js/.eslintrc.js 32 | --ext .js,.jsx,.ts,.tsx 33 | --output-file eslint-results.sarif 34 | continue-on-error: false 35 | 36 | - name: Run ESLint 37 | run: npx eslint inst/htmlwidgets/js/src/test/**/*.js 38 | --config inst/htmlwidgets/js/.eslintrc.js 39 | --ext .js,.jsx,.ts,.tsx 40 | --output-file eslint-results.sarif 41 | continue-on-error: false 42 | 43 | # - name: Upload analysis results to GitHub 44 | # uses: github/codeql-action/upload-sarif@v2 45 | # with: 46 | # sarif_file: eslint-results.sarif 47 | # wait-for-processing: true 48 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: [push] 7 | 8 | defaults: 9 | run: 10 | working-directory: inst/htmlwidgets/js/src 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [12.x, 14.x, 16.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | - run: npm run build --if-present 31 | - run: npm test 32 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | - master 6 | tags: 7 | -'*' 8 | 9 | name: pkgdown 10 | 11 | jobs: 12 | pkgdown: 13 | runs-on: macOS-latest 14 | env: 15 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - uses: r-lib/actions/setup-r@v1 20 | 21 | - uses: r-lib/actions/setup-pandoc@v1 22 | 23 | - name: Query dependencies 24 | run: | 25 | install.packages('remotes') 26 | saveRDS(remotes::dev_package_deps(dependencies = TRUE), ".github/depends.Rds", version = 2) 27 | writeLines(sprintf("R-%i.%i", getRversion()$major, getRversion()$minor), ".github/R-version") 28 | shell: Rscript {0} 29 | 30 | - name: Restore R package cache 31 | uses: actions/cache@v2 32 | with: 33 | path: ${{ env.R_LIBS_USER }} 34 | key: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1-${{ hashFiles('.github/depends.Rds') }} 35 | restore-keys: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1- 36 | 37 | - name: Install dependencies 38 | run: | 39 | remotes::install_deps(dependencies = TRUE) 40 | install.packages("pkgdown", type = "binary") 41 | shell: Rscript {0} 42 | 43 | - name: Install package 44 | run: R CMD INSTALL . 45 | 46 | - name: Deploy package 47 | run: | 48 | git config --local user.email "actions@github.com" 49 | git config --local user.name "GitHub Actions" 50 | Rscript -e 'pkgdown::deploy_to_branch(new_process = FALSE)' 51 | -------------------------------------------------------------------------------- /.github/workflows/pylint.yml: -------------------------------------------------------------------------------- 1 | name: Pylint 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ["3.8", "3.9", "3.10"] 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Set up Python ${{ matrix.python-version }} 14 | uses: actions/setup-python@v3 15 | with: 16 | python-version: ${{ matrix.python-version }} 17 | - name: Install dependencies 18 | run: | 19 | python -m pip install --upgrade pip 20 | pip install pylint 21 | - name: Analysing the code with pylint 22 | run: | 23 | pylint $(git ls-files '*.py') 24 | -------------------------------------------------------------------------------- /.github/workflows/python-package-conda.yml: -------------------------------------------------------------------------------- 1 | name: Python Package using Conda 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build-linux: 7 | runs-on: ubuntu-20.04 8 | strategy: 9 | max-parallel: 5 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Set up Python 3.8.8 14 | uses: actions/setup-python@v2 15 | with: 16 | python-version: 3.8.8 17 | - name: Add conda to system path 18 | run: | 19 | # $CONDA is an environment variable pointing to the root of the miniconda directory 20 | echo $CONDA/bin >> $GITHUB_PATH 21 | - name: Install dependencies 22 | run: | 23 | #conda env update --file environment.yml --name base 24 | # install python package 25 | python setup.py install 26 | - name: Lint with flake8 27 | run: | 28 | conda install flake8 29 | # stop the build if there are Python syntax errors or undefined names 30 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 31 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 32 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 33 | - name: Test with pytest 34 | run: | 35 | conda install pytest 36 | pytest 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rhistory 2 | .RData 3 | .Rproj.user 4 | .DS_Store 5 | rsconnect/ 6 | .vs/ 7 | .vscode/ 8 | .idea/ 9 | *.egg-info/ 10 | lib/ 11 | dist/ 12 | build/ 13 | __pycache__/ 14 | *.pyc 15 | .ipynb_checkpoints 16 | *.xml 17 | .venv 18 | node_modules 19 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MESSAGES CONTROL] 2 | ; General diable of the convention, refactor, warning pylint error. Forporbable bugs and fatal--ifan error are enabled 3 | ; disable=C, W, R, import-error 4 | 5 | ; below is diabling each specific of each not needed error 6 | disable=too-many-branches, 7 | unspecified-encoding, 8 | import-error, 9 | line-too-long, 10 | consider-using-f-string, 11 | missing-module-docstring, 12 | missing-function-docstring, 13 | missing-class-docstring, 14 | too-few-public-methods, 15 | too-many-arguments, 16 | too-many-statements, 17 | too-many-locals, 18 | redefined-builtin, 19 | arguments-differ, 20 | unused-variable, ; warning of unused variable diabled 21 | invalid-name, ; warning of invalid name disabled 22 | unused-argument, ; warning of unused argument diabled 23 | super-with-arguments, ; warning of supper argument diabled 24 | protected-access, 25 | arguments-renamed, ; overridden warning diabled 26 | too-many-nested-blocks, 27 | arguments-renamed, ; warning of parameter changed after overridden 28 | too-many-ancestors, 29 | duplicate-code, 30 | no-self-argument, 31 | redefined-outer-name, 32 | cyclic-import ; Cyclic import (%s) Used when a cyclic import between two or more modules is detected -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // List of extensions which should be recommended for users of this workspace. 3 | "recommendations": [ 4 | "ms-python.python", 5 | "dbaeumer.vscode-eslint", 6 | "esbenp.prettier-vscode" 7 | ], 8 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 9 | "unwantedRecommendations": [] 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Edge", 9 | "request": "launch", 10 | "type": "msedge", 11 | "url": "http://localhost:3000/inst/htmlwidgets/dev-app/", 12 | "webRoot": "${workspaceFolder}" 13 | }, 14 | { 15 | "type": "node", 16 | "request": "launch", 17 | "name": "Mocha Tests", 18 | "cwd": "${workspaceFolder}/inst/htmlwidgets/js/src/", 19 | "program": "${workspaceFolder}/inst/htmlwidgets/js/src/node_modules/mocha/bin/_mocha", 20 | "args": [ 21 | "--reporter", 22 | "dot", 23 | "--require", 24 | "esm", 25 | "--require", 26 | "@babel/register", 27 | "--colors", 28 | "${workspaceFolder}/inst/htmlwidgets/js/src/test/**/*.js" 29 | ], 30 | "internalConsoleOptions": "openOnSessionStart", 31 | "skipFiles": ["/**"] 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.pytestArgs": [ 3 | "datamations" 4 | ], 5 | "python.testing.unittestEnabled": false, 6 | "python.testing.pytestEnabled": true, 7 | "python.linting.enabled": true, 8 | "task.allowAutomaticTasks": "on" 9 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "build", 7 | "path": "inst/htmlwidgets/js/src/", 8 | "group": "build", 9 | "problemMatcher": [], 10 | "label": "npm: build - inst/htmlwidgets/js/src", 11 | "detail": "rollup -c" 12 | }, 13 | { 14 | "type": "npm", 15 | "script": "test", 16 | "path": "inst/htmlwidgets/js/src/", 17 | "group": { 18 | "kind": "test", 19 | "isDefault": true 20 | }, 21 | "problemMatcher": [], 22 | "label": "npm: test - inst/htmlwidgets/js/src", 23 | "detail": "mocha" 24 | }, 25 | { 26 | "type": "npm", 27 | "script": "dev", 28 | "path": "inst/htmlwidgets/js/src", 29 | "problemMatcher": [], 30 | "label": "npm: dev - inst/htmlwidgets/js/src", 31 | "detail": "rollup -c -w", 32 | "presentation": { 33 | "clear": true, 34 | "panel": "dedicated" 35 | }, 36 | "runOptions": { "runOn": "folderOpen" } 37 | } 38 | ] 39 | } -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: datamations 2 | Type: Package 3 | Title: Animated Explanations of Data Analysis Pipelines 4 | Version: 0.0.0.9009 5 | Authors@R: c( 6 | person("Xiaoying", "Pu", email = "xpu@umich.edu", role = c("aut")), 7 | person("Sean", "Kross", email = "smk240@gmail.com", role = c("aut")), 8 | person("Sharla", "Gelfand", role = c("aut")), 9 | person("Giorgi", "Ghviniashvili", role = c("aut")), 10 | person("Will", "Bonnell", role = c("aut")), 11 | person("Dan", "Goldstein", email = "dgg@microsoft.com", role = c("aut")), 12 | person("Jake", "Hofman", email = "jmh@microsoft.com", role = c("cre", "aut"))) 13 | Description: Generates animations that explain plots and tables from tidyverse pipelines. 14 | License: MIT + file LICENSE 15 | Encoding: UTF-8 16 | LazyData: true 17 | Roxygen: list(markdown = TRUE) 18 | RoxygenNote: 7.1.1 19 | Imports: 20 | rlang, 21 | dplyr, 22 | ggplot2, 23 | gganimate, 24 | magick, 25 | tibble, 26 | magrittr, 27 | purrr (>= 0.3.4.9000), 28 | scales, 29 | stringr, 30 | shiny, 31 | vegawidget, 32 | forcats, 33 | glue, 34 | tidyselect, 35 | htmlwidgets, 36 | reactable, 37 | shinyWidgets, 38 | shinydashboard, 39 | shinyAce, 40 | jsonlite, 41 | palmerpenguins, 42 | golem, 43 | styler, 44 | shinyjs 45 | Suggests: 46 | rmarkdown, 47 | knitr, 48 | testthat (>= 2.1.0) 49 | Depends: 50 | R (>= 2.10) 51 | Remotes: 52 | tidyverse/purrr 53 | Config/testthat/edition: 3 54 | VignetteBuilder: knitr 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2021 2 | COPYRIGHT HOLDER: Microsoft 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2021 Microsoft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export("%>%") 4 | export(datamationSandDanceOutput) 5 | export(datamation_sanddance) 6 | export(datamation_tibble) 7 | export(renderDatamationSandDance) 8 | export(run_app) 9 | importFrom(dplyr,any_of) 10 | importFrom(dplyr,arrange) 11 | importFrom(dplyr,as_tibble) 12 | importFrom(dplyr,bind_rows) 13 | importFrom(dplyr,filter) 14 | importFrom(dplyr,group_by) 15 | importFrom(dplyr,group_indices) 16 | importFrom(dplyr,group_size) 17 | importFrom(dplyr,group_split) 18 | importFrom(dplyr,group_vars) 19 | importFrom(dplyr,if_else) 20 | importFrom(dplyr,is_grouped_df) 21 | importFrom(dplyr,left_join) 22 | importFrom(dplyr,mutate) 23 | importFrom(dplyr,n) 24 | importFrom(dplyr,n_groups) 25 | importFrom(dplyr,pull) 26 | importFrom(dplyr,select) 27 | importFrom(dplyr,summarize) 28 | importFrom(dplyr,tibble) 29 | importFrom(dplyr,ungroup) 30 | importFrom(gganimate,anim_save) 31 | importFrom(gganimate,ease_aes) 32 | importFrom(gganimate,transition_states) 33 | importFrom(gganimate,view_follow) 34 | importFrom(ggplot2,aes) 35 | importFrom(ggplot2,element_blank) 36 | importFrom(ggplot2,geom_point) 37 | importFrom(ggplot2,ggplot) 38 | importFrom(ggplot2,ggtitle) 39 | importFrom(ggplot2,scale_color_manual) 40 | importFrom(ggplot2,theme) 41 | importFrom(magick,image_read) 42 | importFrom(magick,image_write) 43 | importFrom(magrittr,"%>%") 44 | importFrom(purrr,accumulate) 45 | importFrom(purrr,flatten) 46 | importFrom(purrr,map) 47 | importFrom(purrr,map2) 48 | importFrom(purrr,map2_chr) 49 | importFrom(purrr,map2_dbl) 50 | importFrom(purrr,map2_dfr) 51 | importFrom(purrr,map_chr) 52 | importFrom(purrr,map_dbl) 53 | importFrom(purrr,map_dfr) 54 | importFrom(purrr,map_if) 55 | importFrom(purrr,pmap_dbl) 56 | importFrom(purrr,pmap_dfr) 57 | importFrom(purrr,reduce) 58 | importFrom(rlang,":=") 59 | importFrom(rlang,.data) 60 | importFrom(rlang,parse_expr) 61 | importFrom(shiny,NS) 62 | importFrom(shiny,tagList) 63 | importFrom(stats,median) 64 | importFrom(tibble,as_tibble) 65 | importFrom(tibble,tibble) 66 | -------------------------------------------------------------------------------- /PythonSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "TestFramework": "pytest", 3 | "UnitTestRootDirectory": "datamations/tests", 4 | "UnitTestPattern": "test_*.py" 5 | } -------------------------------------------------------------------------------- /R/app_server.R: -------------------------------------------------------------------------------- 1 | #' The application server 2 | #' 3 | #' @param input,output,session Internal parameters for {shiny}. 4 | #' @noRd 5 | app_server <- function(input, output, session) { 6 | inputs <- mod_inputs_server("inputs") 7 | pipeline <- mod_pipeline_server("pipeline", inputs) 8 | 9 | mod_datamation_sanddance_server("datamation_sanddance", pipeline) 10 | 11 | slider_state <- shiny::reactiveVal() 12 | tab_change <- shiny::reactiveVal() 13 | 14 | shiny::observeEvent(input$slider_state, { 15 | slider_state(input$slider_state) 16 | tab_change("slider") 17 | }) 18 | 19 | mod_data_tabs_server("data_tabs", inputs, pipeline, slider_state, tab_change) 20 | } 21 | -------------------------------------------------------------------------------- /R/app_ui.R: -------------------------------------------------------------------------------- 1 | #' The application User Interface 2 | #' 3 | #' @param request Internal parameter for `{shiny}`. 4 | #' @noRd 5 | app_ui <- function(request) { 6 | shiny::tagList( 7 | shinyWidgets::useShinydashboard(), 8 | shinyjs::useShinyjs(), 9 | golem_add_external_resources(), 10 | # Send slider value, for changing tabs 11 | shiny::tags$script(shiny::HTML(' 12 | $(document).ready(function() { 13 | $(document).on("change", ".slider", function() { 14 | Shiny.onInputChange("slider_state", $(this).val()); 15 | }) 16 | })')), 17 | # Listen to tab value, for changing slider! 18 | shiny::tags$script(" 19 | Shiny.addCustomMessageHandler('slider-from-tab', function(tab) { 20 | document.getElementById('datamation_sanddance-datamation').getElementsByClassName('slider')[0].value = tab; 21 | onSlide('datamation_sanddance-datamation'); 22 | }); 23 | "), 24 | shiny::fluidPage( 25 | style = "max-width: 1200px;", 26 | shiny::h1("Datamations"), 27 | mod_inputs_ui("inputs"), 28 | mod_pipeline_ui("pipeline"), 29 | shiny::fluidRow( 30 | shinydashboard::box( 31 | width = 12, 32 | solidHeader = TRUE, 33 | shiny::column( 34 | width = 6, 35 | shiny::h2("datamation"), 36 | mod_datamation_sanddance_ui("datamation_sanddance") 37 | ), 38 | shiny::column( 39 | width = 6, 40 | shiny::h2("data stages"), 41 | mod_data_tabs_ui("data_tabs") 42 | ) 43 | ) 44 | ) 45 | ) 46 | ) 47 | } 48 | 49 | #' Add external Resources to the Application 50 | #' 51 | #' This function is internally used to add external 52 | #' resources inside the Shiny application. 53 | #' 54 | #' @noRd 55 | golem_add_external_resources <- function() { 56 | golem::add_resource_path( 57 | "www", app_sys("app/www") 58 | ) 59 | 60 | shiny::tags$head( 61 | golem::favicon(ext = "png"), 62 | golem::bundle_resources( 63 | path = app_sys("app/www"), 64 | app_title = "Datamations" 65 | ) 66 | ) 67 | } 68 | 69 | app_sys <- function(...) { 70 | system.file(..., package = "datamations") 71 | } 72 | -------------------------------------------------------------------------------- /R/cat_gifs.R: -------------------------------------------------------------------------------- 1 | #' Concatenate GIFs 2 | #' 3 | #' @param paths A vector of paths to `.gif` files. 4 | #' @param output A path where the GIF will be written. 5 | #' @importFrom magick image_read image_write 6 | #' @importFrom purrr map reduce 7 | #' @importFrom magrittr "%>%" 8 | #' @noRd 9 | #' @examples 10 | #' \dontrun{ 11 | #' 12 | #' library(ggplot2) 13 | #' library(gganimate) 14 | #' library(tibble) 15 | #' library(dplyr) 16 | #' 17 | #' mt_animate <- mtcars %>% 18 | #' tibble::as_tibble() %>% 19 | #' dplyr::mutate(Row = row_number()) 20 | #' 21 | #' part1 <- mt_animate %>% 22 | #' dplyr::filter(Row < 17) 23 | #' 24 | #' part2 <- mt_animate %>% 25 | #' dplyr::filter(Row >= 17) 26 | #' 27 | #' temp_dir <- tempdir() 28 | #' 29 | #' part1 %>% 30 | #' ggplot(aes(mpg, hp)) + 31 | #' geom_point() + 32 | #' labs(title = "Row: {frame_time}") + 33 | #' transition_time(Row) + 34 | #' ease_aes("linear") 35 | #' 36 | #' anim_save(filename = file.path(temp_dir, "part1.gif")) 37 | #' 38 | #' part2 %>% 39 | #' ggplot(aes(mpg, hp)) + 40 | #' geom_point() + 41 | #' labs(title = "Row: {frame_time}") + 42 | #' transition_time(Row) + 43 | #' ease_aes("linear") 44 | #' 45 | #' anim_save(filename = file.path(temp_dir, "part2.gif")) 46 | #' 47 | #' cat_gifs( 48 | #' c( 49 | #' file.path(temp_dir, "part1.gif"), 50 | #' file.path(temp_dir, "part2.gif") 51 | #' ) 52 | #' ) 53 | #' } 54 | cat_gifs <- function(paths, output = "output.gif") { 55 | paths %>% 56 | map(image_read) %>% 57 | reduce(c) %>% 58 | image_write(output) 59 | 60 | invisible(output) 61 | } 62 | -------------------------------------------------------------------------------- /R/data.R: -------------------------------------------------------------------------------- 1 | #' Hypothetical salary data for 30 workers 2 | #' 3 | "small_salary" 4 | 5 | #' Hit data for Derek Jeter and David Justice from the 1995 and 1996 seasons 6 | #' 7 | "jeter_justice" -------------------------------------------------------------------------------- /R/datamation_tibble.R: -------------------------------------------------------------------------------- 1 | #' Create a tibble datamation 2 | #' @importFrom dplyr any_of arrange bind_rows filter group_by group_size group_split group_vars is_grouped_df left_join mutate n n_groups pull select summarize ungroup group_indices 3 | #' @importFrom gganimate anim_save ease_aes transition_states view_follow 4 | #' @importFrom ggplot2 aes element_blank geom_point ggplot ggtitle scale_color_manual theme 5 | #' @importFrom magick image_read image_write 6 | #' @importFrom purrr accumulate map map2 map2_dbl map2_dfr map_chr map_dbl map_dfr map_if pmap_dbl pmap_dfr reduce 7 | #' @importFrom rlang parse_expr 8 | #' @importFrom stats median 9 | #' @importFrom tibble as_tibble tibble 10 | #' @importFrom magrittr "%>%" 11 | #' @importFrom purrr map map_chr 12 | #' @importFrom rlang parse_expr 13 | #' @param pipeline A tidyverse pipeline. 14 | #' @param envir An environment. 15 | #' @param output Path to where gif will be saved. 16 | #' @param titles Optional titles for the datamation frames 17 | #' @param xlim Optional x limits 18 | #' @param ylim Optional y limits 19 | #' @export 20 | datamation_tibble <- function(pipeline, envir = rlang::global_env(), 21 | output = "output.gif", titles = NA, 22 | xlim = c(NA, NA), ylim = c(NA, NA)) { 23 | 24 | # Specify which functions are supported, for parsing functions out and for erroring if any are not in this list 25 | supported_tidy_functions <- c("group_by", "summarize", "filter") 26 | 27 | # Convert pipeline into list 28 | fittings <- pipeline %>% 29 | parse_pipeline(supported_tidy_functions) 30 | 31 | data_states <- fittings %>% 32 | snake(envir = envir) 33 | 34 | if (length(data_states) < 2) { 35 | stop("No data transformation detected by datamation_tibble", call. = FALSE) 36 | } 37 | 38 | tidy_functions_list <- fittings %>% 39 | map(as.list) %>% 40 | map(~ .x[[1]]) %>% 41 | map_chr(as.character) 42 | 43 | tidy_functions_list <- tidy_functions_list[-1] 44 | 45 | supported_tidy_functions <- c("group_by", "ungroup", "summarize", "summarise", "filter") 46 | 47 | map(tidy_functions_list, ~ if (!(.x %in% supported_tidy_functions)) { 48 | stop(paste(.x, "not supported by datamation_tibble"), call. = FALSE) 49 | }) 50 | 51 | anim_list <- list() 52 | dimensions <- list( 53 | xmin = xlim[1], 54 | xmax = xlim[2], 55 | ymin = ylim[1], 56 | ymax = ylim[2] 57 | ) 58 | 59 | current_state <- list( 60 | df = data_states[[1]], 61 | fitting = fittings[[2]], 62 | title_state = list(titles = titles, current_title = 1), 63 | coords = make_coords(data_states[[1]], 64 | row_ceiling = dimensions$ymax 65 | ) %>% 66 | mutate(Color = "#C0C0C0") 67 | ) 68 | next_state <- list(df = data_states[[2]]) 69 | 70 | for (i in 1:(length(data_states) - 1)) { 71 | if (tidy_functions_list[i] == "group_by") { 72 | result <- dmta_group_by(current_state, next_state, 73 | dimensions = dimensions, anim_title = titles[i] 74 | ) 75 | anim_list <- c(anim_list, result$anim_path) 76 | current_state <- result 77 | current_state[["df"]] <- data_states[[i + 1]] 78 | if (length(data_states) >= i + 2) { 79 | current_state[["fitting"]] <- fittings[[i + 2]] 80 | next_state <- list(df = data_states[[i + 2]]) 81 | } 82 | } else if (tidy_functions_list[i] == "ungroup") { 83 | result <- dmta_ungroup(current_state, next_state, 84 | dimensions = dimensions, 85 | anim_title = titles[i] 86 | ) 87 | anim_list <- c(anim_list, result$anim_path) 88 | current_state <- result 89 | current_state[["df"]] <- data_states[[i + 1]] 90 | if (length(data_states) >= i + 2) { 91 | current_state[["fitting"]] <- fittings[[i + 2]] 92 | next_state <- list(df = data_states[[i + 2]]) 93 | } 94 | } else if (tidy_functions_list[i] %in% c("summarize", "summarise")) { 95 | result <- dmta_summarize(current_state, next_state, 96 | dimensions = dimensions, anim_title = titles[i] 97 | ) 98 | anim_list <- c(anim_list, result$anim_path) 99 | current_state <- result 100 | current_state[["df"]] <- data_states[[i + 1]] 101 | if (length(data_states) >= i + 2) { 102 | current_state[["fitting"]] <- fittings[[i + 2]] 103 | next_state <- list(df = data_states[[i + 2]]) 104 | } 105 | } 106 | } 107 | 108 | anim_list <- unlist(anim_list) 109 | if (length(anim_list) == 1) { 110 | file.copy(anim_list[[1]], output, overwrite = TRUE) 111 | } else if (length(anim_list > 1)) { 112 | suppressWarnings(cat_gifs(anim_list, output = output)) 113 | } 114 | 115 | invisible(output) 116 | } 117 | -------------------------------------------------------------------------------- /R/datamations-package.R: -------------------------------------------------------------------------------- 1 | #' @importFrom rlang .data := 2 | NULL 3 | -------------------------------------------------------------------------------- /R/dmta_group_by.R: -------------------------------------------------------------------------------- 1 | #' @importFrom dplyr any_of arrange bind_rows filter group_by group_size group_split group_vars is_grouped_df left_join mutate n n_groups pull select summarize ungroup group_indices if_else 2 | #' @importFrom gganimate anim_save ease_aes transition_states view_follow 3 | #' @importFrom ggplot2 aes element_blank geom_point ggplot ggtitle scale_color_manual theme 4 | #' @importFrom magick image_read image_write 5 | #' @importFrom purrr accumulate map map2 map2_dbl map2_dfr map_chr map_dbl map_dfr map_if pmap_dbl pmap_dfr reduce 6 | #' @importFrom rlang parse_expr 7 | #' @importFrom stats median 8 | #' @importFrom tibble as_tibble tibble 9 | dmta_group_by <- function(state1, state2, dimensions, anim_title = NA) { 10 | grouping_columns <- state2$df %>% 11 | group_vars() 12 | 13 | grouping_columns <- which(names(state2$df) %in% grouping_columns) 14 | 15 | n_columns <- length(state1$df) 16 | n_groups_ <- n_groups(state2$df) 17 | 18 | time1 <- state1$coords %>% 19 | mutate(Time = 1) 20 | 21 | if (!tibble::has_name(time1, "Row_Ungrouped_Coord")) { 22 | time1 <- time1 %>% 23 | mutate(Row_Ungrouped_Coord = .data$Row_Coord) 24 | } 25 | 26 | color_tbl <- state2$df %>% 27 | select(any_of(group_vars(state2$df))) %>% 28 | map(as.factor) %>% 29 | map(as.numeric) %>% 30 | map(~ scales::hue_pal()(max(.x))[.x]) %>% 31 | as_tibble() 32 | 33 | time2 <- time1 %>% 34 | mutate(Color = map2(.data$Col, .data$Row, ~ color_tbl[[colnames(state2$df)[.x]]][.y]) %>% 35 | map_chr(~ if_else(is.null(.x), "#C0C0C0", .x))) %>% 36 | arrange(.data$Row, .data$Col, .data$Time) %>% 37 | mutate(Time = 2) 38 | 39 | time3 <- time2 %>% 40 | arrange(.data$Row, .data$Col) %>% 41 | mutate(Group_Index = rep(state2$df %>% group_indices(), each = n_columns)) %>% 42 | arrange(.data$Group_Index, .data$Row) %>% 43 | mutate(Row_Coord = rep(max(.$Row_Coord):min(.$Row_Coord), each = n_columns)) %>% 44 | mutate(Time = 3) %>% 45 | mutate(Row_Coord = .data$Row_Coord - (.data$Group_Index - 1)) 46 | 47 | anim_data <- bind_rows( 48 | time1, 49 | time2, 50 | time3 %>% 51 | select(-.data$Group_Index) 52 | ) 53 | 54 | anim <- anim_data %>% 55 | ggplot(aes(x = .data$Col, y = .data$Row_Coord)) + 56 | geom_point(aes(color = .data$Color, group = .data$Row_Ungrouped_Coord), shape = "\u25AC", size = 3) + 57 | scale_color_manual( 58 | breaks = unique(anim_data$Color), 59 | values = as.character(unique(anim_data$Color)) 60 | ) + 61 | theme_zilch() 62 | 63 | if (is.na(anim_title)) { 64 | anim <- anim + ggtitle(deparse(state1$fitting)) 65 | } else { 66 | anim <- anim + ggtitle(anim_title) 67 | } 68 | 69 | anim <- anim + 70 | transition_states(.data$Time, 71 | transition_length = 12, 72 | state_length = 10, wrap = FALSE 73 | ) + 74 | ease_aes("cubic-in-out") + 75 | view_follow(fixed_x = c(-15, 19), fixed_y = c(-5, max(anim_data$Row_Coord))) 76 | 77 | anim_path <- tempfile(fileext = ".gif") 78 | anim_save(animation = anim, filename = anim_path) 79 | 80 | list(coords = time3, anim_path = anim_path) 81 | } 82 | -------------------------------------------------------------------------------- /R/dmta_ungroup.R: -------------------------------------------------------------------------------- 1 | #' @importFrom dplyr is_grouped_df mutate select arrange bind_rows 2 | #' @importFrom ggplot2 ggplot aes geom_point scale_color_manual ggtitle 3 | #' @importFrom gganimate transition_states ease_aes view_follow anim_save 4 | #' @importFrom dplyr any_of arrange bind_rows filter group_by group_size group_split group_vars is_grouped_df left_join mutate n n_groups pull select summarize ungroup group_indices 5 | #' @importFrom gganimate anim_save ease_aes transition_states view_follow 6 | #' @importFrom ggplot2 aes element_blank geom_point ggplot ggtitle scale_color_manual theme 7 | #' @importFrom magick image_read image_write 8 | #' @importFrom purrr accumulate map map2 map2_dbl map2_dfr map_chr map_dbl map_dfr map_if pmap_dbl pmap_dfr reduce 9 | #' @importFrom rlang parse_expr 10 | #' @importFrom stats median 11 | #' @importFrom tibble as_tibble tibble 12 | dmta_ungroup <- function(state1, state2, dimensions, anim_title = NA) { 13 | # tibble is not grouped to begin with 14 | if (!is_grouped_df(state1$df)) { 15 | if (!tibble::has_name(state1$coords, "Row_Coord")) { 16 | time1 <- state1$coords %>% 17 | mutate(Time = 1, Row_Coord = .data$Row) 18 | } else { 19 | time1 <- state1$coords %>% 20 | mutate(Time = 1) 21 | } 22 | 23 | anim_data <- time1 24 | 25 | anim <- anim_data %>% 26 | ggplot(aes(x = .data$Col, y = .data$Row_Coord)) + 27 | geom_point(aes(color = .data$Color, group = .data$Row_Coord), shape = 15, size = 3) + 28 | # xlim(-15, 20) + 29 | scale_color_manual( 30 | breaks = unique(anim_data$Color), 31 | values = as.character(unique(anim_data$Color)) 32 | ) + 33 | theme_zilch() + 34 | transition_states(.data$Time, 35 | transition_length = 12, 36 | state_length = 10, wrap = FALSE 37 | ) + 38 | ease_aes("cubic-in-out") + 39 | view_follow(fixed_x = c(-15, 19)) 40 | 41 | anim_path <- tempfile(fileext = ".gif") 42 | anim_save(animation = anim, filename = anim_path) 43 | 44 | return(list(coords = time1 %>% 45 | select(.data$Color, .data$Row, .data$Col, .data$Row_Coord, .data$Col_Coord) %>% 46 | arrange(.data$Row, .data$Col), anim_path = anim_path)) 47 | } 48 | 49 | # tibble is grouped 50 | time1 <- state1$coords %>% 51 | mutate(Time = 1) 52 | 53 | time2 <- state1$coords %>% 54 | mutate(Time = 2, Color = "#C0C0C0") 55 | 56 | time3 <- state1$coords %>% 57 | mutate(Time = 3, Row_Coord = .data$Row_Ungrouped_Coord, Color = "#C0C0C0") 58 | 59 | anim_data <- bind_rows( 60 | time1, 61 | time2, 62 | time3 63 | ) 64 | 65 | anim <- anim_data %>% 66 | ggplot(aes(x = .data$Col, y = .data$Row_Coord)) + 67 | geom_point(aes(color = .data$Color, group = .data$Row_Ungrouped_Coord), shape = 15, size = 3) + 68 | scale_color_manual( 69 | breaks = unique(anim_data$Color), 70 | values = as.character(unique(anim_data$Color)) 71 | ) + 72 | theme_zilch() 73 | 74 | if (is.na(anim_title)) { 75 | anim <- anim + ggtitle(deparse(state1$fitting)) 76 | } else { 77 | anim <- anim + ggtitle(anim_title) 78 | } 79 | 80 | anim <- anim + 81 | transition_states(.data$Time, 82 | transition_length = 12, 83 | state_length = 10, wrap = FALSE 84 | ) + 85 | ease_aes("cubic-in-out") + 86 | view_follow(fixed_x = c(-15, 19), fixed_y = c(-5, 34)) 87 | 88 | anim_path <- tempfile(fileext = ".gif") 89 | anim_save(animation = anim, filename = anim_path) 90 | 91 | list(coords = time3 %>% 92 | mutate(Row = .data$Row_Ungrouped_Coord, Row_Coord = .data$Row_Ungrouped_Coord) %>% 93 | select(.data$Color, .data$Row, .data$Col, .data$Row_Coord, .data$Col_Coord) %>% 94 | arrange(.data$Row, .data$Col), anim_path = anim_path) 95 | } 96 | -------------------------------------------------------------------------------- /R/generate_mapping_from_plot.R: -------------------------------------------------------------------------------- 1 | generate_mapping_from_plot <- function(plot) { 2 | # Extract x and color variable 3 | x <- plot$mapping$x %>% 4 | rlang::quo_name() 5 | 6 | color <- plot$mapping$colour %>% 7 | rlang::quo_name() 8 | 9 | # If there is no plot mapping, the mapping was done in the geom, so grab from there instead 10 | if (x == "NULL" & "GeomPoint" %in% class(plot$layers[[1]]$geom)) { 11 | x <- plot$layers[[1]]$mapping$x %>% 12 | rlang::quo_name() 13 | 14 | color <- plot$layers[[1]]$mapping$colour %>% 15 | rlang::quo_name() 16 | } 17 | 18 | if (color == "NULL") { 19 | color <- NULL 20 | } 21 | 22 | # Need to get y variable from the pipeline, since we want the "original" variable, not the transformed one that appears on the plot 23 | 24 | plot_mapping <- list(x = x, color = color) 25 | 26 | # Extract facets - error that facet_wrap() is not supported, use facet_grid()? 27 | row <- plot$facet$params$rows %>% 28 | unlist() %>% 29 | purrr::pluck(1) 30 | 31 | if (!is.null(row)) { 32 | plot_mapping <- append(plot_mapping, list(row = rlang::quo_name(row))) 33 | } 34 | 35 | column <- plot$facet$params$cols %>% 36 | unlist() %>% 37 | purrr::pluck(1) 38 | 39 | if (!is.null(column)) { 40 | plot_mapping <- append(plot_mapping, list(column = rlang::quo_name(column))) 41 | } 42 | 43 | # Remove empty elements 44 | purrr::compact(plot_mapping) 45 | } 46 | -------------------------------------------------------------------------------- /R/make_coords.R: -------------------------------------------------------------------------------- 1 | #' @importFrom dplyr any_of arrange bind_rows filter group_by group_size group_split group_vars is_grouped_df left_join mutate n n_groups pull select summarize ungroup group_indices 2 | #' @importFrom gganimate anim_save ease_aes transition_states view_follow 3 | #' @importFrom ggplot2 aes element_blank geom_point ggplot ggtitle scale_color_manual theme 4 | #' @importFrom magick image_read image_write 5 | #' @importFrom purrr accumulate map map2 map2_dbl map2_dfr map_chr map_dbl map_dfr map_if pmap_dbl pmap_dfr reduce 6 | #' @importFrom rlang parse_expr 7 | #' @importFrom stats median 8 | #' @importFrom tibble as_tibble tibble 9 | make_coords <- function(df, row_ceiling = nrow(df)) { 10 | n_values <- nrow(df) * length(df) 11 | 12 | coordinates <- tibble( 13 | Color = rep(NA, n_values), 14 | Row = rep(NA, n_values), 15 | Col = rep(NA, n_values), 16 | Row_Coord = rep(NA, n_values), 17 | Col_Coord = rep(NA, n_values) 18 | ) 19 | 20 | i <- 1 21 | for (row in seq_along(1:nrow(df))) { 22 | for (col in seq_along(1:length(df))) { 23 | coordinates$Row[i] <- row 24 | coordinates$Col[i] <- col 25 | coordinates$Row_Coord[i] <- rev(seq_along(1:nrow(df)))[row] 26 | # coordinates$Row_Coord[i] <- (row_ceiling:1)[seq_along(1:nrow(df))][row] 27 | coordinates$Col_Coord[i] <- col 28 | i <- i + 1 29 | } 30 | } 31 | 32 | coordinates 33 | } 34 | -------------------------------------------------------------------------------- /R/map_coords.R: -------------------------------------------------------------------------------- 1 | #' @importFrom tibble tibble 2 | #' @importFrom dplyr any_of arrange bind_rows filter group_by group_size group_split group_vars is_grouped_df left_join mutate n n_groups pull select summarize ungroup group_indices 3 | #' @importFrom gganimate anim_save ease_aes transition_states view_follow 4 | #' @importFrom ggplot2 aes element_blank geom_point ggplot ggtitle scale_color_manual theme 5 | #' @importFrom magick image_read image_write 6 | #' @importFrom purrr accumulate map map2 map2_dbl map2_dfr map_chr map_dbl map_dfr map_if pmap_dbl pmap_dfr reduce 7 | #' @importFrom rlang parse_expr 8 | #' @importFrom stats median 9 | #' @importFrom tibble as_tibble tibble 10 | map_coords <- function(rows, cols) { 11 | # rows <- 26:29;cols <- 1:3 12 | n_values <- length(rows) * length(cols) 13 | 14 | rows <- sort(rows) %>% rev() 15 | cols <- sort(cols) 16 | 17 | coordinates <- tibble( 18 | Row = rep(NA, n_values), 19 | Col = rep(NA, n_values), 20 | Row_Coord = rep(NA, n_values), 21 | Col_Coord = rep(NA, n_values) 22 | ) 23 | 24 | i <- 1 25 | for (row in rows) { 26 | for (col in cols) { 27 | coordinates$Row[i] <- which(row == rows) 28 | coordinates$Col[i] <- which(col == cols) 29 | coordinates$Row_Coord[i] <- row 30 | coordinates$Col_Coord[i] <- col 31 | i <- i + 1 32 | } 33 | } 34 | 35 | coordinates 36 | } 37 | -------------------------------------------------------------------------------- /R/mod_datamation_sanddance.R: -------------------------------------------------------------------------------- 1 | #' datamation_sanddance UI Function 2 | #' 3 | #' @description A shiny Module. 4 | #' 5 | #' @param id,input,output,session Internal parameters for {shiny}. 6 | #' 7 | #' @noRd 8 | #' 9 | #' @importFrom shiny NS tagList 10 | mod_datamation_sanddance_ui <- function(id) { 11 | ns <- NS(id) 12 | datamations::datamationSandDanceOutput(ns("datamation")) 13 | # See in server re: why this is commented out 14 | # shiny::uiOutput(ns("datamation_ui")) 15 | } 16 | 17 | #' datamation_sanddance Server Functions 18 | #' 19 | #' @noRd 20 | mod_datamation_sanddance_server <- function(id, pipeline) { 21 | shiny::moduleServer(id, function(input, output, session) { 22 | ns <- session$ns 23 | 24 | # Render UI 25 | 26 | shiny::observeEvent(pipeline(), { 27 | 28 | # Generate datamation ----- 29 | datamation <- shiny::reactive({ 30 | datamation_sanddance(pipeline(), height = 300, width = 300) 31 | }) 32 | 33 | # Create an output for it 34 | output$datamation <- datamations::renderDatamationSandDance({ 35 | datamation() 36 | }) 37 | 38 | # For some reason, doing renderUI is causing the javascript code to run twice (even when the actual R code is not) - so just use datamationSandDanceOutput directly, even if it means the slider shows initially! 39 | # output$datamation_ui <- shiny::renderUI({ 40 | # datamations::datamationSandDanceOutput(ns("datamation")) 41 | # }) 42 | }) 43 | }) 44 | } 45 | 46 | ## To be copied in the UI 47 | # mod_datamation_sanddance_ui("datamation_sanddance") 48 | 49 | ## To be copied in the server 50 | # mod_datamation_sanddance_server("datamation_sanddance") 51 | -------------------------------------------------------------------------------- /R/mod_inputs.R: -------------------------------------------------------------------------------- 1 | #' Inputs UI 2 | #' 3 | #' @description A shiny Module. 4 | #' 5 | #' @param id,input,output,session Internal parameters for {shiny}. 6 | #' 7 | #' @noRd 8 | #' 9 | #' @importFrom shiny NS tagList 10 | mod_inputs_ui <- function(id) { 11 | ns <- shiny::NS(id) 12 | tagList( 13 | shiny::p("Construct a tidyverse pipeline by choosing from the options below. You select a data set, then up to three variables to group by, and finally a variable to summarize and a summary function to apply to it."), 14 | shiny::hr(), 15 | shiny::fluidRow( 16 | shiny::column( 17 | width = 3, 18 | shiny::selectInput(ns("dataset"), 19 | "Dataset", 20 | choices = c("small_salary", "penguins") 21 | ) 22 | ), 23 | shiny::column( 24 | width = 3, 25 | shiny::selectInput( 26 | ns("group_by"), 27 | "Group by", 28 | choices = c("Work", "Degree"), 29 | selected = "Degree", 30 | multiple = TRUE 31 | ) 32 | ), 33 | shiny::column( 34 | width = 2, 35 | shiny::selectInput( 36 | ns("summary_variable"), 37 | "Summary variable", 38 | choices = "Salary" 39 | ) 40 | ), 41 | shiny::column( 42 | width = 2, 43 | shiny::selectInput( 44 | ns("summary_function"), 45 | "Summary function", 46 | choices = c("mean", "median", "min", "max") 47 | ) 48 | ), 49 | shiny::column( 50 | width = 2, 51 | shiny::actionButton(ns("go"), "Go", width = "100%", style = "margin-top: 28px;") 52 | ) 53 | ) 54 | ) 55 | } 56 | 57 | #' Inputs server 58 | #' 59 | #' @noRd 60 | mod_inputs_server <- function(id) { 61 | shiny::moduleServer(id, function(input, output, session) { 62 | ns <- session$ns 63 | 64 | # Select dataset ---- 65 | 66 | dataset <- shiny::reactive({ 67 | switch(input$dataset, 68 | mtcars = datasets::mtcars, 69 | penguins = palmerpenguins::penguins, 70 | small_salary = datamations::small_salary 71 | ) 72 | }) 73 | 74 | # Update group by and summary variables based on dataset ---- 75 | 76 | shiny::observe({ 77 | group_by_vars <- dataset() %>% 78 | dplyr::select_if(~ is.factor(.x) | is.character(.x)) %>% 79 | names() 80 | 81 | shiny::updateSelectInput( 82 | session = session, 83 | inputId = "group_by", 84 | choices = group_by_vars, 85 | selected = group_by_vars[[1]] 86 | ) 87 | 88 | summarise_vars <- dataset() %>% 89 | dplyr::select_if(is.numeric) %>% 90 | names() 91 | 92 | shiny::updateSelectInput( 93 | session = session, 94 | inputId = "summary_variable", 95 | choices = summarise_vars 96 | ) 97 | }) 98 | 99 | inputs <- list( 100 | dataset = shiny::reactive(input$dataset), 101 | group_by = shiny::reactive(input$group_by), 102 | summary_variable = shiny::reactive(input$summary_variable), 103 | summary_function = shiny::reactive(input$summary_function), 104 | height = shiny::reactive(input$height), 105 | width = shiny::reactive(input$width), 106 | go = shiny::reactive(input$go) 107 | ) 108 | 109 | return(inputs) 110 | }) 111 | } 112 | 113 | ## To be copied in the UI 114 | # mod_inputs_ui("inputs") 115 | 116 | ## To be copied in the server 117 | # mod_inputs_server("inputs") 118 | -------------------------------------------------------------------------------- /R/mod_pipeline.R: -------------------------------------------------------------------------------- 1 | #' Pipeline UI Function 2 | #' 3 | #' @description A shiny Module. 4 | #' 5 | #' @param id,input,output,session Internal parameters for {shiny}. 6 | #' 7 | #' @noRd 8 | #' 9 | #' @importFrom shiny NS tagList 10 | mod_pipeline_ui <- function(id) { 11 | ns <- NS(id) 12 | shiny::tagList( 13 | shiny::hr(), 14 | shiny::fluidRow( 15 | shinydashboard::box( 16 | width = 12, 17 | solidHeader = TRUE, 18 | shiny::h2("tidyverse pipeline"), 19 | shinyAce::aceEditor( 20 | outputId = ns("pipeline_editor"), 21 | mode = "r", 22 | fontSize = 16, 23 | readOnly = TRUE, 24 | highlightActiveLine = FALSE, 25 | autoScrollEditorIntoView = TRUE, 26 | minLines = 2, 27 | maxLines = 30, 28 | value = "# Code will appear here based on selections above" 29 | ), 30 | ) 31 | ) 32 | ) 33 | } 34 | 35 | #' Pipeline Server Functions 36 | #' 37 | #' @noRd 38 | mod_pipeline_server <- function(id, inputs) { 39 | shiny::moduleServer(id, function(input, output, session) { 40 | ns <- session$ns 41 | 42 | pipeline <- shiny::eventReactive(inputs$go(), { 43 | pipeline_group_by <- !is.null(inputs$group_by()) 44 | if (pipeline_group_by) { 45 | glue::glue("{inputs$dataset()} %>% group_by({paste0(inputs$group_by(), collapse = ', ')}) %>% summarize({inputs$summary_function()} = {inputs$summary_function()}({inputs$summary_variable()}, na.rm = TRUE))") 46 | } else { 47 | glue::glue("{inputs$dataset()} %>% summarize({inputs$summary_function()} = {inputs$summary_function()}({inputs$summary_var()}, na.rm = TRUE))") 48 | } 49 | }) 50 | 51 | # Update editor with pipeline 52 | 53 | shiny::observeEvent(inputs$go(), { 54 | text <- c("library(dplyr)\n", pipeline()) 55 | # Load dplyr 56 | eval(parse(text = "library(dplyr)")) 57 | if (inputs$dataset() == "penguins") { 58 | text <- c("library(palmerpenguins)\n", text) 59 | # Load palmerpenguins 60 | eval(parse(text = "library(palmerpenguins)")) 61 | } 62 | text <- styler::style_text(text) 63 | shinyAce::updateAceEditor(session, "pipeline_editor", value = paste0(text, collapse = "\n")) 64 | }) 65 | 66 | return(pipeline) 67 | }) 68 | } 69 | 70 | ## To be copied in the UI 71 | # mod_pipeline_ui("pipeline") 72 | 73 | ## To be copied in the server 74 | # mod_pipeline_server("pipeline") 75 | -------------------------------------------------------------------------------- /R/parse_functions.R: -------------------------------------------------------------------------------- 1 | #' Parse functions from pipeline steps 2 | #' 3 | #' @param fittings List of pipeline steps 4 | #' @noRd 5 | parse_functions <- function(fittings) { 6 | functions <- fittings %>% 7 | # Splits expressions into the functions and arguments 8 | purrr::map(as.list) %>% 9 | # Keeping the functions only 10 | purrr::map(~ .x[[1]]) %>% 11 | purrr::map_chr(as.character) 12 | 13 | # Rename the first "function" to just be data 14 | functions[1] <- "data" 15 | 16 | functions 17 | } 18 | 19 | #' Parse ggplot2 code for validity 20 | #' 21 | #' @param ggplot2_fittings List of ggplot2 steps 22 | #' @noRd 23 | parse_ggplot2_code <- function(ggplot2_fittings) { 24 | 25 | ## Facet wrap 26 | contains_facet_wrap <- purrr::map_lgl(ggplot2_fittings, function(x) { 27 | stringr::str_detect(x, "facet_wrap") 28 | }) %>% 29 | any() 30 | 31 | if (contains_facet_wrap) { 32 | stop("datamations does not support `facet_wrap()`. Please use `facet_grid()` if you would like to see a faceted datamation.", call. = FALSE) 33 | } 34 | 35 | ## geom other than geom_point() 36 | contains_geom_not_point <- purrr::map_lgl(ggplot2_fittings, function(x) { 37 | stringr::str_detect(x, "geom") & !stringr::str_detect(x, "geom_point") 38 | }) %>% 39 | any() 40 | 41 | if (contains_geom_not_point) { 42 | stop("datamations with ggplot2 code only supports `geom_point()`.", call. = FALSE) 43 | } 44 | 45 | ## No geom_point 46 | doesnt_contain_geom_point <- purrr::map_lgl(ggplot2_fittings, function(x) { 47 | !stringr::str_detect(x, "geom_point") 48 | }) %>% 49 | all() 50 | 51 | if (doesnt_contain_geom_point) { 52 | stop("datamations using ggplot2 code requires a call to `geom_point()`.", call. = FALSE) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /R/parse_pipeline.R: -------------------------------------------------------------------------------- 1 | #' Parse a tidyverse pipeline 2 | #' 3 | #' Parses a tidyverse pipeline, input as a string, into a list of its components as expressions for parsing later on in the datamations process. 4 | #' 5 | #' @param pipeline Input pipeline, as a string. 6 | #' @param supported_tidy_functions Functions that are supported by datamations: \code{group_by} and \code{summarize}/\code{summarise}. 7 | #' @noRd 8 | #' 9 | #' @examples 10 | #' "small_salary %>% group_by(Degree) %>% summarize(mean = mean(Salary))" %>% 11 | #' datamations:::parse_pipeline() 12 | #' 13 | #' "group_by(small_salary, Degree) %>% summarize(mean = mean(Salary))" %>% 14 | #' datamations:::parse_pipeline() 15 | parse_pipeline <- function(pipeline, supported_tidy_functions = c("group_by", "summarize", "filter", "count")) { 16 | pipeline %>% 17 | split_pipeline(supported_tidy_functions = supported_tidy_functions) %>% 18 | purrr::map(rlang::parse_expr) 19 | } 20 | 21 | #' Split pipeline into components 22 | #' @noRd 23 | split_pipeline <- function(pipeline, supported_tidy_functions = c("group_by", "summarize", "filter", "count")) { 24 | pipeline <- pipeline %>% 25 | stringr::str_split("%>%") %>% 26 | purrr::pluck(1) %>% 27 | stringr::str_trim() 28 | 29 | # Convert summarise to summarize 30 | pipeline <- stringr::str_replace(pipeline, "summarise", "summarize") 31 | 32 | # Extract out data if it's the first argument of the first function 33 | pipeline <- parse_data_from_first_function(pipeline, supported_tidy_functions = supported_tidy_functions) 34 | 35 | pipeline 36 | } 37 | 38 | #' Parse out data from the first function in a pipeline 39 | #' @noRd 40 | parse_data_from_first_function <- function(pipeline, supported_tidy_functions = c("group_by", "summarize", "filter", "count")) { 41 | # If the first element of the pipeline is a supported function, the data is probably embedded in it 42 | if (any(stringr::str_detect(pipeline[[1]], supported_tidy_functions))) { 43 | # Extract the data and check that it is a valid data frame 44 | first_function_data <- stringr::str_extract(pipeline[[1]], pattern = "(?<=\\()(.*?)(?=,)") # Regex is everything between ( and , 45 | first_function_data_expr <- rlang::parse_expr(first_function_data) 46 | 47 | if (is.na(first_function_data)) { 48 | stop("No data detected in pipeline.", call. = FALSE) 49 | } 50 | 51 | # Check that the data exists 52 | data_exists <- try(eval(first_function_data_expr), silent = TRUE) 53 | data_exists <- all(class(data_exists) != "try-error") 54 | 55 | if (!data_exists) { 56 | stop("No data detected in pipeline.", call. = FALSE) 57 | } 58 | 59 | # Check that the data is a data frame 60 | data <- eval(first_function_data_expr) 61 | data_is_df <- is.data.frame(data) 62 | 63 | if (!data_is_df) { 64 | stop("Passed data is not a data frame or tibble.", call. = FALSE) 65 | } 66 | 67 | # Remove data call from first function 68 | pipeline[[1]] <- stringr::str_remove(pipeline[[1]], first_function_data) 69 | # And the first comma (done separately because there can be any amount of spacing) 70 | pipeline[[1]] <- stringr::str_remove(pipeline[[1]], ",") 71 | 72 | # Make the data the first element of the pipeline 73 | pipeline <- append(first_function_data, pipeline) 74 | } 75 | 76 | pipeline 77 | } 78 | -------------------------------------------------------------------------------- /R/prep_specs_count.R: -------------------------------------------------------------------------------- 1 | #' Generate specs of data for count step of datamation 2 | #' 3 | #' @param .data Input data 4 | #' @param mapping A list that describes mapping for the datamations, including x and y variables, summary variable and operation, variables used in facets and in colors, etc. Generated in \code{datamation_sanddance} using \code{generate_mapping}. 5 | #' @inheritParams datamation_sanddance 6 | #' @inheritParams prep_specs_data 7 | #' @noRd 8 | prep_specs_count <- function(.data, mapping, toJSON = TRUE, pretty = TRUE, height = 300, width = 300, mutation_before, ...) { 9 | 10 | # Treat count as group_by + summarize(n = n()) steps 11 | # Call prep_specs_group_by and prep_specs_summarize 12 | 13 | res <- list() 14 | 15 | # Group by ---- 16 | 17 | res[["group_by"]] <- prep_specs_group_by(.data, mapping, toJSON = toJSON, pretty = pretty, height = height, width = width, mutation_before = FALSE) 18 | 19 | # Summarize ---- 20 | 21 | # Fake mapping by adding summary_function and summary_name 22 | 23 | mapping$summary_function <- mapping$summary_name <- "n" 24 | 25 | # Fake "previous frame" (group_by) data 26 | 27 | group_vars <- mapping$groups %>% 28 | as.list() %>% 29 | purrr::map(rlang::parse_expr) 30 | 31 | .data <- .data %>% 32 | dplyr::group_by(!!!group_vars) 33 | 34 | res[["summarize"]] <- prep_specs_summarize(.data, mapping, toJSON = toJSON, pretty = pretty, height = height, width = width, mutation_before = FALSE) 35 | 36 | # Add meta field for "count" custom animation 37 | res[["summarize"]][[1]][["meta"]][["custom_animation"]] <- "count" 38 | 39 | # Update title of spec 40 | title <- ifelse(length(group_vars) == 0, "Plot count", "Plot count of each group") 41 | res[["summarize"]][[1]][["meta"]][["description"]] <- title 42 | 43 | # Unlist and return 44 | res <- res %>% 45 | unlist(recursive = FALSE) 46 | 47 | names(res) <- NULL 48 | 49 | res 50 | } 51 | -------------------------------------------------------------------------------- /R/prep_specs_data.R: -------------------------------------------------------------------------------- 1 | #' Generate spec of data for initial data step of datamations 2 | #' 3 | #' @param .data Input data 4 | #' @param mapping Mapping, unused. 5 | #' @param toJSON Whether to converts the spec to JSON. Defaults to TRUE. 6 | #' @inheritParams datamation_sanddance 7 | #' @noRd 8 | prep_specs_data <- function(.data, mapping, toJSON = TRUE, pretty = TRUE, height = 300, width = 300, ...) { 9 | 10 | # Generate the data and specs for each state 11 | specs_list <- vector("list", length = 1) 12 | 13 | # Prep encoding 14 | x_encoding <- list(field = X_FIELD_CHR, type = "quantitative", axis = NULL) 15 | y_encoding <- list(field = Y_FIELD_CHR, type = "quantitative", axis = NULL) 16 | 17 | spec_encoding <- list(x = x_encoding, y = y_encoding) 18 | 19 | # State 1: Ungrouped icon array 20 | 21 | # Add a count (total) to each record 22 | 23 | data_1 <- .data %>% 24 | dplyr::count() %>% 25 | add_ids_to_count_data(.data) 26 | 27 | # Generate description 28 | 29 | description <- "Initial data" 30 | 31 | # These are not "real specs" as they don't actually have an x or y, only n 32 | # meta = list(parse = "grid") communicates to the JS code to turn these into real specs 33 | 34 | specs_list[[1]] <- generate_vega_specs(data_1, 35 | mapping = mapping, 36 | meta = list(parse = "grid", description = description), 37 | spec_encoding = spec_encoding, 38 | height = height, width = width, 39 | column = FALSE, row = FALSE, color = FALSE 40 | ) 41 | 42 | # Convert specs to JSON 43 | if (toJSON) { 44 | specs_list <- specs_list %>% 45 | purrr::map(vegawidget::vw_as_json, pretty = pretty) 46 | } 47 | 48 | specs_list 49 | } 50 | -------------------------------------------------------------------------------- /R/prep_specs_tally.R: -------------------------------------------------------------------------------- 1 | #' Generate specs of data for tally step of datamation 2 | #' 3 | #' @param .data Input data 4 | #' @param mapping A list that describes mapping for the datamations, including x and y variables, summary variable and operation, variables used in facets and in colors, etc. Generated in \code{datamation_sanddance} using \code{generate_mapping}. 5 | #' @inheritParams datamation_sanddance 6 | #' @inheritParams prep_specs_data 7 | #' @noRd 8 | prep_specs_tally <- function(.data, mapping, toJSON = TRUE, pretty = TRUE, height = 300, width = 300, mutation_before, ...) { 9 | 10 | # Treat tally as summarize(n = n()) with optional group mappings 11 | # Call prep_specs_group_by and prep_specs_summarize 12 | 13 | res <- list() 14 | 15 | # Group by ---- 16 | 17 | res[["group_by"]] <- prep_specs_group_by(.data, mapping, toJSON = toJSON, pretty = pretty, height = height, width = width, mutation_before = FALSE) 18 | 19 | # Summarize ---- 20 | 21 | # Fake mapping by adding summary_function and summary_name 22 | 23 | mapping$summary_function <- mapping$summary_name <- "n" 24 | 25 | # Fake "previous frame" (group_by) data 26 | 27 | group_vars <- mapping$groups %>% 28 | as.list() %>% 29 | purrr::map(rlang::parse_expr) 30 | 31 | .data <- .data %>% 32 | dplyr::group_by(!!!group_vars) 33 | 34 | res[["summarize"]] <- prep_specs_summarize(.data, mapping, toJSON = toJSON, pretty = pretty, height = height, width = width, mutation_before = FALSE) 35 | 36 | # Add meta field for "tally" custom animation 37 | res[["summarize"]][[1]][["meta"]][["custom_animation"]] <- "tally" 38 | 39 | # Update title of spec 40 | title <- ifelse(length(group_vars) == 0, "Plot tally", "Plot tally of each group") 41 | res[["summarize"]][[1]][["meta"]][["description"]] <- title 42 | 43 | # Unlist and return 44 | res <- res %>% 45 | unlist(recursive = FALSE) 46 | 47 | names(res) <- NULL 48 | 49 | res 50 | 51 | } 52 | -------------------------------------------------------------------------------- /R/run_app.R: -------------------------------------------------------------------------------- 1 | #' Run datamations shiny application 2 | #' 3 | #' @export 4 | run_app <- function() { 5 | shiny::shinyApp( 6 | ui = app_ui, 7 | server = app_server 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /R/snake.R: -------------------------------------------------------------------------------- 1 | #' Evaluate each pipeline step 2 | #' 3 | #' @param fittings Pipeline steps 4 | #' @param envir Evaluation environment 5 | #' @noRd 6 | snake <- function(fittings, envir = parent.frame()) { 7 | c( 8 | # Initial data 9 | eval(fittings[[1]], envir = envir) %>% 10 | dplyr::as_tibble() %>% 11 | list(), 12 | # Evaluate each subsequent pipeline step 13 | suppressMessages(purrr::accumulate(fittings, ~ call("%>%", .x, .y) %>% eval(envir = envir))[-1]) 14 | ) %>% 15 | purrr::map_if(~ (is.data.frame(.x) || is.vector(.x)) && !dplyr::is_grouped_df(.x), dplyr::as_tibble) 16 | } 17 | -------------------------------------------------------------------------------- /R/theme_zilch.R: -------------------------------------------------------------------------------- 1 | theme_zilch <- function(...) { 2 | theme( 3 | axis.line = element_blank(), 4 | axis.text.x = element_blank(), 5 | axis.text.y = element_blank(), 6 | axis.ticks = element_blank(), 7 | axis.title.x = element_blank(), 8 | axis.title.y = element_blank(), 9 | legend.position = "none", 10 | panel.background = element_blank(), 11 | panel.border = element_blank(), 12 | panel.grid.major = element_blank(), 13 | panel.grid.minor = element_blank(), 14 | plot.background = element_blank(), ... 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /R/utils-pipe.R: -------------------------------------------------------------------------------- 1 | #' Pipe operator 2 | #' 3 | #' See \code{magrittr::\link[magrittr:pipe]{\%>\%}} for details. 4 | #' 5 | #' @name %>% 6 | #' @rdname pipe 7 | #' @keywords internal 8 | #' @export 9 | #' @importFrom magrittr %>% 10 | #' @usage lhs \%>\% rhs 11 | NULL 12 | -------------------------------------------------------------------------------- /R/zzz.R: -------------------------------------------------------------------------------- 1 | X_FIELD_CHR <- "datamations_x" 2 | X_FIELD <- rlang::sym(X_FIELD_CHR) 3 | Y_FIELD_CHR <- "datamations_y" 4 | Y_FIELD <- rlang::sym(Y_FIELD_CHR) 5 | Y_RAW_FIELD_CHR <- "datamations_y_raw" 6 | Y_RAW_FIELD <- rlang::sym(Y_RAW_FIELD_CHR) 7 | Y_TOOLTIP_FIELD_CHR <- "datamations_y_tooltip" 8 | Y_TOOLTIP_FIELD <- rlang::sym(Y_TOOLTIP_FIELD_CHR) 9 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | editor_options: 4 | chunk_output_type: console 5 | --- 6 | 7 | 8 | 9 | ```{r, include = FALSE} 10 | # Global chunk options 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>", 14 | fig.path = "man/figures/README-", 15 | out.width = "80%", 16 | warning = FALSE, 17 | message = FALSE 18 | ) 19 | 20 | # Specific options 21 | knitr::opts_template$set( 22 | # Actually generating the datamations - don't show the code or results (returns FALSE right now), and cache the image so it's not regenerated each time 23 | datamations_generate = list( 24 | cache = TRUE, 25 | echo = FALSE, 26 | results = "hide" 27 | ) 28 | ) 29 | ``` 30 | 31 | # datamations 32 | 33 | 34 | [![R-CMD-check](https://github.com/microsoft/datamations/workflows/R-CMD-check/badge.svg)](https://github.com/microsoft/datamations/actions) 35 | 36 | 37 | datamations is a framework for the automatic generation of explanation of the steps of an analysis pipeline. It automatically turns code into animations, showing the state of the data at each step of an analysis. 38 | 39 | For more information, please visit the [package website](https://microsoft.github.io/datamations/), which includes [additional examples](https://microsoft.github.io/datamations/articles/Examples.html), [defaults and conventions](https://microsoft.github.io/datamations/articles/details.html), and more. 40 | 41 | ## Installation 42 | 43 | You can install datamations from GitHub with: 44 | 45 | ```r 46 | # install.packages("devtools") 47 | devtools::install_github("microsoft/datamations") 48 | ``` 49 | 50 | ## Usage 51 | 52 | To get started, load datamations and dplyr: 53 | 54 | ```{r load-libraries, echo = FALSE} 55 | library(datamations) 56 | library(dplyr) 57 | ``` 58 | 59 | A datamation shows a plot of what the data looks like at each step of a tidyverse pipeline, animated by the transitions that lead to each state. The following shows an example taking the built-in `small_salary` data set, grouping by `Degree`, and calculating the mean `Salary`. 60 | 61 | First, define the code for the pipeline, then generate the datamation with `datamation_sanddance()`: 62 | 63 | ```{r mean-salary-degree-plot-setup, eval = FALSE} 64 | library(datamations) 65 | library(dplyr) 66 | 67 | "small_salary %>% 68 | group_by(Degree) %>% 69 | summarize(mean = mean(Salary))" %>% 70 | datamation_sanddance() 71 | ``` 72 | 73 | ```{r mean-salary-degree-plot-datamations-gif, echo = FALSE} 74 | knitr::include_graphics("man/figures/README-mean_salary_grouped_degree.gif") 75 | ``` 76 | 77 | datamations supports the following `dplyr` functions: 78 | 79 | * `group_by()` (up to three grouping variables) 80 | * `summarize()`/`summarise()` (limited to summarizing one variable) 81 | * `filter()` 82 | * `count()`/`tally` 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # datamations 5 | 6 | 7 | 8 | [![R-CMD-check](https://github.com/microsoft/datamations/workflows/R-CMD-check/badge.svg)](https://github.com/microsoft/datamations/actions) 9 | 10 | 11 | datamations is a framework for the automatic generation of explanation 12 | of the steps of an analysis pipeline. It automatically turns code into 13 | animations, showing the state of the data at each step of an analysis. 14 | 15 | For more information, please visit the [package 16 | website](https://microsoft.github.io/datamations/), which includes 17 | [additional 18 | examples](https://microsoft.github.io/datamations/articles/Examples.html), 19 | [defaults and 20 | conventions](https://microsoft.github.io/datamations/articles/details.html), 21 | and more. 22 | 23 | ## Installation 24 | 25 | You can install datamations from GitHub with: 26 | 27 | ``` r 28 | # install.packages("devtools") 29 | devtools::install_github("microsoft/datamations") 30 | ``` 31 | 32 | ## Usage 33 | 34 | To get started, load datamations and dplyr: 35 | 36 | A datamation shows a plot of what the data looks like at each step of a 37 | tidyverse pipeline, animated by the transitions that lead to each state. 38 | The following shows an example taking the built-in `small_salary` data 39 | set, grouping by `Degree`, and calculating the mean `Salary`. 40 | 41 | First, define the code for the pipeline, then generate the datamation 42 | with `datamation_sanddance()`: 43 | 44 | ``` r 45 | library(datamations) 46 | library(dplyr) 47 | 48 | "small_salary %>% 49 | group_by(Degree) %>% 50 | summarize(mean = mean(Salary))" %>% 51 | datamation_sanddance() 52 | ``` 53 | 54 | 55 | 56 | datamations supports the following `dplyr` functions: 57 | 58 | - `group_by()` (up to three grouping variables) 59 | - `summarize()`/`summarise()` (limited to summarizing one variable) 60 | - `filter()` 61 | - `count()`/`tally` 62 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /_pkgdown.yml: -------------------------------------------------------------------------------- 1 | reference: 2 | - title: Plot-based datamation 3 | contents: 4 | - datamation_sanddance 5 | - renderDatamationSandDance 6 | - datamationSandDanceOutput 7 | - title: Table-based datamation 8 | contents: 9 | - datamation_tibble 10 | - title: Data 11 | contents: 12 | - small_salary 13 | - jeter_justice 14 | - title: Shiny app 15 | contents: 16 | - run_app 17 | -------------------------------------------------------------------------------- /app.R: -------------------------------------------------------------------------------- 1 | # Launch the ShinyApp (Do not remove this comment) 2 | # To deploy, run: rsconnect::deployApp() 3 | # Or use the blue button on top of this file 4 | 5 | pkgload::load_all(export_all = FALSE,helpers = FALSE,attach_testthat = FALSE) 6 | options( "golem.app.prod" = TRUE) 7 | datamations::run_app() # add parameters here (if any) 8 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Starter pipeline 2 | # Start with a minimal pipeline that you can customize to build and deploy your code. 3 | # Add steps that build, run tests, deploy, and more: 4 | # https://aka.ms/yaml 5 | 6 | trigger: 7 | - main 8 | 9 | pool: 10 | vmImage: 'windows-latest' 11 | 12 | steps: 13 | 14 | - task: CredScan@2 15 | inputs: 16 | toolMajorVersion: 'V1' 17 | 18 | - task: ComponentGovernanceComponentDetection@0 19 | inputs: 20 | scanType: 'Register' 21 | verbosity: 'Verbose' 22 | alertWarningLevel: 'High' 23 | -------------------------------------------------------------------------------- /data-raw/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/datamations/66d127edf9390e1f14aa3ae624b542afcb5cc415/data-raw/.DS_Store -------------------------------------------------------------------------------- /data-raw/jeter.R: -------------------------------------------------------------------------------- 1 | jeter_1995 <- data.frame( 2 | player = "Derek Jeter", 3 | year = 1995, 4 | is_hit = c(rep(1, 12), rep(0, 48-12)) 5 | ) 6 | 7 | jeter_1996 <- data.frame( 8 | player = "Derek Jeter", 9 | year = 1996, 10 | is_hit = c(rep(1, 183), rep(0, 582-183)) 11 | ) 12 | 13 | justice_1995 <- data.frame( 14 | player = "David Justice", 15 | year = 1995, 16 | is_hit = c(rep(1, 104), rep(0, 411-104)) 17 | ) 18 | 19 | justice_1996 <- data.frame( 20 | player = "David Justice", 21 | year = 1996, 22 | is_hit = c(rep(1, 45), rep(0, 140-45)) 23 | ) 24 | 25 | jeter_justice <- bind_rows(jeter_1995, 26 | jeter_1996, 27 | justice_1995, 28 | justice_1996) 29 | 30 | usethis::use_data(jeter_justice, overwrite = TRUE) -------------------------------------------------------------------------------- /data-raw/products.csv: -------------------------------------------------------------------------------- 1 | Year,Category,Product,Sales,Rating 2 | 2017,Components,Chains,20000,0.75 3 | 2015,Clothing,Socks,3700,0.22 4 | 2017,Clothing,Bib-Shorts,4000,0.22 5 | 2015,Clothing,Shorts,13300,0.56 6 | 2017,Clothing,Tights,36000,1 7 | 2015,Components,Handlebars,2300,0.35 8 | 2016,Clothing,Socks,2300,0.28 9 | 2016,Components,Brakes,3400,0.36 10 | 2016,Bikes,Mountain Bikes,6300,0.4 11 | 2017,Components,Brakes,5400,0.38 12 | 2016,Accessories,Helmets,17000,0.9 13 | 2016,Accessories,Lights,21600,0.9 14 | 2016,Accessories,Locks,29800,0.9 15 | 2016,Components,Bottom Brackets,1000,0.23 16 | 2015,Clothing,Jerseys,6700,0.05 17 | 2017,Components,Bottom Brackets,600,0.27 18 | 2015,Bikes,Road Bikes,3500,0.5 19 | 2017,Clothing,Jerseys,7500,0.4 20 | 2017,Accessories,Tires and Tubes,63700,0.9 21 | 2017,Bikes,Cargo Bike,9300,0.6 22 | 2017,Bikes,Mountain Bikes,8500,0.46 23 | 2017,Accessories,Bike Racks,33700,0.92 24 | 2017,Clothing,Caps,600,0.15 25 | 2015,Bikes,Mountain Bikes,3100,0.35 26 | 2017,Accessories,Pumps,30700,0.95 27 | 2016,Accessories,Pumps,16400,0.8 28 | 2016,Accessories,Bike Racks,22100,0.9 29 | 2017,Accessories,Helmets,34000,0.95 30 | 2015,Accessories,Pumps,700,0.1 31 | 2015,Clothing,Tights,3300,0.3 32 | 2017,Bikes,Road Bikes,16900,0.65 33 | 2017,Accessories,Lights,36700,0.9 34 | 2015,Accessories,Helmets,8300,0.99 35 | 2016,Clothing,Bib-Shorts,2900,0.36 36 | 2015,Accessories,Tires and Tubes,8700,0.9 37 | 2017,Accessories,Locks,35000,1 38 | 2016,Bikes,Road Bikes,8300,0.46 39 | 2016,Components,Wheels,16700,0.75 40 | 2016,Bikes,Touring Bikes,1800,0.15 41 | 2017,Clothing,Socks,3700,0.48 42 | 2016,Clothing,Shorts,12000,0.66 43 | 2015,Accessories,Locks,10000,0.85 44 | 2015,Components,Bottom Brackets,500,0.35 45 | 2017,Components,Wheels,21800,0.96 46 | 2016,Components,Chains,16400,0.7 47 | 2016,Clothing,Caps,400,0.2 48 | 2015,Clothing,Vests,3300,0.36 49 | 2017,Components,Handlebars,5000,0.35 50 | 2016,Components,Handlebars,3300,0.38 51 | 2015,Components,Pedals,800,0.36 52 | 2016,Clothing,Gloves,15600,0.65 53 | 2016,Components,Pedals,1500,0.17 54 | 2017,Components,Pedals,6200,0.38 55 | 2017,Clothing,Gloves,27000,0.88 56 | 2016,Components,Saddles,2800,0.38 57 | 2016,Bikes,Cargo Bike,6700,0.46 58 | 2015,Clothing,Gloves,13300,0.5 59 | 2016,Accessories,Tires and Tubes,13800,0.85 60 | 2017,Clothing,Vests,2400,0.35 61 | 2015,Accessories,Bike Racks,300,0.05 62 | 2015,Components,Saddles,2100,0.49 63 | 2015,Components,Brakes,2300,0.34 64 | 2015,Components,Wheels,10000,0.66 65 | 2015,Bikes,Touring Bikes,500,0.22 66 | 2016,Clothing,Jerseys,3800,0.48 67 | 2015,Bikes,Cargo Bike,3200,0.48 68 | 2017,Clothing,Shorts,23000,1 69 | 2015,Clothing,Bib-Shorts,700,0.28 70 | 2015,Accessories,Lights,1300,0.9 71 | 2016,Clothing,Vests,1300,0.25 72 | 2016,Clothing,Tights,22100,0.99 73 | 2017,Components,Saddles,3100,0.42 74 | 2015,Clothing,Caps,500,0.5 75 | 2017,Bikes,Touring Bikes,3100,0.22 76 | 2015,Components,Chains,8700,0.92 77 | -------------------------------------------------------------------------------- /data-raw/small_salary.R: -------------------------------------------------------------------------------- 1 | library(readr) 2 | 3 | small_salary <- read_csv(here::here("data-raw", "small_salary.csv")) 4 | 5 | usethis::use_data(small_salary, overwrite = TRUE) 6 | -------------------------------------------------------------------------------- /data-raw/small_salary.csv: -------------------------------------------------------------------------------- 1 | Degree,Work,Salary 2 | Masters,Academia,81.94450138369575 3 | PhD,Academia,84.48683335236274 4 | Masters,Academia,82.89530056482181 5 | PhD,Academia,83.84691398846917 6 | PhD,Academia,83.75313821574673 7 | PhD,Academia,85.26832443126477 8 | PhD,Industry,91.40521181118675 9 | PhD,Academia,85.33091764966957 10 | Masters,Academia,83.26561724720523 11 | PhD,Industry,92.34894080343656 12 | Masters,Academia,82.89954726654105 13 | PhD,Academia,84.73846479947679 14 | Masters,Industry,90.18779113679193 15 | Masters,Industry,90.31389033375308 16 | Masters,Industry,90.33657557494007 17 | Masters,Industry,89.68851578235626 18 | Masters,Industry,89.657391943736 19 | Masters,Industry,89.81031844741665 20 | PhD,Academia,85.00685522030108 21 | Masters,Industry,90.14455051301047 22 | Masters,Industry,90.25970029272139 23 | PhD,Academia,85.16300668986514 24 | Masters,Industry,89.58072764403187 25 | PhD,Academia,85.37737160664983 26 | PhD,Academia,85.0828845105134 27 | Masters,Industry,91.47756954631768 28 | Masters,Industry,90.54214000562206 29 | Masters,Industry,90.95082530193031 30 | Masters,Academia,84.32561727589928 31 | Masters,Industry,90.76082284515724 32 | Masters,Industry,91.21495885658078 33 | Masters,Industry,91.48115599853918 34 | Masters,Industry,91.20350587111898 35 | Masters,Academia,84.14068312523887 36 | Masters,Industry,90.90009946771897 37 | Masters,Industry,91.34475201112218 38 | Masters,Industry,90.6169246234931 39 | Masters,Industry,91.17985331243835 40 | Masters,Industry,91.24937468231656 41 | Masters,Industry,91.40493377088569 42 | Masters,Industry,91.1165353488177 43 | Masters,Industry,91.10686429939233 44 | PhD,Academia,86.0538539250847 45 | Masters,Industry,90.67042803671211 46 | Masters,Industry,91.46984667144716 47 | Masters,Industry,91.05790561670437 48 | PhD,Industry,93.16026216419414 49 | PhD,Academia,85.91064599249512 50 | PhD,Industry,93.40985550289042 51 | Masters,Industry,90.94344162987545 52 | Masters,Industry,91.1740557027515 53 | PhD,Academia,86.04063351545483 54 | PhD,Industry,93.05706270388328 55 | Masters,Industry,91.24015223653987 56 | Masters,Industry,90.50113883288577 57 | Masters,Academia,85.33032013499178 58 | Masters,Industry,90.60402934253216 59 | Masters,Academia,85.49514183099382 60 | Masters,Industry,91.40050469245762 61 | Masters,Industry,91.14769996097311 62 | Masters,Industry,90.70458788215183 63 | Masters,Industry,90.61883719847538 64 | Masters,Industry,90.56335103302263 65 | Masters,Industry,90.59819543571211 66 | Masters,Industry,91.17003402672708 67 | Masters,Industry,90.74728795373812 68 | PhD,Academia,86.44472820917144 69 | Masters,Industry,92.01611478556879 70 | Masters,Industry,92.28194353799336 71 | Masters,Industry,91.52015182306059 72 | Masters,Industry,92.24062326690182 73 | PhD,Industry,92.7338204937987 74 | PhD,Academia,86.36788870207965 75 | Masters,Academia,84.54085364961065 76 | Masters,Industry,91.61614312464371 77 | Masters,Industry,92.04146937443875 78 | Masters,Industry,92.29214980686083 79 | Masters,Industry,91.69815122988075 80 | Masters,Industry,91.8960476492066 81 | Masters,Industry,92.41877216659486 82 | PhD,Academia,87.04902912862599 83 | Masters,Industry,91.8766731780488 84 | Masters,Industry,92.38043254078366 85 | Masters,Industry,92.17296759225428 86 | PhD,Industry,93.16009169165045 87 | Masters,Industry,92.1522822889965 88 | Masters,Academia,85.46124948980287 89 | Masters,Industry,92.19455609074794 90 | PhD,Academia,86.68271354306489 91 | Masters,Industry,91.55971896206029 92 | Masters,Industry,91.99221259285696 93 | Masters,Industry,92.46854129247367 94 | PhD,Industry,93.70305993850343 95 | PhD,Academia,87.43917947425507 96 | Masters,Industry,91.98929925682023 97 | Masters,Industry,91.99563476326875 98 | Masters,Industry,92.30848569469526 99 | Masters,Industry,91.74357159482315 100 | PhD,Industry,93.83377221622504 101 | PhD,Industry,94.0215112566948 102 | -------------------------------------------------------------------------------- /data/jeter_justice.rda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/datamations/66d127edf9390e1f14aa3ae624b542afcb5cc415/data/jeter_justice.rda -------------------------------------------------------------------------------- /data/small_salary.rda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/datamations/66d127edf9390e1f14aa3ae624b542afcb5cc415/data/small_salary.rda -------------------------------------------------------------------------------- /datamations.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: Default 4 | SaveWorkspace: Default 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | 18 | BuildType: Package 19 | PackageUseDevtools: Yes 20 | PackageInstallArgs: --no-multiarch --with-keep.source 21 | PackageCheckArgs: --no-multiarch 22 | -------------------------------------------------------------------------------- /datamations/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Installation 3 | 4 | You can install datamations from this folder with: 5 | 6 | ``` 7 | pip install -e ../ 8 | ``` 9 | 10 | ## Usage 11 | To get started, start Jupyter server and open a notebook: 12 | ``` 13 | jupyter notebook ../notebooks/Datamations.ipynb 14 | ``` 15 | 16 | [datamation_sanddance()](https://github.com/microsoft/datamations/blob/main/datamations/datamation_frame.py#L350) is the main function that a user will call to generate a datamation. 17 | 18 | ```python 19 | from datamations import * 20 | 21 | df = DatamationFrame(small_salary().df) 22 | 23 | df.groupby('Degree').mean().datamation_sanddance() 24 | ``` 25 | 26 | 27 | You can group by multiple variables, as in this example, grouping by 28 | `Degree` and `Work` before calculating the mean `Salary`: 29 | 30 | ```python 31 | df.groupby(['Degree', 'Work']).mean().datamation_sanddance() 32 | ``` 33 | 34 | 35 | -------------------------------------------------------------------------------- /datamations/__init__.py: -------------------------------------------------------------------------------- 1 | # Create a plot datamation 2 | # 3 | # Create a plot datamation from a pandas pipeline. 4 | # 5 | 6 | from .utils import utils 7 | from .small_salary import small_salary 8 | from .datamation_frame import DatamationFrame 9 | from .datamation_groupby import DatamationGroupBy 10 | 11 | __all__ = [ 12 | "utils", "small_salary", "DatamationFrame", "DatamationGroupBy" 13 | ] 14 | -------------------------------------------------------------------------------- /datamations/pytest.ini: -------------------------------------------------------------------------------- 1 | # pytest.ini 2 | [pytest] 3 | minversion = 6.0 4 | addopts = -ra -q 5 | testpaths = 6 | tests -------------------------------------------------------------------------------- /datamations/small_salary.py: -------------------------------------------------------------------------------- 1 | # Read small salary csv file 2 | # 3 | 4 | import os 5 | import pandas as pd 6 | 7 | class small_salary: 8 | 9 | def __init__(self): 10 | script_dir = os.path.dirname( __file__ ) 11 | small_salary = os.path.join(script_dir, '../data-raw/small_salary.csv') 12 | self._data = pd.read_csv(small_salary) 13 | 14 | @property 15 | def df(self): 16 | return self._data 17 | -------------------------------------------------------------------------------- /datamations/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Create a plot datamation 2 | # 3 | # 4 | -------------------------------------------------------------------------------- /datamations/tests/test_datamation_groupby.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation 2 | # 3 | from pytest import approx 4 | from palmerpenguins import load_penguins 5 | from datamations import DatamationFrame 6 | from datamations import small_salary 7 | 8 | def test_datamation_groupby(): 9 | df = small_salary().df 10 | df = DatamationFrame(df) 11 | 12 | # Group by Degree 13 | mean = df.groupby('Degree').mean() 14 | 15 | assert "groupby" in mean.operations 16 | assert "mean" in mean.operations 17 | 18 | assert len(mean.states) == 2 19 | assert df.equals(mean.states[0]) 20 | 21 | assert mean.Salary.Masters == 90.22633400617633 22 | assert mean.Salary.PhD == 88.24560612632195 23 | 24 | # median 25 | median = df.groupby('Degree').median() 26 | 27 | assert "groupby" in median.operations 28 | assert "median" in median.operations 29 | 30 | assert len(median.states) == 2 31 | assert df.equals(median.states[0]) 32 | 33 | assert median.Salary.Masters == 91.13211765489541 34 | assert median.Salary.PhD == 86.40630845562555 35 | 36 | # sum 37 | sum = df.groupby('Degree').sum() 38 | 39 | assert "groupby" in sum.operations 40 | assert "sum" in sum.operations 41 | 42 | assert len(sum.states) == 2 43 | assert df.equals(sum.states[0]) 44 | 45 | assert sum.Salary.Masters == 6496.296048444696 46 | assert sum.Salary.PhD == 2470.8769715370145 47 | 48 | # product 49 | product = df.groupby('Degree').prod() 50 | 51 | assert "groupby" in product.operations 52 | assert "product" in product.operations 53 | 54 | assert len(product.states) == 2 55 | assert df.equals(product.states[0]) 56 | 57 | assert product.Salary.PhD == 2.9426590692781414e+54 58 | assert product.Salary.Masters == 5.892246828184284e+140 59 | 60 | # Group by Work 61 | mean = df.groupby('Work').mean() 62 | 63 | assert "groupby" in mean.operations 64 | assert "mean" in mean.operations 65 | 66 | assert len(mean.states) == 2 67 | assert df.equals(mean.states[0]) 68 | 69 | assert mean.Salary.Academia == 85.01222196154829 70 | assert mean.Salary.Industry == 91.48376118136609 71 | 72 | 73 | def test_datamation_groupby_multiple(): 74 | df = small_salary().df 75 | df = DatamationFrame(df) 76 | 77 | # Group by Degree, Work 78 | mean = df.groupby(['Degree', 'Work']).mean() 79 | 80 | assert "groupby" in mean.operations 81 | assert "mean" in mean.operations 82 | 83 | assert len(mean.states) == 2 84 | assert df.equals(mean.states[0]) 85 | 86 | assert mean.Salary.Masters.Academia == 84.0298831968801 87 | assert mean.Salary.Masters.Industry == 91.22576155606282 88 | assert mean.Salary.PhD.Academia == 85.55796571969728 89 | assert mean.Salary.PhD.Industry == 93.08335885824636 90 | 91 | # sum 92 | sum = df.groupby(['Degree', 'Work']).sum() 93 | 94 | assert "groupby" in sum.operations 95 | assert "sum" in sum.operations 96 | 97 | assert len(sum.states) == 2 98 | assert df.equals(sum.states[0]) 99 | 100 | assert sum.Salary.Masters.Academia == 840.2988319688011 101 | assert sum.Salary.Masters.Industry == 5655.997216475895 102 | assert sum.Salary.PhD.Academia == 1540.043382954551 103 | assert sum.Salary.PhD.Industry == 930.8335885824636 104 | 105 | # product 106 | product = df.groupby(['Degree', 'Work']).prod() 107 | 108 | assert "groupby" in product.operations 109 | assert "product" in product.operations 110 | 111 | assert len(product.states) == 2 112 | assert df.equals(product.states[0]) 113 | 114 | assert product.Salary.Masters.Academia == 1.753532557780977e+19 115 | assert product.Salary.Masters.Industry == 3.3602152421057308e+121 116 | assert product.Salary.PhD.Academia == 6.027761935702164e+34 117 | assert product.Salary.PhD.Industry == 4.8818435443657834e+19 118 | 119 | # Group by species, island, sex 120 | df = DatamationFrame(load_penguins()) 121 | mean = df.groupby(['species', 'island', 'sex']).mean() 122 | 123 | assert "groupby" in mean.operations 124 | assert "mean" in mean.operations 125 | 126 | assert len(mean.states) == 2 127 | assert df.equals(mean.states[0]) 128 | 129 | assert mean.bill_length_mm.Adelie.Biscoe.male == approx(40.5909090909091) 130 | assert mean.bill_length_mm.Adelie.Biscoe.female == approx(37.35909090909092) 131 | -------------------------------------------------------------------------------- /datamations/tests/test_small_salary.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation 2 | # 3 | 4 | from datamations import small_salary 5 | 6 | def test_small_salary(capsys): 7 | df = small_salary().df 8 | print(df.groupby('Work').mean(numeric_only=True)) 9 | captured = capsys.readouterr() 10 | 11 | assert "Work" in captured.out 12 | assert "Salary" in captured.out 13 | assert "Academia" in captured.out 14 | assert "Industry" in captured.out 15 | assert "85.012222" in captured.out 16 | assert "91.483761" in captured.out 17 | -------------------------------------------------------------------------------- /datamations/utils.py: -------------------------------------------------------------------------------- 1 | # Utility functions 2 | # 3 | 4 | import copy 5 | 6 | class utils(): 7 | 8 | X_FIELD_CHR = "datamations_x" 9 | Y_FIELD_CHR = "datamations_y" 10 | Y_RAW_FIELD_CHR = "datamations_y_raw" 11 | Y_TOOLTIP_FIELD_CHR = "datamations_y_tooltip" 12 | 13 | def generate_vega_specs(data, meta, spec_encoding, facet_encoding=None, facet_dims=None, errorbar=False, height=300, width=300): 14 | mark = { 15 | "type": "point", 16 | "filled": True, 17 | "strokeWidth": 1 18 | } 19 | if not errorbar: 20 | if facet_encoding: 21 | spec = { 22 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 23 | "meta": meta, 24 | "data": { 25 | "values": data 26 | }, 27 | "facet": facet_encoding, 28 | "spec": { 29 | "height": height / facet_dims["nrow"] if facet_dims else 1, 30 | "width": width / facet_dims["ncol"] if facet_dims else 1, 31 | "mark": mark, 32 | "encoding": spec_encoding 33 | } 34 | } 35 | else: 36 | spec = { 37 | "height": height, 38 | "width": width, 39 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 40 | "meta": meta, 41 | "data": { 42 | "values": data 43 | }, 44 | "mark": mark, 45 | "encoding": spec_encoding 46 | } 47 | 48 | else: 49 | errorbar_spec_encoding = copy.deepcopy(spec_encoding) 50 | errorbar_spec_encoding["y"]["field"] = utils.Y_RAW_FIELD_CHR 51 | 52 | if facet_encoding: 53 | spec = { 54 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 55 | "meta": meta, 56 | "data": { 57 | "values": data 58 | }, 59 | "facet": facet_encoding, 60 | "spec": { 61 | "height": height / facet_dims["nrow"] if facet_dims else 1, 62 | "width": width / facet_dims["ncol"] if facet_dims else 1, 63 | "layer": [ 64 | { 65 | "mark": "errorbar", 66 | "encoding": errorbar_spec_encoding 67 | }, 68 | { 69 | "mark": mark, 70 | "encoding": spec_encoding 71 | } 72 | ] 73 | } 74 | } 75 | else: 76 | spec = { 77 | "height": height, 78 | "width": width, 79 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 80 | "data": { 81 | "values": data 82 | }, 83 | "meta": meta, 84 | "layer": [ 85 | { 86 | "mark": "errorbar", 87 | "encoding": errorbar_spec_encoding 88 | }, 89 | { 90 | "mark": mark, 91 | "encoding": spec_encoding 92 | } 93 | ] 94 | } 95 | return spec 96 | -------------------------------------------------------------------------------- /demo-app/githublink.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /demo-app/www/style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap'); 2 | 3 | :root { 4 | --background-color: #FAF9F6; 5 | --font-color: #555; 6 | } 7 | 8 | body { 9 | font-size: 14px; 10 | background-color: var(--background-color); 11 | color: var(--font-color); 12 | font-family: 'IBM Plex Sans', sans-serif; 13 | } 14 | 15 | h1, h2, h3, h4 { 16 | font-weight:400; 17 | } 18 | 19 | .form-control, .action-button { 20 | font-size: 18px; 21 | } 22 | 23 | .form-group > label { 24 | font-size: 14px; 25 | } 26 | 27 | .h3 { 28 | font-size:18px; 29 | } 30 | 31 | .header { 32 | font-size:4rem; 33 | font-weight:300; 34 | margin-top:0; 35 | } 36 | 37 | .header > span { 38 | font-size:5rem; 39 | font-weight:500; 40 | background-color: lightyellow; 41 | padding: 0 4px 4px 4px; 42 | margin-right:-4px; 43 | margin-left:-4px; 44 | } 45 | 46 | .header-container { 47 | display:flex; 48 | justify-content: flex-start; 49 | align-items: center; 50 | flex-wrap: wrap; 51 | margin:32px; 52 | gap: 32px; 53 | } 54 | 55 | .header-container > p { 56 | max-width:700px; 57 | font-size:16px; 58 | margin-top:10px; 59 | margin-bottom: 0; 60 | line-height:2.2; 61 | display: inline; 62 | margin-bottom: 1rem; 63 | display: inline-block; 64 | } 65 | 66 | .header-container > p > span { 67 | display: inline; 68 | background: lightyellow; 69 | padding: 0.5rem; 70 | padding-left: 0; 71 | padding-right: 0; 72 | box-shadow: 10px 0 0 lightyellow, -10px 0 0 lightyellow; 73 | } 74 | 75 | .container { 76 | background-color:white; 77 | padding:16px; 78 | box-shadow: rgba(0, 0, 0, 0.05) 0px 6px 24px 0px, rgba(0, 0, 0, 0.08) 0px 0px 0px 1px; 79 | border-radius:0.25em; 80 | margin:16px; 81 | } 82 | 83 | .underlines { 84 | box-shadow: inset 0 -0.5em 5px 0 #f1f386; 85 | width:max-content; 86 | } 87 | 88 | #go { 89 | background-color:darkred; 90 | color:white; 91 | width:60px; 92 | transform:translateY(-8px); 93 | } 94 | 95 | /* scroll box */ 96 | @keyframes shift { 97 | 0%, 98 | 100% { 99 | transform: translateX(0.6rem) scale(1.1); 100 | opacity: 1; 101 | } 102 | 50% { 103 | transform: translateX(-0.1rem) scale(1); 104 | opacity: 0.8; 105 | } 106 | } 107 | 108 | 109 | 110 | #go > span { 111 | display: inline-block; 112 | animation: shift 4s ease-out infinite; 113 | 114 | } 115 | 116 | .header-two-long, .header-two-short, .header-two-medium { 117 | margin-top:-10px; 118 | margin-bottom:20px; 119 | width:max-content; 120 | isolation:isolate; 121 | } 122 | 123 | .header-two-short::after, 124 | .header-two-medium::after, 125 | .header-two-long::after { 126 | content: ''; 127 | height: 50px; 128 | background-image: linear-gradient(to top right, #FFD580 , #ffffe0); 129 | position:relative; 130 | display: inline-block; 131 | clip-path: polygon(4% 78%, 13% 72%, 35% 66%, 48% 66%, 62% 68%, 78% 72%, 92% 77%, 96% 83%, 93% 87%, 84% 84%, 67% 81%, 48% 79%, 28% 79%, 10% 85%, 5% 83%); 132 | right: 50%; 133 | top: 15px; 134 | z-index:-1; 135 | } 136 | 137 | 138 | .header-two-short::after { 139 | width: 110px; 140 | } 141 | 142 | .header-two-medium::after { 143 | width: 135px; 144 | } 145 | 146 | .header-two-long::after { 147 | width: 160px; 148 | } 149 | 150 | .button-container { 151 | display:flex; 152 | flex-wrap:wrap; 153 | gap:32px; 154 | padding:0 16px; 155 | align-items: center; 156 | justify-content: center; 157 | } 158 | 159 | .add-button { 160 | width:30px; 161 | height: 30px; 162 | background-color:white; 163 | border: 1px solid lightgrey; 164 | border-radius:4px; 165 | margin-top:-4px; 166 | margin-left:-4px; 167 | } 168 | 169 | .add-button > i { 170 | color:darkgrey; 171 | } 172 | 173 | .add-button:hover, 174 | .add-button:focus { 175 | box-shadow: 1px black; 176 | } -------------------------------------------------------------------------------- /dist/datamations-1.0-py2.7.egg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/datamations/66d127edf9390e1f14aa3ae624b542afcb5cc415/dist/datamations-1.0-py2.7.egg -------------------------------------------------------------------------------- /dist/datamations-1.0-py3.9.egg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/datamations/66d127edf9390e1f14aa3ae624b542afcb5cc415/dist/datamations-1.0-py3.9.egg -------------------------------------------------------------------------------- /inst/README.md: -------------------------------------------------------------------------------- 1 | # Datamations 2 | 3 | Datamations Javascript code relies on [vega](https://vega.github.io/vega/), [vega-lite](https://vega.github.io/vega-lite/) for rendering and [gemini](https://github.com/uwdata/gemini) for animations. An animation is a collection of vega-lite specs with a datamations specific fields. 4 | 5 | ### Development 6 | 7 | js code is an npm project, run these commands below to build datamations js module any time that the js code is updated so that backend languages have access to the updated datamations.min.js file (there's also a map file that should make for easy debugging despite the minified code) 8 | 9 | ``` 10 | npm install 11 | npm run build 12 | ``` 13 | 14 | ### Tests 15 | ``` 16 | npm run test 17 | ``` 18 | 19 | For more detailed code documentation, see README [here](./htmlwidgets/js/) 20 | 21 | 22 | 23 | 24 | A `datamations spec` is a superset of vega-lite spec, meaning it has some more fields on top on the vega-lite spec. 25 | 26 | * meta - a configuration metafields used to instruct vega-lite spec processing 27 | * data.values are handled differently when `meta.parse = grid`. More on that, below. 28 | 29 | ``` 30 | { 31 | 32 | "meta": { 33 | "parse": "grid" | "jitter", 34 | "description": "Plot Salary within each group", 35 | "custom_animation: "min" | "max" | "median" | "mean" | "quantile" | "count", 36 | "splitField": "Degree", 37 | "axes": true, 38 | "xAxisLabels": [ 39 | "Masters", 40 | "PhD" 41 | ] 42 | }, 43 | "data": { 44 | "values: [] 45 | }, 46 | "mark": { 47 | "type": "point", 48 | "filled": true, 49 | "strokeWidth": 1 50 | }, 51 | "encoding": { 52 | "x": { 53 | "field": "datamations_x", 54 | "type": "quantitative", 55 | "axis": null 56 | }, 57 | "y": { 58 | "field": "datamations_y", 59 | "type": "quantitative", 60 | "axis": null 61 | } 62 | } 63 | } 64 | ``` 65 | 66 | Meta fields explanation: 67 | 68 | 69 | | Field | Description | 70 | |-----------------------|------------------------------------------------------------------------| 71 | | meta.parse | Generating grid or jittered spec. | 72 | | meta.description | Spec description shown above the chart. | 73 | | meta.custom_animation | Custom animation types. | 74 | | meta.splitField | If specified, splits points on x axis and forms inner groups. | 75 | | meta.axes | If true, renders a spec as a separate layout under the the main chart. | 76 | | meta.xAxisLabels | Labels for x axis. Used when splitField is specified. | 77 | 78 | Data format: 79 | 80 | * `data.values` is an array of objects with `gemini_id`, which is used to keep track of circle during animation 81 | * If `meta.parse = "grid"`, `data.values` should be: 82 | 83 | ``` 84 | [ 85 | { n: [group size], gemini_ids: [1, 2, ...n], ...[any other fields] } 86 | ] 87 | ``` 88 | 89 | 90 | ### Usage 91 | 92 | # App() · [Source](https://github.com/microsoft/datamations/blob/main/inst/htmlwidgets/js/app.js) 93 | 94 | ```html 95 | 96 | 97 |
98 |
99 |
100 |
101 |
102 | 103 |
104 |
105 | 112 |
113 |
114 | 117 |
118 |
119 |
120 | 121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 | ``` 132 | 133 | ```javascript 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 151 | ``` 152 | 153 | Methods: 154 | 155 | #### app.onSlide - Slider callback. Draws a frame when slider value changes. 156 | #### app.play - play the animation. 157 | #### app.exportPNG - export array of png images. 158 | #### app.exportGif - export gif. 159 | #### app.animateFrame - animates a single frame 160 | #### app.getFrames - returns frames. 161 | -------------------------------------------------------------------------------- /inst/app/www/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 18px; 3 | } 4 | 5 | .form-control, .action-button { 6 | font-size: 18px; 7 | } 8 | -------------------------------------------------------------------------------- /inst/export-gif/exported.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/datamations/66d127edf9390e1f14aa3ae624b542afcb5cc415/inst/export-gif/exported.gif -------------------------------------------------------------------------------- /inst/export-gif/index.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const puppeteer = require("puppeteer"); 3 | 4 | (async () => { 5 | const browser = await puppeteer.launch({ devtools: false }); 6 | const page = await browser.newPage(); 7 | 8 | // put shiny app url here 9 | await page.goto("http://localhost:8080/test/"); 10 | 11 | console.log("Generating gif...") 12 | 13 | const gif = await page.evaluate(() => { 14 | // access window property here 15 | return window.app.exportGif(); 16 | }); 17 | 18 | console.log("Done.") 19 | 20 | const buffer = Buffer.from(gif.replace(/^data:image\/gif;base64,/,""), "base64"); 21 | await fs.writeFile("./exported.gif", buffer, function (err, result) { 22 | if (err) console.log("error", err); 23 | }); 24 | 25 | await browser.close(); 26 | })(); 27 | -------------------------------------------------------------------------------- /inst/export-gif/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datamations-test", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "puppeteer": "^13.5.2", 13 | "puppeteer-core": "^13.5.2" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /inst/golem-config.yml: -------------------------------------------------------------------------------- 1 | default: 2 | golem_name: datamations 3 | golem_version: 0.0.0.9007 4 | app_prod: no 5 | production: 6 | app_prod: yes 7 | dev: 8 | golem_wd: !expr here::here() 9 | -------------------------------------------------------------------------------- /inst/htmlwidgets/README.md: -------------------------------------------------------------------------------- 1 | Contains files for the Javascript side of generating a datamation. 2 | 3 | * [datamationSandDance.js](https://github.com/microsoft/datamations/blob/main/inst/htmlwidgets/datamationSandDance.js) is called by [datamationSandDance()](https://github.com/microsoft/datamations/blob/main/R/datamation_sanddance.R#L146) as the last step of generating a datamation via [datamation_sanddance()](https://github.com/microsoft/datamations/blob/main/R/datamation_sanddance.R#L31) - it just takes the specs and passed them off to the javascript code, in `js/` 4 | * [datamationSandDance.yaml](https://github.com/microsoft/datamations/blob/main/inst/htmlwidgets/datamationSandDance.yaml) defines javascript dependencies for the widget 5 | * `css/` styles the datamation HTML elements, with layout controlled by [datamationSandDance_html()](https://github.com/microsoft/datamations/blob/main/R/datamation_sanddance.R#L194) 6 | * `js/` contains all of the actual JS code for the datamation 7 | * `d3/`, `gemini/`, `vega/`, `vega-embed/`, and `vega-lite/` are dependencies 8 | -------------------------------------------------------------------------------- /inst/htmlwidgets/css/datamationSandDance.css: -------------------------------------------------------------------------------- 1 | .vega-vis-wrapper { 2 | position: relative; 3 | } 4 | 5 | .vega-vis-wrapper > div { 6 | position: absolute; 7 | top: 0; 8 | left: 0; 9 | } 10 | 11 | .vega-vis-wrapper .vega-vis svg { 12 | background-color: transparent !important; 13 | } 14 | 15 | .vega-vis-wrapper .vega-vis.with-axes .background { 16 | stroke: none !important; 17 | } 18 | 19 | .vega-vis-wrapper .vega-vis.with-axes details { 20 | display: none; 21 | } 22 | 23 | .vega-vis-wrapper .vega-for-axis { 24 | opacity: 0; 25 | } 26 | 27 | .vega-vis-wrapper .vega-for-axis .mark-symbol { 28 | display: none; 29 | } 30 | 31 | .vega-vis-wrapper .vega-for-axis .role-title { 32 | display: none; 33 | } 34 | 35 | .vega-vis-wrapper .vega-for-axis .role-legend { 36 | display: none; 37 | } 38 | 39 | .control-bar { 40 | width: 100%; 41 | display: flex; 42 | align-items: center; 43 | } 44 | 45 | .slider-wrapper { 46 | padding-left: 10px; 47 | padding-right: 10px; 48 | flex-grow: 1; 49 | } 50 | 51 | .slider-wrapper input { 52 | width: 100%; 53 | } 54 | 55 | details { 56 | display: none !important; 57 | } 58 | 59 | .vega-embed.has-actions { 60 | padding-right: 0px !important; 61 | } 62 | 63 | .description { 64 | text-align: center; 65 | padding-top: 10px; 66 | margin-bottom: 10px; 67 | } 68 | 69 | .vega-other-layers { 70 | position: relative; 71 | } 72 | 73 | .vega-other-layers > div { 74 | position: absolute; 75 | top: 0; 76 | left: 0; 77 | } 78 | 79 | .vega-other-layers > div svg { 80 | background-color: transparent !important; 81 | } 82 | 83 | .vega-other-layers .vega-hidden-layer { 84 | transition: 1s all ease-in-out; 85 | } 86 | 87 | .vega-other-layers .vega-hidden-layer .role-legend { 88 | display: none; 89 | } 90 | 91 | .vega-other-layers .vega-hidden-layer .role-axis { 92 | display: none; 93 | } 94 | 95 | .vega-other-layers .vega-hidden-layer .role-title { 96 | display: none; 97 | } 98 | 99 | .vega-other-layers .vega-hidden-layer .background { 100 | stroke: none !important; 101 | } 102 | 103 | .vega-other-layers.with-axes details { 104 | display: none; 105 | } 106 | 107 | .export-wrapper { 108 | height: 575px; 109 | width: 575px; 110 | } 111 | 112 | .spin { 113 | animation: spin 2s linear infinite; 114 | } 115 | 116 | @keyframes spin { 117 | 0% { transform: rotate(0deg); } 118 | 100% { transform: rotate(360deg); } 119 | } -------------------------------------------------------------------------------- /inst/htmlwidgets/d3/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2010-2021 Mike Bostock 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose 4 | with or without fee is hereby granted, provided that the above copyright notice 5 | and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 9 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 11 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 12 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 13 | THIS SOFTWARE. 14 | t 15 | -------------------------------------------------------------------------------- /inst/htmlwidgets/datamationSandDance.js: -------------------------------------------------------------------------------- 1 | HTMLWidgets.widget({ 2 | name: "datamationSandDance", 3 | 4 | type: "output", 5 | 6 | factory: function (el, width, height) { 7 | return { 8 | 9 | renderValue: function (x) { 10 | el.id = el.id.replace(/-/g, ""); 11 | window['app' + el.id] = datamations.App(el.id, {specs: x.specs, autoPlay: true}); 12 | }, 13 | 14 | resize: function (width, height) { }, 15 | }; 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /inst/htmlwidgets/datamationSandDance.yaml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - name: vega 3 | version: 5.20.2 4 | src: htmlwidgets/vega 5 | script: vega.js 6 | - name: vega-lite 7 | version: 5.1.0 8 | src: htmlwidgets/vega-lite 9 | script: vega-lite.js 10 | - name: vega-embed 11 | version: 6.17.0 12 | src: htmlwidgets/vega-embed 13 | script: vega-embed.js 14 | - name: d3 15 | version: 6.7.0 16 | src: htmlwidgets/d3 17 | script: d3.js 18 | - name: gemini 19 | version: 0.1.0 20 | src: htmlwidgets/gemini 21 | script: gemini.web.js 22 | - name: gifshot 23 | version: 0.0.1 24 | src: htmlwidgets/js 25 | script: gifshot.min.js 26 | - name: html2canvas 27 | version: 0.0.1 28 | src: htmlwidgets/js 29 | script: html2canvas.min.js 30 | - name: download2 31 | version: 0.0.1 32 | src: htmlwidgets/js 33 | script: download2.js 34 | - name: datamations 35 | version: 0.0.1 36 | src: htmlwidgets/js/src/dist 37 | script: datamations.min.js 38 | - name: css 39 | version: 0.0.1 40 | src: htmlwidgets/css 41 | stylesheet: datamationSandDance.css 42 | - name: fa-icons 43 | version: 0.0.1 44 | src: htmlwidgets/css 45 | stylesheet: fa-all.min.css 46 | -------------------------------------------------------------------------------- /inst/htmlwidgets/dev-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Datamations 8 | 9 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
30 |
31 |
32 | 33 |
34 |
35 | 43 |
44 |
45 | 48 |
49 |
50 |
51 | 52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | 63 | 64 | 65 | 66 | 72 | 73 | 74 | 75 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /inst/htmlwidgets/gemini/LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, UW Interactive Data Lab 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /inst/htmlwidgets/js/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true 5 | }, 6 | extends: [ 7 | 'standard' 8 | ], 9 | parserOptions: { 10 | ecmaVersion: 'latest' 11 | }, 12 | rules: { 13 | "no-use-before-define": ["off"], 14 | "max-lines-per-function": ["off"], 15 | "promise/param-names" : ["off"], 16 | "n/no-callback-literal" : ["off"], 17 | "camelcase" : ["off"], 18 | "no-undef" : ["off"], 19 | "no-unused-vars" : ["off"], 20 | "promise/param-names" : ["off"], 21 | "brace-style" : ["off"], 22 | "no-var" : ["off"] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /inst/htmlwidgets/js/README.md: -------------------------------------------------------------------------------- 1 | #### Welcome to javascript code documentation! 2 | 3 | * app.js - entry point [read docs](./docs/app.js.md) 4 | * custom-animations.js - entry point [read docs](./docs/custom-animations.js.md) 5 | * hack-facet-view.js - hacks vega-lite facet views, because gemini does not support multi layer animations [read docs](./docs/hack-facet-view.js.md) 6 | * layout.js - computes layouts: grid, jitter, etc. [read docs](./docs/layout.js.md) 7 | * utils.js - some utility functions [read docs](./docs/utils.js.md) 8 | 9 | Documentation generated by [jsdoc-to-markdown](https://github.com/jsdoc2md/jsdoc-to-markdown) 10 | 11 | Generate javascript documentations: 12 | 13 | ```sh 14 | cd ./inst/htmlwidgets/js/docs 15 | sh generate.sh 16 | ``` -------------------------------------------------------------------------------- /inst/htmlwidgets/js/docs/app.js.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/datamations/66d127edf9390e1f14aa3ae624b542afcb5cc415/inst/htmlwidgets/js/docs/app.js.md -------------------------------------------------------------------------------- /inst/htmlwidgets/js/docs/custom-animations.js.md: -------------------------------------------------------------------------------- 1 | ## Constants 2 | 3 |
4 |
getCountStep
5 |

Generates a spec for count animation

6 |
7 |
getMedianStep
8 |

Generates a spec for median and quantile animations

9 |
10 |
getMeanStep
11 |

Generates a spec for mean animation

12 |
13 |
getMinMaxStep
14 |

Generates a spec for min and max animations

15 |
16 |
CustomAnimations
17 |

Configuration for custom animations. 18 | When meta.custom_animation is present, 19 | it looks up a function here and generates custom animation specifications

20 |
21 |
22 | 23 | 24 | 25 | ## getCountStep ⇒ 26 | Generates a spec for count animation 27 | 28 | **Kind**: global constant 29 | **Returns**: a vega lite spec 30 | 31 | | Param | Type | Description | 32 | | --- | --- | --- | 33 | | source | Object | source spec | 34 | | target | Object | target spec | 35 | | shrink | Object | if truthy, circles will be pulled up | 36 | 37 | 38 | 39 | ## getMedianStep ⇒ 40 | Generates a spec for median and quantile animations 41 | 42 | **Kind**: global constant 43 | **Returns**: a vega lite spec 44 | 45 | | Param | Type | Description | 46 | | --- | --- | --- | 47 | | source | Object | source spec | 48 | | target | Object | target spec | 49 | | step | Number | step counter. null is the last step | 50 | | p | Number | a percentile | 51 | 52 | 53 | 54 | ## getMeanStep ⇒ 55 | Generates a spec for mean animation 56 | 57 | **Kind**: global constant 58 | **Returns**: a vega lite spec 59 | 60 | | Param | Type | Description | 61 | | --- | --- | --- | 62 | | source | Object | source spec | 63 | | target | Object | target spec | 64 | 65 | 66 | 67 | ## getMinMaxStep ⇒ 68 | Generates a spec for min and max animations 69 | 70 | **Kind**: global constant 71 | **Returns**: a vega lite spec 72 | 73 | | Param | Type | Description | 74 | | --- | --- | --- | 75 | | source | Object | source spec | 76 | | target | Object | target spec | 77 | | minOrMax | String | "min" or "max" | 78 | 79 | 80 | 81 | ## CustomAnimations 82 | Configuration for custom animations. 83 | When meta.custom_animation is present, 84 | it looks up a function here and generates custom animation specifications 85 | 86 | **Kind**: global constant 87 | 88 | - [Constants](#constants) 89 | - [getCountStep ⇒](#getcountstep-) 90 | - [getMedianStep ⇒](#getmedianstep-) 91 | - [getMeanStep ⇒](#getmeanstep-) 92 | - [getMinMaxStep ⇒](#getminmaxstep-) 93 | - [CustomAnimations](#customanimations) 94 | - [CustomAnimations.count(rawSource, target) ⇒](#customanimationscountrawsource-target-) 95 | - [CustomAnimations.min(rawSource, target) ⇒](#customanimationsminrawsource-target-) 96 | - [CustomAnimations.max(rawSource, target) ⇒](#customanimationsmaxrawsource-target-) 97 | - [CustomAnimations.mean(rawSource, target) ⇒](#customanimationsmeanrawsource-target-) 98 | - [CustomAnimations.median(rawSource, target) ⇒](#customanimationsmedianrawsource-target-) 99 | 100 | 101 | 102 | ### CustomAnimations.count(rawSource, target) ⇒ 103 | steps: 104 | 1) stack sets 105 | 2) put rules (lines) using aggregate count 106 | 3) replace with count bubbles (aggregate count) (basically target spec) 107 | 108 | **Kind**: static method of [CustomAnimations](#CustomAnimations) 109 | **Returns**: an array of vega-lite specs 110 | 111 | | Param | Type | Description | 112 | | --- | --- | --- | 113 | | rawSource | Object | source spec | 114 | | target | Object | target spec | 115 | 116 | 117 | 118 | ### CustomAnimations.min(rawSource, target) ⇒ 119 | min animation steps: 120 | 1) source spec 121 | 2) stack sets, with a rule line at min circle 122 | 3) pull circles down 123 | 4) target spec 124 | 125 | **Kind**: static method of [CustomAnimations](#CustomAnimations) 126 | **Returns**: an array of vega-lite specs 127 | 128 | | Param | Type | Description | 129 | | --- | --- | --- | 130 | | rawSource | Object | source spec | 131 | | target | Object | target spec | 132 | 133 | 134 | 135 | ### CustomAnimations.max(rawSource, target) ⇒ 136 | max animation steps: 137 | 1) source spec 138 | 2) stack sets, with a rule line at max circle 139 | 3) pull circles up 140 | 4) target spec 141 | 142 | **Kind**: static method of [CustomAnimations](#CustomAnimations) 143 | **Returns**: an array of vega-lite specs 144 | 145 | | Param | Type | Description | 146 | | --- | --- | --- | 147 | | rawSource | Object | source spec | 148 | | target | Object | target spec | 149 | 150 | 151 | 152 | ### CustomAnimations.mean(rawSource, target) ⇒ 153 | mean animation steps: 154 | 1) source spec 155 | 2) intermediate: circles will be placed diagonally "/" 156 | 3) add lines (rules) at mean level 157 | 4) convert circles to small ticks 158 | 5) show vertical lines 159 | 6) collapse the lines to mean level 160 | 7) target spec 161 | 162 | **Kind**: static method of [CustomAnimations](#CustomAnimations) 163 | **Returns**: an array of vega-lite specs 164 | 165 | | Param | Type | Description | 166 | | --- | --- | --- | 167 | | rawSource | Object | source spec | 168 | | target | Object | target spec | 169 | 170 | 171 | 172 | ### CustomAnimations.median(rawSource, target) ⇒ 173 | median and quantile animation steps: 174 | 1) source spec 175 | 2) show rules at the top and bottom 176 | 3) split circles by median and move to the right and left and move rules to median level 177 | 4) target spec 178 | 179 | **Kind**: static method of [CustomAnimations](#CustomAnimations) 180 | **Returns**: an array of vega-lite specs 181 | 182 | | Param | Type | Description | 183 | | --- | --- | --- | 184 | | rawSource | Object | source spec | 185 | | target | Object | target spec | 186 | 187 | -------------------------------------------------------------------------------- /inst/htmlwidgets/js/docs/generate.sh: -------------------------------------------------------------------------------- 1 | jsdoc2md ../src/scripts/app.js > app.js.md 2 | jsdoc2md ../src/scripts/custom-animations.js > custom-animations.js.md 3 | jsdoc2md ../src/scripts/hack-facet-view.js > hack-facet-view.js.md 4 | jsdoc2md ../src/scripts/layout.js > layout.js.md 5 | jsdoc2md ../src/scripts/utils.js > utils.js.md -------------------------------------------------------------------------------- /inst/htmlwidgets/js/docs/hack-facet-view.js.md: -------------------------------------------------------------------------------- 1 | ## Functions 2 | 3 |
4 |
getEmptySpec(spec)
5 |

Get empty spec, if no data is present

6 |
7 |
getSpecTemplate(width, height, axes, spec)
8 |

Creates and returns a template for vega spec

9 |
10 |
getHackedSpec(param0)
11 |

Get hacked spec 12 | Finding coordinates of each circle and treat them as real values in the one axis view 13 | Adding axis layer underneath to look exactly same as faceted view

14 |
15 |
hackFacet(spec)
16 |

turns faceted spec to regular spec, using hacking technique

17 |
18 |
19 | 20 | 21 | 22 | ## getEmptySpec(spec) ⇒ 23 | Get empty spec, if no data is present 24 | 25 | **Kind**: global function 26 | **Returns**: vega-lite spec 27 | 28 | | Param | Type | 29 | | --- | --- | 30 | | spec | Object | 31 | 32 | 33 | 34 | ## getSpecTemplate(width, height, axes, spec) ⇒ 35 | Creates and returns a template for vega spec 36 | 37 | **Kind**: global function 38 | **Returns**: vega-lite spec 39 | 40 | | Param | Type | Description | 41 | | --- | --- | --- | 42 | | width | Number | spec width | 43 | | height | Number | spec height | 44 | | axes | Object | which axes to add | 45 | | spec | Object | original spec | 46 | 47 | 48 | 49 | ## getHackedSpec(param0) ⇒ 50 | Get hacked spec 51 | Finding coordinates of each circle and treat them as real values in the one axis view 52 | Adding axis layer underneath to look exactly same as faceted view 53 | 54 | **Kind**: global function 55 | **Returns**: vega-lite spec 56 | 57 | | Param | Type | Description | 58 | | --- | --- | --- | 59 | | param0 | Object | parameters | 60 | | param0.view | Object | a vega view instance | 61 | | param0.spec | Object | a vega spec | 62 | | param0.width | Object | spec width | 63 | | param0.height | Object | spec height | 64 | 65 | 66 | 67 | ## hackFacet(spec) ⇒ 68 | turns faceted spec to regular spec, using hacking technique 69 | 70 | **Kind**: global function 71 | **Returns**: vega-lite spec 72 | 73 | | Param | Type | Description | 74 | | --- | --- | --- | 75 | | spec | Object | vega lite spec with facets | 76 | 77 | -------------------------------------------------------------------------------- /inst/htmlwidgets/js/docs/layout.js.md: -------------------------------------------------------------------------------- 1 | ## Functions 2 | 3 |
4 |
generateGrid(spec, rows, stacked)
5 |

Generates data for grid specs

6 |
7 |
getGridSpec(spec, rows)
8 |

Generates infogrid specification

9 |
10 |
getJitterSpec(spec)
11 |

Generates jittered specification using d3-force

12 |
13 |
14 | 15 | 16 | 17 | ## generateGrid(spec, rows, stacked) ⇒ 18 | Generates data for grid specs 19 | 20 | **Kind**: global function 21 | **Returns**: an array of objects 22 | 23 | | Param | Type | Description | 24 | | --- | --- | --- | 25 | | spec | Object | vega-lite spec | 26 | | rows | Number | number of rows | 27 | | stacked | Boolean | if true, circles are stacked and vertically aliged | 28 | 29 | 30 | 31 | ## getGridSpec(spec, rows) ⇒ 32 | Generates infogrid specification 33 | 34 | **Kind**: global function 35 | **Returns**: grid specification 36 | 37 | | Param | Type | Description | 38 | | --- | --- | --- | 39 | | spec | Object | vega-lite specification | 40 | | rows | Number | number of rows in a grid | 41 | 42 | 43 | 44 | ## getJitterSpec(spec) ⇒ 45 | Generates jittered specification using d3-force 46 | 47 | **Kind**: global function 48 | **Returns**: jittered spec 49 | 50 | | Param | Type | Description | 51 | | --- | --- | --- | 52 | | spec | Object | vega-lite specification | 53 | 54 | -------------------------------------------------------------------------------- /inst/htmlwidgets/js/docs/utils.js.md: -------------------------------------------------------------------------------- 1 | ## Functions 2 | 3 |
4 |
getSelectors(id)
5 |

Gets selectors for each componenent, such as slider and animation divs

6 |
7 |
splitLayers(input)
8 |

Splits layers into separate vega-lite specifications, removes layer field

9 |
10 |
lookupByBucket(words, buckets, value)
11 |

Looks up a word based of buckets and value. 12 | Example:

13 |
    14 |
  • words: ['a', 'b', 'c']
  • 15 |
  • buckets: [10, 20, 30]
  • 16 |
  • value: 25 17 | will return 'c'
  • 18 |
19 |
20 |
getRows(vegaLiteSpecs)
21 |

Finds correct number of rows for grid based on biggest group

22 |
23 |
24 | 25 | 26 | 27 | ## getSelectors(id) ⇒ 28 | Gets selectors for each componenent, such as slider and animation divs 29 | 30 | **Kind**: global function 31 | **Returns**: object of selectors 32 | 33 | | Param | Type | Description | 34 | | --- | --- | --- | 35 | | id | String | root container id where all the animation components are rendered | 36 | 37 | 38 | 39 | ## splitLayers(input) ⇒ 40 | Splits layers into separate vega-lite specifications, removes layer field 41 | 42 | **Kind**: global function 43 | **Returns**: a list of specs 44 | 45 | | Param | Type | Description | 46 | | --- | --- | --- | 47 | | input | Object | vega-lite specification with layers | 48 | 49 | 50 | 51 | ## lookupByBucket(words, buckets, value) 52 | Looks up a word based of buckets and value. 53 | Example: 54 | - words: ['a', 'b', 'c'] 55 | - buckets: [10, 20, 30] 56 | - value: 25 57 | will return 'c' 58 | 59 | **Kind**: global function 60 | 61 | | Param | Type | Description | 62 | | --- | --- | --- | 63 | | words | Array | list of words | 64 | | buckets | Array | list of numbers | 65 | | value | Number | score to lookup | 66 | 67 | 68 | 69 | ## getRows(vegaLiteSpecs) ⇒ 70 | Finds correct number of rows for grid based on biggest group 71 | 72 | **Kind**: global function 73 | **Returns**: a number of rows 74 | 75 | | Param | Type | Description | 76 | | --- | --- | --- | 77 | | vegaLiteSpecs | Array | an array of vega lite specs | 78 | 79 | -------------------------------------------------------------------------------- /inst/htmlwidgets/js/download2.js: -------------------------------------------------------------------------------- 1 | //download.js v3.0, by dandavis; 2008-2014. [CCBY2] see http://danml.com/download.html for tests/usage 2 | // v1 landed a FF+Chrome compat way of downloading strings to local un-named files, upgraded to use a hidden frame and optional mime 3 | // v2 added named files via a[download], msSaveBlob, IE (10+) support, and window.URL support for larger+faster saves than dataURLs 4 | // v3 added dataURL and Blob Input, bind-toggle arity, and legacy dataURL fallback was improved with force-download mime and base64 support 5 | 6 | // data can be a string, Blob, File, or dataURL 7 | 8 | 9 | 10 | 11 | function download(data, strFileName, strMimeType) { 12 | 13 | var self = window, // this script is only for browsers anyway... 14 | u = "application/octet-stream", // this default mime also triggers iframe downloads 15 | m = strMimeType || u, 16 | x = data, 17 | D = document, 18 | a = D.createElement("a"), 19 | z = function(a){return String(a);}, 20 | 21 | 22 | B = self.Blob || self.MozBlob || self.WebKitBlob || z, 23 | BB = self.MSBlobBuilder || self.WebKitBlobBuilder || self.BlobBuilder, 24 | fn = strFileName || "download", 25 | blob, 26 | b, 27 | ua, 28 | fr; 29 | 30 | //if(typeof B.bind === 'function' ){ B=B.bind(self); } 31 | 32 | if(String(this)==="true"){ //reverse arguments, allowing download.bind(true, "text/xml", "export.xml") to act as a callback 33 | x=[x, m]; 34 | m=x[0]; 35 | x=x[1]; 36 | } 37 | 38 | 39 | 40 | //go ahead and download dataURLs right away 41 | if(String(x).match(/^data\:[\w+\-]+\/[\w+\-]+[,;]/)){ 42 | return navigator.msSaveBlob ? // IE10 can't do a[download], only Blobs: 43 | navigator.msSaveBlob(d2b(x), fn) : 44 | saver(x) ; // everyone else can save dataURLs un-processed 45 | }//end if dataURL passed? 46 | 47 | try{ 48 | 49 | blob = x instanceof B ? 50 | x : 51 | new B([x], {type: m}) ; 52 | }catch(y){ 53 | if(BB){ 54 | b = new BB(); 55 | b.append([x]); 56 | blob = b.getBlob(m); // the blob 57 | } 58 | 59 | } 60 | 61 | 62 | 63 | function d2b(u) { 64 | var p= u.split(/[:;,]/), 65 | t= p[1], 66 | dec= p[2] == "base64" ? atob : decodeURIComponent, 67 | bin= dec(p.pop()), 68 | mx= bin.length, 69 | i= 0, 70 | uia= new Uint8Array(mx); 71 | 72 | for(i;i { 35 | const obj = JSON.parse(JSON.stringify(input)) 36 | const animated = i === spec.layer.length - 1 37 | 38 | if (obj.meta) { 39 | obj.meta.animated = animated 40 | } else { 41 | obj.meta = { animated } 42 | } 43 | 44 | obj.spec.encoding = d.encoding 45 | obj.spec.mark = d.mark 46 | delete obj.spec.layer 47 | specArray.push(obj) 48 | }) 49 | } else if (input.layer) { 50 | input.layer.forEach((d, i) => { 51 | const obj = JSON.parse(JSON.stringify(input)) 52 | const animated = i === input.layer.length - 1 53 | 54 | if (obj.meta) { 55 | obj.meta.animated = animated 56 | } else { 57 | obj.meta = { animated } 58 | } 59 | 60 | obj.encoding = d.encoding 61 | obj.mark = d.mark 62 | delete obj.layer 63 | specArray.push(obj) 64 | }) 65 | } 66 | 67 | return specArray 68 | } 69 | 70 | /** 71 | * Looks up a word based of buckets and value. 72 | * Example: 73 | * - words: ['a', 'b', 'c'] 74 | * - buckets: [10, 20, 30] 75 | * - value: 25 76 | * will return 'c' 77 | * @param {Array} words list of words 78 | * @param {Array} buckets list of numbers 79 | * @param {Number} value score to lookup 80 | */ 81 | export function lookupByBucket (words, buckets, value) { 82 | return words[buckets.findIndex((d) => value <= d)] 83 | } 84 | 85 | /** 86 | * Finds correct number of rows for grid based on biggest group 87 | * @param {Array} vegaLiteSpecs an array of vega lite specs 88 | * @returns a number of rows 89 | */ 90 | export function getRows (vegaLiteSpecs) { 91 | let maxRows = 0 92 | 93 | vegaLiteSpecs 94 | .filter((d) => d.meta.parse === 'grid') 95 | .forEach((spec) => { 96 | let { width: specWidth, height: specHeight } = spec.spec || spec 97 | const encoding = spec.spec ? spec.spec.encoding : spec.encoding 98 | const splitField = spec.meta.splitField 99 | const groupKeys = [] 100 | 101 | if (spec.facet) { 102 | if (spec.facet.column) { 103 | groupKeys.push(spec.facet.column.field) 104 | } 105 | if (spec.facet.row) { 106 | groupKeys.push(spec.facet.row.field) 107 | } 108 | } 109 | 110 | let specValues = spec.data.values 111 | 112 | const gap = 2 113 | const distance = 4 + gap 114 | 115 | const secondarySplit = Object.keys(encoding).filter((d) => { 116 | const field = encoding[d].field 117 | return ( 118 | field !== splitField && 119 | IGNORE_FIELDS.indexOf(d) === -1 && 120 | groupKeys.indexOf(field) === -1 121 | ) 122 | })[0] 123 | 124 | // combine groups if secondarySplit 125 | if (splitField && secondarySplit) { 126 | const secondaryField = encoding[secondarySplit].field 127 | const keys = [...groupKeys, splitField] 128 | 129 | const grouped = d3.rollups( 130 | specValues, 131 | (arr) => { 132 | const obj = {} 133 | let sum = 0 134 | 135 | arr.forEach((x) => { 136 | sum += x.n 137 | obj[x[secondaryField]] = sum 138 | }) 139 | 140 | const o = { 141 | [splitField]: arr[0][splitField], 142 | [secondaryField]: obj, 143 | n: sum 144 | } 145 | 146 | groupKeys.forEach((x) => { 147 | o[x] = arr[0][x] 148 | }) 149 | 150 | return o 151 | }, 152 | ...keys.map((key) => { 153 | return (d) => d[key] 154 | }) 155 | ) 156 | 157 | specValues = grouped.flatMap((d) => { 158 | if (keys.length === 1) { 159 | return d[1] 160 | } else { 161 | return d[1].flatMap((d) => d[1]) 162 | } 163 | }) 164 | 165 | specWidth = specWidth / grouped.length 166 | } 167 | 168 | const maxN = d3.max(specValues, (d) => d.n) 169 | 170 | let rows = Math.ceil(Math.sqrt(maxN)) 171 | const maxCols = Math.ceil(maxN / rows) 172 | 173 | // if horizontal gap is less than 5, 174 | // then take up all vertical space to increase rows and reduce columns 175 | if (specWidth / maxCols < 5) { 176 | rows = Math.floor(specHeight / distance) 177 | } 178 | 179 | if (rows > maxRows) { 180 | maxRows = rows 181 | } 182 | }) 183 | 184 | return maxRows 185 | } 186 | -------------------------------------------------------------------------------- /inst/htmlwidgets/vega-embed/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, University of Washington Interactive Data Lab 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its contributors 15 | may be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /inst/htmlwidgets/vega-lite/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, University of Washington Interactive Data Lab. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its contributors 15 | may be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /inst/htmlwidgets/vega/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2018, University of Washington Interactive Data Lab 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its contributors 15 | may be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /inst/htmlwidgets/webfonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/datamations/66d127edf9390e1f14aa3ae624b542afcb5cc415/inst/htmlwidgets/webfonts/fa-brands-400.eot -------------------------------------------------------------------------------- /inst/htmlwidgets/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/datamations/66d127edf9390e1f14aa3ae624b542afcb5cc415/inst/htmlwidgets/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /inst/htmlwidgets/webfonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/datamations/66d127edf9390e1f14aa3ae624b542afcb5cc415/inst/htmlwidgets/webfonts/fa-brands-400.woff -------------------------------------------------------------------------------- /inst/htmlwidgets/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/datamations/66d127edf9390e1f14aa3ae624b542afcb5cc415/inst/htmlwidgets/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /inst/htmlwidgets/webfonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/datamations/66d127edf9390e1f14aa3ae624b542afcb5cc415/inst/htmlwidgets/webfonts/fa-regular-400.eot -------------------------------------------------------------------------------- /inst/htmlwidgets/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/datamations/66d127edf9390e1f14aa3ae624b542afcb5cc415/inst/htmlwidgets/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /inst/htmlwidgets/webfonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/datamations/66d127edf9390e1f14aa3ae624b542afcb5cc415/inst/htmlwidgets/webfonts/fa-regular-400.woff -------------------------------------------------------------------------------- /inst/htmlwidgets/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/datamations/66d127edf9390e1f14aa3ae624b542afcb5cc415/inst/htmlwidgets/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /inst/htmlwidgets/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/datamations/66d127edf9390e1f14aa3ae624b542afcb5cc415/inst/htmlwidgets/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /inst/htmlwidgets/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/datamations/66d127edf9390e1f14aa3ae624b542afcb5cc415/inst/htmlwidgets/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /inst/htmlwidgets/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/datamations/66d127edf9390e1f14aa3ae624b542afcb5cc415/inst/htmlwidgets/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /inst/htmlwidgets/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/datamations/66d127edf9390e1f14aa3ae624b542afcb5cc415/inst/htmlwidgets/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /man/datamationSandDance-shiny.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/datamation_sanddance.R 3 | \name{datamationSandDance-shiny} 4 | \alias{datamationSandDance-shiny} 5 | \alias{datamationSandDanceOutput} 6 | \alias{renderDatamationSandDance} 7 | \title{Output and render functions for using datamation_sanddance in Shiny} 8 | \usage{ 9 | datamationSandDanceOutput(outputId, width = "100\%", height = "400px") 10 | 11 | renderDatamationSandDance(expr, env = parent.frame(), quoted = FALSE) 12 | } 13 | \arguments{ 14 | \item{outputId}{output variable to read from} 15 | 16 | \item{width, height}{Must be a valid CSS unit (like \code{'100\%'}, 17 | \code{'400px'}, \code{'auto'}) or a number, which will be coerced to a 18 | string and have \code{'px'} appended.} 19 | 20 | \item{expr}{An expression that generates a datamationSandDance} 21 | 22 | \item{env}{The environment in which to evaluate \code{expr}.} 23 | 24 | \item{quoted}{Is \code{expr} a quoted expression (with \code{quote()})? This 25 | is useful if you want to save an expression in a variable.} 26 | } 27 | \description{ 28 | Output and render functions for using datamation_sanddance within Shiny 29 | applications and interactive Rmd documents. 30 | } 31 | -------------------------------------------------------------------------------- /man/datamation_sanddance.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/datamation_sanddance.R 3 | \name{datamation_sanddance} 4 | \alias{datamation_sanddance} 5 | \title{Create a plot datamation} 6 | \usage{ 7 | datamation_sanddance( 8 | pipeline, 9 | envir = rlang::global_env(), 10 | pretty = TRUE, 11 | elementId = NULL, 12 | height = 300, 13 | width = 300 14 | ) 15 | } 16 | \arguments{ 17 | \item{pipeline}{Tidyverse pipeline} 18 | 19 | \item{envir}{Environment where code is evaluated. Defaults to the global environment.} 20 | 21 | \item{pretty}{Whether to pretty the JSON output. Defaults to TRUE.} 22 | 23 | \item{elementId}{Optional ID for the widget element.} 24 | 25 | \item{height}{Height of the plotting area of the widget (excluding axes and legends). This is an approximation and not an exact science, since sizes may vary depending on labels, legends, facets, etc! Defaults to 300 (pixels).} 26 | 27 | \item{width}{Width of the plotting area of the widget (excluding axes and legends). This is an approximation and not an exact science, since sizes may vary depending on labels, legends, facets, etc! Defaults to 300 (pixels).} 28 | } 29 | \description{ 30 | Create a plot datamation from a tidyverse pipeline. 31 | } 32 | \examples{ 33 | { 34 | library(dplyr) 35 | 36 | "small_salary \%>\% 37 | group_by(Degree) \%>\% 38 | summarize(mean = mean(Salary))" \%>\% 39 | datamation_sanddance() 40 | 41 | library(ggplot2) 42 | 43 | "small_salary \%>\% 44 | group_by(Work, Degree) \%>\% 45 | summarize(mean_salary = mean(Salary)) \%>\% 46 | ggplot(aes(x = Work, y = mean_salary)) + 47 | geom_point() + 48 | facet_grid(rows = vars(Degree))" \%>\% 49 | datamation_sanddance() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /man/datamation_tibble.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/datamation_tibble.R 3 | \name{datamation_tibble} 4 | \alias{datamation_tibble} 5 | \title{Create a tibble datamation} 6 | \usage{ 7 | datamation_tibble( 8 | pipeline, 9 | envir = rlang::global_env(), 10 | output = "output.gif", 11 | titles = NA, 12 | xlim = c(NA, NA), 13 | ylim = c(NA, NA) 14 | ) 15 | } 16 | \arguments{ 17 | \item{pipeline}{A tidyverse pipeline.} 18 | 19 | \item{envir}{An environment.} 20 | 21 | \item{output}{Path to where gif will be saved.} 22 | 23 | \item{titles}{Optional titles for the datamation frames} 24 | 25 | \item{xlim}{Optional x limits} 26 | 27 | \item{ylim}{Optional y limits} 28 | } 29 | \description{ 30 | Create a tibble datamation 31 | } 32 | -------------------------------------------------------------------------------- /man/figures/README-ggplot2-existing-plot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/datamations/66d127edf9390e1f14aa3ae624b542afcb5cc415/man/figures/README-ggplot2-existing-plot-1.png -------------------------------------------------------------------------------- /man/figures/README-mean_salary_group_by_degree-table.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/datamations/66d127edf9390e1f14aa3ae624b542afcb5cc415/man/figures/README-mean_salary_group_by_degree-table.gif -------------------------------------------------------------------------------- /man/figures/README-mean_salary_group_by_degree.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/datamations/66d127edf9390e1f14aa3ae624b542afcb5cc415/man/figures/README-mean_salary_group_by_degree.gif -------------------------------------------------------------------------------- /man/figures/README-mean_salary_group_by_degree_work.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/datamations/66d127edf9390e1f14aa3ae624b542afcb5cc415/man/figures/README-mean_salary_group_by_degree_work.gif -------------------------------------------------------------------------------- /man/figures/README-mean_salary_group_by_degree_work_ggplot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/datamations/66d127edf9390e1f14aa3ae624b542afcb5cc415/man/figures/README-mean_salary_group_by_degree_work_ggplot.gif -------------------------------------------------------------------------------- /man/figures/README-mean_salary_grouped_degree.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/datamations/66d127edf9390e1f14aa3ae624b542afcb5cc415/man/figures/README-mean_salary_grouped_degree.gif -------------------------------------------------------------------------------- /man/figures/README-mtcars_group_cyl.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/datamations/66d127edf9390e1f14aa3ae624b542afcb5cc415/man/figures/README-mtcars_group_cyl.gif -------------------------------------------------------------------------------- /man/jeter_justice.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/data.R 3 | \docType{data} 4 | \name{jeter_justice} 5 | \alias{jeter_justice} 6 | \title{Hit data for Derek Jeter and David Justice from the 1995 and 1996 seasons} 7 | \format{ 8 | An object of class \code{data.frame} with 1181 rows and 3 columns. 9 | } 10 | \usage{ 11 | jeter_justice 12 | } 13 | \description{ 14 | Hit data for Derek Jeter and David Justice from the 1995 and 1996 seasons 15 | } 16 | \keyword{datasets} 17 | -------------------------------------------------------------------------------- /man/pipe.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils-pipe.R 3 | \name{\%>\%} 4 | \alias{\%>\%} 5 | \title{Pipe operator} 6 | \usage{ 7 | lhs \%>\% rhs 8 | } 9 | \description{ 10 | See \code{magrittr::\link[magrittr:pipe]{\%>\%}} for details. 11 | } 12 | \keyword{internal} 13 | -------------------------------------------------------------------------------- /man/run_app.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/run_app.R 3 | \name{run_app} 4 | \alias{run_app} 5 | \title{Run datamations shiny application} 6 | \usage{ 7 | run_app() 8 | } 9 | \description{ 10 | Run datamations shiny application 11 | } 12 | -------------------------------------------------------------------------------- /man/small_salary.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/data.R 3 | \docType{data} 4 | \name{small_salary} 5 | \alias{small_salary} 6 | \title{Hypothetical salary data for 30 workers} 7 | \format{ 8 | An object of class \code{spec_tbl_df} (inherits from \code{tbl_df}, \code{tbl}, \code{data.frame}) with 100 rows and 3 columns. 9 | } 10 | \usage{ 11 | small_salary 12 | } 13 | \description{ 14 | Hypothetical salary data for 30 workers 15 | } 16 | \keyword{datasets} 17 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datamations", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /renv/.gitignore: -------------------------------------------------------------------------------- 1 | cellar/ 2 | local/ 3 | library/ 4 | lock/ 5 | python/ 6 | staging/ 7 | -------------------------------------------------------------------------------- /renv/settings.dcf: -------------------------------------------------------------------------------- 1 | external.libraries: 2 | ignored.packages: 3 | package.dependency.fields: Imports, Depends, LinkingTo 4 | snapshot.type: implicit 5 | use.cache: TRUE 6 | vcs.ignore.library: TRUE 7 | -------------------------------------------------------------------------------- /sandbox/simpsons_paradox/simpsons_paradox_specs.R: -------------------------------------------------------------------------------- 1 | library(dplyr) 2 | devtools::load_all() 3 | 4 | jeter_1995 <- data.frame( 5 | player = "Derek Jeter", 6 | year = 1995, 7 | is_hit = c(rep(1, 12), rep(0, 48 - 12)) 8 | ) 9 | 10 | jeter_1996 <- data.frame( 11 | player = "Derek Jeter", 12 | year = 1996, 13 | is_hit = c(rep(1, 183), rep(0, 582 - 183)) 14 | ) 15 | 16 | justice_1995 <- data.frame( 17 | player = "David Justice", 18 | year = 1995, 19 | is_hit = c(rep(1, 104), rep(0, 411 - 104)) 20 | ) 21 | 22 | justice_1996 <- data.frame( 23 | player = "David Justice", 24 | year = 1996, 25 | is_hit = c(rep(1, 45), rep(0, 140 - 45)) 26 | ) 27 | 28 | df <- bind_rows( 29 | jeter_1995, 30 | jeter_1996, 31 | justice_1995, 32 | justice_1996 33 | ) %>% 34 | sample_frac(0.3) 35 | 36 | # datamation #1: 37 | # jeter has a higher batting average than justice overall 38 | x <- "df %>% 39 | group_by(player) %>% 40 | summarize( 41 | batting_average = mean(is_hit) 42 | )" %>% 43 | datamation_sanddance() 44 | 45 | x 46 | 47 | write(x$x$specs, here::here("sandbox", "simpsons_paradox", "group_by_player.json")) 48 | 49 | # datamation #2: 50 | # but justice has a higher batting average than jeter within each year 51 | x <- "df %>% 52 | group_by(player, year) %>% 53 | summarize( 54 | batting_average = mean(is_hit) 55 | )" %>% 56 | datamation_sanddance() 57 | 58 | x 59 | 60 | write(x$x$specs, here::here("sandbox", "simpsons_paradox", "group_by_player_year.json")) 61 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | setuptools.setup( 4 | name='datamations', 5 | version='1.0', 6 | description='Automatic generation of explanation of plots and tables from analysis code', 7 | author='Chinmay Singh', 8 | author_email='chsingh@microsoft.com', 9 | packages=['datamations'], 10 | package_data={'datamations': ['../data-raw/small_salary.csv']}, 11 | install_requires=['pandas', 'ipython', 'palmerpenguins'] 12 | ) 13 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(datamations) 3 | 4 | test_check("datamations") 5 | -------------------------------------------------------------------------------- /tests/testthat/helper.R: -------------------------------------------------------------------------------- 1 | # Test for a specific value of meta.parse 2 | expect_meta_parse_value <- function(specs, parse_value) { 3 | meta_parse <- specs %>% 4 | purrr::map(jsonlite::fromJSON) %>% 5 | purrr::transpose() %>% 6 | purrr::pluck("meta") %>% 7 | purrr::transpose() %>% 8 | purrr::pluck("parse") %>% 9 | unlist() 10 | 11 | expect_true(identical(unique(meta_parse), parse_value)) 12 | } 13 | 14 | # Test that meta.axes is true or false 15 | expect_meta_axes <- function(specs, axes_value) { 16 | meta_axes <- specs %>% 17 | purrr::map(jsonlite::fromJSON) %>% 18 | purrr::transpose() %>% 19 | purrr::pluck("meta") %>% 20 | purrr::transpose() %>% 21 | purrr::pluck("axes") %>% 22 | unlist() 23 | 24 | expect_true(identical(unique(meta_axes), axes_value)) 25 | } 26 | 27 | # Test that the data.values are the same as the passed data frame 28 | expect_data_values <- function(single_spec, df) { 29 | df <- df %>% 30 | dplyr::mutate_if(is.factor, as.character) %>% 31 | dplyr::mutate_if(is.character, dplyr::coalesce, "NA") 32 | 33 | spec_data <- single_spec %>% 34 | jsonlite::fromJSON() %>% 35 | purrr::pluck("data") %>% 36 | purrr::pluck("values") %>% 37 | dplyr::select(-tidyselect::any_of("gemini_ids")) 38 | 39 | expect_equal(spec_data, df, ignore_attr = TRUE) 40 | } 41 | 42 | # Test that "mark" and "encoding" are within `spec` - this happens when there is a facet (any grouping) 43 | expect_spec_contains_mark_encoding <- function(specs) { 44 | spec_names <- specs %>% 45 | purrr::map(function(x) { 46 | jsonlite::fromJSON(x) %>% 47 | purrr::pluck("spec") %>% 48 | names() 49 | }) 50 | 51 | mark_encoding_in_spec <- spec_names %>% 52 | purrr::map(~ all(c("mark", "encoding") %in% .x)) %>% 53 | unlist() 54 | 55 | expect_true(all(mark_encoding_in_spec)) 56 | } 57 | 58 | # Test that "mark" and "encoding" are at the top level - when there is no faceting / grouping 59 | expect_mark_encoding_top_level <- function(specs) { 60 | spec_names <- specs %>% 61 | purrr::map(function(x) { 62 | jsonlite::fromJSON(x) %>% 63 | names() 64 | }) 65 | 66 | mark_encoding_in_spec <- spec_names %>% 67 | purrr::map(~ all(c("mark", "encoding") %in% .x)) %>% 68 | unlist() 69 | 70 | expect_true(all(mark_encoding_in_spec)) 71 | } 72 | 73 | # TODO: need to rework these because the groupings are different now depending on the number of grouping variables: 74 | # 1: x 75 | # 2: column, x/color 76 | # 3. column, row, x/color 77 | 78 | # Test that grouped specs are in a specific order - 1 group, 2 groups, then 3 groups 79 | expect_grouping_order <- function(specs) { 80 | expect_grouping_order_1(specs[[1]]) 81 | 82 | if (length(specs) %in% c(2, 3)) { 83 | expect_grouping_order_2(specs[[2]]) 84 | } 85 | 86 | if (length(specs) == 3) { 87 | expect_grouping_order_3(specs[[3]]) 88 | } 89 | } 90 | 91 | # Test 1 grouping variable case - column facet, no color encoding 92 | expect_grouping_order_1 <- function(first_spec) { 93 | # Expect column facet only 94 | facet <- first_spec %>% 95 | jsonlite::fromJSON() %>% 96 | purrr::pluck("facet") 97 | 98 | expect_named(facet, "column") 99 | 100 | # No color in encoding 101 | encoding <- first_spec %>% 102 | jsonlite::fromJSON() %>% 103 | purrr::pluck("spec") %>% 104 | purrr::pluck("encoding") 105 | 106 | expect_false("color" %in% names(encoding)) 107 | } 108 | 109 | # Test 2 grouping variable case - column and row facet, no color encoding 110 | expect_grouping_order_2 <- function(second_spec) { 111 | # Expect column and row facet 112 | facet <- second_spec %>% 113 | jsonlite::fromJSON() %>% 114 | purrr::pluck("facet") 115 | 116 | expect_named(facet, c("column", "row")) 117 | 118 | # No color in encoding 119 | encoding <- second_spec %>% 120 | jsonlite::fromJSON() %>% 121 | purrr::pluck("spec") %>% 122 | purrr::pluck("encoding") 123 | 124 | expect_false("color" %in% names(encoding)) 125 | } 126 | 127 | # Test 3 grouping variable case - column and row facet, color encoding 128 | expect_grouping_order_3 <- function(third_spec) { 129 | # Expect column and row facet 130 | facet <- third_spec %>% 131 | jsonlite::fromJSON() %>% 132 | purrr::pluck("facet") 133 | 134 | expect_named(facet, c("column", "row")) 135 | 136 | # Expect color in encoding 137 | encoding <- third_spec %>% 138 | jsonlite::fromJSON() %>% 139 | purrr::pluck("spec") %>% 140 | purrr::pluck("encoding") 141 | 142 | expect_true("color" %in% names(encoding)) 143 | } 144 | 145 | # Expect that there isn't any grouping - no facets, no color encoding 146 | expect_no_grouping <- function(specs) { 147 | no_facets <- specs %>% 148 | purrr::map(function(x) { 149 | spec_names <- jsonlite::fromJSON(x) %>% 150 | names() 151 | 152 | !"facet" %in% spec_names 153 | }) %>% 154 | unlist() 155 | 156 | expect_true(all(no_facets)) 157 | 158 | no_color_encoding <- specs %>% 159 | purrr::map(function(x) { 160 | encoding_names <- jsonlite::fromJSON(x) %>% 161 | purrr::pluck("encoding") %>% 162 | names() 163 | 164 | !"color" %in% encoding_names 165 | }) %>% 166 | unlist() 167 | 168 | expect_true(all(no_color_encoding)) 169 | } 170 | 171 | expect_python_identical_to_r <- function(python_specs, r_specs) { 172 | 173 | # Reconcile differences in names - reorder all elements alphabetically 174 | r_specs <- r_specs %>% 175 | purrr::map(reorder_list_alphabetically) 176 | 177 | python_specs <- python_specs %>% 178 | purrr::map(reorder_list_alphabetically) 179 | 180 | # Reconcile differences in data value column orders 181 | r_values_order <- purrr::map(r_specs, function(x) { 182 | purrr::map(x$data$values, names) 183 | }) 184 | 185 | python_specs <- purrr::map2(python_specs, r_values_order, function(x, y) { 186 | x$data$values <- purrr::map2(x$data$values, y, ~ .x[.y]) 187 | 188 | x 189 | }) 190 | 191 | expect_equal(python_specs, r_specs) 192 | } 193 | 194 | reorder_list_alphabetically <- function(x) { 195 | if (is.list(x)) { 196 | if (!is.null(names(x))) { 197 | x <- x[sort(names(x))] 198 | } 199 | purrr::map(x, reorder_list_alphabetically) 200 | } else { 201 | x 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /tests/testthat/setup.R: -------------------------------------------------------------------------------- 1 | library(dplyr) 2 | library(palmerpenguins) 3 | -------------------------------------------------------------------------------- /tests/testthat/test-datamation_sanddance.R: -------------------------------------------------------------------------------- 1 | library(ggplot2) 2 | 3 | test_that("datamation_sanddance returns a frame for the data, one for each group, and three or four for summarize (three if the summary function is not mean, and four if it is - includes an errorbar frame too)", { 4 | pipeline <- "penguins %>% group_by(species)" 5 | specs <- datamation_sanddance(pipeline) %>% 6 | purrr::pluck("x") %>% 7 | purrr::pluck("specs") %>% 8 | jsonlite::fromJSON(simplifyDataFrame = FALSE) 9 | expect_length(specs, 2) 10 | 11 | pipeline <- "penguins %>% group_by(species, island)" 12 | specs <- datamation_sanddance(pipeline) %>% 13 | purrr::pluck("x") %>% 14 | purrr::pluck("specs") %>% 15 | jsonlite::fromJSON(simplifyDataFrame = FALSE) 16 | expect_length(specs, 3) 17 | 18 | pipeline <- "penguins %>% group_by(species, island, sex)" 19 | specs <- datamation_sanddance(pipeline) %>% 20 | purrr::pluck("x") %>% 21 | purrr::pluck("specs") %>% 22 | jsonlite::fromJSON(simplifyDataFrame = FALSE) 23 | expect_length(specs, 4) 24 | 25 | pipeline <- "penguins %>% group_by(species, island) %>% summarize(median = median(bill_length_mm))" 26 | specs <- datamation_sanddance(pipeline) %>% 27 | purrr::pluck("x") %>% 28 | purrr::pluck("specs") %>% 29 | jsonlite::fromJSON(simplifyDataFrame = FALSE) 30 | expect_length(specs, 6) 31 | 32 | pipeline <- "penguins %>% group_by(species, island) %>% summarize(mean = mean(bill_length_mm))" 33 | specs <- datamation_sanddance(pipeline) %>% 34 | purrr::pluck("x") %>% 35 | purrr::pluck("specs") %>% 36 | jsonlite::fromJSON(simplifyDataFrame = FALSE) 37 | expect_length(specs, 7) 38 | }) 39 | 40 | test_that("datamation_sanddance errors when no data transformation is present", { 41 | expect_error(datamation_sanddance("mtcars"), "data transformation") 42 | }) 43 | 44 | test_that("datamation_sanddance errors when an unsupported function is passed", { 45 | expect_error(datamation_sanddance("palmerpenguins::penguins %>% group_by(species) %>% ungroup()"), "not supported by") 46 | }) 47 | 48 | test_that("Results are identical when data is contained in first function versus when it is piped in as first step", { 49 | data_piped <- datamation_sanddance("penguins %>% group_by(species, island) %>% summarize(mean = mean(bill_length_mm))") 50 | data_arg <- datamation_sanddance("group_by(penguins, species, island) %>% summarize(mean = mean(bill_length_mm))") 51 | 52 | expect_identical(data_piped, data_arg) 53 | 54 | # data_piped <- datamation_sanddance("palmerpenguins::penguins %>% group_by(species, island) %>% summarize(mean = mean(bill_length_mm))") 55 | # data_arg <- datamation_sanddance("group_by(palmerpenguins::penguins, species, island) %>% summarize(mean = mean(bill_length_mm))") 56 | 57 | # expect_identical(data_piped, data_arg) 58 | }) 59 | 60 | test_that("Results are identical regardless of whether summary operation is named or not", { 61 | summary_named <- datamation_sanddance("small_salary %>% summarize(mean = mean(Salary))") 62 | summary_not_named <- datamation_sanddance("small_salary %>% summarize(mean(Salary))") 63 | 64 | expect_identical(summary_named, summary_not_named) 65 | }) 66 | 67 | test_that("datamation_sanddance returns an htmlwidget", { 68 | widget <- datamation_sanddance("penguins %>% group_by(species, island) %>% summarize(mean = mean(bill_length_mm))") 69 | expect_s3_class(widget, "htmlwidget") 70 | }) 71 | 72 | test_that("datamation_sanddance errors if facet_wrap is used", { 73 | expect_error( 74 | "small_salary %>% 75 | group_by(Work, Degree) %>% 76 | summarize(mean_salary = median(Salary)) %>% 77 | ggplot() + 78 | geom_point(aes(x = Work, y = mean_salary, color = Degree)) + 79 | facet_wrap(vars(Degree))" %>% 80 | datamation_sanddance(), 81 | "does not support `facet_wrap" 82 | ) 83 | }) 84 | 85 | test_that("datamation_sanddance errors on non-geom_point", { 86 | expect_error( 87 | "small_salary %>% 88 | group_by(Work) %>% 89 | summarize(mean_salary = median(Salary)) %>% 90 | ggplot() + 91 | geom_col(aes(x = Work, y = mean_salary))" %>% 92 | datamation_sanddance(), 93 | "only supports `geom_point" 94 | ) 95 | }) 96 | 97 | test_that("datamation_sanddance requires a call to geom_point", { 98 | expect_error("small_salary %>% 99 | group_by(Work) %>% 100 | summarize(mean_salary = median(Salary)) %>% 101 | ggplot(aes(x = Work, y = mean_salary))" %>% datamation_sanddance(), "requires a call to `geom_point") 102 | }) 103 | 104 | # Python specs ---- 105 | if(TRUE) skip("Skip python congruence tests as Python is behind R in development") 106 | 107 | test_that("python specs are identical to R specs", { 108 | 109 | # One grouping variable ---- 110 | 111 | python_specs <- jsonlite::fromJSON(system.file("specs/groupby_work.json", package = "datamations"), simplifyDataFrame = FALSE) 112 | 113 | r_specs <- "small_salary %>% group_by(Work) %>% summarise(mean = mean(Salary))" %>% 114 | datamation_sanddance() %>% 115 | purrr::pluck("x") %>% 116 | purrr::pluck("specs") %>% 117 | jsonlite::fromJSON(simplifyDataFrame = FALSE) 118 | 119 | expect_python_identical_to_r(python_specs, r_specs) 120 | 121 | # Two grouping variables ---- 122 | 123 | python_specs <- jsonlite::fromJSON(system.file("specs/groupby_work_degree.json", package = "datamations"), simplifyDataFrame = FALSE) 124 | 125 | r_specs <- "small_salary %>% group_by(Work, Degree) %>% summarise(mean = mean(Salary))" %>% 126 | datamation_sanddance() %>% 127 | purrr::pluck("x") %>% 128 | purrr::pluck("specs") %>% 129 | jsonlite::fromJSON(simplifyDataFrame = FALSE) 130 | 131 | expect_python_identical_to_r(python_specs, r_specs) 132 | 133 | python_specs <- jsonlite::fromJSON(system.file("specs/groupby_degree_work.json", package = "datamations"), simplifyDataFrame = FALSE) 134 | 135 | r_specs <- "small_salary %>% group_by(Degree, Work) %>% summarise(mean = mean(Salary))" %>% 136 | datamation_sanddance() %>% 137 | purrr::pluck("x") %>% 138 | purrr::pluck("specs") %>% 139 | jsonlite::fromJSON(simplifyDataFrame = FALSE) 140 | 141 | expect_python_identical_to_r(python_specs, r_specs) 142 | }) 143 | -------------------------------------------------------------------------------- /tests/testthat/test-generate_mapping_from_plot.R: -------------------------------------------------------------------------------- 1 | test_that("generate_mapping_from_plot can extract x mapping from ggplot", { 2 | p <- ggplot2::ggplot(mtcars, ggplot2::aes(x = mpg, y = cyl)) + 3 | ggplot2::geom_point() 4 | mapping <- generate_mapping_from_plot(p) 5 | expect_identical(mapping, list(x = "mpg")) 6 | }) 7 | 8 | test_that("generate_mapping_from_plot can extract x mapping from geo_point", { 9 | p <- ggplot2::ggplot(mtcars) + 10 | ggplot2::geom_point(ggplot2::aes(x = mpg, y = cyl)) 11 | mapping <- generate_mapping_from_plot(p) 12 | expect_identical(mapping, list(x = "mpg")) 13 | }) 14 | 15 | test_that("generate_mapping_from_plot extracts facets", { 16 | p <- ggplot2::ggplot(mtcars) + 17 | ggplot2::geom_point(ggplot2::aes(x = mpg, y = cyl)) + 18 | ggplot2::facet_grid(dplyr::vars(vs), dplyr::vars(am)) 19 | mapping <- generate_mapping_from_plot(p) 20 | expect_identical(mapping, list(x = "mpg", row = "vs", column = "am")) 21 | 22 | p <- ggplot2::ggplot(mtcars) + 23 | ggplot2::geom_point(ggplot2::aes(x = mpg, y = cyl)) + 24 | ggplot2::facet_grid(dplyr::vars(vs)) 25 | mapping <- generate_mapping_from_plot(p) 26 | expect_identical(mapping, list(x = "mpg", row = "vs")) 27 | 28 | p <- ggplot2::ggplot(mtcars) + 29 | ggplot2::geom_point(ggplot2::aes(x = mpg, y = cyl)) + 30 | ggplot2::facet_grid(am ~ vs) 31 | mapping <- generate_mapping_from_plot(p) 32 | expect_identical(mapping, list(x = "mpg", row = "am", column = "vs")) 33 | }) 34 | -------------------------------------------------------------------------------- /tests/testthat/test-mod_data_tabs.R: -------------------------------------------------------------------------------- 1 | test_that("determine_slider_from_tab returns 0 for the first tab", { 2 | expect_equal(determine_slider_from_tab(1, NULL), 0) 3 | expect_equal(determine_slider_from_tab(1, "salary"), 0) 4 | expect_equal(determine_slider_from_tab(1, c("salary", "work")), 0) 5 | }) 6 | 7 | test_that("determine_slider_from_tab returns 1 for the second tab (group by) if there are groups", { 8 | expect_equal(determine_slider_from_tab(2, "salary"), 1) 9 | expect_equal(determine_slider_from_tab(2, c("salary", "work")), 1) 10 | }) 11 | 12 | test_that("determine_slider_from_tab returns 3 + n_groups for the third tab (summarize) when there are groups. 3 is initial frame + distribution frame + 1 more to place after - minus 1 because of javascript indexing of course!", { 13 | expect_equal(determine_slider_from_tab(3, "salary"), 3) 14 | expect_equal(determine_slider_from_tab(3, c("salary", "work")), 4) 15 | expect_equal(determine_slider_from_tab(3, c("salary", "work", "x")), 5) 16 | }) 17 | 18 | test_that("determine_slider_from_tab returns 2 when the tab is 2 and there is no group by - 1 for the initial, 1 for the distribution, 1 after, then minus 1 because of indexing", { 19 | expect_equal(determine_slider_from_tab(2, NULL), 2) 20 | }) 21 | 22 | test_that("determine_tab_from_slider returns 1 for the first slider position", { 23 | expect_equal(determine_tab_from_slider(0, NULL), 1) 24 | expect_equal(determine_tab_from_slider(0, "island"), 1) 25 | expect_equal(determine_tab_from_slider(0, c("island", "species")), 1) 26 | }) 27 | 28 | test_that("determine_tab_from_slider returns 2 for the second through 2 + n_groups (-1 bc indexing) slider positions, when there are groups", { 29 | expect_equal(determine_tab_from_slider(1, "island"), 2) 30 | expect_equal(determine_tab_from_slider(2, "island"), 2) 31 | expect_equal(determine_tab_from_slider(1, c("island", "species")), 2) 32 | expect_equal(determine_tab_from_slider(2, c("island", "species")), 2) 33 | expect_equal(determine_tab_from_slider(3, c("island", "species")), 2) 34 | expect_equal(determine_tab_from_slider(3, c("island", "species", "sex")), 2) 35 | expect_equal(determine_tab_from_slider(4, c("island", "species", "sex")), 2) 36 | }) 37 | 38 | test_that("determine_tab_from_slider returns 3 for any slider after the grouping (1 + 1 + 1 + n_groups - 1) if there are group", { 39 | expect_equal(determine_tab_from_slider(3, "island"), 3) 40 | expect_equal(determine_tab_from_slider(4, c("island", "species")), 3) 41 | expect_equal(determine_tab_from_slider(5, c("island", "species", "sex")), 3) 42 | }) 43 | 44 | test_that("determine_tab_from_slider returns 1 on the second slider position if there are no groups, because it's the distribution", { 45 | expect_equal(determine_tab_from_slider(1, NULL), 1) 46 | }) 47 | 48 | test_that("determine_tab_from_slider returns 2 for any slider position after the first 2, if there are no groups", { 49 | expect_equal(determine_tab_from_slider(2, NULL), 2) 50 | expect_equal(determine_tab_from_slider(3, NULL), 2) 51 | expect_equal(determine_tab_from_slider(4, NULL), 2) 52 | expect_equal(determine_tab_from_slider(5, NULL), 2) 53 | }) 54 | -------------------------------------------------------------------------------- /tests/testthat/test-parse_functions.R: -------------------------------------------------------------------------------- 1 | test_that("parse_functions identifies functions used in a pipeline", { 2 | pipeline <- "small_salary %>% group_by(Degree) %>% summarise(mean = mean(Salary))" 3 | 4 | fittings <- pipeline %>% 5 | parse_pipeline() 6 | 7 | functions <- parse_functions(fittings) 8 | 9 | expect_equal(functions, c("data", "group_by", "summarize")) 10 | }) 11 | 12 | test_that("parse_functions properly identifies when the first pipe in a pipeline contains the data, and still grabs the function", { 13 | pipeline <- "group_by(small_salary, Degree) %>% summarise(mean = mean(Salary))" 14 | 15 | fittings <- pipeline %>% 16 | parse_pipeline() 17 | 18 | functions <- parse_functions(fittings) 19 | 20 | expect_equal(functions, c("data", "group_by", "summarize")) 21 | }) 22 | -------------------------------------------------------------------------------- /tests/testthat/test-parse_pipeline.R: -------------------------------------------------------------------------------- 1 | test_that("split_pipeline can handle namespaced data", { 2 | sp <- split_pipeline("palmerpenguins::penguins %>% group_by(species, sex)") 3 | expect_identical(sp, c("palmerpenguins::penguins", "group_by(species, sex)")) 4 | 5 | sp <- split_pipeline("palmerpenguins::penguins %>% group_by(species, sex) %>% summarize(mean = mean(bill_length_mm))") 6 | expect_identical(sp, c("palmerpenguins::penguins", "group_by(species, sex)", "summarize(mean = mean(bill_length_mm))")) 7 | }) 8 | 9 | test_that("split_pipeline splits by the pipe", { 10 | sp <- split_pipeline("mtcars %>% group_by(vs, am)") 11 | expect_identical(sp, c("mtcars", "group_by(vs, am)")) 12 | }) 13 | 14 | test_that("split_pipeline can split code across multiple lines", { 15 | sp <- split_pipeline(" 16 | mtcars %>% 17 | group_by(vs, am) 18 | ") 19 | expect_identical(sp, c("mtcars", "group_by(vs, am)")) 20 | }) 21 | 22 | test_that("split_pipeline can handle the data being contained in the first function, and splits it out properly into its own step", { 23 | sp <- split_pipeline("group_by(small_salary, Degree, Work)") 24 | expect_identical(sp, c("small_salary", "group_by( Degree, Work)")) 25 | 26 | sp <- split_pipeline("group_by(small_salary, Degree) %>% summarize(mean = mean(x))") 27 | expect_identical(sp, c("small_salary", "group_by( Degree)", "summarize(mean = mean(x))")) 28 | 29 | sp <- split_pipeline("summarize(small_salary, mean = mean(x))") 30 | expect_identical(sp, c("small_salary", "summarize( mean = mean(x))")) 31 | }) 32 | 33 | test_that("split_pipeline can handle data in first function, when data is namespaced from package", { 34 | sp <- split_pipeline("summarize(palmerpenguins::penguins, mean = mean(x))") 35 | expect_identical(sp, c("palmerpenguins::penguins", "summarize( mean = mean(x))")) 36 | }) 37 | 38 | test_that("split_pipeline errors if first element is function, but there is no data", { 39 | expect_error(split_pipeline("group_by(species)"), "No data detected") 40 | expect_error(split_pipeline("group_by(species, island)"), "No data detected") 41 | }) 42 | 43 | test_that("split_pipeline errors if first element is a function containing data, but it is not a data frame", { 44 | df <- list(palmerpenguins::penguins) 45 | expect_error(split_pipeline("group_by(df, species)"), "not a data frame") 46 | }) 47 | 48 | test_that("split_pipeline converts summarise to summarize", { 49 | sp <- split_pipeline("mtcars %>% summarise(mean = mean(gear))") 50 | expect_identical(sp, c("mtcars", "summarize(mean = mean(gear))")) 51 | }) 52 | -------------------------------------------------------------------------------- /tests/testthat/test-prep_specs_count.R: -------------------------------------------------------------------------------- 1 | test_that("specs produced by prep_specs_count are equivalent to specs from group_by + summarize, except for title and meta.custom_animation", { 2 | # Grouped by one variable ---- 3 | count_specs <- "small_salary %>% count(Degree)" %>% 4 | datamation_sanddance() %>% 5 | purrr::pluck("x") %>% 6 | purrr::pluck("specs") %>% 7 | jsonlite::fromJSON(simplifyDataFrame = FALSE) 8 | 9 | group_by_specs <- "small_salary %>% group_by(Degree) %>% summarize(n = n())" %>% 10 | datamation_sanddance() %>% 11 | purrr::pluck("x") %>% 12 | purrr::pluck("specs") %>% 13 | jsonlite::fromJSON(simplifyDataFrame = FALSE) 14 | 15 | # Check title 16 | expect_identical(count_specs[[length(count_specs)]][["meta"]][["description"]], "Plot count of each group") 17 | 18 | # Check meta field 19 | expect_identical(count_specs[[length(count_specs)]][["meta"]][["custom_animation"]], "count") 20 | 21 | # Check identical otherwise 22 | count_specs[[length(count_specs)]][["meta"]][["description"]] <- NULL 23 | count_specs[[length(count_specs)]][["meta"]][["custom_animation"]] <- NULL 24 | group_by_specs[[length(group_by_specs)]][["meta"]][["description"]] <- NULL 25 | expect_identical(count_specs, group_by_specs) 26 | 27 | # Count df on its own ---- 28 | count_specs <- "small_salary %>% count()" %>% 29 | datamation_sanddance() %>% 30 | purrr::pluck("x") %>% 31 | purrr::pluck("specs") %>% 32 | jsonlite::fromJSON(simplifyDataFrame = FALSE) 33 | 34 | group_by_specs <- "small_salary %>% summarize(n = n())" %>% 35 | datamation_sanddance() %>% 36 | purrr::pluck("x") %>% 37 | purrr::pluck("specs") %>% 38 | jsonlite::fromJSON(simplifyDataFrame = FALSE) 39 | 40 | # Check title 41 | expect_identical(count_specs[[length(count_specs)]][["meta"]][["description"]], "Plot count") 42 | 43 | # Check meta field 44 | expect_identical(count_specs[[length(count_specs)]][["meta"]][["custom_animation"]], "count") 45 | 46 | # Check identical otherwise 47 | count_specs[[length(count_specs)]][["meta"]][["description"]] <- NULL 48 | count_specs[[length(count_specs)]][["meta"]][["custom_animation"]] <- NULL 49 | group_by_specs[[length(group_by_specs)]][["meta"]][["description"]] <- NULL 50 | expect_identical(count_specs, group_by_specs) 51 | }) 52 | -------------------------------------------------------------------------------- /tests/testthat/test-prep_specs_data.R: -------------------------------------------------------------------------------- 1 | test_that("prep_specs_data returns a vega lite spec of length 1, with one data value containing n, meta.parse that specifies 'grid', no facets, with mark and encoding at the top level", { 2 | specs <- prep_specs_data(mtcars) 3 | expect_length(specs, 1) 4 | 5 | expect_data_values(specs[[1]], mtcars %>% dplyr::count()) 6 | expect_meta_parse_value(specs, "grid") 7 | expect_meta_axes(specs, NULL) # Axes are not shown 8 | expect_no_grouping(specs) 9 | expect_mark_encoding_top_level(specs) 10 | }) 11 | -------------------------------------------------------------------------------- /tests/testthat/test-prep_specs_filter.R: -------------------------------------------------------------------------------- 1 | test_that("specs from prep_specs_filter are the same as the previous frame, with transform.filter", { 2 | # Infogrid 3 | specs <- "small_salary %>% filter(Degree == 'Masters')" %>% 4 | datamation_sanddance() %>% 5 | purrr::pluck("x") %>% 6 | purrr::pluck("specs") %>% 7 | jsonlite::fromJSON(simplifyDataFrame = FALSE) 8 | 9 | filter_spec_id <- 2 10 | filter_specs <- specs[[filter_spec_id]][["transform"]][[1]] 11 | 12 | # Had expected fields 13 | expect_true("filter" %in% names(filter_specs)) 14 | expect_true(filter_specs$filter$field == "gemini_id") 15 | expect_true("oneOf" %in% names(filter_specs$filter)) 16 | 17 | # Identical to previous frame, minus filter and description 18 | specs[[filter_spec_id]]$transform <- NULL 19 | specs[[filter_spec_id - 1]]$meta$description <- NULL 20 | specs[[filter_spec_id]]$meta$description <- NULL 21 | expect_identical(specs[[filter_spec_id - 1]], specs[[filter_spec_id]]) 22 | 23 | # Non-infogrid 24 | specs <- "small_salary %>% group_by(Degree, Work) %>% summarise(median = median(Salary)) %>% filter(median > 85)" %>% 25 | datamation_sanddance() %>% 26 | purrr::pluck("x") %>% 27 | purrr::pluck("specs") %>% 28 | jsonlite::fromJSON(simplifyDataFrame = FALSE) 29 | filter_spec_id <- 7 30 | filter_specs <- specs[[filter_spec_id]][["transform"]][[1]] 31 | 32 | # Had expected fields 33 | expect_true("filter" %in% names(filter_specs)) 34 | expect_true(filter_specs$filter$field == "gemini_id") 35 | expect_true("oneOf" %in% names(filter_specs$filter)) 36 | 37 | # Identical to previous frame, minus filter and description 38 | specs[[filter_spec_id]]$transform <- NULL 39 | specs[[filter_spec_id - 1]]$meta$description <- NULL 40 | specs[[filter_spec_id]]$meta$description <- NULL 41 | expect_equal(specs[[filter_spec_id - 1]], specs[[filter_spec_id]]) 42 | }) 43 | 44 | test_that("transform.filter uses oneOf if number of IDs > 1, and == if number of IDs is 1", { 45 | specs <- "small_salary %>% filter(row_number() == 1)" %>% 46 | datamation_sanddance() %>% 47 | purrr::pluck("x") %>% 48 | purrr::pluck("specs") %>% 49 | jsonlite::fromJSON(simplifyDataFrame = FALSE) 50 | 51 | expect_identical(specs[[2]]$transform[[1]]$filter, "datum.gemini_id == 1") 52 | 53 | specs <- "small_salary %>% filter(row_number() %in% c(1,2))" %>% 54 | datamation_sanddance() %>% 55 | purrr::pluck("x") %>% 56 | purrr::pluck("specs") %>% 57 | jsonlite::fromJSON(simplifyDataFrame = FALSE) 58 | 59 | expect_equal(specs[[2]]$transform[[1]]$filter, list(field = "gemini_id", oneOf = c(1, 2))) 60 | }) 61 | 62 | test_that("filtered specs have an updated x axis, defaulting to dropping x-axis values that no longer exist", { 63 | # One X filtered out ----- 64 | specs <- "small_salary %>% group_by(Degree) %>% summarise(median = median(Salary)) %>% filter(median > 90)" %>% 65 | datamation_sanddance() %>% 66 | purrr::pluck("x") %>% 67 | purrr::pluck("specs") %>% 68 | jsonlite::fromJSON(simplifyDataFrame = FALSE) 69 | 70 | filter_spec_id <- 6 71 | filter_frame_specs <- specs[[filter_spec_id]] 72 | previous_frame_specs <- specs[[filter_spec_id - 1]] 73 | 74 | # X domain is different 75 | expect_equal(previous_frame_specs$encoding$x$axis$values, c(1, 2)) 76 | expect_equal(filter_frame_specs$encoding$x$axis$values, 1) 77 | 78 | # All X filtered out ---- 79 | specs <- "small_salary %>% group_by(Degree) %>% summarise(median = median(Salary)) %>% filter(median > 100)" %>% 80 | datamation_sanddance() %>% 81 | purrr::pluck("x") %>% 82 | purrr::pluck("specs") %>% 83 | jsonlite::fromJSON(simplifyDataFrame = FALSE) 84 | 85 | filter_spec_id <- 6 86 | filter_frame_specs <- specs[[filter_spec_id]] 87 | previous_frame_specs <- specs[[filter_spec_id - 1]] 88 | 89 | # X domain is different 90 | expect_equal(previous_frame_specs$encoding$x$axis$values, c(1, 2)) 91 | expect_equal(filter_frame_specs$encoding$x$axis$values, list()) 92 | }) 93 | 94 | test_that("specs from prep_specs_filter have color.scale.domain explicitly set, so color remains the same even if values are filtered out", { 95 | specs <- "small_salary %>% group_by(Degree, Work) %>% filter(Work == 'Industry')" %>% 96 | datamation_sanddance() %>% 97 | purrr::pluck("x") %>% 98 | purrr::pluck("specs") %>% 99 | jsonlite::fromJSON(simplifyDataFrame = FALSE) 100 | 101 | filter_spec <- specs[[length(specs)]] 102 | 103 | expect_identical(filter_spec$spec$encoding$color$scale$domain, filter_spec$spec$encoding$color$legend$values) 104 | expect_identical(filter_spec$spec$encoding$color$scale$domain, c("Academia", "Industry")) 105 | }) 106 | 107 | test_that("setting color.domain when there is no color variables doesn't cause an issue, since value being set is NULL", { 108 | specs <- "small_salary %>% group_by(Work) %>% filter(Work == 'Industry')" %>% 109 | datamation_sanddance() %>% 110 | purrr::pluck("x") %>% 111 | purrr::pluck("specs") %>% 112 | jsonlite::fromJSON(simplifyDataFrame = FALSE) 113 | 114 | filter_spec <- specs[[length(specs)]] 115 | 116 | expect_null(filter_spec$spec$encoding$color$scale$domain) 117 | }) 118 | -------------------------------------------------------------------------------- /tests/testthat/test-prep_specs_group_by.R: -------------------------------------------------------------------------------- 1 | test_that("prep_specs_group_by returns a list, with one element for each grouping variable. There is one data value for each group combination containing the group levels and n, mark and encoding are within `spec`, meta.parse specifies 'grid', and the order of grouping is: column facet, row facet, colour encoding. If there is an NA group value, that variable will not appear in the data.", { 2 | 3 | # one group ---- 4 | specs <- prep_specs_group_by(palmerpenguins::penguins, mapping = list(x = 1, y = NULL, summary_function = NULL, column = "species", groups = "species")) 5 | 6 | expect_length(specs, 1) # one element for each grouping variable 7 | expect_data_values(specs[[1]], dplyr::count(palmerpenguins::penguins, species)) # one data value for each group combination, containing group levels and n 8 | expect_spec_contains_mark_encoding(specs) # mark and encoding within spec 9 | expect_meta_parse_value(specs, "grid") # meta.parse specifies grid 10 | expect_meta_axes(specs, NULL) # Axes are not shown 11 | expect_grouping_order(specs) # order of grouping is column, row, colour 12 | 13 | # two groups ---- 14 | specs <- prep_specs_group_by(palmerpenguins::penguins, mapping = list(x = 1, y = NULL, summary_function = NULL, column = "species", row = "island", groups = c("species", "island"))) 15 | 16 | expect_length(specs, 2) # one element for each grouping variable 17 | expect_data_values(specs[[1]], dplyr::count(palmerpenguins::penguins, species)) # one data value for each group combination, containing group levels and n 18 | expect_data_values(specs[[2]], dplyr::count(palmerpenguins::penguins, species, island)) 19 | expect_spec_contains_mark_encoding(specs) # mark and encoding within spec 20 | expect_meta_parse_value(specs, "grid") # meta.parse specifies grid 21 | expect_meta_axes(specs, NULL) # Axes are not shown 22 | expect_grouping_order(specs) # order of grouping is column, row, colour 23 | 24 | # three groups 25 | # TODO, color is "turned off" 26 | # specs <- prep_specs_group_by(palmerpenguins::penguins, list(x = 1, y = NULL, summary_function = NULL, column = "species", row = "island", color = "sex", groups = c("species", "island", "sex"))) 27 | # 28 | # # This test is failing because color is "turned off" right now 29 | # # But for some reason it's still animated in 3 steps?? TODO 30 | # expect_length(specs, 3) # one element for each grouping variable 31 | # expect_data_values(specs[[1]], dplyr::count(palmerpenguins::penguins, species)) # one data value for each group combination, containing group levels and n 32 | # expect_data_values(specs[[2]], dplyr::count(palmerpenguins::penguins, species, island)) 33 | # # Failing expected as above 34 | # expect_data_values(specs[[3]], dplyr::count(palmerpenguins::penguins, species, island, sex)) 35 | # expect_spec_contains_mark_encoding(specs) # mark and encoding within spec 36 | # expect_meta_parse_value(specs, "grid") # meta.parse specifies grid 37 | # expect_meta_axes(specs, NULL) # Axes are not shown 38 | # expect_grouping_order(specs) # order of grouping is column, row, colour 39 | }) 40 | 41 | # TODO: need to test 42 | # if NA group value, variable not in data 43 | -------------------------------------------------------------------------------- /tests/testthat/test-snake.R: -------------------------------------------------------------------------------- 1 | test_that("snake properly evaluates the pipeline at each stage", { 2 | pipeline <- "small_salary %>% group_by(Degree) %>% summarise(mean = mean(Salary))" 3 | 4 | supported_tidy_functions <- c("group_by", "summarize") 5 | 6 | fittings <- pipeline %>% 7 | parse_pipeline(supported_tidy_functions) 8 | 9 | states <- fittings %>% 10 | snake() 11 | 12 | expect_equal(states[[1]], small_salary, ignore_attr = TRUE) 13 | expect_equal(states[[2]], small_salary %>% 14 | group_by(Degree), ignore_attr = TRUE) 15 | expect_equal(states[[3]], small_salary %>% 16 | group_by(Degree) %>% 17 | summarise(mean = mean(Salary)), ignore_attr = TRUE) 18 | }) 19 | -------------------------------------------------------------------------------- /vignettes/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.R 3 | -------------------------------------------------------------------------------- /vignettes/Examples.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Examples" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Examples} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>", 14 | message = FALSE 15 | ) 16 | ``` 17 | 18 | ```{r setup} 19 | library(datamations) 20 | library(dplyr) 21 | ``` 22 | 23 | # group_by() 24 | 25 | ## One grouping variable 26 | 27 | ```{r} 28 | "small_salary %>% 29 | group_by(Work)" %>% 30 | datamation_sanddance() 31 | ``` 32 | 33 | ## Two grouping variables 34 | 35 | ```{r} 36 | "small_salary %>% 37 | group_by(Work, Degree)" %>% 38 | datamation_sanddance() 39 | ``` 40 | 41 | ## Three grouping variables 42 | 43 | ```{r} 44 | library(palmerpenguins) 45 | 46 | "penguins %>% 47 | group_by(sex, island, species)" %>% 48 | datamation_sanddance() 49 | ``` 50 | 51 | # summarise() 52 | 53 | ## mean of a variable 54 | 55 | ```{r} 56 | "small_salary %>% 57 | group_by(Work) %>% 58 | summarise(mean_salary = mean(Salary))" %>% 59 | datamation_sanddance() 60 | ``` 61 | 62 | ## Other summary functions 63 | 64 | ```{r} 65 | "small_salary %>% 66 | group_by(Work) %>% 67 | summarise(median_salary = median(Salary))" %>% 68 | datamation_sanddance() 69 | ``` 70 | 71 | ```{r} 72 | "small_salary %>% 73 | group_by(Degree) %>% 74 | summarise(quan = quantile(Salary, probs = 0.01))" %>% 75 | datamation_sanddance() 76 | ``` 77 | 78 | ```{r} 79 | "small_salary %>% 80 | group_by(Degree) %>% 81 | summarise(sum = sum(Salary))" %>% 82 | datamation_sanddance() 83 | ``` 84 | 85 | # filter() 86 | 87 | ## Filtering initial data 88 | 89 | ```{r} 90 | "small_salary %>% 91 | filter(Salary > 90)" %>% 92 | datamation_sanddance() 93 | ``` 94 | 95 | ## Filtering within groups 96 | 97 | ```{r} 98 | "small_salary %>% 99 | group_by(Work) %>% 100 | filter(Salary == mean(Salary))" %>% 101 | datamation_sanddance() 102 | ``` 103 | 104 | ## Filtering after summarize 105 | 106 | ```{r} 107 | "small_salary %>% 108 | group_by(Work) %>% 109 | summarise(median_salary = median(Salary)) %>% 110 | filter(median_salary > 90)" %>% 111 | datamation_sanddance() 112 | ``` 113 | 114 | # count() 115 | 116 | ```{r} 117 | "small_salary %>% 118 | count(Work)" %>% 119 | datamation_sanddance() 120 | ``` 121 | 122 | # Binary variables 123 | 124 | A basic example of a dataframe containing data that represents Simpson's paradox. 125 | 126 | ```{r} 127 | head(jeter_justice) 128 | ``` 129 | 130 | 131 | In this datamation, Jeter has a higher batting average than Justice overall. 132 | 133 | ```{r} 134 | 'jeter_justice %>% 135 | group_by(player) %>% 136 | summarize(batting_average = mean(is_hit), 137 | se = sqrt(batting_average * (1 - batting_average) / n()) )' %>% 138 | datamation_sanddance() 139 | ``` 140 |
141 |
142 | In this datamation, the visual shows that Justice has a higher batting average than Jeter within each year. 143 | 144 | ```{r} 145 | 'jeter_justice %>% 146 | group_by(player, year) %>% 147 | summarize(batting_average = mean(is_hit), 148 | se = sqrt(batting_average * (1 - batting_average) / n()) )' %>% 149 | datamation_sanddance() 150 | ``` 151 | -------------------------------------------------------------------------------- /vignettes/details.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "datamations steps and conventions" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{datamations steps and conventions} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>", 14 | message = FALSE 15 | ) 16 | ``` 17 | 18 | datamations constructs one _or more_ frames for each step of a pipeline. For example, in the following pipeline: 19 | 20 | ```{r mean-salary-degree} 21 | library(datamations) 22 | library(dplyr) 23 | 24 | "small_salary %>% 25 | group_by(Degree) %>% 26 | summarize(mean = mean(Salary))" %>% 27 | datamation_sanddance() 28 | ``` 29 | 30 | there are three steps: 31 | 32 | 1. The initial data (`small_salary`) 33 | 34 | An information grid is shown, laying out the number of points in the data set. 35 | 36 | 2. The grouped data (grouped by `Degree`) 37 | 38 | The data is separated into groups, retaining the informaton grid structure. 39 | 40 | 3. The summarized data (mean of `Salary`) 41 | 42 | The distribution of `Salary` within the groups is shown, then the summary function (mean) is applied. Error bars are added to the mean and the final frame zooms in on the data. 43 | 44 | ## `group_by()` frames 45 | 46 | datamations supports *up to three* grouping variables, showing one frame 47 | per variable. The placement of the variables is as follows: 48 | 49 | - **One variable**: On the x-axis 50 | - **Two variables**: The first variable in column facets, the second on the x-axis 51 | - **Three variables**: The first variable in column facets, the second in row facets, the third in on the x-axis 52 | 53 | ## `summarize()` frames 54 | 55 | datamations supports summarizing _one_ variable. The `summarize()` section of a pipeline will have the following frames: 56 | 57 | 1. Distribution of the variable to be summarized 58 | 2. Summarized variable 59 | 3. Summarized variable with standard error (only if summary function is mean) 60 | 4. Zoomed version of summarized variable 61 | 62 | ## `count()` frames 63 | 64 | datamations treats `count()` equivalently to `group_by()` + `summarize(n = n())`. It supports up to three "grouping" variables. 65 | 66 | ## `filter()` frames 67 | 68 | datamation supports `filter()` at any point in the pipeline, whether it comes after the initial data, while the data is grouped, or after it has been summarized. 69 | -------------------------------------------------------------------------------- /vignettes/finer_control.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Controlling appearance with ggplot2" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Controlling appearance with ggplot2} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>", 14 | message = FALSE 15 | ) 16 | 17 | ggplot2::theme_set(ggplot2::theme_minimal()) 18 | ``` 19 | 20 | If you would like to change the default conventions, or to match an existing plot style, datamations can take ggplot2 code as input. 21 | 22 | For example, to match this plot, which has Work on the x-axis and Degree in row facets: 23 | 24 | ```{r ggplot2-existing-plot, dpi = 300} 25 | library(datamations) 26 | library(dplyr) 27 | library(ggplot2) 28 | 29 | small_salary %>% 30 | group_by(Work, Degree) %>% 31 | summarize(mean_salary = mean(Salary)) %>% 32 | ggplot(aes(x = Work, y = mean_salary)) + 33 | geom_point() + 34 | facet_grid(rows = vars(Degree)) 35 | ``` 36 | 37 | Simply define the code and pass to `datamation_sanddance()`, which will produce an animation with desired plot layout. 38 | 39 | ```{r mean-salary-degree-work-ggplot-setup, eval = TRUE} 40 | "small_salary %>% 41 | group_by(Work, Degree) %>% 42 | summarize(mean_salary = mean(Salary)) %>% 43 | ggplot(aes(x = Work, y = mean_salary)) + 44 | geom_point() + 45 | facet_grid(rows = vars(Degree))" %>% 46 | datamation_sanddance() 47 | ``` 48 | 49 | When ggplot2 code is provided, the order of animation is not determined by the order in `group_by()`, but by the plot layout. Variables are first animated by what's in the column facets, then the row facets, by the x-axis, and finally by color. 50 | 51 | Some limitations: 52 | 53 | * `facet_wrap()` is not supported - please use `facet_grid()` 54 | * datamations expects different variables in the column and row facets than in the x-axis. datamations generated that do not match this layout may look different than the final plot! 55 | * Only `geom_point()` is supported, e.g. specifying `geom_bar()` will not produce a bar in the datamation. 56 | -------------------------------------------------------------------------------- /vignettes/groupby_api.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "datamations group_by api" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{datamations group_by api} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>", 14 | message = FALSE 15 | ) 16 | ``` 17 | 18 | datamations allows the visualization of `dplyr::group_by` calls containing up to three variables. 19 | 20 | ## group_by api 21 | 22 | The `datamations_sanddance` call can finish with a `group_by` call, displaying proportional data faceted by the grouping variables. 23 | 24 | ## Single grouping variable 25 | 26 | ```{r} 27 | library(datamations) 28 | library(dplyr) 29 | 30 | "small_salary %>% 31 | group_by(Work, Degree)" 32 | ``` 33 | ## Two grouping variables 34 | 35 | ```{r} 36 | "small_salary %>% 37 | group_by(Work, Degree)" %>% 38 | datamation_sanddance() 39 | ``` 40 | 41 | ## Three grouping variables 42 | 43 | ```{r} 44 | library(palmerpenguins) 45 | 46 | "penguins %>% 47 | group_by(sex, island, species)" %>% 48 | datamation_sanddance() 49 | ``` 50 | 51 | ## summarizing and filtering by group 52 | 53 | The `datamations_sanddance` call can also show the filtering and summarization of data based on group facets. 54 | 55 | ### group_by into filter 56 | 57 | ```{r} 58 | "small_salary %>% 59 | group_by(Degree) %>% 60 | filter(Salary > 90)" %>% 61 | datamation_sanddance() 62 | ``` 63 | 64 | ### group_by into summarize 65 | 66 | ```{r} 67 | "small_salary %>% 68 | group_by(Degree) %>% 69 | summarize(mean = mean(Salary))" %>% 70 | datamation_sanddance() 71 | 72 | "penguins %>% 73 | group_by(sex, island, species) %>% 74 | summarize(mean = median(bill_length_mm))" %>% 75 | datamation_sanddance() 76 | ``` 77 | 78 | 79 | -------------------------------------------------------------------------------- /vignettes/mutations.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "datamations mutation api" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{datamations mutation api} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>", 14 | message = FALSE 15 | ) 16 | ``` 17 | 18 | {datamations} supports the limited definition of mutations in a pipeline. It is capable of showing a single mutation involving multiple variables in a scatterplot or grid fashion. 19 | 20 | We can define new data to use in some example mutations. 21 | 22 | ### New data with added variable 23 | 24 | ```{r newdata} 25 | library(dplyr) 26 | library(datamations) 27 | 28 | small_salary <- dplyr::mutate( 29 | small_salary, 30 | supplementalIncome = runif(nrow(small_salary), min = 60, max = 110), 31 | logNorm = rlnorm(nrow(small_salary), meanlog = 0, sdlog = 1) 32 | ) 33 | 34 | ``` 35 | 36 | {datamations} can visualize mutations to help one understand mathematical distributions, scales, and relationships. 37 | 38 | ### Log normal mutation 39 | 40 | ```{r lognorm} 41 | 42 | "small_salary %>% 43 | mutate(logged = log10(logNorm)) %>% 44 | group_by(Degree) %>% 45 | summarize(mean = mean(logged))" %>% 46 | datamation_sanddance() 47 | ``` 48 | 49 | 50 | ### Mathematical Notation 51 | 52 | ```{r mutations} 53 | 54 | "small_salary %>% 55 | mutate(salarySquared = Salary^2) %>% 56 | group_by(Degree) %>% 57 | summarize(mean = mean(salarySquared))" %>% 58 | datamation_sanddance() 59 | 60 | "small_salary %>% 61 | mutate(inverseSalary = 1 / Salary) %>% 62 | group_by(Degree) %>% 63 | summarize(mean = mean(inverseSalary))" %>% 64 | datamation_sanddance() 65 | 66 | ``` 67 | 68 | 69 | ### Multivariate mutates 70 | 71 | {datamations} can also showcase the relationship between more than variable in your data pipelines. We can see below the relationship between our Salary variable and a new variable and use the mutation in grouping, filtering, and summarization. 72 | 73 | ```{r mutivar} 74 | 75 | "small_salary %>% 76 | mutate(totalIncome = Salary + supplementalIncome) %>% 77 | group_by(Degree) %>% 78 | summarize(mean = mean(totalIncome))" %>% 79 | datamation_sanddance() 80 | 81 | "small_salary %>% 82 | mutate(incomePer = Salary / supplementalIncome) %>% 83 | group_by(Degree) %>% 84 | summarize(mean = mean(incomePer))" %>% 85 | datamation_sanddance() 86 | 87 | ``` 88 | 89 | ### Two variable mutates 90 | 91 | {datamations} will allow the definition of a mutate statement with multiple mutates, but it will ignore anything after the first defined mutate. Two variable mutates results in a warning. 92 | 93 | ```{r twovar, error = TRUE} 94 | "small_salary %>% 95 | mutate(totalIncome = Salary + supplementalIncome, squaredIncome = Salary^2) %>% 96 | group_by(Degree) %>% 97 | summarize(mean = mean(totalIncome))" %>% 98 | datamation_sanddance() 99 | ``` 100 | -------------------------------------------------------------------------------- /vignettes/simpsons_paradox.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Binary variables and simpson's paradox" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Binary variables and simpson's paradox} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>", 14 | message = FALSE 15 | ) 16 | ``` 17 | 18 | {datamations} is useful for visualizing unique statistical effects and occurrences. Visualizing the steps in a data transformation that lead to a statistical calculate can help uncover phenomena in underlying data. 19 | 20 | 21 | ## Simpson's paradox 22 | 23 | A working example of using {datamations} in this way is visualizing a [Simpson's paradox](https://en.wikipedia.org/wiki/Simpson%27s_paradox). Simpson's paradox is a phenomenon in probability where a trend might occurs in several groups of data individually, but disappears when groups are combined. {datamations} is the perfect package to understand effects of grouping variables. 24 | 25 | ### Derek Jeter and David Justice 26 | 27 | One common example of Simpson's paradox involves combined hit averages of two well known baseball players, Derek Jeter and David Justice, during the 1995 and 1996 seasons. During these two years of play, David Justice individually had higher batting averages than Derek Jeter. However, combined, Jeter had a higher average than Justice across the two years. 28 | 29 | The total values of hits and batting averages are displayed here: 30 | 31 | | Year Batter | 1995 | | 1996 | | Combined | | 32 | |--------------:|:-------:|:----:|:-------:|:----:|:--------:|:----:| 33 | | Derek Jeter | 12/48 | .250 | 183/582 | .314 | 195/630 | .310 | 34 | | David Justice | 104/411 | .253 | 45/140 | .321 | 149/551 | .270 | 35 | 36 | We can use datamations to visualize each step of these binary data. {datamations} includes a built in dataset called `jeter_justice` to explore this phenomenon. Call `?jeter_justice` for documentation on this dataset. 37 | 38 | When we just group by individual player, Jeter overall has a higher batting average. 39 | 40 | ```{r setup} 41 | library(datamations) 42 | library(dplyr) 43 | ``` 44 | 45 | ```{r} 46 | 'jeter_justice %>% 47 | group_by(player) %>% 48 | summarize(batting_average = mean(is_hit), 49 | se = sqrt(batting_average * (1 - batting_average) / n()) )' %>% 50 | datamation_sanddance() 51 | ``` 52 | 53 |
54 | 55 | Grouping by both year and player though, we can see Justice has a higher batting average than Jeter within each year. 56 | 57 | ```{r} 58 | 'jeter_justice %>% 59 | group_by(player, year) %>% 60 | summarize(batting_average = mean(is_hit), 61 | se = sqrt(batting_average * (1 - batting_average) / n()) )' %>% 62 | datamation_sanddance() 63 | ``` 64 | 65 |
66 | 67 | Viewing each data step, we can see how the frequency imbalance of each player within each year creates this phenomenon. {datamations} can help us understand how data is transformed when binary frequency data is used to produce statistics. -------------------------------------------------------------------------------- /vignettes/summarizing.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "summarize, count, and tally" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{summarize, count, and tally} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>", 14 | message = FALSE 15 | ) 16 | ``` 17 | 18 | The datamations API can visualize the summarization of variables with several dplyr verbs, like `summarize()`/`summarise()`, `count()`, and `tally()` 19 | 20 | ## summary functions 21 | 22 | `datamations_sanddance` has special animations to visualize certain common summary functions passed to `summarize`. These custom animations include: 23 | 24 | - `mean` 25 | - `median` 26 | - `max`/`min` 27 | - `quantile` 28 | 29 | 30 | ### mean of a variable 31 | 32 | ```{r setup} 33 | library(datamations) 34 | library(dplyr) 35 | ``` 36 | 37 | ```{r} 38 | "small_salary %>% 39 | group_by(Work) %>% 40 | summarise(mean_salary = mean(Salary))" %>% 41 | datamation_sanddance() 42 | ``` 43 | 44 | ### median of a variable 45 | 46 | ```{r} 47 | "small_salary %>% 48 | group_by(Work) %>% 49 | summarise(median_salary = median(Salary))" %>% 50 | datamation_sanddance() 51 | ``` 52 | 53 | ### min and max of a variable 54 | 55 | ```{r} 56 | "small_salary %>% 57 | group_by(Work) %>% 58 | summarise(min_salary = min(Salary))" %>% 59 | datamation_sanddance() 60 | ``` 61 | 62 | ```{r} 63 | "small_salary %>% 64 | group_by(Work) %>% 65 | summarise(max_salary = max(Salary))" %>% 66 | datamation_sanddance() 67 | ``` 68 | 69 | ### quantile() function 70 | 71 | The summarize function includes the capacity to pass some parameterized functions that result in custom animations. Currently datamations supports the `quantile()` function with the `probs` parameter as an example of this capability. 72 | 73 | ```{r} 74 | "small_salary %>% 75 | group_by(Degree) %>% 76 | summarise(quan = quantile(Salary, probs = 0.01))" %>% 77 | datamation_sanddance() 78 | ``` 79 | 80 | ## Other summary functions 81 | 82 | Common summary functions may include count-like operations, like n_distinct. 83 | 84 | ### n_distinct() 85 | 86 | ```{r} 87 | library(palmerpenguins) 88 | 89 | "penguins %>% 90 | group_by(island) %>% 91 | summarise(n = n_distinct(species))" %>% 92 | datamation_sanddance() 93 | ``` 94 | 95 | You can find the API can support a large variety of mathematical functions and dplyr functions 96 | 97 | 98 | ```{r} 99 | "small_salary %>% 100 | group_by(Degree) %>% 101 | summarize(floor_salary = trunc(Salary))" %>% 102 | datamation_sanddance() 103 | ``` 104 | 105 | ```{r} 106 | "small_salary %>% 107 | group_by(Degree) %>% 108 | summarize(first_salary = first(Salary))" %>% 109 | datamation_sanddance() 110 | ``` 111 | 112 | ```{r} 113 | "small_salary %>% 114 | group_by(Degree) %>% 115 | summarize(lagged_salary = lag(Salary))" %>% 116 | datamation_sanddance() 117 | ``` 118 | 119 | ## count() and tally() 120 | 121 | datamations treats `count()` and `tally()` calls equivalently to `group_by()` + `summarize(n = n())`. 122 | 123 | ```{r} 124 | "small_salary %>% 125 | group_by(Degree) %>% 126 | count(Salary)" %>% 127 | datamation_sanddance() 128 | ``` -------------------------------------------------------------------------------- /vignettes/variable_types.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Variable types" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Variable types} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>", 14 | message = FALSE 15 | ) 16 | ``` 17 | 18 | {datamations} provides useful visual encodings to help visualize transformations that occur in various types of data: 19 | 20 | - numeric 21 | - categorical 22 | - binary 23 | 24 | ## Numeric data 25 | 26 | {datamations} can accept calculations on numeric data and provides encodings on faceted numeric scales. 27 | 28 | Test out the functionality on the built in dataset `small_salary`, which includes synthetic salary data for different types of work and different degree qualifications. 29 | 30 | ```{r setup} 31 | library(datamations) 32 | library(dplyr) 33 | ``` 34 | 35 | ```{r} 36 | "small_salary %>% 37 | group_by(Degree) %>% 38 | summarize(mean = mean(Salary, trim=0.1))" %>% 39 | datamation_sanddance() 40 | ``` 41 | 42 | ## Categorical data 43 | 44 | {datamations} generates shape encoding for categorical variables passed to `group_by` or in summary functions. A useful example is in the {palmerpenguins} `penguins` dataset. 45 | 46 | ```{r} 47 | library(palmerpenguins) 48 | 49 | "penguins %>% 50 | group_by(year, island) %>% 51 | summarise(n = n_distinct(species))" %>% 52 | datamation_sanddance() 53 | ``` 54 | 55 | ## Binary data 56 | 57 | {datamations} similarly can produce visualizations for binary outcomes, displaying variables encoded with opacity and stroke. Binary variables can be passed as a true binary class in R, e.g. `TRUE`/`FALSE`, or via a numeric variable encoded to `1`/`0`. This is examplified in the built in dataset `jeter_justice`, where outcome values are encoded as `1` and `0`. 58 | 59 | ```{r} 60 | head(jeter_justice) 61 | ``` 62 | 63 | ```{r} 64 | "jeter_justice %>% 65 | group_by(year) %>% 66 | summarise(mean = mean(is_hit))" %>% datamation_sanddance() 67 | ``` 68 | 69 | The appearance of this encoding can also be controlled more finely with [ggplot aesthetics](https://microsoft.github.io/datamations/articles/finer_control.html). --------------------------------------------------------------------------------