diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md deleted file mode 100644 index 27bd9cec7..000000000 --- a/.claude/CLAUDE.md +++ /dev/null @@ -1,233 +0,0 @@ -# JASP Module - -ALWAYS follow these instructions first and fallback to additional search and context gathering ONLY if the information in these instructions is incomplete or found to be in error. - -This is a JASP module. It contains QML user-facing interfaces and R backend computations. - -In all interactions and commit messages, be extremely concise and sacrifice grammar for the sake of concision. - -## Detailed Instructions - -For comprehensive guidance on specific topics, see: - -- **[Module Architecture](.claude/rules/jasp-module-architecture.md)** - **Start here.** QML-Desktop-R reactive loop, jaspResults persistence, options mapping, data flow -- **[Dependency Management](.claude/rules/jasp-dependency-management.md)** - $dependOn mechanics, inheritance, vectors, per-value deps, sentinel pattern -- **[State Management](.claude/rules/jasp-state-management.md)** - createJaspState caching, model fit patterns, metadata state, dynamic containers -- **[R Backend Development](.claude/rules/r-instructions.md)** - R function structure, validation, style conventions -- **[Tables](.claude/rules/jasp-tables.md)** - Table lifecycle, columns, rows, footnotes, error display -- **[Plots](.claude/rules/jasp-plots.md)** - Plot lifecycle, composite plots, subgroup/facet patterns -- **[Containers & Errors](.claude/rules/jasp-containers-and-errors.md)** - Container patterns, HTML output, error handling -- **[QML Interface Development](.claude/rules/qml-instructions.md)** - QML controls, validation, bindings, and UI patterns -- **[Testing & Test Writing](.claude/rules/testing-instructions.md)** - Test framework, snapshots, and test workflow -- **[Translation (i18n)](.claude/rules/translation-instructions.md)** - gettext/gettextf/qsTr usage, formatting, plurals -- **[Output Structure](.claude/rules/jasp-output-structure.md)** - Reading/testing serialized output (containers, tables, plots, state) - -## R Session via MCP - -This project uses the `btw` MCP server (`.claude/mcp-server.R`) to provide a persistent R session via `btw_tool_run_r`. The MCP server config (`.mcp.json`) is module-specific and NOT committed to git. - -**Session handoff:** The user sets up their R session (RStudio/Positron/radian), runs `btw::btw_mcp_session()`, and hands it over. Connect via `list_r_sessions` / `select_r_session`. All `btw_tool_run_r` calls then execute in the user's session with full access to loaded packages and objects. The following R packages are required for the mcp server: `btw`, `mcptools`. - -### Available MCP Tools - -Use these R-specific tools instead of Bash when possible: - -| Tool | Use for | -|------|---------| -| `btw_tool_run_r` | Execute R code in persistent session (variables persist between calls) | -| `btw_tool_docs_help_page` | Look up R function documentation | -| `btw_tool_docs_package_news` | Check package changelogs | -| `btw_tool_docs_available_vignettes` | Find package vignettes | -| `btw_tool_env_describe_environment` | Inspect objects in the R session | -| `btw_tool_env_describe_data_frame` | Inspect data frame structure | -| `btw_tool_search_packages` | Search CRAN for packages | -| `btw_tool_session_platform_info` | Check R version and platform | -| `btw_tool_session_check_package_installed` | Verify package availability | - -**Use Claude Code native tools** (Read, Edit, Write, Glob, Grep, Bash) for file editing, git operations, and file search -- they are faster than MCP equivalents. - -## Working Effectively - -### Session Setup (done by user) - -At the start of a session, check for a connected R session via `list_r_sessions`. If none is available, **prompt the user** to run in their interactive R console: - -```r -source(".claude/session_startup.R") -``` - -This restores dependencies, installs the module, configures jaspTools, and registers the session. Then connect via `list_r_sessions` / `select_r_session`. - -### Hot-Reload After Code Changes - -- **R code only changed:** `devtools::load_all()` via `btw_tool_run_r` -- **QML, dependencies, or imports changed:** `renv::install(".", prompt = FALSE)` - -### Running Tests - -Run via `btw_tool_run_r` in the persistent session: - -```r -# Full test suite (300+ sec, NEVER CANCEL) -testAll() - -# Specific analysis tests (for quick iteration) -testAnalysis("AnalysisName") -``` - -- `testAll()` at session start to verify baseline, and after all fixes to check regressions -- `testAnalysis("Name")` for quick iteration while fixing specific analyses -- Analysis names are PascalCase exports from NAMESPACE -- Some tests may skip on certain platforms (e.g., Windows) -- this is expected - -**See [testing-instructions.md](.claude/rules/testing-instructions.md) for detailed test writing guidelines, snapshots, and workflows.** - -### Running a Specific Analysis - -**With built-in debug dataset:** -```r -options <- jaspTools::analysisOptions("AnalysisName") -options$someOption <- value -set.seed(1) -results <- jaspTools::runAnalysis("AnalysisName", "debug.csv", options) -``` - -**From a .jasp example file:** -```r -jaspFile <- file.path("examples", "Example Name.jasp") -opts <- jaspTools::analysisOptions(jaspFile) -dataset <- jaspTools::extractDatasetFromJASPFile(jaspFile) -encoded <- jaspTools:::encodeOptionsAndDataset(opts, dataset) -set.seed(1) -results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE) -``` - -The encoding step is required because JASP internally encodes variable names and options to resolve ambiguities (e.g., same variable used with different types). - -**From a user-provided .jasp file:** Use the same pattern above. This is the primary way to reproduce bugs reported by users. - -### Inspecting Results - -After `runAnalysis()`, check: -- `results$status` -- `"complete"` or `"fatalError"` -- `results$results` -- nested list of output containers, tables, plots -- `results$results$errorMessage` -- if status is fatalError - -### Finding Analysis Names - -1. Check roxygen documentation in R files (if available) -2. Parse `NAMESPACE` for `export()` directives - -### Test Snapshots - -- Snapshots stored in `tests/testthat/_snaps/` -- **NEVER automatically accept snapshot changes** -- always notify user for manual inspection -- When a snapshot is newly created, inform the user - -### Repository Structure -``` -/ -├── R/ # Backend R analysis functions -├── inst/ -│ ├── qml/ # QML interface definitions -│ ├── Descriptions/ # Analysis descriptions (Description.qml) -│ ├── help/ # Markdown help files -│ └── Upgrades.qml # Version upgrade mappings -├── examples/ # Example .jasp files for testing -├── tests/testthat/ # Unit tests using jaspTools -├── .claude/ # Claude Code instructions and MCP server -│ ├── CLAUDE.md # This file -│ ├── mcp-server.R # MCP server startup script -│ └── rules/ # Path-specific rules -├── .github/workflows/ # CI/CD automation -├── DESCRIPTION # R package metadata -├── NAMESPACE # Exported analysis names -└── renv.lock # R dependency lockfile -``` - -### Key Files to Check After Changes -- Always check corresponding test file in `tests/testthat/` when modifying R functions -- Update `inst/Upgrades.qml` when renaming QML options to maintain backward compatibility - -## Development Rules - -### Dependencies -- Avoid new dependencies -- re-implement simple functions instead of importing a whole package -- If a new dependency is truly needed, add it to DESCRIPTION and update renv.lock - -### QML Interface Rules -- QML interfaces in `inst/qml/` define user-facing options passed to R functions -- Each analysis links: `inst/Description.qml/` -> `inst/qml/` -> `R/` functions -- QML elements use `name` (camelCase internal) and `title`/`label` (user-facing) -- Document QML elements using `info` property for help generation -- Use existing QML files as examples for structure and style -- Add default values to unit tests when adding new QML options - -**See [qml-instructions.md](.claude/rules/qml-instructions.md) for comprehensive QML controls reference, validation patterns, and UI conventions.** - -### R Backend Rules -- R functions in `R/` directory called by analyses in `inst/Descriptions/` -- Use camelCase for all function and variable names -- NEVER use `library()` or `require()` - use `package::function()` syntax -- Access `options` list via `options[["name"]]` notation to avoid partial matching -- Follow CRAN guidelines for code structure and documentation - -**See [r-instructions.md](.claude/rules/r-instructions.md) for complete R function structure, jaspResults API, output components (tables/plots/containers/state), and coding conventions.** - -### Input Validation and Error Handling -- **TARGETED VALIDATION ONLY**: Since `options` are validated in the GUI, R functions should NOT check user input validity except for specific cases -- **VALIDATE ONLY**: `dataset` object (data.frame from GUI), `TextField` options, and `FormulaField` options (arbitrary text input) -- Use `gettext()` and `gettextf()` for all user-visible messages (internationalization) -- For `dataset` validation, check: missing values, infinity, negative values, insufficient observations, factor levels, variance -- Example: `.hasErrors(dataset, type = c('observations', 'variance', 'infinity'), all.target = options$variables, observations.amount = '< 3', exitAnalysisIfErrors = TRUE)` -- Validate dataset assumptions automatically when required for analysis validity -- Use footnotes for assumption violations that affect specific cells/values -- Place critical errors that invalidate entire analysis over the results table - -### Error Message Guidelines -- Write clear, actionable error messages that prevent user confusion -- Use `gettextf()` with placeholders for dynamic content: `gettextf("Number of factor levels is %1$s in %2$s", levels, variable)` -- For multiple arguments, use `%1$s`, `%2$s` format for translator clarity -- Use `ngettext()` for singular/plural forms -- Never mark empty strings for translation -- Use UTF-8 encoding for non-ASCII characters: `\u03B2` for beta -- Double `%` characters in format strings: `gettextf("%s%% CI for Mean")` - -**See [translation-instructions.md](.claude/rules/translation-instructions.md) for comprehensive i18n guidelines including QML qsTr(), R gettext/gettextf/ngettext, formatting rules, and Weblate workflow.** - -## CI/CD Pipeline -- GitHub Actions in `.github/workflows/unittests.yml` runs on every push -- Triggers on changes to R, test, or package files -- Uses jasp-stats/jasp-actions reusable workflow - -## Git Workflow - -- **ALWAYS work on feature branches** -- never commit directly to `master` -- **NEVER push/create PRs/merge without explicit human approval** -- Commit locally freely, but wait for approval before pushing to remote - -## Common Tasks - -### Adding New Analysis - -1. Create R function in `R/` directory following camelCase naming -2. Add QML interface in `inst/qml/` -3. Define analysis in `inst/Description.qml` -4. Add unit tests in `tests/testthat/` -5. Run `testAll()` to validate (300+ seconds, NEVER CANCEL) - -### Modifying Existing Analysis - -1. Update R function maintaining existing interface -2. Update QML if adding/changing options -3. Update unit tests and expected results -4. Add upgrade mapping to `inst/Upgrades.qml` if renaming options -5. Run tests: `testAll()` (NEVER CANCEL, 300+ seconds) - -### Detailed Development Process -- **Step 1**: Create main analysis function with `jaspResults`, `dataset`, `options` arguments -- **Step 2**: **CRITICAL** - Use `.quitAnalysis()` for `dataset`, `TextField`, `FormulaField` validation only -- **Step 3**: Create output tables/plots with proper dependencies, citations, column specs -- Use `createJaspTable()`, `createJaspPlot()`, `createJaspHtml()` for output elements -- Always set `$dependOn()` for proper caching and state management -- Use containers for grouping related elements, state objects for reusing computed results diff --git a/.claude/README.md b/.claude/README.md deleted file mode 100644 index b059d11b4..000000000 --- a/.claude/README.md +++ /dev/null @@ -1,125 +0,0 @@ -# Claude Code Instructions - -This directory contains project-specific instructions for Claude Code, Anthropic's CLI tool. - -## Purpose - -These files are automatically loaded when Claude Code starts, providing context about: - -- JASP module structure and conventions -- Development workflows and best practices -- Testing requirements -- Translation guidelines - -## Structure - -``` -.claude/ -├── CLAUDE.md # Main project instructions (always loaded) -├── README.md # This file -├── mcp-server.R # MCP server startup script (R session tools) -├── settings.local.json # Local Claude Code settings (not committed) -└── rules/ # Path-specific rules - ├── r-instructions.md # R backend guidelines (**/R/*.R) - ├── qml-instructions.md # QML interface guidelines (**/inst/qml/*.qml) - ├── testing-instructions.md # Test framework guidelines (**/tests/testthat/*.R) - ├── git-workflow.md # Git and commit conventions - └── translation-instructions.md # i18n/l10n guidelines -``` - -## MCP Server Setup - -The `.claude/mcp-server.R` script configures the `btw` MCP server for JASP module development. It: - -1. Enables `btw_tool_run_r` for R code execution in a persistent session -2. Fixes `cli.spinner` option for testthat compatibility in the evaluate context -3. Exposes btw tool groups: docs, env, run, search, session - -The user sets up their R session, then registers it via `btw::btw_mcp_session()`. Claude connects with `list_r_sessions` / `select_r_session` and executes R code in the user's session. - -### Configuration - -The MCP server is configured via `.mcp.json` in the module root (NOT committed to git). To set up: - -```json -{ - "mcpServers": { - "r-mcptools": { - "type": "stdio", - "command": "Rscript", - "args": ["-e", "source('.claude/mcp-server.R')"] - } - } -} -``` - -Or via CLI: `claude mcp add r-mcptools -- Rscript -e "source('.claude/mcp-server.R')"` - -### Connecting an Interactive R Session - -To route MCP tool calls to your interactive R session (RStudio/Positron/radian): - -```r -btw::btw_mcp_session() -``` - -This gives Claude Code access to your loaded objects and environment. - -## How It Works - -**Automatic Loading:** - -- `CLAUDE.md` is automatically loaded in every Claude Code session -- Files in `rules/` are loaded based on their `paths:` frontmatter -- Path-specific rules apply only when working on matching files - -**Path Scoping:** -Rules use YAML frontmatter to scope to specific files: - -```yaml ---- -paths: - - "**/R/*.R" ---- -``` - -## Copying to Other JASP Modules - -To use these instructions in another JASP module: - -1. Copy the `.claude/` directory to the target module -2. Create a `.mcp.json` in the module root (see Configuration above) -3. Adjust the `Rscript` command path if needed for your system -4. The `.mcp.json` file should be added to `.gitignore` (machine-specific paths) -5. The `.claude/mcp-server.R` script is portable and can be committed - -## Personal Preferences - -To add personal project-specific preferences that aren't shared with the team: - -1. Create `CLAUDE.local.md` in this directory -2. Add your personal preferences -3. File is already in `.gitignore` and won't be committed - -## Maintenance - -**When to update:** - -- Adding new development conventions -- Changing testing requirements -- Updating build/deployment processes -- Adding new repository-specific workflows - -**What to include:** - -- Information Claude can't infer from code -- Project-specific conventions that differ from defaults -- Critical commands and workflows -- Non-obvious patterns and gotchas - -**What to exclude:** - -- Standard language conventions -- Detailed API documentation (link to it instead) -- Frequently changing information -- Information easily discovered by reading code diff --git a/.claude/hooks/block-test-edits.js b/.claude/hooks/block-test-edits.js deleted file mode 100644 index ccc3315fa..000000000 --- a/.claude/hooks/block-test-edits.js +++ /dev/null @@ -1,18 +0,0 @@ -// PreToolUse hook: blocks Edit/Write on files under tests/ -// Contract: read JSON from stdin, echo it to stdout, exit 2 to block -let d = ''; -process.stdin.on('data', c => d += c); -process.stdin.on('end', () => { - try { - const input = JSON.parse(d); - const filePath = input.tool_input?.file_path || ''; - if (/(^|[/\\])tests[/\\]/.test(filePath)) { - process.stderr.write( - '[Hook] BLOCKED: test files are human-owned. Fix source code instead.\n' - ); - console.log(d); - process.exit(2); - } - } catch {} - console.log(d); -}); diff --git a/.claude/mcp-server.R b/.claude/mcp-server.R deleted file mode 100644 index 275d7a071..000000000 --- a/.claude/mcp-server.R +++ /dev/null @@ -1,12 +0,0 @@ -# Custom MCP server for JASP modules -# Provides btw tools with JASP-specific fixes -options( - btw.run_r.enabled = TRUE, - # Fix cli::get_spinner() returning FALSE in evaluate context, - # which breaks testthat reporter initialization (which$frames error) - cli.spinner = "line" -) - -btw::btw_mcp_server( - tools = btw::btw_tools("docs", "env", "run", "search", "session") -) diff --git a/.claude/rules/git-workflow.md b/.claude/rules/git-workflow.md deleted file mode 100644 index 682eaa4e4..000000000 --- a/.claude/rules/git-workflow.md +++ /dev/null @@ -1,202 +0,0 @@ ---- -# Git Workflow Instructions ---- - -## Commit Message Style - -**Be extremely concise. Sacrifice grammar for concision.** - -### Format: -``` -: - -[optional body if needed] - -Co-Authored-By: Claude Sonnet 4.5 -``` - -### Types: -- `feat:` - New feature or analysis -- `fix:` - Bug fix -- `refactor:` - Code restructuring without behavior change -- `test:` - Adding or updating tests -- `docs:` - Documentation only -- `i18n:` - Translation updates -- `chore:` - Maintenance tasks - -### Examples: -``` -feat: add equivalence bounds plot - -fix: correct CI calculation in paired t-test - -test: update snapshots for descriptives table - -refactor: extract common validation logic - -i18n: update translation files -``` - -## Commit Workflow - -### 0. Ensure on feature branch: -```bash -# Check current branch -git branch - -# If on master, create feature branch -git checkout -b feature/descriptive-name -``` - -### 1. Before committing: -```bash -# Run full test suite -Rscript -e "library(jaspTools); testAll()" - -# Check git status -git status - -# Review changes -git diff -``` - -### 2. Stage specific files: -```bash -# Stage specific files (preferred) -git add R/equivalenceonesamplettest.R -git add tests/testthat/test-equivalenceonesamplettest.R - -# Avoid staging everything unless you're certain -# git add -A # Be careful with this -``` - -### 3. Commit locally with co-author: -```bash -git commit -m "$(cat <<'EOF' -feat: add descriptives table - -Co-Authored-By: Claude Sonnet 4.5 -EOF -)" -``` - -**Local commits are OK. Pushing to remote requires human approval.** - -## Pre-Commit Requirements - -Before every commit, ensure: -- ✅ All tests pass (`jaspTools::testAll()`) -- ✅ No unintended files staged (.env, credentials, etc.) -- ✅ Commit message is concise and descriptive -- ✅ Changes are focused and related - -## Branch Strategy - -- **Main branch:** `master` -- **NEVER work directly on `master` branch** -- **ALWAYS create a feature branch for any changes:** - ```bash - git checkout -b feature/descriptive-name - ``` -- Branch naming conventions: - - `feature/description` - New features or analyses - - `fix/description` - Bug fixes - - `refactor/description` - Code restructuring - - `test/description` - Test updates - -## Pull Request Guidelines - -**CRITICAL: NEVER push to remote, create PRs, or merge without explicit human approval.** - -Human must review all local changes before they go online. - -When human approves creating a PR: -1. Ensure all tests pass locally first -2. Keep PR scope focused and small -3. Use concise PR title (same style as commits) -4. Summarize changes in bullet points -5. Note any breaking changes -6. Wait for human to review the PR description before posting - -## What NOT to Commit - -- ❌ `.Rhistory`, `.RData`, `.Rproj.user/` -- ❌ Test artifacts or temporary files -- ❌ Personal IDE settings -- ❌ Large data files -- ❌ Credentials or API keys -- ❌ `CLAUDE.local.md` (personal preferences) - -## CI/CD Integration - -- GitHub Actions runs tests on every push -- Workflow file: `.github/workflows/unittests.yml` -- Tests must pass for PR to be merged -- Translation workflows run on schedule - -## Git Safety - -- **NEVER** work directly on `master` branch - always use feature branches -- **NEVER** push to remote without explicit human approval -- **NEVER** create pull requests without explicit human approval -- **NEVER** merge changes without explicit human approval -- **NEVER** force push to any branch -- **NEVER** amend published commits -- **NEVER** skip hooks unless explicitly needed -- **NEVER** commit without running tests first - -**Human must approve all changes before they go online.** - -## Common Git Commands - -```bash -# Check current branch -git branch - -# Create and switch to feature branch -git checkout -b feature/description - -# Check status -git status - -# View changes -git diff -git diff --staged - -# Stage specific files -git add - -# Commit locally (OK to do without approval) -git commit -m "message" - -# View recent commits -git log --oneline -5 - -# View commit history with graph -git log --graph --oneline --all -10 - -# === REQUIRE HUMAN APPROVAL BEFORE RUNNING: === - -# Push to remote (WAIT FOR APPROVAL) -git push origin feature/description - -# Pull latest changes (usually safe, but confirm first) -git pull origin master -``` - -## Handling Test Failures - -If CI tests fail after human has pushed: -1. Check GitHub Actions output -2. Reproduce failure locally -3. Fix the issue -4. Run tests to confirm fix -5. Commit locally -6. Ask human for approval to push fix - -## Translation Commits - -Translation updates are handled automatically: -- Weblate integration updates translation files -- Automated commits from translation workflow -- Don't manually edit translation files unless necessary diff --git a/.claude/rules/jasp-containers-and-errors.md b/.claude/rules/jasp-containers-and-errors.md deleted file mode 100644 index d7d345d10..000000000 --- a/.claude/rules/jasp-containers-and-errors.md +++ /dev/null @@ -1,146 +0,0 @@ ---- -paths: - - "**/R/*.R" ---- - -# JASP Containers, HTML Output & Error Handling - -Patterns for grouping output elements and handling errors in jaspResults. - -For tables see [jasp-tables.md](jasp-tables.md). -For plots see [jasp-plots.md](jasp-plots.md). -For state/caching see [jasp-state-management.md](jasp-state-management.md). - ---- - -## 1) Containers - -Containers group related output elements under a collapsible section. - -### Get-or-create pattern (reusable across multiple builder functions) - -```r -.myExtractContainer <- function(jaspResults) { - if (!is.null(jaspResults[["myContainer"]])) - return(jaspResults[["myContainer"]]) - - container <- createJaspContainer(gettext("My Section Title")) - container$dependOn(.myBaseDependencies) - container$position <- 1 - jaspResults[["myContainer"]] <- container - - return(container) -} -``` - -- Use a dedicated extractor when **multiple builder functions** write to the same container -- `$position` controls display order (lower = higher on page) -- `$dependOn()` on the container invalidates **all children** when base options change - -### Direct creation (when only one function writes to it) - -```r -if (is.null(jaspResults[["sectionContainer"]])) { - container <- createJaspContainer(gettext("Section Title")) - container$dependOn(c(.baseDependencies, "specificOption")) - container$position <- 4 - jaspResults[["sectionContainer"]] <- container -} -``` - -### Nested containers - -For deeply hierarchical output (e.g., per-variable tables): - -```r -outerContainer <- jaspResults[["outer"]] -innerContainer <- createJaspContainer(title = "Variable X") -innerContainer$position <- i -outerContainer[["variableX"]] <- innerContainer -# then add tables/plots to innerContainer -``` - -### Dynamic container management - -When the set of children depends on user-selected variables: - -```r -# Track existing vs selected variables via metadata state -existingVariables <- metaData[["existingVariables"]] -selectedVariables <- getSelectedVariables(options) - -# Remove deselected -for (v in setdiff(existingVariables, selectedVariables)) - container[[v]] <- NULL - -# Add new -for (v in setdiff(selectedVariables, existingVariables)) { - childContainer <- createJaspContainer(title = v) - container[[v]] <- childContainer - .buildChildTable(childContainer, fit, options, v) -} - -# Update metadata -metaDataState$object <- list(existingVariables = selectedVariables) -``` - -See [jasp-state-management.md](jasp-state-management.md) for the metadata state pattern that powers this. - ---- - -## 2) HTML Output - -For raw HTML content (e.g., displaying R code or formatted messages): - -```r -htmlOutput <- createJaspHtml(title = gettext("R Code")) -htmlOutput$dependOn(c(.baseDependencies, "showCode")) -htmlOutput$position <- 99 -htmlOutput$text <- "
myFunction(yi = ..., sei = ...)
" -jaspResults[["rCode"]] <- htmlOutput -``` - ---- - -## 3) Error Handling Patterns - -### Create-then-error - -Always **attach the element to jaspResults before checking errors**. This ensures the empty table (with error message) is displayed rather than nothing: - -```r -table <- createJaspTable(gettext("Title")) -container[["table"]] <- table # attach FIRST - -# THEN check for errors -if (someError) { - table$setError(errorMessage) - return() -} -``` - -### Graceful degradation with groups - -When some per-group fits fail but others succeed, show partial results with per-group error footnotes: - -```r -# Row builders return skeleton data.frames on error (labels only, NAs for numeric columns) -# Tables show partial results with error footnotes per failed group -for (i in which(sapply(fit, jaspBase::isTryError))) { - table$addFootnote( - gettextf("Group '%1$s' failed: %2$s", attr(fit[[i]], "group"), .cleanError(fit[[i]])), - symbol = gettext("Error:") - ) -} -``` - -### Total failure - -When the entire fit fails: - -```r -if (length(fit) == 1 && jaspBase::isTryError(fit[[1]])) { - table$setError(.cleanErrorMessage(fit[[1]])) - return() -} -``` diff --git a/.claude/rules/jasp-dependency-management.md b/.claude/rules/jasp-dependency-management.md deleted file mode 100644 index 8f0291a40..000000000 --- a/.claude/rules/jasp-dependency-management.md +++ /dev/null @@ -1,136 +0,0 @@ ---- -paths: - - "**/R/*.R" ---- - -# JASP Dependency Management ($dependOn) - -How `$dependOn()` controls caching and invalidation of output elements in jaspResults. - -For the reactive loop context see [jasp-module-architecture.md](jasp-module-architecture.md). - -Note that you cannot test this by running analysis via `runAnalysis()` because you only generate one state at a time -(with no initial elements - ask the human maintainer to validate the dependencies manually if you suspect an issue!). - ---- - -## 1) What $dependOn Does - -When you write: -```r -table$dependOn(c("method", "ciLevel")) -``` - -You tell JASP Desktop: "If `options[["method"]]` or `options[["ciLevel"]]` changes, set this element to NULL before calling R." On the next R invocation, the builder's `if (!is.null(...))` guard sees NULL and recreates the element. - -Elements whose dependencies are NOT hit survive across invocations -- the builder returns early and the existing output stays on screen. - ---- - -## 2) Dependency Inheritance - -Container dependencies propagate to ALL children: - -```r -container$dependOn(c("dependentVariable", "method")) # base deps -table$dependOn(c("showCI")) # additional dep -container[["myTable"]] <- table -``` - -The table is invalidated if `dependentVariable`, `method`, OR `showCI` changes. Never repeat parent deps on children. - -This means you can put shared model-level dependencies on the container and only add output-specific deps to individual tables/plots. - ---- - -## 3) Dependency Vectors as Constants - -Define at file top for reuse across builders: -```r -.baseDeps <- c("dependentVariable", "covariates", "method", "ciLevel") -.plotDeps <- c("plotColor", "plotSize", "plotTheme") -``` - -Use in builders: -```r -container$dependOn(.baseDeps) # container holds base deps -table$dependOn(c("showResiduals")) # child adds specific dep -plot$dependOn(c(.baseDeps, .plotDeps)) # or combine for standalone elements -``` - -Keep dependency vectors comprehensive -- missing a dependency means stale output when that option changes. - ---- - -## 4) Conditional / Dynamic Dependencies - -When different analysis modes need different dependency sets: -```r -if (options[["variant"]] == "classical") { - fitState$dependOn(.classicalDeps) -} else { - fitState$dependOn(.bayesianDeps) -} -``` - -Or combine dynamically: -```r -plot$dependOn(c(.plotDeps, - if (options[["variant"]] == "classical") .classicalDeps else .bayesianDeps -)) -``` - ---- - -## 5) Per-Value Dependencies (optionContainsValue) - -For containers with one child per user-selected variable, invalidate only when that specific variable is removed: - -```r -for (v in options[["variables"]]) { - if (!is.null(container[[v]])) next - plot <- createJaspPlot(title = v) - plot$dependOn(optionContainsValue = list(variables = v)) - container[[v]] <- plot - # ... fill plot ... -} -``` - -If the user removes variable `"x"` from the list, only `container[["x"]]` is NULLed. Other children survive. - ---- - -## 6) Sentinel Pattern (Narrow Dependencies) - -When an expensive computation (e.g., model fit) should NOT be invalidated by visualization-only options, but the visualization data still needs updating: - -```r -# Broad deps: model options → invalidate and re-fit -fitState <- createJaspState() -fitState$dependOn(.modelDeps) -jaspResults[["fit"]] <- fitState - -# Narrow deps: plotting options → update auxiliary data without re-fitting -sentinel <- createJaspState() -sentinel$dependOn(.plottingDeps) -jaspResults[["fitDataUpdate"]] <- sentinel -``` - -When a plotting option changes: -- `jaspResults[["fit"]]` survives (model deps not hit) -- `jaspResults[["fitDataUpdate"]]` is NULLed (plotting deps hit) -- The update function sees the NULL sentinel, re-attaches updated auxiliary data to the existing fit - -This avoids expensive re-computation when only display options change. - ---- - -## 7) Common Pitfalls - -**Missing dependency:** If you forget to list an option in `$dependOn()`, changing that option won't invalidate the element. The user sees stale output. - -**Over-broad dependencies:** Putting ALL options on every element means everything gets recomputed on any change. Split into base deps (container) + specific deps (children). - -**Duplicate dependencies:** Listing a parent container's dep on a child is harmless but redundant. Keep it clean. - -**Forgetting $dependOn entirely:** The element will never be invalidated -- it's created once and persists forever, even when relevant options change. diff --git a/.claude/rules/jasp-module-architecture.md b/.claude/rules/jasp-module-architecture.md deleted file mode 100644 index 3d8af0882..000000000 --- a/.claude/rules/jasp-module-architecture.md +++ /dev/null @@ -1,267 +0,0 @@ -# JASP Module Architecture - -How QML, JASP Desktop, and R interact. This explains *why* the patterns in the other rule files exist. - -For dependency details see [jasp-dependency-management.md](jasp-dependency-management.md). -For state/caching see [jasp-state-management.md](jasp-state-management.md). -For R coding patterns see [jasp-tables.md](jasp-tables.md), [jasp-plots.md](jasp-plots.md), [jasp-containers-and-errors.md](jasp-containers-and-errors.md). -For serialized output format see [jasp-output-structure.md](jasp-output-structure.md). - ---- - -## 1) The Reactive Loop - -``` -User changes option in QML GUI - │ - ▼ -JASP Desktop collects ALL current option values into a flat named list - │ - ▼ -Desktop calls: AnalysisName(jaspResults, dataset, options) - │ │ │ │ - │ │ │ └─ named list of ALL QML option values - │ │ └─ data.frame loaded from the active dataset - │ └─ PERSISTENT container surviving across invocations - │ - ▼ -R function builds/updates output in jaspResults - │ - ▼ -Desktop reads jaspResults and renders tables/plots/text in the GUI -``` - -**Key insight:** Every time the user changes *anything* in the QML interface, Desktop calls the R analysis function again with a fresh `options` list but the **same** `jaspResults` object. This is why: - -1. Every builder checks `if (!is.null(jaspResults[["key"]])) return()` -- skip if output already exists and dependencies haven't changed. -2. `$dependOn()` tells Desktop which option changes should invalidate (NULL out) an element. See [jasp-dependency-management.md](jasp-dependency-management.md). -3. `createJaspState()` caches expensive computations so they survive across invocations. See [jasp-state-management.md](jasp-state-management.md). - ---- - -## 2) jaspResults: The Persistent Bridge - -`jaspResults` is an R5 reference class that persists between R invocations for the same analysis instance. It is NOT recreated each time. - -### Element lifecycle - -``` -1. Element does not exist → builder creates it, attaches to jaspResults -2. Options change, deps NOT hit → element survives, builder returns early -3. Options change, deps ARE hit → Desktop NULLs the element before calling R - → builder sees NULL, recreates it -4. User removes the analysis → jaspResults is destroyed entirely -``` - -### What can live in jaspResults - -| Create function | Purpose | Displayed? | -|----------------|---------|------------| -| `createJaspTable()` | Tabular output | Yes | -| `createJaspPlot()` | Plot output | Yes | -| `createJaspHtml()` | Raw HTML/text | Yes | -| `createJaspContainer()` | Groups children | Yes (collapsible section) | -| `createJaspState()` | Cache arbitrary R objects | **No** (invisible to user) | - -All five support `$dependOn()`. All five can be stored in jaspResults or nested inside a container. - -### Display ordering - -Every element has `$position` (integer). Lower = higher on page. Children within a container also have positions. - ---- - -## 3) Options: The Flat Named List - -### QML name → R options key - -Every QML control has a `name:` property. Desktop flattens ALL controls into a single named list regardless of QML nesting: - -```qml -CheckBox { - name: "showCI" // options[["showCI"]] = TRUE/FALSE - DoubleField { - name: "ciLevel" // options[["ciLevel"]] = 0.95 - defaultValue: 0.95 - } -} -``` - -Both `showCI` and `ciLevel` appear at the top level of `options`. QML nesting controls UI visibility/enabling but does NOT create nested R structures. - -### QML control → R value type - -| QML control | R type | Example value | -|-------------|--------|---------------| -| `CheckBox` | logical | `TRUE` / `FALSE` | -| `DropDown` | character | `"restrictedML"` | -| `RadioButtonGroup` | character | `"estimated"` (selected button's `value:`) | -| `AssignedVariablesList` | character | `"myColumn"` (single) or `c("a","b")` (multi) | -| `DoubleField` | numeric | `0.95` | -| `IntegerField` | integer | `1000L` | -| `TextField` | character | `"user text"` | -| `CIField` | numeric | `0.95` (0-1 scale) | -| `PercentField` | numeric | `95` (0-100 scale) | - -### Empty/unset variable slots - -When no variable is assigned to an `AssignedVariablesList`, the value is `""` (empty string): - -```r -if (options[["dependentVariable"]] != "") { ... } -``` - -For multi-variable lists, check `length(options[["variables"]]) > 0`. - -### Column encoding - -JASP internally encodes column names. In R analysis code, the encoding is transparent -- `dataset` columns are already encoded. Use `jaspBase::decodeColNames()` when displaying names in plot axes/labels. In tests, use `jaspTools:::encodeOptionsAndDataset()` when loading from .jasp files. - ---- - -## 4) Data Flow (Generic) - -``` -QML assigns variable names → options[["dependentVariable"]] = "score" - │ - ▼ -Desktop loads dataset with requested columns → dataset (data.frame) - │ - ▼ -Entry point: readiness check + data validation - - Are required variables assigned? - - .hasErrors(): infinity, observations, variance, etc. - │ - ▼ -Compute function: expensive model fitting, cached in state - - Wrap in try() for error handling - - Store result via createJaspState() - │ - ▼ -Builder functions: extract cached results, build output - - Tables: define columns, build rows, setData() - - Plots: build ggplot, assign to plotObject - - Errors: attach element FIRST, then setError() -``` - -Builders should handle the "not ready" case gracefully -- create empty tables (column headers but no data) so the user sees the output structure before assigning variables. - ---- - -## 5) The Entry Point → Common → Builder Pattern - -### Three-layer architecture - -``` -Layer 1: Entry point (thin wrapper per analysis) - MyAnalysis(jaspResults, dataset, options) - - Sets dispatch flags if sharing code with other analyses - - Validates data - - Delegates to orchestrator - -Layer 2: Orchestrator (flat sequence of builder calls) - MyAnalysisCommon(jaspResults, dataset, options) - - Calls .computeModel() # state - - Calls .summaryTable() # table - - Calls .coefficientsTable() # table - - Calls .mainPlot() # plot - - Conditional sections based on options - -Layer 3: Builders (idempotent, self-contained) - .summaryTable(jaspResults, options) - - Checks if output exists (return early if so) - - Gets/creates container - - Creates table, defines columns - - Extracts cached results - - Builds rows, sets data -``` - -### Multiple entry points sharing one orchestrator - -When related analyses share logic, they set a dispatch flag and delegate: - -```r -AnalysisVariantA <- function(jaspResults, dataset, options) { - options[["variant"]] <- "A" - if (.isReady(options)) { - dataset <- .checkData(dataset, options) - .checkErrors(dataset, options) - } - AnalysisCommon(jaspResults, dataset, options) -} - -AnalysisVariantB <- function(jaspResults, dataset, options) { - options[["variant"]] <- "B" - # ... same pattern ... - AnalysisCommon(jaspResults, dataset, options) -} -``` - -Builders branch on the flag: -```r -if (options[["variant"]] == "B") - .additionalTable(jaspResults, options) -``` - -### The readiness check - -Before model fitting, verify required inputs exist: - -```r -.isReady <- function(options) { - options[["dependentVariable"]] != "" && length(options[["covariates"]]) > 0 -} -``` - -In the entry point: -```r -if (.isReady(options)) { - dataset <- .checkData(dataset, options) - .checkErrors(dataset, options) -} -AnalysisCommon(jaspResults, dataset, options) -``` - ---- - -## 6) Registration & Backward Compatibility - -### Description.qml - -Registers analyses with their R function names: -```qml -Analysis { - title: qsTr("My Analysis") - func: "MyAnalysis" // must match R function name exactly (case-sensitive) -} -``` - -### NAMESPACE - -Every analysis entry point must be exported: -```r -export(MyAnalysis) -``` - -### Upgrades.qml - -When renaming QML option names, add a migration so old .jasp files load correctly: -```qml -Upgrade { - functionName: "MyAnalysis" - fromVersion: "0.17.2" - toVersion: "0.17.3" - - ChangeRename { from: "oldOptionName"; to: "newOptionName" } - - ChangeJS { - name: "transformedOption" - jsFunction: function(options) { - switch(options["transformedOption"]) { - case "oldValue": return "newValue"; - default: return options["transformedOption"]; - } - } - } -} -``` diff --git a/.claude/rules/jasp-output-structure.md b/.claude/rules/jasp-output-structure.md deleted file mode 100644 index bc3af47af..000000000 --- a/.claude/rules/jasp-output-structure.md +++ /dev/null @@ -1,196 +0,0 @@ ---- -paths: - - "**/tests/testthat/*.R" - - "**/R/*.R" ---- - -# JASP Analysis Output Structure - -Reading and testing the serialized output from `jaspTools::runAnalysis()`. -For building tables see [jasp-tables.md](jasp-tables.md). For plots see [jasp-plots.md](jasp-plots.md). - -## 1) Top-Level `results` Object - -After `jaspTools::runAnalysis()`, the returned list has 5 keys: -- `status` -- `"complete"` or `"fatalError"` -- `results` -- nested list of all output elements (containers, tables, plots) -- `state` -- cached figures and computed objects -- `progress` -- progress info (usually empty after completion) -- `typeRequest` -- internal type info - -## 2) `results$results` Structure - -Contains: -- `.meta` -- recursive metadata describing the tree (type, name, title for each element) -- `name` -- analysis name -- Named elements for each output component (containers, tables, plots) - -### Element Types - -| Type | Key fields | How to identify | -|------|-----------|-----------------| -| **Container** | `collection`, `name`, `title`, `initCollapsed` | Has `$collection` (named list of children) | -| **Table** | `data`, `schema`, `name`, `title`, `status`, `footnotes`, `casesAcrossColumns` | Has `$schema` with `$fields` | -| **Plot/Image** | `data` (string path), `name`, `title`, `width`, `height`, `status`, `convertible` | Has `$data` as character string (e.g., `"plots/1.png"`) | - -## 3) Containers - -Containers group related output elements. Structure: -``` -container$collection -- named list of child elements (containers, tables, or plots) -container$name -- unique identifier (underscore-separated path) -container$title -- display title (can be "") -container$initCollapsed -- whether collapsed by default -``` - -**Naming convention:** Child names are parent name + `_` + child suffix. This creates a hierarchical path: -``` -modelSummaryContainer - modelSummaryContainer_testsTable - modelSummaryContainer_pooledEstimatesTable -``` - -Containers can nest arbitrarily deep: -``` -estimatedMarginalMeansAndContrastsContainer - estimatedMarginalMeansAndContrastsContainer_effectSize - estimatedMarginalMeansAndContrastsContainer_effectSize_adjustedEstimate - ..._adjustedEstimate_estimatedMarginalMeansTable -``` - -**Accessing deeply nested elements:** Chain `$collection` at each container level: -```r -results[["results"]][["containerName"]][["collection"]][["containerName_child"]][["collection"]][["containerName_child_table"]][["data"]] -``` - -## 4) Tables - -### Schema (`table$schema$fields`) -List of column definitions, each with: -- `name` -- field identifier (used as key in data rows) -- `title` -- display column header -- `type` -- `"string"`, `"number"`, `"integer"`, `"pvalue"` -- `format` (optional) -- formatting spec, e.g., `"sf:4;dp:3"`, `"dp:3;p:.001"` -- `overTitle` (optional) -- grouped column header (e.g., `"95% CI"` spanning Lower/Upper) - -### Data (`table$data`) -List of rows. Each row is a named list with field names as keys: -```r -table$data[[1]] # first row -# $est, $se, $lCi, $uCi, $pval, ... -``` - -**Key:** Fields within each row are **alphabetically sorted by name** (from JSON deserialization). - -### Footnotes (`table$footnotes`) -List of footnote objects: -```r -footnote$text -- footnote text -footnote$symbol -- HTML symbol (e.g., "Note.") -footnote$cols -- columns it applies to (NULL = all) -footnote$rows -- rows it applies to (NULL = all) -``` - -### Special Row Fields -- `.isNewGroup` -- boolean, marks visual row separator in JASP GUI -- These appear in `expect_equal_tables` flattened output - -## 5) Plots - -### In `results$results` -Plot entries store metadata only: -```r -plot$data -- string key into state$figures (e.g., "plots/1.png") -plot$name -- identifier -plot$title -- display title -plot$width -- pixel width -plot$height -- pixel height -plot$status -- "complete" -``` - -### In `results$state$figures` -Actual plot objects stored here, keyed by the `data` path: -```r -results$state$figures[["plots/1.png"]]$obj -- the plot object -results$state$figures[["plots/1.png"]]$width -results$state$figures[["plots/1.png"]]$height -``` - -### Plot Object Types -- **`jaspGraphsPlot`** (R6 class) -- composite plot with `$subplots` list of ggplot objects -- **Plain `ggplot`** -- single ggplot object (no subplots) - -### Retrieving Plot for Testing -```r -plotName <- results[["results"]][["plotElement"]][["data"]] -testPlot <- results[["state"]][["figures"]][[plotName]][["obj"]] -jaspTools::expect_equal_plots(testPlot, "snapshot-name") -``` - -## 6) State Object (`results$state`) - -- `state$figures` -- named list of plot objects (keyed by "plots/N.png") -- `state$other` -- named list of cached R objects (keyed by "state_N") - - Used by `createJaspState()` for caching expensive computations between output elements - -## 7) Testing Utilities - -### `expect_equal_tables(table_data, reference_list)` -1. Takes `table$data` (list of row-lists) -2. Flattens via `unname(unlist(rows))` -- row-by-row, fields in alphabetical order within each row -3. Converts numeric strings back to numbers via `charVec2MixedList` -4. Replaces unicode characters with `` placeholder -5. Compares element-by-element against flat reference list - -**Reference list format:** Single flat `list(...)` with all values row-by-row, fields alphabetically sorted: -```r -# For a table with fields: df, est, name, pval (alphabetical) -# Row 1: df=9, est=-0.69, name="Intercept", pval=0.50 -# Row 2: df=9, est=0.29, name="Slope", pval=0.01 -jaspTools::expect_equal_tables(table_data, - list(9, -0.69, "Intercept", 0.50, # row 1 - 9, 0.29, "Slope", 0.01)) # row 2 -``` - -### `expect_equal_plots(plot_obj, snapshot_name)` -- If `jaspGraphsPlot`: splits into subplots, each compared via `vdiffr::expect_doppelganger` with name `"snapshot-name-subplot-N"` -- If plain `ggplot`: compared directly via `vdiffr::expect_doppelganger` -- SVG snapshots stored in `tests/testthat/_snaps/` - -## 8) Quick Reference: Navigating Results - -```r -# Run analysis -results <- jaspTools::runAnalysis("AnalysisName", dataset, options) - -# Check status -results$status # "complete" or "fatalError" -results$results$errorMessage # if fatalError - -# Get table data (for expect_equal_tables) -results[["results"]][["containerName"]][["collection"]][["containerName_tableName"]][["data"]] - -# Get plot object (for expect_equal_plots) -plotKey <- results[["results"]][["plotName"]][["data"]] -plotObj <- results[["state"]][["figures"]][[plotKey]][["obj"]] - -# Inspect table schema -table$schema$fields # list of {name, title, type, format, overTitle} - -# Map entire tree (debug helper) -mapResults <- function(x, depth = 0) { - indent <- paste(rep(" ", depth), collapse = "") - if (is.list(x) && !is.null(x$collection)) { - cat(sprintf("%s[container] %s: '%s'\n", indent, x$name, x$title)) - for (child in x$collection) mapResults(child, depth + 1) - } else if (is.list(x) && !is.null(x$schema)) { - cat(sprintf("%s[table] %s: '%s' (%d rows x %d cols)\n", - indent, x$name, x$title, length(x$data), length(x$schema$fields))) - } else if (is.list(x) && !is.null(x$data) && is.character(x$data)) { - cat(sprintf("%s[plot] %s: '%s'\n", indent, x$name, x$title)) - } -} -for (item in results$results[setdiff(names(results$results), c(".meta", "name"))]) { - mapResults(item) -} -``` diff --git a/.claude/rules/jasp-plots.md b/.claude/rules/jasp-plots.md deleted file mode 100644 index e1a1735df..000000000 --- a/.claude/rules/jasp-plots.md +++ /dev/null @@ -1,136 +0,0 @@ ---- -paths: - - "**/R/*.R" ---- - -# JASP Plot Building Patterns - -How to create and configure plots in jaspResults. - -For dependency mechanics see [jasp-dependency-management.md](jasp-dependency-management.md). -For testing plots see [testing-instructions.md](testing-instructions.md) (`expect_equal_plots`). - ---- - -## 1) Simple Plot - -```r -.myPlot <- function(jaspResults, options) { - - if (!is.null(jaspResults[["myPlot"]])) - return() - - fit <- .extractFit(jaspResults, options) - if (is.null(fit) || jaspBase::isTryError(fit[[1]])) - return() - - myPlot <- createJaspPlot( - title = gettext("My Plot"), - width = 400, - height = 320 - ) - myPlot$position <- 5 - myPlot$dependOn(c(.baseDependencies, "plotSpecificOption")) - jaspResults[["myPlot"]] <- myPlot - - # Build ggplot - plotObj <- ggplot2::ggplot(...) + ... - - # Add JASP theme and (plot frame b = bottom, r = right, t = top, l = left) - plotObj <- plotObj + - jaspGraphs::geom_rangeframe(sides = "bl") + - jaspGraphs::themeJaspRaw() - - myPlot$plotObject <- plotObj -} -``` - ---- - -## 2) Plot with Error Handling - -Wrap plot construction in `try()` and display the error on the plot element: - -```r -plotOut <- try(.makePlot(fit, options)) - -if (inherits(plotOut, "try-error")) { - myPlot <- createJaspPlot(title = gettext("My Plot")) - myPlot$dependOn(dependencies) - myPlot$setError(plotOut) - jaspResults[["myPlot"]] <- myPlot - return() -} - -myPlot <- createJaspPlot(title = gettext("My Plot"), width = w, height = h) -myPlot$plotObject <- plotOut -jaspResults[["myPlot"]] <- myPlot -``` - ---- - -## 3) Composite Plot (jaspGraphsPlot) - -For plots with multiple panels (e.g., a left annotation panel + right data panel): - -```r -plotObj <- jaspGraphs:::jaspGraphsPlot$new( - subplots = list(leftPanel, rightPanel), - layout = matrix(1:2, ncol = 2), - heights = 1, - widths = c(0.4, 0.6) -) -myPlot$plotObject <- plotObj -``` - -In tests, each subplot gets its own SVG snapshot: `"name-subplot-1"`, `"name-subplot-2"`. - ---- - -## 4) Per-Group Plot Pattern - -When a single fit produces a single plot, but multiple groups produce a container of plots: - -```r -if (options[["groupingVariable"]] == "") { - # Single plot, attach directly - plot <- .makePlotFun(fit[[1]], options) - plot$title <- gettext("My Plot") - plot$dependOn(dependencies) - jaspResults[["myPlot"]] <- plot - -} else { - # Container with one plot per group - container <- createJaspContainer() - container$title <- gettext("My Plot") - container$dependOn(dependencies) - jaspResults[["myPlot"]] <- container - - for (i in seq_along(fit)) { - container[[names(fit)[i]]] <- .makePlotFun(fit[[i]], options) - container[[names(fit)[i]]]$title <- gettextf("Group: %1$s", attr(fit[[i]], "group")) - container[[names(fit)[i]]]$position <- i - } -} -``` - ---- - -## 5) Separate-Plots-by-Variable Pattern - -When a variable creates multiple faceted plots: - -```r -if (length(options[["separatePlots"]]) > 0) { - container <- createJaspContainer() - for (i in seq_along(levels)) { - tempPlot <- createJaspPlot(title = levels[i], width = w, height = h) - tempPlot$position <- i - tempPlot$plotObject <- makePlot(data[data$facet == levels[i], ]) - container[[paste0("plot", i)]] <- tempPlot - } -} else { - plot <- createJaspPlot(width = w, height = h) - plot$plotObject <- makePlot(data) -} -``` diff --git a/.claude/rules/jasp-state-management.md b/.claude/rules/jasp-state-management.md deleted file mode 100644 index 0643e9766..000000000 --- a/.claude/rules/jasp-state-management.md +++ /dev/null @@ -1,256 +0,0 @@ ---- -paths: "**/R/*.R" ---- - -# JASP State Management (createJaspState) - -How to cache expensive computations and track dynamic output state. - -For the reactive loop context see [jasp-module-architecture.md](jasp-module-architecture.md). -For dependency mechanics see [jasp-dependency-management.md](jasp-dependency-management.md). - -Note that you cannot test this by running analysis via `runAnalysis()` because you only generate one state at a time -(with no initial elements - ask the human maintainer to validate the dependencies manually if you suspect an issue!). - ---- - -## 1) Why State Objects Exist - -Model fitting is expensive. Without caching, every option change (even toggling a checkbox for an unrelated table) would re-run the computation. State objects solve this by caching results that persist across R invocations as long as their dependencies hold. - -```r -.computeModel <- function(jaspResults, dataset, options) { - if (!is.null(jaspResults[["modelFit"]])) - return() # cached → skip - - fitState <- createJaspState() - fitState$dependOn(.modelDeps) # only model options - jaspResults[["modelFit"]] <- fitState - - result <- try(expensiveFit(dataset, options)) - fitState$object <- result # cache -} -``` - -Now when the user toggles "Show CI" (a table option, not a model option), `jaspResults[["modelFit"]]` survives. Only when a model option changes does the fit get invalidated and recomputed. - ---- - -## 2) The $object Property - -`createJaspState()` stores arbitrary R objects via `$object`: - -```r -# Store anything: model fits, lists, data.frames -jaspResults[["modelFit"]]$object <- list(model = fitResult, residuals = resid) - -# Retrieve in another builder function -cached <- jaspResults[["modelFit"]]$object -if (is.null(cached)) return() # not yet computed -model <- cached$model -``` - ---- - -## 3) State vs Output Elements - -| | State | Table/Plot/Html | -|---|---|---| -| Visible to user | No | Yes | -| Has `$object` | Yes | No (use `$setData()`, `$plotObject`) | -| Purpose | Cache computations | Display results | -| `$dependOn()` | Yes | Yes | -| Can nest in container | Yes | Yes | - ---- - -## 4) Pattern: Model Fit Caching - -The most common pattern -- fit a model once, reuse across multiple tables and plots: - -```r -.computeModel <- function(jaspResults, dataset, options) { - if (!is.null(jaspResults[["modelFit"]])) - return() - - fitState <- createJaspState() - fitState$dependOn(.modelDeps) - jaspResults[["modelFit"]] <- fitState - - fit <- try(myPackage::fitModel( - formula = .buildFormula(options), - data = dataset - )) - - fitState$object <- fit -} - -# Used by multiple builders: -.extractFit <- function(jaspResults) { - cached <- jaspResults[["modelFit"]]$object - if (is.null(cached)) return(NULL) - return(cached) -} -``` - ---- - -## 5) Pattern: Multiple Fits (Per Group / Per Variable) - -When the analysis computes separate fits for groups or variables, store them as a named list: - -```r -.computeModel <- function(jaspResults, dataset, options) { - if (!is.null(jaspResults[["modelFit"]])) - return() - - fitState <- createJaspState() - fitState$dependOn(.modelDeps) - jaspResults[["modelFit"]] <- fitState - - results <- list() - - # Overall fit - results[["overall"]] <- try(fitFun(dataset, options)) - - # Per-group fits (if grouping variable selected) - if (options[["groupingVariable"]] != "") { - groups <- unique(dataset[[options[["groupingVariable"]]]]) - for (g in groups) { - subData <- dataset[dataset[[options[["groupingVariable"]]]] == g, ] - fit <- try(fitFun(subData, options)) - attr(fit, "group") <- as.character(g) # preserve metadata even on error - results[[paste0("group_", g)]] <- fit - } - } - - fitState$object <- results -} -``` - -**Key conventions:** -- Use `attr(fit, "group")` to tag each fit with its group label (survives `try()` errors) -- Extractors can filter: include/exclude overall, handle errors per group -- Row builders iterate over fits via `lapply()`, returning skeleton data.frames on error - -### Extractor with filtering - -```r -.extractFit <- function(jaspResults, options) { - results <- jaspResults[["modelFit"]]$object - if (is.null(results)) return(NULL) - - # Optionally exclude overall fit - if (options[["groupingVariable"]] != "" && !options[["includeOverall"]]) - results <- results[names(results) != "overall"] - - return(results) -} -``` - ---- - -## 6) Pattern: Shared Computation Cache - -When multiple output elements (table + plot) need the same intermediate result: - -```r -.computeDiagnostics <- function(jaspResults, options) { - if (!is.null(jaspResults[["diagnosticsCache"]])) - return(jaspResults[["diagnosticsCache"]]$object) - - state <- createJaspState() - state$dependOn(.diagnosticsDeps) - jaspResults[["diagnosticsCache"]] <- state - - results <- expensiveComputation(...) - state$object <- results - return(results) -} -``` - -Both `.diagnosticsTable()` and `.diagnosticsPlot()` call `.computeDiagnostics()` -- the second call returns the cached result immediately. - ---- - -## 7) Pattern: Metadata State for Dynamic Containers - -When the set of output children depends on user-selected variables, track what's currently rendered: - -```r -.buildVariableOutputs <- function(jaspResults, options) { - - container <- .extractContainer(jaspResults) - - # Get or create metadata state - if (!is.null(container[["metaData"]])) { - meta <- container[["metaData"]]$object - } else { - metaState <- createJaspState() - metaState$dependOn(c("selectedVariables")) - container[["metaData"]] <- metaState - meta <- list(existing = character(0)) - } - - selected <- options[["selectedVariables"]] - existing <- meta$existing - - # Remove deselected - for (v in setdiff(existing, selected)) - container[[v]] <- NULL - - # Add new - for (v in setdiff(selected, existing)) { - child <- createJaspContainer(title = v) - child$position <- which(selected == v) - container[[v]] <- child - .buildTableForVariable(child, jaspResults, options, v) - } - - # Update tracking - container[["metaData"]]$object <- list(existing = selected) -} -``` - -This avoids rebuilding the entire container when the user adds or removes a single variable. - ---- - -## 8) Pattern: Dataset Update Sentinel - -When an expensive fit should NOT be re-run for visualization-only option changes, but auxiliary data attached to the fit needs updating: - -```r -.updateFitData <- function(jaspResults, dataset, options) { - if (is.null(jaspResults[["modelFit"]])) - return() - if (!is.null(jaspResults[["fitDataUpdate"]])) - return() - - # Create sentinel with narrow deps - sentinel <- createJaspState() - sentinel$dependOn(.plottingVariableDeps) - jaspResults[["fitDataUpdate"]] <- sentinel - - # Update auxiliary data on the existing (cached) fit - fit <- jaspResults[["modelFit"]]$object - fit$plotData <- .prepPlotData(fit, dataset, options) - jaspResults[["modelFit"]]$object <- fit - - sentinel$object <- TRUE # mark as done -} -``` - -When a plotting variable changes: sentinel is NULLed, data is re-attached. The model fit itself survives. - ---- - -## 9) Common Pitfalls - -**Forgetting to store:** Creating a state but never assigning `$object` -- extractors see NULL. - -**Circular extraction:** An extractor that calls the compute function which calls the extractor. Use the `if (!is.null(...)) return()` guard pattern consistently. - -**Overwriting state from extractors:** Extractors should be read-only. Only the compute function should write to `$object`. - -**State without dependencies:** A state with no `$dependOn()` is never invalidated -- it persists forever with potentially stale data. diff --git a/.claude/rules/jasp-tables.md b/.claude/rules/jasp-tables.md deleted file mode 100644 index 4bc2b5e19..000000000 --- a/.claude/rules/jasp-tables.md +++ /dev/null @@ -1,202 +0,0 @@ ---- -paths: - - "**/R/*.R" ---- - -# JASP Table Building Patterns - -How to create, configure, and populate tables in jaspResults. - -For dependency mechanics see [jasp-dependency-management.md](jasp-dependency-management.md). -For state/caching see [jasp-state-management.md](jasp-state-management.md). -For containers and error handling see [jasp-containers-and-errors.md](jasp-containers-and-errors.md). - ---- - -## 1) Complete Table Lifecycle - -```r -.myTable <- function(jaspResults, options) { - - container <- .myExtractContainer(jaspResults) - - # 1. SKIP if already created (idempotency) - if (!is.null(container[["myTable"]])) - return() - - fit <- .extractFit(jaspResults, options) - - # 2. CREATE table and attach to parent BEFORE filling data - myTable <- createJaspTable(gettext("My Table Title")) - myTable$position <- 1 - myTable$dependOn(c("optionA", "optionB")) - container[["myTable"]] <- myTable - - # 3. DEFINE columns - myTable$addColumnInfo(name = "term", type = "string", title = "") - myTable$addColumnInfo(name = "est", type = "number", title = gettext("Estimate")) - myTable$addColumnInfo(name = "se", type = "number", title = gettext("Standard Error")) - myTable$addColumnInfo(name = "pval", type = "pvalue", title = gettext("p")) - - # 4. EARLY RETURN on error (table shows as empty with error) - if (is.null(fit)) - return() - if (length(fit) == 1 && jaspBase::isTryError(fit[[1]])) { - myTable$setError(.cleanErrorMessage(fit[[1]])) - return() - } - - # 5. BUILD row data (list of data.frames → rbind) - rows <- do.call(rbind, lapply(fit, .myRowBuilder, options = options)) - - # 6. ADD footnotes - myTable$addFootnote(gettext("Some methodological note.")) - - # 7. SET data - myTable$setData(rows) -} -``` - -**Key**: Always attach the table to jaspResults (step 2) **before** checking errors (step 4). This ensures the empty table with error message displays rather than nothing. See [jasp-containers-and-errors.md](jasp-containers-and-errors.md) for the create-then-error pattern. - ---- - -## 2) Column Types - -| Type | Use for | Format examples | -|------|---------|-----------------| -| `"string"` | Labels, names, formatted test stats | -- | -| `"number"` | Numeric values | `"sf:4;dp:3"` (4 sig figs, 3 decimal places) | -| `"integer"` | Counts, df | -- | -| `"pvalue"` | p-values | `"dp:3;p:.001"` (3 dp, threshold at .001) | - ---- - -## 3) Column Modifiers - -```r -# Grouped column header (e.g., "95% CI" spanning Lower/Upper) -table$addColumnInfo(name = "lCi", type = "number", title = gettext("Lower"), - overtitle = gettextf("%s%% CI", 100 * options[["ciLevel"]])) - -# Show only explicitly added columns (hide data columns not in schema) -table$showSpecifiedColumnsOnly <- TRUE -``` - ---- - -## 4) DRY Pattern: Reusable Column Helpers - -When multiple tables share the same column groups (e.g., CI columns, SE columns, test statistics), factor out repeated `addColumnInfo()` calls into shared helper functions. For example, a helper that conditionally adds a CI lower/upper pair with a dynamic overtitle avoids duplicating those 3-4 lines across every table builder. - -Apply the same pattern for any column group that appears in more than one table — each helper takes the table and relevant options, and adds the columns conditionally. - ---- - -## 5) Parameterized Tables - -When the same table structure serves multiple purposes, parametrize the builder: - -```r -.myTable <- function(jaspResults, options, parameter = "main") { - - container <- .extractContainer(jaspResults) - tableKey <- paste0(parameter, "Table") - - if (!is.null(container[[tableKey]])) - return() - - table <- createJaspTable(switch(parameter, - main = gettext("Main Results"), - summary = gettext("Summary Results") - )) - table$position <- switch(parameter, main = 1, summary = 2) - container[[tableKey]] <- table - # ... columns and data -} -``` - ---- - -## 6) Row Builder Pattern - -Each row builder takes a **single fit** and returns a **data.frame** (one or more rows): - -```r -.myRowBuilder <- function(fit, options) { - - # Handle failed fits gracefully (return skeleton with NAs) - if (jaspBase::isTryError(fit)) { - return(data.frame( - term = gettext("My term"), - group = attr(fit, "group") - )) - } - - row <- data.frame( - term = gettext("My term"), - group = attr(fit, "group"), - est = fit$beta[1], - se = fit$se[1], - pval = fit$pval[1] - ) - - return(row) -} -``` - -**Key conventions:** -- Include `group = attr(fit, "group")` for per-group support -- On error, return data.frame with labels but missing numeric columns (renders as empty cells) -- Use `gettext()` / `gettextf()` for all user-visible strings - ---- - -## 7) DRY Pattern: Safe Data Aggregation - -When combining data.frames from multiple fits — especially when some fits may fail and return fewer columns — create a helper that: - -1. Filters out NULL/empty data.frames -2. Computes the union of all column names -3. Pads each data.frame with NA for missing columns -4. Calls `do.call(rbind, ...)` on the aligned data.frames - -This avoids `rbind()` failures when partial errors produce data.frames with heterogeneous columns. Apply the same helper pattern for ordering rows by grouping variable and simplifying output (e.g., dropping a grouping column when no groups are selected). - ---- - -## 8) Footnotes - -```r -# Simple footnote (appears at bottom) -table$addFootnote(gettext("Fixed effects tested using Knapp and Hartung adjustment.")) - -# Warning-style footnote -table$addFootnote(warningMsg, symbol = gettext("Warning:")) - -# Per-group error footnotes -for (i in which(sapply(fit, jaspBase::isTryError))) { - table$addFootnote( - gettextf("The model for group '%1$s' failed: %2$s", - attr(fit[[i]], "group"), .cleanError(fit[[i]])), - symbol = gettext("Error:") - ) -} - -# Cell-specific footnote -table$addFootnote(message, colNames = "est", rowNames = "rowLabel") -``` - ---- - -## 9) Error Display on Tables - -```r -# Error message replaces entire table content -table$setError(gettext("Feature not available for this model type.")) - -# Error from a try-error object -table$setError(.cleanErrorMessage(tryResult)) -``` - -See [jasp-containers-and-errors.md](jasp-containers-and-errors.md) for the full create-then-error and graceful degradation patterns. diff --git a/.claude/rules/qml-instructions.md b/.claude/rules/qml-instructions.md deleted file mode 100644 index 6811a3120..000000000 --- a/.claude/rules/qml-instructions.md +++ /dev/null @@ -1,177 +0,0 @@ ---- -paths: - - "**/inst/qml/*.qml" ---- - -# JASP QML Instructions - -## 0) QML Syntax Validation - -**ALWAYS validate QML files after editing** using `qmllint` to catch syntax errors: - -```powershell -qmllint inst\qml\path\to\file.qml -``` - -- **Ignore import warnings**: Warnings about missing `JASP.Controls` and `JASP` modules are expected (qmllint lacks JASP's custom modules) -- **Focus on syntax errors**: Look for missing braces `{}`, brackets `[]`, parentheses `()`, semicolons, or malformed property assignments -- **Exit code matters**: Non-zero exit with syntax errors blocks parsing; zero exit means parseable (even with import warnings) -- **Run before committing**: Catch structural issues (extra/missing braces) that break QML parsing - -Example of ignorable warnings: -``` -Warning: Failed to import JASP.Controls [import] -Warning: IntegerField was not found [import] -``` - -Example of critical errors: -``` -Error: Expected token `}' [syntax] -``` - -## 1) Core Basics - -- **Imports:** - ```qml - import QtQuick - import QtQuick.Layouts - import JASP.Controls - import JASP - ``` - -- **Form as root:** Every analysis UI is a `Form { ... }` containing controls, usually a `VariablesForm` block and option controls. -- **Binding & IDs:** Prefer *property bindings* (reactive JS expressions) over imperative changes; reference other items via `id:` and bind (`enabled: show.checked || useAlt.checked`). -- **Stable storage names:** The `name:` of a control maps to stored options in JASP files; **avoid renaming**. If you must, handle migrations in `Upgrades.qml`. -- **Translation & docs:** - - Wrap **all user-visible strings** in `qsTr("Text")`. - - Populate `info:` with a short, user-facing description (also wrapped in `qsTr`) to feed module help. -- **Variables workflow:** Place variable pickers inside a `VariablesForm`; connect lists with `source:` (can read all data columns, other lists, levels, or R sources). - -## 2) Input Validation - -Prefer **declarative validation** via built-in field properties: - -- **Numeric fields** (`DoubleField`, `IntegerField`): set `min`, `max`, and `inclusive` (e.g., `MinMax`), `decimals` (for doubles), and allow negatives only when needed. Use `fieldWidth` for compact UI. -- **Percent & CI** (`PercentField`, `CIField`): sensible defaults (e.g., 95), `afterLabel` defaults to `"%"`. -- **Slider:** set `min`, `max`, `decimals`; prefer horizontal sliders unless space constrained. -- **FormulaField:** accepts R-style expressions; constrain with `min`, `max`, `inclusive`; use `multiple: true` only when arrays are intended. Read via `realValue` / `realValues`. -- **TableView:** for mixed types, define validators and override `getValidator(col,row)`; optionally specify `itemTypePerRow/Column`. -- **Variables lists:** enforce data types via `allowedColumns: ["scale"|"ordinal"|"nominal"]` and `singleVariable: true` where appropriate. - -## 3) Main Custom Components - -### General input -- **CheckBox** — `name`, `label`, `checked`, `childrenOnSameRow`, `columns` (nested controls auto-enable/disable). -- **RadioButtonGroup / RadioButton** — group has `name`, `title`, `radioButtonsOnSameRow`, `columns`; each button has `value`, `label`, `checked`; can contain nested controls per choice. -- **DropDown** — `name`, `label`, `values` (array or `{label, value}`), or `source`; selection via `startValue` / `currentValue`; `addEmptyValue`, `placeHolderText`. -- **Slider** — `name`, `label`, `value`, `min`, `max`, `decimals`. -- **DoubleField / IntegerField** — `label`, `defaultValue`, `min`, `max`, `inclusive`, (`decimals` for DoubleField). -- **PercentField / CIField** — percent-specific shorthand; defaults appropriate for CIs. -- **TextField** — `defaultValue` or `placeholderText` (mutually exclusive), `afterLabel`, `fieldWidth`. -- **FormulaField** — adds `realValue`, `min/max`, `inclusive`, `multiple`, `realValues`. -- **TextArea** — `title`, `text`, `textType` (e.g., R code / JAGS / Lavaan / Model / Source), `separator(s)`, `applyScriptInfo` (submit with **Ctrl+Enter**). - -### Variable specification -- **AvailableVariablesList** — `name`, `label`, **rich `source`** (other lists, levels, filters, `rSource`, combinations), or `values`; `width`, `count` (read-only). -- **AssignedVariablesList** — `name`, `label`, `allowedColumns`, `singleVariable`, `maxRows`, `listViewType` (e.g., `Interaction`), optional `rowComponent` (+ `rowComponentTitle`), `optionKey`, `count`. -- **FactorLevelList** — define RM factors/levels: `factorName`, `levelName`, `minFactors`, `minLevels`, `width`, `height`. Often paired with an `AssignedVariablesList` of type `MeasuresCells`. - -### Complex composition -- **ComponentsList** — templated rows of controls from a `source` or `values`; `titles`, `rowComponent`, manual rows via `addItemManually`, bounds via `minimumItems` / `maximumItems`, collected under `optionKey`. -- **TabView** — `ComponentsList` rendered as tabs. -- **InputListView** — user adds rows via an input field; `title`, `placeHolder`, `defaultValues`, `minRows`, `inputComponent` (Text/Double/Integer), optional `rowComponent`, `optionKey`. -- **TableView** — `name`, `modelType` (`MultinomialChi2Model`, `JAGSDataInputModel`, `FilteredDataEntryModel`, `CustomContrasts`), `itemType` or per-row/column types, `source`; may override `getColHeaderText`, `getRowHeaderText`, `getDefaultValue`, `getValidator`. - -### Grouping & structure -- **Group** — logical block with `title`, `columns`. Nest options inside. -- **Section** — collapsible panel for advanced options; `title`, `columns`. Use for lower-priority / expert settings. - -## 4) Style & UX Conventions - -- **Titles & labels:** Title Case for section/group titles; concise labels; every visible string uses `qsTr()`. The `name` is always the title transformed into camelCase. Options within groups inherit their names as a prefix. -- **Consistency:** Prefer the provided JASP controls over ad-hoc QML; nest subordinate options inside the control that enables them (e.g., a `CheckBox` containing its dependent fields). -- **Two-column rhythm:** Let the grid flow naturally; use `rowSpan/columnSpan` to avoid awkward gaps; avoid long single-column scrollers. -- **Variables first:** Place `VariablesForm` at the top; align list widths; restrict types with `allowedColumns`. -- **Defaults & placeholders:** Prefer meaningful `defaultValue`; use `placeholderText` only when input is optional. Don't set both. -- **Dropdowns:** Use `{label, value}` pairs when R-side value differs; add an explicit empty choice with `addEmptyValue` if "no selection" is valid. -- **Advanced options:** Tuck rare/expert settings into a `Section` titled "Advanced Options". -- **Docs:** Fill `info:` succinctly for every major control. -- **Spacing:** Always use tabs for spacing. Each argument on a new line. (See examples below.) - -## 5) Quick Patterns - -- **Enable dependent field(s):** - ```qml - CheckBox - { - id: show - name: "showX" - label: qsTr("Show X") - } - - DoubleField - { - name: "Alpha" - label: qsTr("Alpha") - defaultValue: 0.05 - min: 0 - max: 1 - decimals: 3 - enabled: show.checked - } - ``` - -- **Radio choice with per-choice inputs:** - ```qml - RadioButtonGroup - { - name: "crit" - title: qsTr("Criterion") - - RadioButton - { - value: "pValue" - label: qsTr("p-value") - checked: true - - DoubleField - { - name: "pValueValue" - label: "" - defaultValue: 0.05 - min: 0 - max: 1 - } - } - - RadioButton - { - ... - } - } - ``` - -- **Variables form (single DV):** - ```qml - VariablesForm - { - AvailableVariablesList - { - name: "availableVariables" - } - - AssignedVariablesList - { - name: "dependentVariable" - label: qsTr("Dependent Variable") - allowedColumns: ["scale"] - singleVariable: true - } - } - ``` - -## 6) When in doubt - -- Prefer built-in JASP controls. -- Keep `name:` stable; translate strings; validate inputs. -- Put rare/expert options in a `Section` and document via `info:`. diff --git a/.claude/rules/r-instructions.md b/.claude/rules/r-instructions.md deleted file mode 100644 index 586610c88..000000000 --- a/.claude/rules/r-instructions.md +++ /dev/null @@ -1,115 +0,0 @@ ---- -paths: - - "**/R/*.R" ---- - -# R Instructions - -## 1) Core Basics - -- **Main entry point (name matters):** - - The R function name **must match** the case-sensitive `"function"` field in `Description.qml`. - - Signature is always: - ```r - AnalysisName <- function(jaspResults, dataset, options) { ... } - ``` - - `jaspResults` is a container that stores all of the analysis output and byproducts (if they are supposed to be kept for later use). - - `dataset` is the loaded dataset in JASP - - `options` are the UI choices from QML; **do not rename** option keys (they're your API). - -- **Recommended structure (3 roles):** - 1) **Main function** orchestrates and wires output elements. - 2) **create* functions** declare output markup (tables/plots/text). - 3) **fill* (or compute*) functions** compute results and fill outputs. - -- **Dependencies (cache & reuse):** - Add `$dependOn()` to every output (table/plot/text/container/state) so JASP knows when to reuse or drop it. - Outputs nested within containers inherit all dependencies from the container. - -- **Errors:** - - Catch run-time errors with `try(...)` and report via `$setError()`. - - Wrap user-visible text with `gettext()` / `gettextf()` for translation. - ---- - -## 2) Input Validation - -Only validate the `dataset`. `options` input is validated in the QML automatically. - -Common checks (prefix arguments with the check name): -```r -.hasErrors( - dataset, type = c("factorLevels", "observations", "variance", "infinity", "missingValues"), - factorLevels.target = options$variables, - factorLevels.amount = "< 1", - observations.target = options$variables, - observations.amount = "< 1" -) -``` -Other useful checks: -- `limits.min/max` (inclusive bounds), -- `varCovData.target/corFun` (positive-definiteness), -- `modelInteractions` (ensure lower-order terms exist). - ---- - -## 3) Output Components - -For detailed patterns, examples, and lifecycle guides: - -- Tables: see [jasp-tables.md](jasp-tables.md) -- Plots: see [jasp-plots.md](jasp-plots.md) -- Containers, HTML, errors: see [jasp-containers-and-errors.md](jasp-containers-and-errors.md) -- State/caching: see [jasp-state-management.md](jasp-state-management.md) - -**Quick API reference:** - -| Element | Create | Key properties | -|---------|--------|----------------| -| Table | `createJaspTable(title)` | `$addColumnInfo()`, `$setData(df)`, `$addFootnote()`, `$setError()`, `$showSpecifiedColumnsOnly` | -| Plot | `createJaspPlot(title, width, height)` | `$plotObject <- ggplot(...)`, `$setError()` | -| HTML | `createJaspHtml(text)` | `$text`, `$dependOn()` | -| Container | `createJaspContainer(title)` | `$dependOn()` (propagates to children), nest freely | -| State | `createJaspState()` | `$object` (store/retrieve), `$dependOn()` | - -All elements support `$dependOn()`, `$position`, and `$addCitation()`. - ---- - -## 4) Style & Conventions - -- **Follow the project R style guide.** Keep functions short; prefer pure helpers; avoid global state; no I/O or printing in analyses. -- **Naming:** - - Helpers start with a dot, e.g., `.computeFoo()`, `.fillBarTable()`, `.plotBaz()`. - - Stable keys in `jaspResults[["..."]]` (don't rename them later). -- **Internationalization:** All visible text via `gettext()`/`gettextf()`. -- **Performance:** Read only needed columns; postpone decoding; reuse `createJaspState()` when multiple outputs share results. -- **Robustness:** Validate early; guard long loops with `if (!ready) return()`; wrap risky code in `try()` and call `$setError()`. -- **Reproducibility:** Set column formats explicitly in tables; document assumptions in footnotes/citations. -- **Assignment alignment:** -For related assignments allign them at the arrow `<-`, i.e., -``` -variableOne <- foo() -variableFive <- foo() -``` -and allign function arguments in the similar way for function whose call is too long to be on a single line: -``` -out <- foo( - argumentOne = variableOne, - argumentFive = variableFive, - ... -) - ---- - -## 5) Minimal main() template (copy/paste) - -```r -MyAnalysis <- function(jaspResults, dataset, options) { - - ready <- length(options[["variables"]]) > 0 - - .createMyTable(jaspResults, dataset, options, ready) - .createMyPlot(jaspResults, dataset, options, ready) -} -``` diff --git a/.claude/rules/testing-instructions.md b/.claude/rules/testing-instructions.md deleted file mode 100644 index b3de39610..000000000 --- a/.claude/rules/testing-instructions.md +++ /dev/null @@ -1,152 +0,0 @@ ---- -paths: - - "**/tests/testthat/*.R" ---- - -# JASP Testing Instructions - -## 1) Test Framework - -This module uses the `jaspTools` testing framework. Tests are **critical** and must always pass before committing code. - -## 2) Running Tests - -Run via `btw_tool_run_r` in the persistent R session: - -```r -# Full test suite (300+ sec, NEVER CANCEL) -testAll() - -# Specific analysis tests (for quick iteration) -testAnalysis("AnalysisName") -``` - -**Critical rules:** - -- Tests take 300+ seconds to complete -- **NEVER CANCEL** tests -- always let them run to completion -- Some deprecation warnings are expected and can be ignored -- ALL tests must pass before proceeding -- Some tests skip on certain platforms (e.g., Windows) -- this is expected - -## 3) Test File Structure - -Each test file in `tests/testthat/` corresponds to an R analysis file: - -- `test-penalizedmetaanalysis.R` -> `R/penalizedmetaanalysis.R` -- Test file name pattern: `test-.R` -- Analysis names for `testAnalysis()` come from NAMESPACE exports (PascalCase) - -## 4) Writing Tests - -### Basic test structure - -```r -# 1. Set up analysis options -options <- jaspTools::analysisOptions("AnalysisName") -options$variables <- "contGamma" -options$descriptives <- TRUE - -# 2. Set seed for reproducibility -set.seed(1) - -# 3. Run the analysis -results <- jaspTools::runAnalysis("AnalysisName", "debug.csv", options) - -# 4. Test tables -test_that("Table name matches", { - table <- results[["results"]][["tableName"]][["data"]] - jaspTools::expect_equal_tables(table, list(...expected values...)) -}) - -# 5. Test plots -test_that("Plot name matches", { - plotName <- results[["results"]][["containerName"]][["collection"]][["plotId"]][["data"]] - testPlot <- results[["state"]][["figures"]][[plotName]][["obj"]] - jaspTools::expect_equal_plots(testPlot, "plotname", dir = "AnalysisName") -}) -``` - -### Loading from .jasp example files - -```r -jaspFile <- testthat::test_path("..", "..", "examples", "Example Name.jasp") -opts <- jaspTools::analysisOptions(jaspFile) -dataset <- jaspTools::extractDatasetFromJASPFile(jaspFile) -encoded <- jaspTools:::encodeOptionsAndDataset(opts, dataset) -set.seed(1) -results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE) -``` - -### Key testing functions - -- `jaspTools::analysisOptions(name)` -- Get default options for an analysis -- `jaspTools::runAnalysis(name, dataset, options)` -- Run analysis with options -- `jaspTools::expect_equal_tables(actual, expected)` -- Compare table output -- `jaspTools::expect_equal_plots(plot, name, dir)` -- Compare plot output (snapshot-based) - -## 5) Test Data - -- `"debug.csv"` is a built-in jaspTools dataset containing most data types -- Use `set.seed()` before running analyses for reproducibility -- Example .jasp files in `examples/` provide pre-configured options and datasets - -## 6) Test Snapshots - -- Snapshots stored in `tests/testthat/_snaps/` -- **NEVER automatically accept snapshot changes** -- always notify user for manual inspection -- When a new snapshot is created, inform the user so they can verify it - -## 7) When to Update Tests - -### Always update tests when - -1. Adding new analysis outputs (tables, plots, text) -2. Modifying existing output structure or values -3. Adding new QML options that affect results -4. Changing analysis calculations - -### How to update test expectations - -1. Run tests and capture new output -2. Verify the new output is correct -3. Update expected values in test file -4. Re-run tests to confirm they pass - -## 8) Test Workflow - -### Before making code changes - -Run `testAll()` via `btw_tool_run_r` to establish baseline -- all tests should pass. - -### After making code changes - -1. Run `devtools::load_all()` to hot-reload R changes -2. Run `testAnalysis("AnalysisName")` for quick iteration on the affected analysis -3. Once the specific tests pass, run `testAll()` to check for regressions - -### If tests fail - -1. Review the failure messages carefully -2. Check if failure is expected (due to your intentional changes) -3. If expected: update test expectations and notify user about snapshot changes -4. If unexpected: fix your code -5. Re-run tests until all pass - -## 9) Adding New Tests - -When adding a new analysis: - -1. Create test file: `tests/testthat/test-.R` -2. Set up options with all default values explicitly set -3. Test all output tables and plots -4. Test edge cases and error conditions -5. Use meaningful variable names and test data - -## 10) Best Practices - -- **One test per output element** -- separate `test_that()` blocks for each table/plot -- **Descriptive test names** -- clearly state what is being tested -- **Reproducible** -- always use `set.seed()` for analyses with randomness -- **Complete option coverage** -- test with various option combinations -- **Keep tests focused** -- each test should verify one specific aspect diff --git a/.claude/rules/translation-instructions.md b/.claude/rules/translation-instructions.md deleted file mode 100644 index 6cc48922b..000000000 --- a/.claude/rules/translation-instructions.md +++ /dev/null @@ -1,256 +0,0 @@ ---- -paths: - - "**/R/*.R" - - "**/inst/qml/*.qml" - - "**/po/**" ---- - -# Translation (i18n) Instructions - -## 1) Core Principle - -**ALL user-visible text must be wrapped for translation.** - -This module is translated into multiple languages via Weblate integration. - -## 2) R Code Translation - -### Use `gettext()` for static strings: -```r -# Single string -message <- gettext("Analysis complete") - -# Table titles -tab <- createJaspTable(title = gettext("Descriptive Statistics")) - -# Error messages -tab$setError(gettext("Insufficient observations")) -``` - -### Use `gettextf()` for dynamic strings: -```r -# Single placeholder -msg <- gettextf("Variable %s has insufficient data", varName) - -# Multiple placeholders - use numbered format for translators -msg <- gettextf("Number of factor levels is %1$s in %2$s", nLevels, varName) - -# Percentage signs must be doubled -label <- gettextf("%s%% CI for Mean Difference", 100 * alpha) -``` - -### Use `ngettext()` for plurals: -```r -msg <- ngettext(n, - "One observation removed", - "%d observations removed", - domain = "R-jaspEquivalenceTTests") -``` - -### Column overtitles with dynamic content: -```r -if (options$confidenceInterval) { - ciLabel <- gettextf("%s%% CI", 100 * options$confidenceIntervalLevel) - tab$addColumnInfo("lower", gettext("Lower"), overtitle = ciLabel) - tab$addColumnInfo("upper", gettext("Upper"), overtitle = ciLabel) -} -``` - -## 3) QML Translation - -### Wrap all visible strings with `qsTr()`: -```qml -CheckBox -{ - name: "descriptives" - label: qsTr("Descriptive statistics") - - CheckBox - { - name: "confidenceInterval" - label: qsTr("Confidence interval") - info: qsTr("Display confidence intervals for effect sizes") - } -} -``` - -### For groups and sections: -```qml -Group -{ - title: qsTr("Additional Statistics") - - CheckBox - { - label: qsTr("Effect size") - } -} - -Section -{ - title: qsTr("Advanced Options") - - DoubleField - { - label: qsTr("Prior scale") - } -} -``` - -### Radio buttons and dropdowns: -```qml -RadioButtonGroup -{ - name: "hypothesis" - title: qsTr("Alternative Hypothesis") - - RadioButton - { - value: "twoSided" - label: qsTr("Two-sided") - } - - RadioButton - { - value: "greater" - label: qsTr("Greater than") - } -} - -DropDown -{ - name: "effectSize" - label: qsTr("Effect Size") - values: [ - { label: qsTr("Cohen's d"), value: "cohen" }, - { label: qsTr("Glass' delta"), value: "glass" } - ] -} -``` - -## 4) Translation Rules - -### DO wrap for translation: -- ✅ Table/plot/container titles -- ✅ Column names and overtitles -- ✅ Error messages and warnings -- ✅ Footnotes and citations -- ✅ All QML labels, titles, and info text -- ✅ Help text and descriptions -- ✅ Button labels and tooltips - -### DON'T wrap for translation: -- ❌ Empty strings: `""` (NEVER mark for translation) -- ❌ Variable names (internal identifiers) -- ❌ Statistical symbols: `"β"`, `"p"`, `"t"`, `"df"` -- ❌ Mathematical expressions -- ❌ Code or syntax -- ❌ File paths - -### Format specifications: -```r -# CORRECT - use numbered placeholders for clarity -gettextf("Mean difference is %1$s with SE = %2$s", mean, se) - -# AVOID - unnamed placeholders are harder for translators -gettextf("Mean difference is %s with SE = %s", mean, se) -``` - -### Special characters: -```r -# Use UTF-8 escape sequences for non-ASCII -label <- gettext("Cram\u00E9r's V") # Cramér's V -symbol <- gettext("\u03B2") # β (beta) -``` - -### Percentage signs in format strings: -```r -# WRONG - single % will cause format error -label <- gettextf("%s% CI", 95) - -# CORRECT - double %% in format string -label <- gettextf("%s%% CI", 95) -``` - -## 5) Translation Workflow - -### Automated process: -1. Developers write code with `gettext()`/`gettextf()`/`qsTr()` -2. Translation extraction happens automatically -3. Weblate platform provides translation interface -4. Translators work on Weblate -5. Translation files synced back to repository automatically -6. `.github/workflows/translations.yml` handles automation - -### Translation files location: -``` -po/ # R translation files -inst/qml/translations/ # QML translation files (if exists) -``` - -### Manual updates (rare): -Usually handled automatically, but if needed: -```bash -# Update R translations (done by translation workflow) -# Don't manually edit .po files unless absolutely necessary -``` - -## 6) Testing Translations - -While we can't easily test all languages locally, ensure: -1. All user-visible strings are wrapped -2. Format strings use numbered placeholders -3. Percentage signs are doubled in format strings -4. No empty strings marked for translation -5. Context provided for ambiguous terms - -## 7) Common Mistakes to Avoid - -### ❌ WRONG: -```r -# Missing translation -tab <- createJaspTable(title = "Descriptive Statistics") - -# Empty string marked for translation -label <- gettext("") - -# Unnamed placeholders -msg <- gettextf("Found %s issues in %s", count, name) - -# Single % for percentage -label <- gettextf("%s% Confidence Interval", 95) -``` - -### ✅ CORRECT: -```r -# Proper translation -tab <- createJaspTable(title = gettext("Descriptive Statistics")) - -# No translation for empty string -label <- "" - -# Numbered placeholders for translators -msg <- gettextf("Found %1$s issues in %2$s", count, name) - -# Doubled %% for percentage -label <- gettextf("%s%% Confidence Interval", 95) -``` - -## 8) Translation Context - -For ambiguous terms, consider adding comments: -```r -# "Mean" as in average (not "mean" as in unkind) -columnTitle <- gettext("Mean") - -# "Scale" as in measurement scale (not fish scales) -fieldLabel <- qsTr("Scale variable") -``` - -## 9) Weblate Integration - -- Weblate repo: `jaspequivalencettests-qml` and `jaspequivalencettests-r` -- Automated workflow: `.github/workflows/translations.yml` -- Scheduled runs: Weekly on Saturday at 2:45 AM -- Manual trigger: `workflow_dispatch` available -- Translation updates automatically create commits/PRs diff --git a/.claude/session_startup.R b/.claude/session_startup.R deleted file mode 100644 index f1bb36689..000000000 --- a/.claude/session_startup.R +++ /dev/null @@ -1,22 +0,0 @@ -# JASP Module - R Session Startup for Claude Code -# Run this script in your interactive R session (RStudio/Positron/radian) -# to prepare and hand over the session to Claude Code. -# -# Usage: source(".claude/session_startup.R") - -# Fix cli::get_spinner() conflict with testthat in btw/evaluate context -options(cli.spinner = "line") - -# Fix locale issue with renv.lock files created on non-English systems -if (.Platform$OS.type == "windows") { - Sys.setlocale("LC_ALL", "English_United States.utf8") -} else { - Sys.setlocale("LC_ALL", "C.UTF-8") -} - -renv::restore(prompt = FALSE) -library(jaspTools) -renv::install(".", prompt = FALSE) -setPkgOption("module.dirs", ".") -setPkgOption("reinstall.modules", FALSE) -btw::btw_mcp_session() diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 825a76c3f..000000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(where:*)", - "Bash(grep:*)", - "Bash(Rscript:*)", - "Bash(find:*)", - "WebFetch(domain:posit-dev.github.io)", - "WebFetch(domain:cran.r-project.org)", - "WebFetch(domain:gadenbuie.r-universe.dev)", - "WebFetch(domain:ellmer.tidyverse.org)", - "WebFetch(domain:github.com)", - "mcp__r-mcptools__btw_tool_session_platform_info", - "mcp__r-mcptools__btw_tool_session_package_info", - "mcp__r-mcptools__btw_tool_session_check_package_installed", - "mcp__r-mcptools__btw_tool_files_list_files", - "mcp__r-mcptools__btw_tool_git_status", - "mcp__r-mcptools__list_r_sessions", - "mcp__r-mcptools__select_r_session", - "mcp__r-mcptools__btw_tool_docs_available_vignettes", - "mcp__r-mcptools__btw_tool_docs_vignette", - "mcp__r-mcptools__btw_tool_docs_help_page", - "mcp__r-mcptools__btw_tool_run_r", - "mcp__markitdown__convert_to_markdown", - "Read(//c/JASP-Packages/jaspMetaAnalysis/**)" - ] - }, - "hooks": { - "PreToolUse": [ - { - "matcher": "Edit|Write", - "hooks": [ - { - "type": "command", - "command": "node .claude/hooks/block-test-edits.js" - } - ] - } - ] - }, - "enableAllProjectMcpServers": true, - "enabledMcpjsonServers": [ - "r-mcptools", - "markitdown" - ] -} diff --git a/.claude/skills/fix-debug-analysis.md b/.claude/skills/fix-debug-analysis.md deleted file mode 100644 index 9c09b0b24..000000000 --- a/.claude/skills/fix-debug-analysis.md +++ /dev/null @@ -1,427 +0,0 @@ -# Fix & Debug JASP Analysis (MCP Session) - -Quick reference for debugging JASP analysis functions through MCP sessions. - -**Note**: `browser()` and `recover()` require interactive R console and **do not work** through MCP's `btw_tool_run_r`. - ---- - -## 1) Debugging Approaches - -There are two approaches, in order of preference: - -### Approach A: Code Inspection (try first) - -Many bugs — especially logic errors, missing branches, wrong conditions — are solvable by reading the code and tracing the control flow. This is faster and doesn't require instrumenting code. - -1. **Reproduce**: Bootstrap a `runAnalysis()` call (Step 0) and confirm the issue -2. **Read**: Trace the code path from the entry-point function through the relevant helpers -3. **Identify**: Look for logic errors — wrong conditions, missing option checks, incorrect branching -4. **Fix**: Edit the source, hot-reload, and verify - -**Use this when**: Output is missing, wrong options are checked, a feature works in one analysis type but not another, UI options don't match R-side logic. - -### Approach B: saveRDS State Capture (escalation) - -When the bug depends on runtime values that can't be deduced from code reading alone. - -1. **Instrument**: Add saveRDS() before the error location -2. **Capture**: Hot-reload and run analysis, copy debug path from console -3. **Inspect**: Load saved state and examine values via MCP -4. **Fix**: Develop and test fix using captured state -5. **Verify**: Remove debug code, hot-reload, confirm fix works - -**Use this when**: Error depends on specific data values, unexpected NULL/type, dimension mismatches, or the code path is too complex to trace by reading. - ---- - -## 2) Reproducing the Issue - -### Step 0: Bootstrap a Reproducible Analysis Run - -Before debugging, you need a working `runAnalysis()` call that reproduces the error. Choose the first applicable source: - -#### Option A: User provides a .jasp file - -```r -jaspFile <- "path/to/file.jasp" -opts <- jaspTools::analysisOptions(jaspFile) -dataset <- jaspTools::extractDatasetFromJASPFile(jaspFile) -encoded <- jaspTools:::encodeOptionsAndDataset(opts, dataset) -set.seed(1) -results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE) -``` - -If the .jasp file contains multiple analyses, `analysisOptions()` returns a list — index with `[[1]]`, `[[2]]`, etc. Pick the analysis that matches the error context. - -#### Option B: Extract from existing unit tests (most common fallback) - -When no .jasp file is provided, **search test files first**. Test files contain pre-configured options and dataset references that are known to produce complete output. - -1. **Find the test file** for the analysis in `tests/testthat/`: - ``` - grep -r "AnalysisName" tests/testthat/ - ``` - -2. **Determine the input pattern** used in the test. Tests use one of two patterns: - - **Pattern 1 — .jasp example file** (look for `analysisOptions(jaspFile)` or `extractDatasetFromJASPFile`): - ```r - # Copy the loading code from the test, adjusting the path for non-test context - jaspFile <- file.path("examples", "Example Name.jasp") - opts <- jaspTools::analysisOptions(jaspFile)[[1]] # note: may need [[1]] for multi-analysis files - dataset <- jaspTools::extractDatasetFromJASPFile(jaspFile) - encoded <- jaspTools:::encodeOptionsAndDataset(opts, dataset) - set.seed(1) - results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE) - ``` - - **Pattern 2 — inline options** (look for `analysisOptions("AnalysisName")` with explicit option assignments): - ```r - # Copy the options setup from the test verbatim - options <- jaspTools::analysisOptions("AnalysisName") - options$dependent <- "contNormal" # copy from test - options$group <- "contBinom" # copy from test - # ... copy ALL option assignments from the test ... - set.seed(1) - results <- jaspTools::runAnalysis("AnalysisName", "debug.csv", options) - ``` - -3. **Modify options** to match the bug-triggering scenario (e.g., enable/disable specific checkboxes). - -4. **Verify reproduction**: Check that the issue is reproduced — this could be a `"fatalError"` status, an error message in a specific output element, incorrect values, missing output, etc., depending on what the user reported. - -#### Option C: Build options from scratch (last resort) - -Only when no tests or examples exist: - -```r -options <- jaspTools::analysisOptions("AnalysisName") -# Set required inputs — check .robttCheckReady() or equivalent readiness function -# to discover which options must be non-empty -options$dependent <- "contNormal" -options$group <- "contBinom" -set.seed(1) -results <- jaspTools::runAnalysis("AnalysisName", "debug.csv", options) -``` - -**Tip**: `jaspTools::analysisOptions("AnalysisName")` returns all options with their QML defaults. Inspect it with `str(options)` to understand available options and their types. - ---- - -## 3) saveRDS Workflow (Approach B) - -Use these steps when code inspection alone is insufficient and you need to examine runtime values. - -### Step 1: Identify Error Location - -From the error message and stack trace, locate the function and approximate line where the error occurs. - -**Example**: Stack trace shows `.buildTable()` → `table$addFootnote()` → error - -### Step 2: Instrument Code - -Add saveRDS() just **before** the line that's failing: - -```r -.buildTable <- function(jaspResults, options) { - # ... existing code ... - - someVariable <- computeSomething(data, options) - - # DEBUG: REMOVE - save state before error - debug_dir <- tempdir() - saveRDS(list( - someVariable = someVariable, - relatedData = relatedData, - fit = fit, - options = options - # Include ALL relevant variables - ), file.path(debug_dir, "debug_state.rds")) - message("DEBUG: Saved to ", file.path(debug_dir, "debug_state.rds")) - - # The line that's failing - processData(someVariable) -} -``` - -**Critical rules**: -- Always use marker comment `# DEBUG: REMOVE` -- **Never save `jaspResults`** (crashes R) -- Save to `tempdir()` (auto-cleanup) -- Include `message()` to print path to console -- Save ALL variables that might be relevant - -### Step 3: Hot-Reload and Capture - -```r -# Via btw_tool_run_r in MCP -devtools::load_all() - -# Re-run the analysis (use same code that triggered original error) -set.seed(1) -results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE) - -# Console output will show: -# DEBUG: Saved to C:/Users/.../Temp/RtmpXXX/debug_state.rds -``` - -Copy the debug path from the console output. - -### Step 4: Inspect Captured State - -```r -# Via btw_tool_run_r in MCP -debug_path <- "C:/Users/.../Temp/RtmpXXX/debug_state.rds" -debug_data <- readRDS(debug_path) - -# Examine structure -str(debug_data) - -# Inspect specific variables -print(debug_data$someVariable) -sapply(debug_data$someVariable, class) -any(sapply(debug_data$someVariable, is.null)) - -# Check attributes -for (i in seq_along(debug_data$relatedData)) { - cat("Item", i, "attribute:", attr(debug_data$relatedData[[i]], "someAttr"), "\n") -} -``` - -**Goal**: Identify the exact values causing the error. - -### Step 5: Develop Fix - -Based on inspection, develop fix logic using the saved objects: - -```r -# Via btw_tool_run_r in MCP -# Test the fix logic interactively using saved state - -# Example: Filter out invalid values -someVariable_clean <- Filter(function(x) !is.null(x) && is.finite(x), debug_data$someVariable) -print(someVariable_clean) # Verify it works - -# Try the fix -for (i in seq_along(someVariable_clean)) { - cat("Would process item:", someVariable_clean[[i]], "\n") -} -``` - -Once fix logic works, implement it in the source file. - -### Step 6: Clean Up and Verify - -1. Apply fix to source file -2. **Remove all debug code** (saveRDS(), message(), and "# DEBUG: REMOVE" markers) -3. Hot-reload and verify: - -```r -# Via btw_tool_run_r in MCP -devtools::load_all() - -set.seed(1) -results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE) - -# Check overall status -cat("Status:", results$status, "\n") -``` - -4. **Verify the specific issue is resolved** — don't just check `results$status`: - - If the bug was missing output: confirm the output element now exists in `results$results` - - If the bug was wrong values: check the specific table/cell values - - If the bug was an error in a subcomponent: navigate to that component and verify no error - - If the bug was a crash: confirm status is `"complete"` - -5. Search for any remaining debug code before committing: - -```bash -grep -r "DEBUG: REMOVE" R/ -grep -r "saveRDS.*tempdir" R/ -``` - ---- - -## 4) What to Save - -| Location | Objects to save | DON'T save | -|----------|----------------|------------| -| **Model fitting** | `dataset`, `options`, function args, intermediate values | `jaspResults`, `...` (ellipsis args) | -| **Row building** | `fit`, `attr(fit, "group")`, computed rows, `options` | Parent containers, environments | -| **Table assembly** | `rows` list, intermediate data.frames | Full fit objects if not needed | -| **Error handling** | Error object, variables being processed when error occurred | Large intermediate objects | - -**Golden rule**: When unsure, save it. Missing a variable means re-running the entire capture process. - ---- - -## 5) Real-World Example - -**Error**: `jaspTable$addFootnote expects 'message' to be a string!` - -**Workflow**: - -1. **Loaded .jasp file and reproduced error**: - ```r - jaspFile <- "path/to/file.jasp" - opts <- jaspTools::analysisOptions(jaspFile) - dataset <- jaspTools::extractDatasetFromJASPFile(jaspFile) - encoded <- jaspTools:::encodeOptionsAndDataset(opts, dataset) - set.seed(1) - results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE) - # → Status: fatalError - ``` - -2. **Identified error location**: Stack trace → `.buildTable()` at specific line - -3. **Instrumented code**: - ```r - footnotes <- unique(lapply(dataList, attr, which = "footnote")) - - # DEBUG: REMOVE - saveRDS(list( - footnotes = footnotes, - dataList = dataList - ), file.path(tempdir(), "footnote_debug.rds")) - message("DEBUG: Saved to ", file.path(tempdir(), "footnote_debug.rds")) - - for (i in seq_along(footnotes)) - table$addFootnote(footnotes[[i]]) - ``` - -4. **Captured state**: - ```r - devtools::load_all() - results <- jaspTools::runAnalysis(...) - # Console: DEBUG: Saved to C:/Users/.../RtmpXXX/footnote_debug.rds - ``` - -5. **Inspected**: - ```r - debug_data <- readRDS("C:/Users/.../RtmpXXX/footnote_debug.rds") - str(debug_data$footnotes) - # List of 2 - # $ : chr "Some footnote text..." - # $ : NULL ← THE PROBLEM - ``` - -6. **Root cause**: `unique()` preserves NULL values → loop called `addFootnote(NULL)` → error - -7. **Implemented fix**: - ```r - footnotes <- unique(lapply(dataList, attr, which = "footnote")) - footnotes <- Filter(Negate(is.null), footnotes) # Filter NULLs - for (i in seq_along(footnotes)) - table$addFootnote(footnotes[[i]]) - ``` - -8. **Verified**: - ```r - devtools::load_all() - results <- jaspTools::runAnalysis(...) - # → Status: complete ✓ - ``` - -**Time**: ~5 minutes from error to verified fix. - ---- - -## 6) Advanced Techniques - -### Conditional Saving - -For errors in specific iterations/groups: - -```r -# Only save when condition is met -for (i in seq_along(items)) { - if (i == 47) { # Error only in iteration 47 - saveRDS(list(item = items[[i]], i = i), file.path(tempdir(), "debug_iter47.rds")) - message("DEBUG: Saved iteration 47") - } - result <- process(items[[i]]) -} -``` - -### Multiple Checkpoints - -Narrow down error location by saving at multiple points: - -```r -# Checkpoint 1 -saveRDS(list(step = "before_transform", data = data), - file.path(tempdir(), "checkpoint1.rds")) - -data_transformed <- transform(data) - -# Checkpoint 2 -saveRDS(list(step = "after_transform", data_transformed = data_transformed), - file.path(tempdir(), "checkpoint2.rds")) -``` - -### Save with Timestamp - -For multiple runs: - -```r -timestamp <- format(Sys.time(), "%Y%m%d_%H%M%S") -saveRDS(list(...), file.path(tempdir(), paste0("debug_", timestamp, ".rds"))) -``` - ---- - -## 7) Common Error Patterns - -### Pattern 1: Unexpected NULL - -**Symptom**: "argument is NULL" or "expects X to be a Y" - -**Debugging**: -```r -saveRDS(list(suspect_var = suspect_var, related_vars = list(...)), ...) -# Inspect: is.null(debug_data$suspect_var) -``` - -### Pattern 2: Wrong Type/Class - -**Symptom**: "cannot coerce X to Y" or "is not a valid type" - -**Debugging**: -```r -saveRDS(list(var = var, class = class(var), str = capture.output(str(var))), ...) -# Inspect: class(debug_data$var), attributes(debug_data$var) -``` - -### Pattern 3: Dimension Mismatch - -**Symptom**: "dims [product X] do not match length of object [Y]" - -**Debugging**: -```r -saveRDS(list(obj = obj, dims = dim(obj), length = length(obj)), ...) -# Inspect: dim(debug_data$obj), length(debug_data$obj) -``` - -### Pattern 4: Index Out of Bounds - -**Symptom**: "subscript out of bounds" or "undefined columns selected" - -**Debugging**: -```r -saveRDS(list(container = container, index = i, length = length(container)), ...) -# Inspect: i vs length(debug_data$container), names(debug_data$container) -``` - ---- - -## 8) Safety Checklist - -Before committing code: - -- [ ] All `# DEBUG: REMOVE` markers removed -- [ ] All `saveRDS()` calls removed -- [ ] All debug `message()` calls removed -- [ ] Verified with: `grep -r "DEBUG: REMOVE" R/` -- [ ] Verified with: `grep -r "saveRDS.*tempdir" R/` -- [ ] Hot-reloaded and tested: analysis completes successfully diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index 78c02609e..000000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,230 +0,0 @@ -# JASP Module - -ALWAYS follow these instructions first and fallback to additional search and context gathering ONLY if the information in these instructions is incomplete or found to be in error. - -This is a JASP module. It contains QML user-facing interfaces and R backend computations. - -In all interactions and commit messages, be extremely concise and sacrifice grammar for the sake of concision. - -## Detailed Instructions - -For comprehensive guidance on specific topics, see: - -- **[Module Architecture](.github/instructions/jasp-module-architecture.instructions.md)** - **Start here.** QML-Desktop-R reactive loop, jaspResults persistence, options mapping, data flow -- **[Dependency Management](.github/instructions/jasp-dependency-management.instructions.md)** - $dependOn mechanics, inheritance, vectors, per-value deps, sentinel pattern -- **[State Management](.github/instructions/jasp-state-management.instructions.md)** - createJaspState caching, model fit patterns, metadata state, dynamic containers -- **[R Backend Development](.github/instructions/R.instructions.md)** - R function structure, validation, style conventions -- **[Tables](.github/instructions/jasp-tables.instructions.md)** - Table lifecycle, columns, rows, footnotes, error display -- **[Plots](.github/instructions/jasp-plots.instructions.md)** - Plot lifecycle, composite plots, subgroup/facet patterns -- **[Containers & Errors](.github/instructions/jasp-containers-and-errors.instructions.md)** - Container patterns, HTML output, error handling -- **[QML Interface Development](.github/instructions/inst.qml.instructions.md)** - QML controls, validation, bindings, and UI patterns -- **[Testing & Test Writing](.github/instructions/testing.instructions.md)** - Test framework, snapshots, and test workflow -- **[Translation (i18n)](.github/instructions/translation.instructions.md)** - gettext/gettextf/qsTr usage, formatting, plurals -- **[Output Structure](.github/instructions/jasp-output-structure.instructions.md)** - Reading/testing serialized output (containers, tables, plots, state) -- **[Debug Analysis](.github/instructions/debug-analysis.instructions.md)** - Debugging JASP analyses via saveRDS() state capture in MCP sessions - -## R Session via MCP - -This project uses the `btw` MCP server (`.claude/mcp-server.R`) to provide a persistent R session via `btw_tool_run_r`. The MCP server config (`.mcp.json`) is module-specific and NOT committed to git. - -**Session handoff:** The user sets up their R session (RStudio/Positron/radian), runs `btw::btw_mcp_session()`, and hands it over. Connect via `list_r_sessions` / `select_r_session`. All `btw_tool_run_r` calls then execute in the user's session with full access to loaded packages and objects. The following R packages are required for the mcp server: `btw`, `mcptools`. - -### Available MCP Tools - -Use these R-specific tools instead of Bash when possible: - -| Tool | Use for | -|------|---------| -| `btw_tool_run_r` | Execute R code in persistent session (variables persist between calls) | -| `btw_tool_docs_help_page` | Look up R function documentation | -| `btw_tool_docs_package_news` | Check package changelogs | -| `btw_tool_docs_available_vignettes` | Find package vignettes | -| `btw_tool_env_describe_environment` | Inspect objects in the R session | -| `btw_tool_env_describe_data_frame` | Inspect data frame structure | -| `btw_tool_search_packages` | Search CRAN for packages | -| `btw_tool_session_platform_info` | Check R version and platform | -| `btw_tool_session_check_package_installed` | Verify package availability | - -**Use Copilot Code native tools** (Read, Edit, Write, Glob, Grep, Bash) for file editing, git operations, and file search -- they are faster than MCP equivalents. - -## Working Effectively - -### Session Setup (done by user) - -At the start of a session, check for a connected R session via `list_r_sessions`. If none is available, **prompt the user** to run in their interactive R console: - -```r -source(".claude/session_startup.R") -``` - -This restores dependencies, installs the module, configures jaspTools, and registers the session. Then connect via `list_r_sessions` / `select_r_session`. - -### Hot-Reload After Code Changes - -- **R code only changed:** `devtools::load_all()` via `btw_tool_run_r` -- **QML, dependencies, or imports changed:** `renv::install(".", prompt = FALSE)` - -### Running Tests - -Run via `btw_tool_run_r` in the persistent session: - -```r -# Full test suite (300+ sec, NEVER CANCEL) -testAll() - -# Specific analysis tests (for quick iteration) -testAnalysis("AnalysisName") -``` - -- `testAll()` at session start to verify baseline, and after all fixes to check regressions -- `testAnalysis("Name")` for quick iteration while fixing specific analyses -- Analysis names are PascalCase exports from NAMESPACE -- Some tests may skip on certain platforms (e.g., Windows) -- this is expected - -**See [testing.instructions.md](.github/instructions/testing.instructions.md) for detailed test writing guidelines, snapshots, and workflows.** - -### Running a Specific Analysis - -**With built-in debug dataset:** -```r -options <- jaspTools::analysisOptions("AnalysisName") -options$someOption <- value -set.seed(1) -results <- jaspTools::runAnalysis("AnalysisName", "debug.csv", options) -``` - -**From a .jasp example file:** -```r -jaspFile <- file.path("examples", "Example Name.jasp") -opts <- jaspTools::analysisOptions(jaspFile) -dataset <- jaspTools::extractDatasetFromJASPFile(jaspFile) -encoded <- jaspTools:::encodeOptionsAndDataset(opts, dataset) -set.seed(1) -results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE) -``` - -The encoding step is required because JASP internally encodes variable names and options to resolve ambiguities (e.g., same variable used with different types). - -**From a user-provided .jasp file:** Use the same pattern above. This is the primary way to reproduce bugs reported by users. - -### Inspecting Results - -After `runAnalysis()`, check: -- `results$status` -- `"complete"` or `"fatalError"` -- `results$results` -- nested list of output containers, tables, plots -- `results$results$errorMessage` -- if status is fatalError - -### Finding Analysis Names - -1. Check roxygen documentation in R files (if available) -2. Parse `NAMESPACE` for `export()` directives - -### Test Snapshots - -- Snapshots stored in `tests/testthat/_snaps/` -- **NEVER automatically accept snapshot changes** -- always notify user for manual inspection -- When a snapshot is newly created, inform the user - -### Repository Structure -``` -/ -├── R/ # Backend R analysis functions -├── inst/ -│ ├── qml/ # QML interface definitions -│ ├── Descriptions/ # Analysis descriptions (Description.qml) -│ ├── help/ # Markdown help files -│ └── Upgrades.qml # Version upgrade mappings -├── examples/ # Example .jasp files for testing -├── tests/testthat/ # Unit tests using jaspTools -├── .github/workflows/ # CI/CD automation -├── DESCRIPTION # R package metadata -├── NAMESPACE # Exported analysis names -└── renv.lock # R dependency lockfile -``` - -### Key Files to Check After Changes -- Always check corresponding test file in `tests/testthat/` when modifying R functions -- Update `inst/Upgrades.qml` when renaming QML options to maintain backward compatibility - -## Development Rules - -### Dependencies -- Avoid new dependencies -- re-implement simple functions instead of importing a whole package -- If a new dependency is truly needed, add it to DESCRIPTION and update renv.lock - -### QML Interface Rules -- QML interfaces in `inst/qml/` define user-facing options passed to R functions -- Each analysis links: `inst/Description.qml/` -> `inst/qml/` -> `R/` functions -- QML elements use `name` (camelCase internal) and `title`/`label` (user-facing) -- Document QML elements using `info` property for help generation -- Use existing QML files as examples for structure and style -- Add default values to unit tests when adding new QML options - -**See [inst.qml.instructions.md](.github/instructions/inst.qml.instructions.md) for comprehensive QML controls reference, validation patterns, and UI conventions.** - -### R Backend Rules -- R functions in `R/` directory called by analyses in `inst/Descriptions/` -- Use camelCase for all function and variable names -- NEVER use `library()` or `require()` - use `package::function()` syntax -- Access `options` list via `options[["name"]]` notation to avoid partial matching -- Follow CRAN guidelines for code structure and documentation - -**See [R.instructions.md](.github/instructions/R.instructions.md) for complete R function structure, jaspResults API, output components (tables/plots/containers/state), and coding conventions.** - -### Input Validation and Error Handling -- **TARGETED VALIDATION ONLY**: Since `options` are validated in the GUI, R functions should NOT check user input validity except for specific cases -- **VALIDATE ONLY**: `dataset` object (data.frame from GUI), `TextField` options, and `FormulaField` options (arbitrary text input) -- Use `gettext()` and `gettextf()` for all user-visible messages (internationalization) -- For `dataset` validation, check: missing values, infinity, negative values, insufficient observations, factor levels, variance -- Example: `.hasErrors(dataset, type = c('observations', 'variance', 'infinity'), all.target = options$variables, observations.amount = '< 3', exitAnalysisIfErrors = TRUE)` -- Validate dataset assumptions automatically when required for analysis validity -- Use footnotes for assumption violations that affect specific cells/values -- Place critical errors that invalidate entire analysis over the results table - -### Error Message Guidelines -- Write clear, actionable error messages that prevent user confusion -- Use `gettextf()` with placeholders for dynamic content: `gettextf("Number of factor levels is %1$s in %2$s", levels, variable)` -- For multiple arguments, use `%1$s`, `%2$s` format for translator clarity -- Use `ngettext()` for singular/plural forms -- Never mark empty strings for translation -- Use UTF-8 encoding for non-ASCII characters: `\u03B2` for beta -- Double `%` characters in format strings: `gettextf("%s%% CI for Mean")` - -**See [translation.instructions.md](.github/instructions/translation.instructions.md) for comprehensive i18n guidelines including QML qsTr(), R gettext/gettextf/ngettext, formatting rules, and Weblate workflow.** - -## CI/CD Pipeline -- GitHub Actions in `.github/workflows/unittests.yml` runs on every push -- Triggers on changes to R, test, or package files -- Uses jasp-stats/jasp-actions reusable workflow - -## Git Workflow - -- **ALWAYS work on feature branches** -- never commit directly to `master` -- **NEVER push/create PRs/merge without explicit human approval** -- Commit locally freely, but wait for approval before pushing to remote - -## Common Tasks - -### Adding New Analysis - -1. Create R function in `R/` directory following camelCase naming -2. Add QML interface in `inst/qml/` -3. Define analysis in `inst/Description.qml` -4. Add unit tests in `tests/testthat/` -5. Run `testAll()` to validate (300+ seconds, NEVER CANCEL) - -### Modifying Existing Analysis - -1. Update R function maintaining existing interface -2. Update QML if adding/changing options -3. Update unit tests and expected results -4. Add upgrade mapping to `inst/Upgrades.qml` if renaming options -5. Run tests: `testAll()` (NEVER CANCEL, 300+ seconds) - -### Detailed Development Process -- **Step 1**: Create main analysis function with `jaspResults`, `dataset`, `options` arguments -- **Step 2**: **CRITICAL** - Use `.quitAnalysis()` for `dataset`, `TextField`, `FormulaField` validation only -- **Step 3**: Create output tables/plots with proper dependencies, citations, column specs -- Use `createJaspTable()`, `createJaspPlot()`, `createJaspHtml()` for output elements -- Always set `$dependOn()` for proper caching and state management -- Use containers for grouping related elements, state objects for reusing computed results diff --git a/.github/instructions/R.instructions.md b/.github/instructions/R.instructions.md deleted file mode 100644 index f83315d31..000000000 --- a/.github/instructions/R.instructions.md +++ /dev/null @@ -1,189 +0,0 @@ ---- -applyTo: "**/R/*.R" -description: "R function structure, validation, jaspResults API, output components, style conventions" ---- - -# R Instructions - -## 1) Core Basics - -- **Main entry point (name matters):** - - The R function name **must match** the case-sensitive `"function"` field in `Description.qml`. - - Signature is always: - ```r - AnalysisName <- function(jaspResults, dataset, options) { ... } - ``` - - `jaspResults` is a container that stores all of the analysis output and byproducts (if they are supposed to be kept for later use). - - `dataset` is the loaded dataset in JASP - - `options` are the UI choices from QML; **do not rename** option keys (they’re your API). - -- **Recommended structure (3 roles):** - 1) **Main function** orchestrates and wires output elements. - 2) **create* functions** declare output markup (tables/plots/text). - 3) **fill* (or compute*) functions** compute results and fill outputs. - -- **Dependencies (cache & reuse):** - Add `$dependOn()` to every output (table/plot/text/container/state) so JASP knows when to reuse or drop it. - Outputs nested within containers inherit all dependencies from the container. - -- **Errors:** - - Catch run-time errors with `try(...)` and report via `$setError()`. - - Wrap user-visible text with `gettext()` / `gettextf()` for translation. - ---- - -## 2) Input Validation - -Only validate the `dataset`. `options` input is validated in the QML automatically. - -Common checks (prefix arguments with the check name): -```r -.hasErrors( - dataset, type = c("factorLevels", "observations", "variance", "infinity", "missingValues"), - factorLevels.target = options$variables, - factorLevels.amount = "< 1", - observations.target = options$variables, - observations.amount = "< 1" -) -``` -Other useful checks: -- `limits.min/max` (inclusive bounds), -- `varCovData.target/corFun` (positive-definiteness), -- `modelInteractions` (ensure lower-order terms exist). - ---- - -## 3) Output Components - -### Tables — `createJaspTable()` -**Key methods/properties:** -- `$dependOn()` -- `$addCitation("")` -- `$addColumnInfo(name, title, type = "string|number|integer|pvalue", format = "sf:4;dp:3", combine = FALSE, overtitle = NULL)` -- `$showSpecifiedColumnsOnly <- TRUE` (hide unspecified stats you happen to compute) -- `$setExpectedSize(nRows)` (for long computations) -- `$addFootnote(message, colNames = NULL, rowNames = NULL)` -- `$addRows(list(...))` or `$setData(df)` -- `$setError("")` - -**Skeleton:** -```r -.createMyTable <- function(jaspResults, dataset, options, ready) { - if (!is.null(jaspResults[["mainTable"]])) return() - tab <- createJaspTable(title = gettext("My Table")) - tab$dependOn(c("variables", "alpha", "showCI")) - tab$addColumnInfo("variable", gettext("Variable"), "string", combine = TRUE) - tab$addColumnInfo("estimate", gettext("Estimate"), "number") - if (options$showCI) { - over <- gettextf("%f%% CI", 100 * options[["alpha"]]) - tab$addColumnInfo("lcl", gettext("Lower"), "number", overtitle = over) - tab$addColumnInfo("ucl", gettext("Upper"), "number", overtitle = over) - } - tab$showSpecifiedColumnsOnly <- TRUE - jaspResults[["mainTable"]] <- tab - if (!ready) return() - .fillMyTable(tab, dataset, options) -} -``` - -### Plots — `createJaspPlot()` -**Key methods/properties:** -- `$dependOn()`, `$addCitation()` -- Set `plotObject <- ggplot2::ggplot(...)` -- `$setError("")` - -**Skeleton:** -```r -.createMyPlot <- function(jaspResults, dataset, options, ready) { - if (!is.null(jaspResults[["descPlot"]])) return() - plt <- createJaspPlot(title = gettext("My Plot"), width = 400, height = 300) - plt$dependOn(c("variables", "alpha")) - jaspResults[["descPlot"]] <- plt - if (!ready) return() - .fillMyPlot(plt, dataset, options) -} -``` - -### Text blocks — `createJaspHtml()` -Display formatted messages; can depend on options like other outputs. -```r -if (!is.null(jaspResults[["note"]])) return() -msg <- createJaspHtml(text = gettextf("The variable %s was omitted.", options[["variable"]])) -msg$dependOn(c("variable")) -jaspResults[["note"]] <- msg -``` - -### Containers — `createJaspContainer()` -Group related outputs; container dependencies propagate to children. Useful for “one-per-variable” sections. -- `$dependOn(...)`, `$setError("")`, `$getError()` -- Nest containers freely. - -```r -if (is.null(jaspResults[["descGroup"]])) { - grp <- createJaspContainer(title = gettext("Descriptive Plots")) - grp$dependOn(c("variables", "alpha")) - jaspResults[["descGroup"]] <- grp -} else { - grp <- jaspResults[["descGroup"]] -} -for (v in options[["variables"]]) { - if (!is.null(grp[[v]])) next - p <- createJaspPlot(title = v, width = 480, height = 320) - p$dependOn(optionContainsValue = list(variables = v)) - grp[[v]] <- p -} -``` - -### State (cache) — `createJaspState()` -Cache computed results across reruns (while dependencies hold). -- `$dependOn(...)` -- `$object <- results` (store) / `results <- state$object` (retrieve) - -```r -.stateCompute <- function(jaspResults, dataset, options) { - st <- createJaspState() - st$dependOn(c("variables", "alpha")) - jaspResults[["internalResults"]] <- st - res <- colMeans(dataset[options[["variables"]]], na.rm = TRUE) - st$object <- res -} -``` - ---- - -## 4) Style & Conventions - -- **Follow the project R style guide.** Keep functions short; prefer pure helpers; avoid global state; no I/O or printing in analyses. -- **Naming:** - - Helpers start with a dot, e.g., `.computeFoo()`, `.fillBarTable()`, `.plotBaz()`. - - Stable keys in `jaspResults[["..."]]` (don’t rename them later). -- **Internationalization:** All visible text via `gettext()`/`gettextf()`. -- **Performance:** Read only needed columns; postpone decoding; reuse `createJaspState()` when multiple outputs share results. -- **Robustness:** Validate early; guard long loops with `if (!ready) return()`; wrap risky code in `try()` and call `$setError()`. -- **Reproducibility:** Set column formats explicitly in tables; document assumptions in footnotes/citations. -- **Assignment alignment:** -For related assignments allign them at the arrow `<-`, i.e., -``` -variableOne <- foo() -variableFive <- foo() -``` -and allign function arguments in the similar way for function whose call is too long to be on a single line: -``` -out <- foo( - argumentOne = variableOne, - argumentFive = variableFive, - ... -) - ---- - -## 5) Minimal main() template (copy/paste) - -```r -MyAnalysis <- function(jaspResults, dataset, options) { - - ready <- length(options[["variables"]]) > 0 - - .createMyTable(jaspResults, dataset, options, ready) - .createMyPlot(jaspResults, dataset, options, ready) -} diff --git a/.github/instructions/fix-debug-analysis.instructions.md b/.github/instructions/fix-debug-analysis.instructions.md deleted file mode 100644 index fce43bd1a..000000000 --- a/.github/instructions/fix-debug-analysis.instructions.md +++ /dev/null @@ -1,432 +0,0 @@ ---- -applyTo: "**/R/*.R" -description: "Fixing bugs, debugging errors, and troubleshooting JASP analyses via code inspection and saveRDS state capture" ---- - -# Fix & Debug JASP Analysis (MCP Session) - -Quick reference for debugging JASP analysis functions through MCP sessions. - -**Note**: `browser()` and `recover()` require interactive R console and **do not work** through MCP's `btw_tool_run_r`. - ---- - -## 1) Debugging Approaches - -There are two approaches, in order of preference: - -### Approach A: Code Inspection (try first) - -Many bugs — especially logic errors, missing branches, wrong conditions — are solvable by reading the code and tracing the control flow. This is faster and doesn't require instrumenting code. - -1. **Reproduce**: Bootstrap a `runAnalysis()` call (Step 0) and confirm the issue -2. **Read**: Trace the code path from the entry-point function through the relevant helpers -3. **Identify**: Look for logic errors — wrong conditions, missing option checks, incorrect branching -4. **Fix**: Edit the source, hot-reload, and verify - -**Use this when**: Output is missing, wrong options are checked, a feature works in one analysis type but not another, UI options don't match R-side logic. - -### Approach B: saveRDS State Capture (escalation) - -When the bug depends on runtime values that can't be deduced from code reading alone. - -1. **Instrument**: Add saveRDS() before the error location -2. **Capture**: Hot-reload and run analysis, copy debug path from console -3. **Inspect**: Load saved state and examine values via MCP -4. **Fix**: Develop and test fix using captured state -5. **Verify**: Remove debug code, hot-reload, confirm fix works - -**Use this when**: Error depends on specific data values, unexpected NULL/type, dimension mismatches, or the code path is too complex to trace by reading. - ---- - -## 2) Reproducing the Issue - -### Step 0: Bootstrap a Reproducible Analysis Run - -Before debugging, you need a working `runAnalysis()` call that reproduces the error. Choose the first applicable source: - -#### Option A: User provides a .jasp file - -```r -jaspFile <- "path/to/file.jasp" -opts <- jaspTools::analysisOptions(jaspFile) -dataset <- jaspTools::extractDatasetFromJASPFile(jaspFile) -encoded <- jaspTools:::encodeOptionsAndDataset(opts, dataset) -set.seed(1) -results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE) -``` - -If the .jasp file contains multiple analyses, `analysisOptions()` returns a list — index with `[[1]]`, `[[2]]`, etc. Pick the analysis that matches the error context. - -#### Option B: Extract from existing unit tests (most common fallback) - -When no .jasp file is provided, **search test files first**. Test files contain pre-configured options and dataset references that are known to produce complete output. - -1. **Find the test file** for the analysis in `tests/testthat/`: - ``` - grep -r "AnalysisName" tests/testthat/ - ``` - -2. **Determine the input pattern** used in the test. Tests use one of two patterns: - - **Pattern 1 — .jasp example file** (look for `analysisOptions(jaspFile)` or `extractDatasetFromJASPFile`): - ```r - # Copy the loading code from the test, adjusting the path for non-test context - jaspFile <- file.path("examples", "Example Name.jasp") - opts <- jaspTools::analysisOptions(jaspFile)[[1]] # note: may need [[1]] for multi-analysis files - dataset <- jaspTools::extractDatasetFromJASPFile(jaspFile) - encoded <- jaspTools:::encodeOptionsAndDataset(opts, dataset) - set.seed(1) - results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE) - ``` - - **Pattern 2 — inline options** (look for `analysisOptions("AnalysisName")` with explicit option assignments): - ```r - # Copy the options setup from the test verbatim - options <- jaspTools::analysisOptions("AnalysisName") - options$dependent <- "contNormal" # copy from test - options$group <- "contBinom" # copy from test - # ... copy ALL option assignments from the test ... - set.seed(1) - results <- jaspTools::runAnalysis("AnalysisName", "debug.csv", options) - ``` - -3. **Modify options** to match the bug-triggering scenario (e.g., enable/disable specific checkboxes). - -4. **Verify reproduction**: Check that the issue is reproduced — this could be a `"fatalError"` status, an error message in a specific output element, incorrect values, missing output, etc., depending on what the user reported. - -#### Option C: Build options from scratch (last resort) - -Only when no tests or examples exist: - -```r -options <- jaspTools::analysisOptions("AnalysisName") -# Set required inputs — check .robttCheckReady() or equivalent readiness function -# to discover which options must be non-empty -options$dependent <- "contNormal" -options$group <- "contBinom" -set.seed(1) -results <- jaspTools::runAnalysis("AnalysisName", "debug.csv", options) -``` - -**Tip**: `jaspTools::analysisOptions("AnalysisName")` returns all options with their QML defaults. Inspect it with `str(options)` to understand available options and their types. - ---- - -## 3) saveRDS Workflow (Approach B) - -Use these steps when code inspection alone is insufficient and you need to examine runtime values. - -### Step 1: Identify Error Location - -From the error message and stack trace, locate the function and approximate line where the error occurs. - -**Example**: Stack trace shows `.buildTable()` → `table$addFootnote()` → error - -### Step 2: Instrument Code - -Add saveRDS() just **before** the line that's failing: - -```r -.buildTable <- function(jaspResults, options) { - # ... existing code ... - - someVariable <- computeSomething(data, options) - - # DEBUG: REMOVE - save state before error - debug_dir <- tempdir() - saveRDS(list( - someVariable = someVariable, - relatedData = relatedData, - fit = fit, - options = options - # Include ALL relevant variables - ), file.path(debug_dir, "debug_state.rds")) - message("DEBUG: Saved to ", file.path(debug_dir, "debug_state.rds")) - - # The line that's failing - processData(someVariable) -} -``` - -**Critical rules**: -- Always use marker comment `# DEBUG: REMOVE` -- **Never save `jaspResults`** (crashes R) -- Save to `tempdir()` (auto-cleanup) -- Include `message()` to print path to console -- Save ALL variables that might be relevant - -### Step 3: Hot-Reload and Capture - -```r -# Via btw_tool_run_r in MCP -devtools::load_all() - -# Re-run the analysis (use same code that triggered original error) -set.seed(1) -results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE) - -# Console output will show: -# DEBUG: Saved to C:/Users/.../Temp/RtmpXXX/debug_state.rds -``` - -Copy the debug path from the console output. - -### Step 4: Inspect Captured State - -```r -# Via btw_tool_run_r in MCP -debug_path <- "C:/Users/.../Temp/RtmpXXX/debug_state.rds" -debug_data <- readRDS(debug_path) - -# Examine structure -str(debug_data) - -# Inspect specific variables -print(debug_data$someVariable) -sapply(debug_data$someVariable, class) -any(sapply(debug_data$someVariable, is.null)) - -# Check attributes -for (i in seq_along(debug_data$relatedData)) { - cat("Item", i, "attribute:", attr(debug_data$relatedData[[i]], "someAttr"), "\n") -} -``` - -**Goal**: Identify the exact values causing the error. - -### Step 5: Develop Fix - -Based on inspection, develop fix logic using the saved objects: - -```r -# Via btw_tool_run_r in MCP -# Test the fix logic interactively using saved state - -# Example: Filter out invalid values -someVariable_clean <- Filter(function(x) !is.null(x) && is.finite(x), debug_data$someVariable) -print(someVariable_clean) # Verify it works - -# Try the fix -for (i in seq_along(someVariable_clean)) { - cat("Would process item:", someVariable_clean[[i]], "\n") -} -``` - -Once fix logic works, implement it in the source file. - -### Step 6: Clean Up and Verify - -1. Apply fix to source file -2. **Remove all debug code** (saveRDS(), message(), and "# DEBUG: REMOVE" markers) -3. Hot-reload and verify: - -```r -# Via btw_tool_run_r in MCP -devtools::load_all() - -set.seed(1) -results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE) - -# Check overall status -cat("Status:", results$status, "\n") -``` - -4. **Verify the specific issue is resolved** — don't just check `results$status`: - - If the bug was missing output: confirm the output element now exists in `results$results` - - If the bug was wrong values: check the specific table/cell values - - If the bug was an error in a subcomponent: navigate to that component and verify no error - - If the bug was a crash: confirm status is `"complete"` - -5. Search for any remaining debug code before committing: - -```bash -grep -r "DEBUG: REMOVE" R/ -grep -r "saveRDS.*tempdir" R/ -``` - ---- - -## 4) What to Save - -| Location | Objects to save | DON'T save | -|----------|----------------|------------| -| **Model fitting** | `dataset`, `options`, function args, intermediate values | `jaspResults`, `...` (ellipsis args) | -| **Row building** | `fit`, `attr(fit, "group")`, computed rows, `options` | Parent containers, environments | -| **Table assembly** | `rows` list, intermediate data.frames | Full fit objects if not needed | -| **Error handling** | Error object, variables being processed when error occurred | Large intermediate objects | - -**Golden rule**: When unsure, save it. Missing a variable means re-running the entire capture process. - ---- - -## 5) Real-World Example - -**Error**: `jaspTable$addFootnote expects 'message' to be a string!` - -**Workflow**: - -1. **Loaded .jasp file and reproduced error**: - ```r - jaspFile <- "path/to/file.jasp" - opts <- jaspTools::analysisOptions(jaspFile) - dataset <- jaspTools::extractDatasetFromJASPFile(jaspFile) - encoded <- jaspTools:::encodeOptionsAndDataset(opts, dataset) - set.seed(1) - results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE) - # → Status: fatalError - ``` - -2. **Identified error location**: Stack trace → `.buildTable()` at specific line - -3. **Instrumented code**: - ```r - footnotes <- unique(lapply(dataList, attr, which = "footnote")) - - # DEBUG: REMOVE - saveRDS(list( - footnotes = footnotes, - dataList = dataList - ), file.path(tempdir(), "footnote_debug.rds")) - message("DEBUG: Saved to ", file.path(tempdir(), "footnote_debug.rds")) - - for (i in seq_along(footnotes)) - table$addFootnote(footnotes[[i]]) - ``` - -4. **Captured state**: - ```r - devtools::load_all() - results <- jaspTools::runAnalysis(...) - # Console: DEBUG: Saved to C:/Users/.../RtmpXXX/footnote_debug.rds - ``` - -5. **Inspected**: - ```r - debug_data <- readRDS("C:/Users/.../RtmpXXX/footnote_debug.rds") - str(debug_data$footnotes) - # List of 2 - # $ : chr "Some footnote text..." - # $ : NULL ← THE PROBLEM - ``` - -6. **Root cause**: `unique()` preserves NULL values → loop called `addFootnote(NULL)` → error - -7. **Implemented fix**: - ```r - footnotes <- unique(lapply(dataList, attr, which = "footnote")) - footnotes <- Filter(Negate(is.null), footnotes) # Filter NULLs - for (i in seq_along(footnotes)) - table$addFootnote(footnotes[[i]]) - ``` - -8. **Verified**: - ```r - devtools::load_all() - results <- jaspTools::runAnalysis(...) - # → Status: complete ✓ - ``` - -**Time**: ~5 minutes from error to verified fix. - ---- - -## 6) Advanced Techniques - -### Conditional Saving - -For errors in specific iterations/groups: - -```r -# Only save when condition is met -for (i in seq_along(items)) { - if (i == 47) { # Error only in iteration 47 - saveRDS(list(item = items[[i]], i = i), file.path(tempdir(), "debug_iter47.rds")) - message("DEBUG: Saved iteration 47") - } - result <- process(items[[i]]) -} -``` - -### Multiple Checkpoints - -Narrow down error location by saving at multiple points: - -```r -# Checkpoint 1 -saveRDS(list(step = "before_transform", data = data), - file.path(tempdir(), "checkpoint1.rds")) - -data_transformed <- transform(data) - -# Checkpoint 2 -saveRDS(list(step = "after_transform", data_transformed = data_transformed), - file.path(tempdir(), "checkpoint2.rds")) -``` - -### Save with Timestamp - -For multiple runs: - -```r -timestamp <- format(Sys.time(), "%Y%m%d_%H%M%S") -saveRDS(list(...), file.path(tempdir(), paste0("debug_", timestamp, ".rds"))) -``` - ---- - -## 7) Common Error Patterns - -### Pattern 1: Unexpected NULL - -**Symptom**: "argument is NULL" or "expects X to be a Y" - -**Debugging**: -```r -saveRDS(list(suspect_var = suspect_var, related_vars = list(...)), ...) -# Inspect: is.null(debug_data$suspect_var) -``` - -### Pattern 2: Wrong Type/Class - -**Symptom**: "cannot coerce X to Y" or "is not a valid type" - -**Debugging**: -```r -saveRDS(list(var = var, class = class(var), str = capture.output(str(var))), ...) -# Inspect: class(debug_data$var), attributes(debug_data$var) -``` - -### Pattern 3: Dimension Mismatch - -**Symptom**: "dims [product X] do not match length of object [Y]" - -**Debugging**: -```r -saveRDS(list(obj = obj, dims = dim(obj), length = length(obj)), ...) -# Inspect: dim(debug_data$obj), length(debug_data$obj) -``` - -### Pattern 4: Index Out of Bounds - -**Symptom**: "subscript out of bounds" or "undefined columns selected" - -**Debugging**: -```r -saveRDS(list(container = container, index = i, length = length(container)), ...) -# Inspect: i vs length(debug_data$container), names(debug_data$container) -``` - ---- - -## 8) Safety Checklist - -Before committing code: - -- [ ] All `# DEBUG: REMOVE` markers removed -- [ ] All `saveRDS()` calls removed -- [ ] All debug `message()` calls removed -- [ ] Verified with: `grep -r "DEBUG: REMOVE" R/` -- [ ] Verified with: `grep -r "saveRDS.*tempdir" R/` -- [ ] Hot-reloaded and tested: analysis completes successfully diff --git a/.github/instructions/inst.qml.instructions.md b/.github/instructions/inst.qml.instructions.md deleted file mode 100644 index 83b4c0812..000000000 --- a/.github/instructions/inst.qml.instructions.md +++ /dev/null @@ -1,177 +0,0 @@ ---- -applyTo: "**/inst/qml/*.qml" -description: "QML controls, validation, bindings, UI patterns, and qmllint syntax checking" ---- - -# JASP QML Instructions - -## 0) QML Syntax Validation - -**ALWAYS validate QML files after editing** using `qmllint` to catch syntax errors: - -```powershell -qmllint inst\qml\path\to\file.qml -``` - -- **Ignore import warnings**: Warnings about missing `JASP.Controls` and `JASP` modules are expected (qmllint lacks JASP's custom modules) -- **Focus on syntax errors**: Look for missing braces `{}`, brackets `[]`, parentheses `()`, semicolons, or malformed property assignments -- **Exit code matters**: Non-zero exit with syntax errors blocks parsing; zero exit means parseable (even with import warnings) -- **Run before committing**: Catch structural issues (extra/missing braces) that break QML parsing - -Example of ignorable warnings: -``` -Warning: Failed to import JASP.Controls [import] -Warning: IntegerField was not found [import] -``` - -Example of critical errors: -``` -Error: Expected token `}' [syntax] -``` - -## 1) Core Basics - -- **Imports:** - ```qml - import QtQuick - import QtQuick.Layouts - import JASP.Controls - import JASP - ``` - -- **Form as root:** Every analysis UI is a `Form { ... }` containing controls, usually a `VariablesForm` block and option controls. -- **Binding & IDs:** Prefer *property bindings* (reactive JS expressions) over imperative changes; reference other items via `id:` and bind (`enabled: show.checked || useAlt.checked`). -- **Stable storage names:** The `name:` of a control maps to stored options in JASP files; **avoid renaming**. If you must, handle migrations in `Upgrades.qml`. -- **Translation & docs:** - - Wrap **all user-visible strings** in `qsTr("Text")`. - - Populate `info:` with a short, user-facing description (also wrapped in `qsTr`) to feed module help. -- **Variables workflow:** Place variable pickers inside a `VariablesForm`; connect lists with `source:` (can read all data columns, other lists, levels, or R sources). - -## 2) Input Validation - -Prefer **declarative validation** via built-in field properties: - -- **Numeric fields** (`DoubleField`, `IntegerField`): set `min`, `max`, and `inclusive` (e.g., `MinMax`), `decimals` (for doubles), and allow negatives only when needed. Use `fieldWidth` for compact UI. -- **Percent & CI** (`PercentField`, `CIField`): sensible defaults (e.g., 95), `afterLabel` defaults to `"%"`. -- **Slider:** set `min`, `max`, `decimals`; prefer horizontal sliders unless space constrained. -- **FormulaField:** accepts R-style expressions; constrain with `min`, `max`, `inclusive`; use `multiple: true` only when arrays are intended. Read via `realValue` / `realValues`. -- **TableView:** for mixed types, define validators and override `getValidator(col,row)`; optionally specify `itemTypePerRow/Column`. -- **Variables lists:** enforce data types via `allowedColumns: ["scale"|"ordinal"|"nominal"]` and `singleVariable: true` where appropriate. - -## 3) Main Custom Components - -### General input -- **CheckBox** — `name`, `label`, `checked`, `childrenOnSameRow`, `columns` (nested controls auto-enable/disable). -- **RadioButtonGroup / RadioButton** — group has `name`, `title`, `radioButtonsOnSameRow`, `columns`; each button has `value`, `label`, `checked`; can contain nested controls per choice. -- **DropDown** — `name`, `label`, `values` (array or `{label, value}`), or `source`; selection via `startValue` / `currentValue`; `addEmptyValue`, `placeHolderText`. -- **Slider** — `name`, `label`, `value`, `min`, `max`, `decimals`. -- **DoubleField / IntegerField** — `label`, `defaultValue`, `min`, `max`, `inclusive`, (`decimals` for DoubleField). -- **PercentField / CIField** — percent-specific shorthand; defaults appropriate for CIs. -- **TextField** — `defaultValue` or `placeholderText` (mutually exclusive), `afterLabel`, `fieldWidth`. -- **FormulaField** — adds `realValue`, `min/max`, `inclusive`, `multiple`, `realValues`. -- **TextArea** — `title`, `text`, `textType` (e.g., R code / JAGS / Lavaan / Model / Source), `separator(s)`, `applyScriptInfo` (submit with **Ctrl+Enter**). - -### Variable specification -- **AvailableVariablesList** — `name`, `label`, **rich `source`** (other lists, levels, filters, `rSource`, combinations), or `values`; `width`, `count` (read-only). -- **AssignedVariablesList** — `name`, `label`, `allowedColumns`, `singleVariable`, `maxRows`, `listViewType` (e.g., `Interaction`), optional `rowComponent` (+ `rowComponentTitle`), `optionKey`, `count`. -- **FactorLevelList** — define RM factors/levels: `factorName`, `levelName`, `minFactors`, `minLevels`, `width`, `height`. Often paired with an `AssignedVariablesList` of type `MeasuresCells`. - -### Complex composition -- **ComponentsList** — templated rows of controls from a `source` or `values`; `titles`, `rowComponent`, manual rows via `addItemManually`, bounds via `minimumItems` / `maximumItems`, collected under `optionKey`. -- **TabView** — `ComponentsList` rendered as tabs. -- **InputListView** — user adds rows via an input field; `title`, `placeHolder`, `defaultValues`, `minRows`, `inputComponent` (Text/Double/Integer), optional `rowComponent`, `optionKey`. -- **TableView** — `name`, `modelType` (`MultinomialChi2Model`, `JAGSDataInputModel`, `FilteredDataEntryModel`, `CustomContrasts`), `itemType` or per-row/column types, `source`; may override `getColHeaderText`, `getRowHeaderText`, `getDefaultValue`, `getValidator`. - -### Grouping & structure -- **Group** — logical block with `title`, `columns`. Nest options inside. -- **Section** — collapsible panel for advanced options; `title`, `columns`. Use for lower-priority / expert settings. - -## 4) Style & UX Conventions - -- **Titles & labels:** Title Case for section/group titles; concise labels; every visible string uses `qsTr()`. The `name` is always the title transformed into camelCase. Options within groups inherit their names as a prefix. -- **Consistency:** Prefer the provided JASP controls over ad-hoc QML; nest subordinate options inside the control that enables them (e.g., a `CheckBox` containing its dependent fields). -- **Two-column rhythm:** Let the grid flow naturally; use `rowSpan/columnSpan` to avoid awkward gaps; avoid long single-column scrollers. -- **Variables first:** Place `VariablesForm` at the top; align list widths; restrict types with `allowedColumns`. -- **Defaults & placeholders:** Prefer meaningful `defaultValue`; use `placeholderText` only when input is optional. Don’t set both. -- **Dropdowns:** Use `{label, value}` pairs when R-side value differs; add an explicit empty choice with `addEmptyValue` if “no selection” is valid. -- **Advanced options:** Tuck rare/expert settings into a `Section` titled “Advanced Options”. -- **Docs:** Fill `info:` succinctly for every major control. -- **Spacing:** Always use tabs for spacing. Each argument on a new line. (See examples below.) - -## 5) Quick Patterns - -- **Enable dependent field(s):** - ```qml - CheckBox - { - id: show - name: "showX" - label: qsTr("Show X") - } - - DoubleField - { - name: "Alpha" - label: qsTr("Alpha") - defaultValue: 0.05 - min: 0 - max: 1 - decimals: 3 - enabled: show.checked - } - ``` - -- **Radio choice with per-choice inputs:** - ```qml - RadioButtonGroup - { - name: "crit" - title: qsTr("Criterion") - - RadioButton - { - value: "pValue" - label: qsTr("p-value") - checked: true - - DoubleField - { - name: "pValueValue" - label: "" - defaultValue: 0.05 - min: 0 - max: 1 - } - } - - RadioButton - { - ... - } - } - ``` - -- **Variables form (single DV):** - ```qml - VariablesForm - { - AvailableVariablesList - { - name: "availableVariables" - } - - AssignedVariablesList - { - name: "dependentVariable" - label: qsTr("Dependent Variable") - allowedColumns: ["scale"] - singleVariable: true - } - } - ``` - -## 6) When in doubt - -- Prefer built-in JASP controls. -- Keep `name:` stable; translate strings; validate inputs. -- Put rare/expert options in a `Section` and document via `info:`. diff --git a/.github/instructions/jasp-containers-and-errors.instructions.md b/.github/instructions/jasp-containers-and-errors.instructions.md deleted file mode 100644 index 9ab911e3c..000000000 --- a/.github/instructions/jasp-containers-and-errors.instructions.md +++ /dev/null @@ -1,146 +0,0 @@ ---- -applyTo: "**/R/*.R" -description: "Container patterns, HTML output, and error handling in jaspResults" ---- - -# JASP Containers, HTML Output & Error Handling - -Patterns for grouping output elements and handling errors in jaspResults. - -For tables see [jasp-tables.md](jasp-tables.md). -For plots see [jasp-plots.md](jasp-plots.md). -For state/caching see [jasp-state-management.md](jasp-state-management.md). - ---- - -## 1) Containers - -Containers group related output elements under a collapsible section. - -### Get-or-create pattern (reusable across multiple builder functions) - -```r -.myExtractContainer <- function(jaspResults) { - if (!is.null(jaspResults[["myContainer"]])) - return(jaspResults[["myContainer"]]) - - container <- createJaspContainer(gettext("My Section Title")) - container$dependOn(.myBaseDependencies) - container$position <- 1 - jaspResults[["myContainer"]] <- container - - return(container) -} -``` - -- Use a dedicated extractor when **multiple builder functions** write to the same container -- `$position` controls display order (lower = higher on page) -- `$dependOn()` on the container invalidates **all children** when base options change - -### Direct creation (when only one function writes to it) - -```r -if (is.null(jaspResults[["sectionContainer"]])) { - container <- createJaspContainer(gettext("Section Title")) - container$dependOn(c(.baseDependencies, "specificOption")) - container$position <- 4 - jaspResults[["sectionContainer"]] <- container -} -``` - -### Nested containers - -For deeply hierarchical output (e.g., per-variable tables): - -```r -outerContainer <- jaspResults[["outer"]] -innerContainer <- createJaspContainer(title = "Variable X") -innerContainer$position <- i -outerContainer[["variableX"]] <- innerContainer -# then add tables/plots to innerContainer -``` - -### Dynamic container management - -When the set of children depends on user-selected variables: - -```r -# Track existing vs selected variables via metadata state -existingVariables <- metaData[["existingVariables"]] -selectedVariables <- getSelectedVariables(options) - -# Remove deselected -for (v in setdiff(existingVariables, selectedVariables)) - container[[v]] <- NULL - -# Add new -for (v in setdiff(selectedVariables, existingVariables)) { - childContainer <- createJaspContainer(title = v) - container[[v]] <- childContainer - .buildChildTable(childContainer, fit, options, v) -} - -# Update metadata -metaDataState$object <- list(existingVariables = selectedVariables) -``` - -See [jasp-state-management.md](jasp-state-management.md) for the metadata state pattern that powers this. - ---- - -## 2) HTML Output - -For raw HTML content (e.g., displaying R code or formatted messages): - -```r -htmlOutput <- createJaspHtml(title = gettext("R Code")) -htmlOutput$dependOn(c(.baseDependencies, "showCode")) -htmlOutput$position <- 99 -htmlOutput$text <- "
myFunction(yi = ..., sei = ...)
" -jaspResults[["rCode"]] <- htmlOutput -``` - ---- - -## 3) Error Handling Patterns - -### Create-then-error - -Always **attach the element to jaspResults before checking errors**. This ensures the empty table (with error message) is displayed rather than nothing: - -```r -table <- createJaspTable(gettext("Title")) -container[["table"]] <- table # attach FIRST - -# THEN check for errors -if (someError) { - table$setError(errorMessage) - return() -} -``` - -### Graceful degradation with groups - -When some per-group fits fail but others succeed, show partial results with per-group error footnotes: - -```r -# Row builders return skeleton data.frames on error (labels only, NAs for numeric columns) -# Tables show partial results with error footnotes per failed group -for (i in which(sapply(fit, jaspBase::isTryError))) { - table$addFootnote( - gettextf("Group '%1$s' failed: %2$s", attr(fit[[i]], "group"), .cleanError(fit[[i]])), - symbol = gettext("Error:") - ) -} -``` - -### Total failure - -When the entire fit fails: - -```r -if (length(fit) == 1 && jaspBase::isTryError(fit[[1]])) { - table$setError(.cleanErrorMessage(fit[[1]])) - return() -} -``` diff --git a/.github/instructions/jasp-dependency-management.instructions.md b/.github/instructions/jasp-dependency-management.instructions.md deleted file mode 100644 index 85ac1a35f..000000000 --- a/.github/instructions/jasp-dependency-management.instructions.md +++ /dev/null @@ -1,136 +0,0 @@ ---- -applyTo: "**/R/*.R" -description: "dependOn mechanics, inheritance, vectors, per-value deps, sentinel pattern" ---- - -# JASP Dependency Management ($dependOn) - -How `$dependOn()` controls caching and invalidation of output elements in jaspResults. - -For the reactive loop context see [jasp-module-architecture.md](jasp-module-architecture.md). - -Note that you cannot test this by running analysis via `runAnalysis()` because you only generate one state at a time -(with no initial elements - ask the human maintainer to validate the dependencies manually if you suspect an issue!). - ---- - -## 1) What $dependOn Does - -When you write: -```r -table$dependOn(c("method", "ciLevel")) -``` - -You tell JASP Desktop: "If `options[["method"]]` or `options[["ciLevel"]]` changes, set this element to NULL before calling R." On the next R invocation, the builder's `if (!is.null(...))` guard sees NULL and recreates the element. - -Elements whose dependencies are NOT hit survive across invocations -- the builder returns early and the existing output stays on screen. - ---- - -## 2) Dependency Inheritance - -Container dependencies propagate to ALL children: - -```r -container$dependOn(c("dependentVariable", "method")) # base deps -table$dependOn(c("showCI")) # additional dep -container[["myTable"]] <- table -``` - -The table is invalidated if `dependentVariable`, `method`, OR `showCI` changes. Never repeat parent deps on children. - -This means you can put shared model-level dependencies on the container and only add output-specific deps to individual tables/plots. - ---- - -## 3) Dependency Vectors as Constants - -Define at file top for reuse across builders: -```r -.baseDeps <- c("dependentVariable", "covariates", "method", "ciLevel") -.plotDeps <- c("plotColor", "plotSize", "plotTheme") -``` - -Use in builders: -```r -container$dependOn(.baseDeps) # container holds base deps -table$dependOn(c("showResiduals")) # child adds specific dep -plot$dependOn(c(.baseDeps, .plotDeps)) # or combine for standalone elements -``` - -Keep dependency vectors comprehensive -- missing a dependency means stale output when that option changes. - ---- - -## 4) Conditional / Dynamic Dependencies - -When different analysis modes need different dependency sets: -```r -if (options[["variant"]] == "classical") { - fitState$dependOn(.classicalDeps) -} else { - fitState$dependOn(.bayesianDeps) -} -``` - -Or combine dynamically: -```r -plot$dependOn(c(.plotDeps, - if (options[["variant"]] == "classical") .classicalDeps else .bayesianDeps -)) -``` - ---- - -## 5) Per-Value Dependencies (optionContainsValue) - -For containers with one child per user-selected variable, invalidate only when that specific variable is removed: - -```r -for (v in options[["variables"]]) { - if (!is.null(container[[v]])) next - plot <- createJaspPlot(title = v) - plot$dependOn(optionContainsValue = list(variables = v)) - container[[v]] <- plot - # ... fill plot ... -} -``` - -If the user removes variable `"x"` from the list, only `container[["x"]]` is NULLed. Other children survive. - ---- - -## 6) Sentinel Pattern (Narrow Dependencies) - -When an expensive computation (e.g., model fit) should NOT be invalidated by visualization-only options, but the visualization data still needs updating: - -```r -# Broad deps: model options → invalidate and re-fit -fitState <- createJaspState() -fitState$dependOn(.modelDeps) -jaspResults[["fit"]] <- fitState - -# Narrow deps: plotting options → update auxiliary data without re-fitting -sentinel <- createJaspState() -sentinel$dependOn(.plottingDeps) -jaspResults[["fitDataUpdate"]] <- sentinel -``` - -When a plotting option changes: -- `jaspResults[["fit"]]` survives (model deps not hit) -- `jaspResults[["fitDataUpdate"]]` is NULLed (plotting deps hit) -- The update function sees the NULL sentinel, re-attaches updated auxiliary data to the existing fit - -This avoids expensive re-computation when only display options change. - ---- - -## 7) Common Pitfalls - -**Missing dependency:** If you forget to list an option in `$dependOn()`, changing that option won't invalidate the element. The user sees stale output. - -**Over-broad dependencies:** Putting ALL options on every element means everything gets recomputed on any change. Split into base deps (container) + specific deps (children). - -**Duplicate dependencies:** Listing a parent container's dep on a child is harmless but redundant. Keep it clean. - -**Forgetting $dependOn entirely:** The element will never be invalidated -- it's created once and persists forever, even when relevant options change. diff --git a/.github/instructions/jasp-module-architecture.instructions.md b/.github/instructions/jasp-module-architecture.instructions.md deleted file mode 100644 index 15fe5f701..000000000 --- a/.github/instructions/jasp-module-architecture.instructions.md +++ /dev/null @@ -1,272 +0,0 @@ ---- -applyTo: "**/R/*.R,**/inst/qml/*.qml" -description: "QML-Desktop-R reactive loop, jaspResults persistence, options mapping, data flow" ---- - -# JASP Module Architecture - -How QML, JASP Desktop, and R interact. This explains *why* the patterns in the other rule files exist. - -For dependency details see [jasp-dependency-management.md](jasp-dependency-management.md). -For state/caching see [jasp-state-management.md](jasp-state-management.md). -For R coding patterns see [jasp-tables.md](jasp-tables.md), [jasp-plots.md](jasp-plots.md), [jasp-containers-and-errors.md](jasp-containers-and-errors.md). -For serialized output format see [jasp-output-structure.md](jasp-output-structure.md). - ---- - -## 1) The Reactive Loop - -``` -User changes option in QML GUI - │ - ▼ -JASP Desktop collects ALL current option values into a flat named list - │ - ▼ -Desktop calls: AnalysisName(jaspResults, dataset, options) - │ │ │ │ - │ │ │ └─ named list of ALL QML option values - │ │ └─ data.frame loaded from the active dataset - │ └─ PERSISTENT container surviving across invocations - │ - ▼ -R function builds/updates output in jaspResults - │ - ▼ -Desktop reads jaspResults and renders tables/plots/text in the GUI -``` - -**Key insight:** Every time the user changes *anything* in the QML interface, Desktop calls the R analysis function again with a fresh `options` list but the **same** `jaspResults` object. This is why: - -1. Every builder checks `if (!is.null(jaspResults[["key"]])) return()` -- skip if output already exists and dependencies haven't changed. -2. `$dependOn()` tells Desktop which option changes should invalidate (NULL out) an element. See [jasp-dependency-management.md](jasp-dependency-management.md). -3. `createJaspState()` caches expensive computations so they survive across invocations. See [jasp-state-management.md](jasp-state-management.md). - ---- - -## 2) jaspResults: The Persistent Bridge - -`jaspResults` is an R5 reference class that persists between R invocations for the same analysis instance. It is NOT recreated each time. - -### Element lifecycle - -``` -1. Element does not exist → builder creates it, attaches to jaspResults -2. Options change, deps NOT hit → element survives, builder returns early -3. Options change, deps ARE hit → Desktop NULLs the element before calling R - → builder sees NULL, recreates it -4. User removes the analysis → jaspResults is destroyed entirely -``` - -### What can live in jaspResults - -| Create function | Purpose | Displayed? | -|----------------|---------|------------| -| `createJaspTable()` | Tabular output | Yes | -| `createJaspPlot()` | Plot output | Yes | -| `createJaspHtml()` | Raw HTML/text | Yes | -| `createJaspContainer()` | Groups children | Yes (collapsible section) | -| `createJaspState()` | Cache arbitrary R objects | **No** (invisible to user) | - -All five support `$dependOn()`. All five can be stored in jaspResults or nested inside a container. - -### Display ordering - -Every element has `$position` (integer). Lower = higher on page. Children within a container also have positions. - ---- - -## 3) Options: The Flat Named List - -### QML name → R options key - -Every QML control has a `name:` property. Desktop flattens ALL controls into a single named list regardless of QML nesting: - -```qml -CheckBox { - name: "showCI" // options[["showCI"]] = TRUE/FALSE - DoubleField { - name: "ciLevel" // options[["ciLevel"]] = 0.95 - defaultValue: 0.95 - } -} -``` - -Both `showCI` and `ciLevel` appear at the top level of `options`. QML nesting controls UI visibility/enabling but does NOT create nested R structures. - -### QML control → R value type - -| QML control | R type | Example value | -|-------------|--------|---------------| -| `CheckBox` | logical | `TRUE` / `FALSE` | -| `DropDown` | character | `"restrictedML"` | -| `RadioButtonGroup` | character | `"estimated"` (selected button's `value:`) | -| `AssignedVariablesList` | character | `"myColumn"` (single) or `c("a","b")` (multi) | -| `DoubleField` | numeric | `0.95` | -| `IntegerField` | integer | `1000L` | -| `TextField` | character | `"user text"` | -| `CIField` | numeric | `0.95` (0-1 scale) | -| `PercentField` | numeric | `95` (0-100 scale) | - -### Empty/unset variable slots - -When no variable is assigned to an `AssignedVariablesList`, the value is `""` (empty string): - -```r -if (options[["dependentVariable"]] != "") { ... } -``` - -For multi-variable lists, check `length(options[["variables"]]) > 0`. - -### Column encoding - -JASP internally encodes column names. In R analysis code, the encoding is transparent -- `dataset` columns are already encoded. Use `jaspBase::decodeColNames()` when displaying names in plot axes/labels. In tests, use `jaspTools:::encodeOptionsAndDataset()` when loading from .jasp files. - ---- - -## 4) Data Flow (Generic) - -``` -QML assigns variable names → options[["dependentVariable"]] = "score" - │ - ▼ -Desktop loads dataset with requested columns → dataset (data.frame) - │ - ▼ -Entry point: readiness check + data validation - - Are required variables assigned? - - .hasErrors(): infinity, observations, variance, etc. - │ - ▼ -Compute function: expensive model fitting, cached in state - - Wrap in try() for error handling - - Store result via createJaspState() - │ - ▼ -Builder functions: extract cached results, build output - - Tables: define columns, build rows, setData() - - Plots: build ggplot, assign to plotObject - - Errors: attach element FIRST, then setError() -``` - -Builders should handle the "not ready" case gracefully -- create empty tables (column headers but no data) so the user sees the output structure before assigning variables. - ---- - -## 5) The Entry Point → Common → Builder Pattern - -### Three-layer architecture - -``` -Layer 1: Entry point (thin wrapper per analysis) - MyAnalysis(jaspResults, dataset, options) - - Sets dispatch flags if sharing code with other analyses - - Validates data - - Delegates to orchestrator - -Layer 2: Orchestrator (flat sequence of builder calls) - MyAnalysisCommon(jaspResults, dataset, options) - - Calls .computeModel() # state - - Calls .summaryTable() # table - - Calls .coefficientsTable() # table - - Calls .mainPlot() # plot - - Conditional sections based on options - -Layer 3: Builders (idempotent, self-contained) - .summaryTable(jaspResults, options) - - Checks if output exists (return early if so) - - Gets/creates container - - Creates table, defines columns - - Extracts cached results - - Builds rows, sets data -``` - -### Multiple entry points sharing one orchestrator - -When related analyses share logic, they set a dispatch flag and delegate: - -```r -AnalysisVariantA <- function(jaspResults, dataset, options) { - options[["variant"]] <- "A" - if (.isReady(options)) { - dataset <- .checkData(dataset, options) - .checkErrors(dataset, options) - } - AnalysisCommon(jaspResults, dataset, options) -} - -AnalysisVariantB <- function(jaspResults, dataset, options) { - options[["variant"]] <- "B" - # ... same pattern ... - AnalysisCommon(jaspResults, dataset, options) -} -``` - -Builders branch on the flag: -```r -if (options[["variant"]] == "B") - .additionalTable(jaspResults, options) -``` - -### The readiness check - -Before model fitting, verify required inputs exist: - -```r -.isReady <- function(options) { - options[["dependentVariable"]] != "" && length(options[["covariates"]]) > 0 -} -``` - -In the entry point: -```r -if (.isReady(options)) { - dataset <- .checkData(dataset, options) - .checkErrors(dataset, options) -} -AnalysisCommon(jaspResults, dataset, options) -``` - ---- - -## 6) Registration & Backward Compatibility - -### Description.qml - -Registers analyses with their R function names: -```qml -Analysis { - title: qsTr("My Analysis") - func: "MyAnalysis" // must match R function name exactly (case-sensitive) -} -``` - -### NAMESPACE - -Every analysis entry point must be exported: -```r -export(MyAnalysis) -``` - -### Upgrades.qml - -When renaming QML option names, add a migration so old .jasp files load correctly: -```qml -Upgrade { - functionName: "MyAnalysis" - fromVersion: "0.17.2" - toVersion: "0.17.3" - - ChangeRename { from: "oldOptionName"; to: "newOptionName" } - - ChangeJS { - name: "transformedOption" - jsFunction: function(options) { - switch(options["transformedOption"]) { - case "oldValue": return "newValue"; - default: return options["transformedOption"]; - } - } - } -} -``` diff --git a/.github/instructions/jasp-output-structure.instructions.md b/.github/instructions/jasp-output-structure.instructions.md deleted file mode 100644 index 758ceb579..000000000 --- a/.github/instructions/jasp-output-structure.instructions.md +++ /dev/null @@ -1,195 +0,0 @@ ---- -applyTo: "**/tests/testthat/*.R,**/R/*.R" -description: "Reading and testing serialized output from runAnalysis (containers, tables, plots, state)" ---- - -# JASP Analysis Output Structure - -Reading and testing the serialized output from `jaspTools::runAnalysis()`. -For building tables see [jasp-tables.md](jasp-tables.md). For plots see [jasp-plots.md](jasp-plots.md). - -## 1) Top-Level `results` Object - -After `jaspTools::runAnalysis()`, the returned list has 5 keys: -- `status` -- `"complete"` or `"fatalError"` -- `results` -- nested list of all output elements (containers, tables, plots) -- `state` -- cached figures and computed objects -- `progress` -- progress info (usually empty after completion) -- `typeRequest` -- internal type info - -## 2) `results$results` Structure - -Contains: -- `.meta` -- recursive metadata describing the tree (type, name, title for each element) -- `name` -- analysis name -- Named elements for each output component (containers, tables, plots) - -### Element Types - -| Type | Key fields | How to identify | -|------|-----------|-----------------| -| **Container** | `collection`, `name`, `title`, `initCollapsed` | Has `$collection` (named list of children) | -| **Table** | `data`, `schema`, `name`, `title`, `status`, `footnotes`, `casesAcrossColumns` | Has `$schema` with `$fields` | -| **Plot/Image** | `data` (string path), `name`, `title`, `width`, `height`, `status`, `convertible` | Has `$data` as character string (e.g., `"plots/1.png"`) | - -## 3) Containers - -Containers group related output elements. Structure: -``` -container$collection -- named list of child elements (containers, tables, or plots) -container$name -- unique identifier (underscore-separated path) -container$title -- display title (can be "") -container$initCollapsed -- whether collapsed by default -``` - -**Naming convention:** Child names are parent name + `_` + child suffix. This creates a hierarchical path: -``` -modelSummaryContainer - modelSummaryContainer_testsTable - modelSummaryContainer_pooledEstimatesTable -``` - -Containers can nest arbitrarily deep: -``` -estimatedMarginalMeansAndContrastsContainer - estimatedMarginalMeansAndContrastsContainer_effectSize - estimatedMarginalMeansAndContrastsContainer_effectSize_adjustedEstimate - ..._adjustedEstimate_estimatedMarginalMeansTable -``` - -**Accessing deeply nested elements:** Chain `$collection` at each container level: -```r -results[["results"]][["containerName"]][["collection"]][["containerName_child"]][["collection"]][["containerName_child_table"]][["data"]] -``` - -## 4) Tables - -### Schema (`table$schema$fields`) -List of column definitions, each with: -- `name` -- field identifier (used as key in data rows) -- `title` -- display column header -- `type` -- `"string"`, `"number"`, `"integer"`, `"pvalue"` -- `format` (optional) -- formatting spec, e.g., `"sf:4;dp:3"`, `"dp:3;p:.001"` -- `overTitle` (optional) -- grouped column header (e.g., `"95% CI"` spanning Lower/Upper) - -### Data (`table$data`) -List of rows. Each row is a named list with field names as keys: -```r -table$data[[1]] # first row -# $est, $se, $lCi, $uCi, $pval, ... -``` - -**Key:** Fields within each row are **alphabetically sorted by name** (from JSON deserialization). - -### Footnotes (`table$footnotes`) -List of footnote objects: -```r -footnote$text -- footnote text -footnote$symbol -- HTML symbol (e.g., "Note.") -footnote$cols -- columns it applies to (NULL = all) -footnote$rows -- rows it applies to (NULL = all) -``` - -### Special Row Fields -- `.isNewGroup` -- boolean, marks visual row separator in JASP GUI -- These appear in `expect_equal_tables` flattened output - -## 5) Plots - -### In `results$results` -Plot entries store metadata only: -```r -plot$data -- string key into state$figures (e.g., "plots/1.png") -plot$name -- identifier -plot$title -- display title -plot$width -- pixel width -plot$height -- pixel height -plot$status -- "complete" -``` - -### In `results$state$figures` -Actual plot objects stored here, keyed by the `data` path: -```r -results$state$figures[["plots/1.png"]]$obj -- the plot object -results$state$figures[["plots/1.png"]]$width -results$state$figures[["plots/1.png"]]$height -``` - -### Plot Object Types -- **`jaspGraphsPlot`** (R6 class) -- composite plot with `$subplots` list of ggplot objects -- **Plain `ggplot`** -- single ggplot object (no subplots) - -### Retrieving Plot for Testing -```r -plotName <- results[["results"]][["plotElement"]][["data"]] -testPlot <- results[["state"]][["figures"]][[plotName]][["obj"]] -jaspTools::expect_equal_plots(testPlot, "snapshot-name") -``` - -## 6) State Object (`results$state`) - -- `state$figures` -- named list of plot objects (keyed by "plots/N.png") -- `state$other` -- named list of cached R objects (keyed by "state_N") - - Used by `createJaspState()` for caching expensive computations between output elements - -## 7) Testing Utilities - -### `expect_equal_tables(table_data, reference_list)` -1. Takes `table$data` (list of row-lists) -2. Flattens via `unname(unlist(rows))` -- row-by-row, fields in alphabetical order within each row -3. Converts numeric strings back to numbers via `charVec2MixedList` -4. Replaces unicode characters with `` placeholder -5. Compares element-by-element against flat reference list - -**Reference list format:** Single flat `list(...)` with all values row-by-row, fields alphabetically sorted: -```r -# For a table with fields: df, est, name, pval (alphabetical) -# Row 1: df=9, est=-0.69, name="Intercept", pval=0.50 -# Row 2: df=9, est=0.29, name="Slope", pval=0.01 -jaspTools::expect_equal_tables(table_data, - list(9, -0.69, "Intercept", 0.50, # row 1 - 9, 0.29, "Slope", 0.01)) # row 2 -``` - -### `expect_equal_plots(plot_obj, snapshot_name)` -- If `jaspGraphsPlot`: splits into subplots, each compared via `vdiffr::expect_doppelganger` with name `"snapshot-name-subplot-N"` -- If plain `ggplot`: compared directly via `vdiffr::expect_doppelganger` -- SVG snapshots stored in `tests/testthat/_snaps/` - -## 8) Quick Reference: Navigating Results - -```r -# Run analysis -results <- jaspTools::runAnalysis("AnalysisName", dataset, options) - -# Check status -results$status # "complete" or "fatalError" -results$results$errorMessage # if fatalError - -# Get table data (for expect_equal_tables) -results[["results"]][["containerName"]][["collection"]][["containerName_tableName"]][["data"]] - -# Get plot object (for expect_equal_plots) -plotKey <- results[["results"]][["plotName"]][["data"]] -plotObj <- results[["state"]][["figures"]][[plotKey]][["obj"]] - -# Inspect table schema -table$schema$fields # list of {name, title, type, format, overTitle} - -# Map entire tree (debug helper) -mapResults <- function(x, depth = 0) { - indent <- paste(rep(" ", depth), collapse = "") - if (is.list(x) && !is.null(x$collection)) { - cat(sprintf("%s[container] %s: '%s'\n", indent, x$name, x$title)) - for (child in x$collection) mapResults(child, depth + 1) - } else if (is.list(x) && !is.null(x$schema)) { - cat(sprintf("%s[table] %s: '%s' (%d rows x %d cols)\n", - indent, x$name, x$title, length(x$data), length(x$schema$fields))) - } else if (is.list(x) && !is.null(x$data) && is.character(x$data)) { - cat(sprintf("%s[plot] %s: '%s'\n", indent, x$name, x$title)) - } -} -for (item in results$results[setdiff(names(results$results), c(".meta", "name"))]) { - mapResults(item) -} -``` diff --git a/.github/instructions/jasp-plots.instructions.md b/.github/instructions/jasp-plots.instructions.md deleted file mode 100644 index ad59db3e9..000000000 --- a/.github/instructions/jasp-plots.instructions.md +++ /dev/null @@ -1,136 +0,0 @@ ---- -applyTo: "**/R/*.R" -description: "Plot lifecycle, composite plots, subgroup/facet patterns in jaspResults" ---- - -# JASP Plot Building Patterns - -How to create and configure plots in jaspResults. - -For dependency mechanics see [jasp-dependency-management.md](jasp-dependency-management.md). -For testing plots see [testing.instructions.md](testing.instructions.md) (`expect_equal_plots`). - ---- - -## 1) Simple Plot - -```r -.myPlot <- function(jaspResults, options) { - - if (!is.null(jaspResults[["myPlot"]])) - return() - - fit <- .extractFit(jaspResults, options) - if (is.null(fit) || jaspBase::isTryError(fit[[1]])) - return() - - myPlot <- createJaspPlot( - title = gettext("My Plot"), - width = 400, - height = 320 - ) - myPlot$position <- 5 - myPlot$dependOn(c(.baseDependencies, "plotSpecificOption")) - jaspResults[["myPlot"]] <- myPlot - - # Build ggplot - plotObj <- ggplot2::ggplot(...) + ... - - # Add JASP theme and (plot frame b = bottom, r = right, t = top, l = left) - plotObj <- plotObj + - jaspGraphs::geom_rangeframe(sides = "bl") + - jaspGraphs::themeJaspRaw() - - myPlot$plotObject <- plotObj -} -``` - ---- - -## 2) Plot with Error Handling - -Wrap plot construction in `try()` and display the error on the plot element: - -```r -plotOut <- try(.makePlot(fit, options)) - -if (inherits(plotOut, "try-error")) { - myPlot <- createJaspPlot(title = gettext("My Plot")) - myPlot$dependOn(dependencies) - myPlot$setError(plotOut) - jaspResults[["myPlot"]] <- myPlot - return() -} - -myPlot <- createJaspPlot(title = gettext("My Plot"), width = w, height = h) -myPlot$plotObject <- plotOut -jaspResults[["myPlot"]] <- myPlot -``` - ---- - -## 3) Composite Plot (jaspGraphsPlot) - -For plots with multiple panels (e.g., a left annotation panel + right data panel): - -```r -plotObj <- jaspGraphs:::jaspGraphsPlot$new( - subplots = list(leftPanel, rightPanel), - layout = matrix(1:2, ncol = 2), - heights = 1, - widths = c(0.4, 0.6) -) -myPlot$plotObject <- plotObj -``` - -In tests, each subplot gets its own SVG snapshot: `"name-subplot-1"`, `"name-subplot-2"`. - ---- - -## 4) Per-Group Plot Pattern - -When a single fit produces a single plot, but multiple groups produce a container of plots: - -```r -if (options[["groupingVariable"]] == "") { - # Single plot, attach directly - plot <- .makePlotFun(fit[[1]], options) - plot$title <- gettext("My Plot") - plot$dependOn(dependencies) - jaspResults[["myPlot"]] <- plot - -} else { - # Container with one plot per group - container <- createJaspContainer() - container$title <- gettext("My Plot") - container$dependOn(dependencies) - jaspResults[["myPlot"]] <- container - - for (i in seq_along(fit)) { - container[[names(fit)[i]]] <- .makePlotFun(fit[[i]], options) - container[[names(fit)[i]]]$title <- gettextf("Group: %1$s", attr(fit[[i]], "group")) - container[[names(fit)[i]]]$position <- i - } -} -``` - ---- - -## 5) Separate-Plots-by-Variable Pattern - -When a variable creates multiple faceted plots: - -```r -if (length(options[["separatePlots"]]) > 0) { - container <- createJaspContainer() - for (i in seq_along(levels)) { - tempPlot <- createJaspPlot(title = levels[i], width = w, height = h) - tempPlot$position <- i - tempPlot$plotObject <- makePlot(data[data$facet == levels[i], ]) - container[[paste0("plot", i)]] <- tempPlot - } -} else { - plot <- createJaspPlot(width = w, height = h) - plot$plotObject <- makePlot(data) -} -``` diff --git a/.github/instructions/jasp-state-management.instructions.md b/.github/instructions/jasp-state-management.instructions.md deleted file mode 100644 index 0a5450478..000000000 --- a/.github/instructions/jasp-state-management.instructions.md +++ /dev/null @@ -1,257 +0,0 @@ ---- -applyTo: "**/R/*.R" -description: "createJaspState caching, model fit patterns, metadata state, dynamic containers" ---- - -# JASP State Management (createJaspState) - -How to cache expensive computations and track dynamic output state. - -For the reactive loop context see [jasp-module-architecture.md](jasp-module-architecture.md). -For dependency mechanics see [jasp-dependency-management.md](jasp-dependency-management.md). - -Note that you cannot test this by running analysis via `runAnalysis()` because you only generate one state at a time -(with no initial elements - ask the human maintainer to validate the dependencies manually if you suspect an issue!). - ---- - -## 1) Why State Objects Exist - -Model fitting is expensive. Without caching, every option change (even toggling a checkbox for an unrelated table) would re-run the computation. State objects solve this by caching results that persist across R invocations as long as their dependencies hold. - -```r -.computeModel <- function(jaspResults, dataset, options) { - if (!is.null(jaspResults[["modelFit"]])) - return() # cached → skip - - fitState <- createJaspState() - fitState$dependOn(.modelDeps) # only model options - jaspResults[["modelFit"]] <- fitState - - result <- try(expensiveFit(dataset, options)) - fitState$object <- result # cache -} -``` - -Now when the user toggles "Show CI" (a table option, not a model option), `jaspResults[["modelFit"]]` survives. Only when a model option changes does the fit get invalidated and recomputed. - ---- - -## 2) The $object Property - -`createJaspState()` stores arbitrary R objects via `$object`: - -```r -# Store anything: model fits, lists, data.frames -jaspResults[["modelFit"]]$object <- list(model = fitResult, residuals = resid) - -# Retrieve in another builder function -cached <- jaspResults[["modelFit"]]$object -if (is.null(cached)) return() # not yet computed -model <- cached$model -``` - ---- - -## 3) State vs Output Elements - -| | State | Table/Plot/Html | -|---|---|---| -| Visible to user | No | Yes | -| Has `$object` | Yes | No (use `$setData()`, `$plotObject`) | -| Purpose | Cache computations | Display results | -| `$dependOn()` | Yes | Yes | -| Can nest in container | Yes | Yes | - ---- - -## 4) Pattern: Model Fit Caching - -The most common pattern -- fit a model once, reuse across multiple tables and plots: - -```r -.computeModel <- function(jaspResults, dataset, options) { - if (!is.null(jaspResults[["modelFit"]])) - return() - - fitState <- createJaspState() - fitState$dependOn(.modelDeps) - jaspResults[["modelFit"]] <- fitState - - fit <- try(myPackage::fitModel( - formula = .buildFormula(options), - data = dataset - )) - - fitState$object <- fit -} - -# Used by multiple builders: -.extractFit <- function(jaspResults) { - cached <- jaspResults[["modelFit"]]$object - if (is.null(cached)) return(NULL) - return(cached) -} -``` - ---- - -## 5) Pattern: Multiple Fits (Per Group / Per Variable) - -When the analysis computes separate fits for groups or variables, store them as a named list: - -```r -.computeModel <- function(jaspResults, dataset, options) { - if (!is.null(jaspResults[["modelFit"]])) - return() - - fitState <- createJaspState() - fitState$dependOn(.modelDeps) - jaspResults[["modelFit"]] <- fitState - - results <- list() - - # Overall fit - results[["overall"]] <- try(fitFun(dataset, options)) - - # Per-group fits (if grouping variable selected) - if (options[["groupingVariable"]] != "") { - groups <- unique(dataset[[options[["groupingVariable"]]]]) - for (g in groups) { - subData <- dataset[dataset[[options[["groupingVariable"]]]] == g, ] - fit <- try(fitFun(subData, options)) - attr(fit, "group") <- as.character(g) # preserve metadata even on error - results[[paste0("group_", g)]] <- fit - } - } - - fitState$object <- results -} -``` - -**Key conventions:** -- Use `attr(fit, "group")` to tag each fit with its group label (survives `try()` errors) -- Extractors can filter: include/exclude overall, handle errors per group -- Row builders iterate over fits via `lapply()`, returning skeleton data.frames on error - -### Extractor with filtering - -```r -.extractFit <- function(jaspResults, options) { - results <- jaspResults[["modelFit"]]$object - if (is.null(results)) return(NULL) - - # Optionally exclude overall fit - if (options[["groupingVariable"]] != "" && !options[["includeOverall"]]) - results <- results[names(results) != "overall"] - - return(results) -} -``` - ---- - -## 6) Pattern: Shared Computation Cache - -When multiple output elements (table + plot) need the same intermediate result: - -```r -.computeDiagnostics <- function(jaspResults, options) { - if (!is.null(jaspResults[["diagnosticsCache"]])) - return(jaspResults[["diagnosticsCache"]]$object) - - state <- createJaspState() - state$dependOn(.diagnosticsDeps) - jaspResults[["diagnosticsCache"]] <- state - - results <- expensiveComputation(...) - state$object <- results - return(results) -} -``` - -Both `.diagnosticsTable()` and `.diagnosticsPlot()` call `.computeDiagnostics()` -- the second call returns the cached result immediately. - ---- - -## 7) Pattern: Metadata State for Dynamic Containers - -When the set of output children depends on user-selected variables, track what's currently rendered: - -```r -.buildVariableOutputs <- function(jaspResults, options) { - - container <- .extractContainer(jaspResults) - - # Get or create metadata state - if (!is.null(container[["metaData"]])) { - meta <- container[["metaData"]]$object - } else { - metaState <- createJaspState() - metaState$dependOn(c("selectedVariables")) - container[["metaData"]] <- metaState - meta <- list(existing = character(0)) - } - - selected <- options[["selectedVariables"]] - existing <- meta$existing - - # Remove deselected - for (v in setdiff(existing, selected)) - container[[v]] <- NULL - - # Add new - for (v in setdiff(selected, existing)) { - child <- createJaspContainer(title = v) - child$position <- which(selected == v) - container[[v]] <- child - .buildTableForVariable(child, jaspResults, options, v) - } - - # Update tracking - container[["metaData"]]$object <- list(existing = selected) -} -``` - -This avoids rebuilding the entire container when the user adds or removes a single variable. - ---- - -## 8) Pattern: Dataset Update Sentinel - -When an expensive fit should NOT be re-run for visualization-only option changes, but auxiliary data attached to the fit needs updating: - -```r -.updateFitData <- function(jaspResults, dataset, options) { - if (is.null(jaspResults[["modelFit"]])) - return() - if (!is.null(jaspResults[["fitDataUpdate"]])) - return() - - # Create sentinel with narrow deps - sentinel <- createJaspState() - sentinel$dependOn(.plottingVariableDeps) - jaspResults[["fitDataUpdate"]] <- sentinel - - # Update auxiliary data on the existing (cached) fit - fit <- jaspResults[["modelFit"]]$object - fit$plotData <- .prepPlotData(fit, dataset, options) - jaspResults[["modelFit"]]$object <- fit - - sentinel$object <- TRUE # mark as done -} -``` - -When a plotting variable changes: sentinel is NULLed, data is re-attached. The model fit itself survives. - ---- - -## 9) Common Pitfalls - -**Forgetting to store:** Creating a state but never assigning `$object` -- extractors see NULL. - -**Circular extraction:** An extractor that calls the compute function which calls the extractor. Use the `if (!is.null(...)) return()` guard pattern consistently. - -**Overwriting state from extractors:** Extractors should be read-only. Only the compute function should write to `$object`. - -**State without dependencies:** A state with no `$dependOn()` is never invalidated -- it persists forever with potentially stale data. diff --git a/.github/instructions/jasp-tables.instructions.md b/.github/instructions/jasp-tables.instructions.md deleted file mode 100644 index 3731ce057..000000000 --- a/.github/instructions/jasp-tables.instructions.md +++ /dev/null @@ -1,202 +0,0 @@ ---- -applyTo: "**/R/*.R" -description: "Table lifecycle, columns, rows, footnotes, error display in jaspResults" ---- - -# JASP Table Building Patterns - -How to create, configure, and populate tables in jaspResults. - -For dependency mechanics see [jasp-dependency-management.md](jasp-dependency-management.md). -For state/caching see [jasp-state-management.md](jasp-state-management.md). -For containers and error handling see [jasp-containers-and-errors.md](jasp-containers-and-errors.md). - ---- - -## 1) Complete Table Lifecycle - -```r -.myTable <- function(jaspResults, options) { - - container <- .myExtractContainer(jaspResults) - - # 1. SKIP if already created (idempotency) - if (!is.null(container[["myTable"]])) - return() - - fit <- .extractFit(jaspResults, options) - - # 2. CREATE table and attach to parent BEFORE filling data - myTable <- createJaspTable(gettext("My Table Title")) - myTable$position <- 1 - myTable$dependOn(c("optionA", "optionB")) - container[["myTable"]] <- myTable - - # 3. DEFINE columns - myTable$addColumnInfo(name = "term", type = "string", title = "") - myTable$addColumnInfo(name = "est", type = "number", title = gettext("Estimate")) - myTable$addColumnInfo(name = "se", type = "number", title = gettext("Standard Error")) - myTable$addColumnInfo(name = "pval", type = "pvalue", title = gettext("p")) - - # 4. EARLY RETURN on error (table shows as empty with error) - if (is.null(fit)) - return() - if (length(fit) == 1 && jaspBase::isTryError(fit[[1]])) { - myTable$setError(.cleanErrorMessage(fit[[1]])) - return() - } - - # 5. BUILD row data (list of data.frames → rbind) - rows <- do.call(rbind, lapply(fit, .myRowBuilder, options = options)) - - # 6. ADD footnotes - myTable$addFootnote(gettext("Some methodological note.")) - - # 7. SET data - myTable$setData(rows) -} -``` - -**Key**: Always attach the table to jaspResults (step 2) **before** checking errors (step 4). This ensures the empty table with error message displays rather than nothing. See [jasp-containers-and-errors.md](jasp-containers-and-errors.md) for the create-then-error pattern. - ---- - -## 2) Column Types - -| Type | Use for | Format examples | -|------|---------|-----------------| -| `"string"` | Labels, names, formatted test stats | -- | -| `"number"` | Numeric values | `"sf:4;dp:3"` (4 sig figs, 3 decimal places) | -| `"integer"` | Counts, df | -- | -| `"pvalue"` | p-values | `"dp:3;p:.001"` (3 dp, threshold at .001) | - ---- - -## 3) Column Modifiers - -```r -# Grouped column header (e.g., "95% CI" spanning Lower/Upper) -table$addColumnInfo(name = "lCi", type = "number", title = gettext("Lower"), - overtitle = gettextf("%s%% CI", 100 * options[["ciLevel"]])) - -# Show only explicitly added columns (hide data columns not in schema) -table$showSpecifiedColumnsOnly <- TRUE -``` - ---- - -## 4) DRY Pattern: Reusable Column Helpers - -When multiple tables share the same column groups (e.g., CI columns, SE columns, test statistics), factor out repeated `addColumnInfo()` calls into shared helper functions. For example, a helper that conditionally adds a CI lower/upper pair with a dynamic overtitle avoids duplicating those 3-4 lines across every table builder. - -Apply the same pattern for any column group that appears in more than one table — each helper takes the table and relevant options, and adds the columns conditionally. - ---- - -## 5) Parameterized Tables - -When the same table structure serves multiple purposes, parametrize the builder: - -```r -.myTable <- function(jaspResults, options, parameter = "main") { - - container <- .extractContainer(jaspResults) - tableKey <- paste0(parameter, "Table") - - if (!is.null(container[[tableKey]])) - return() - - table <- createJaspTable(switch(parameter, - main = gettext("Main Results"), - summary = gettext("Summary Results") - )) - table$position <- switch(parameter, main = 1, summary = 2) - container[[tableKey]] <- table - # ... columns and data -} -``` - ---- - -## 6) Row Builder Pattern - -Each row builder takes a **single fit** and returns a **data.frame** (one or more rows): - -```r -.myRowBuilder <- function(fit, options) { - - # Handle failed fits gracefully (return skeleton with NAs) - if (jaspBase::isTryError(fit)) { - return(data.frame( - term = gettext("My term"), - group = attr(fit, "group") - )) - } - - row <- data.frame( - term = gettext("My term"), - group = attr(fit, "group"), - est = fit$beta[1], - se = fit$se[1], - pval = fit$pval[1] - ) - - return(row) -} -``` - -**Key conventions:** -- Include `group = attr(fit, "group")` for per-group support -- On error, return data.frame with labels but missing numeric columns (renders as empty cells) -- Use `gettext()` / `gettextf()` for all user-visible strings - ---- - -## 7) DRY Pattern: Safe Data Aggregation - -When combining data.frames from multiple fits — especially when some fits may fail and return fewer columns — create a helper that: - -1. Filters out NULL/empty data.frames -2. Computes the union of all column names -3. Pads each data.frame with NA for missing columns -4. Calls `do.call(rbind, ...)` on the aligned data.frames - -This avoids `rbind()` failures when partial errors produce data.frames with heterogeneous columns. Apply the same helper pattern for ordering rows by grouping variable and simplifying output (e.g., dropping a grouping column when no groups are selected). - ---- - -## 8) Footnotes - -```r -# Simple footnote (appears at bottom) -table$addFootnote(gettext("Fixed effects tested using Knapp and Hartung adjustment.")) - -# Warning-style footnote -table$addFootnote(warningMsg, symbol = gettext("Warning:")) - -# Per-group error footnotes -for (i in which(sapply(fit, jaspBase::isTryError))) { - table$addFootnote( - gettextf("The model for group '%1$s' failed: %2$s", - attr(fit[[i]], "group"), .cleanError(fit[[i]])), - symbol = gettext("Error:") - ) -} - -# Cell-specific footnote -table$addFootnote(message, colNames = "est", rowNames = "rowLabel") -``` - ---- - -## 9) Error Display on Tables - -```r -# Error message replaces entire table content -table$setError(gettext("Feature not available for this model type.")) - -# Error from a try-error object -table$setError(.cleanErrorMessage(tryResult)) -``` - -See [jasp-containers-and-errors.md](jasp-containers-and-errors.md) for the full create-then-error and graceful degradation patterns. diff --git a/.github/instructions/testing.instructions.md b/.github/instructions/testing.instructions.md deleted file mode 100644 index 5b35a5ffa..000000000 --- a/.github/instructions/testing.instructions.md +++ /dev/null @@ -1,152 +0,0 @@ ---- -applyTo: "**/tests/testthat/*.R" -description: "Test framework, snapshots, and test workflow for JASP analyses" ---- - -# JASP Testing Instructions - -## 1) Test Framework - -This module uses the `jaspTools` testing framework. Tests are **critical** and must always pass before committing code. - -## 2) Running Tests - -Run via `btw_tool_run_r` in the persistent R session: - -```r -# Full test suite (300+ sec, NEVER CANCEL) -testAll() - -# Specific analysis tests (for quick iteration) -testAnalysis("AnalysisName") -``` - -**Critical rules:** - -- Tests take 300+ seconds to complete -- **NEVER CANCEL** tests -- always let them run to completion -- Some deprecation warnings are expected and can be ignored -- ALL tests must pass before proceeding -- Some tests skip on certain platforms (e.g., Windows) -- this is expected - -## 3) Test File Structure - -Each test file in `tests/testthat/` corresponds to an R analysis file: - -- `test-penalizedmetaanalysis.R` -> `R/penalizedmetaanalysis.R` -- Test file name pattern: `test-.R` -- Analysis names for `testAnalysis()` come from NAMESPACE exports (PascalCase) - -## 4) Writing Tests - -### Basic test structure - -```r -# 1. Set up analysis options -options <- jaspTools::analysisOptions("AnalysisName") -options$variables <- "contGamma" -options$descriptives <- TRUE - -# 2. Set seed for reproducibility -set.seed(1) - -# 3. Run the analysis -results <- jaspTools::runAnalysis("AnalysisName", "debug.csv", options) - -# 4. Test tables -test_that("Table name matches", { - table <- results[["results"]][["tableName"]][["data"]] - jaspTools::expect_equal_tables(table, list(...expected values...)) -}) - -# 5. Test plots -test_that("Plot name matches", { - plotName <- results[["results"]][["containerName"]][["collection"]][["plotId"]][["data"]] - testPlot <- results[["state"]][["figures"]][[plotName]][["obj"]] - jaspTools::expect_equal_plots(testPlot, "plotname", dir = "AnalysisName") -}) -``` - -### Loading from .jasp example files - -```r -jaspFile <- testthat::test_path("..", "..", "examples", "Example Name.jasp") -opts <- jaspTools::analysisOptions(jaspFile) -dataset <- jaspTools::extractDatasetFromJASPFile(jaspFile) -encoded <- jaspTools:::encodeOptionsAndDataset(opts, dataset) -set.seed(1) -results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE) -``` - -### Key testing functions - -- `jaspTools::analysisOptions(name)` -- Get default options for an analysis -- `jaspTools::runAnalysis(name, dataset, options)` -- Run analysis with options -- `jaspTools::expect_equal_tables(actual, expected)` -- Compare table output -- `jaspTools::expect_equal_plots(plot, name, dir)` -- Compare plot output (snapshot-based) - -## 5) Test Data - -- `"debug.csv"` is a built-in jaspTools dataset containing most data types -- Use `set.seed()` before running analyses for reproducibility -- Example .jasp files in `examples/` provide pre-configured options and datasets - -## 6) Test Snapshots - -- Snapshots stored in `tests/testthat/_snaps/` -- **NEVER automatically accept snapshot changes** -- always notify user for manual inspection -- When a new snapshot is created, inform the user so they can verify it - -## 7) When to Update Tests - -### Always update tests when - -1. Adding new analysis outputs (tables, plots, text) -2. Modifying existing output structure or values -3. Adding new QML options that affect results -4. Changing analysis calculations - -### How to update test expectations - -1. Run tests and capture new output -2. Verify the new output is correct -3. Update expected values in test file -4. Re-run tests to confirm they pass - -## 8) Test Workflow - -### Before making code changes - -Run `testAll()` via `btw_tool_run_r` to establish baseline -- all tests should pass. - -### After making code changes - -1. Run `devtools::load_all()` to hot-reload R changes -2. Run `testAnalysis("AnalysisName")` for quick iteration on the affected analysis -3. Once the specific tests pass, run `testAll()` to check for regressions - -### If tests fail - -1. Review the failure messages carefully -2. Check if failure is expected (due to your intentional changes) -3. If expected: update test expectations and notify user about snapshot changes -4. If unexpected: fix your code -5. Re-run tests until all pass - -## 9) Adding New Tests - -When adding a new analysis: - -1. Create test file: `tests/testthat/test-.R` -2. Set up options with all default values explicitly set -3. Test all output tables and plots -4. Test edge cases and error conditions -5. Use meaningful variable names and test data - -## 10) Best Practices - -- **One test per output element** -- separate `test_that()` blocks for each table/plot -- **Descriptive test names** -- clearly state what is being tested -- **Reproducible** -- always use `set.seed()` for analyses with randomness -- **Complete option coverage** -- test with various option combinations -- **Keep tests focused** -- each test should verify one specific aspect diff --git a/.github/instructions/translation.instructions.md b/.github/instructions/translation.instructions.md deleted file mode 100644 index f8c7adb56..000000000 --- a/.github/instructions/translation.instructions.md +++ /dev/null @@ -1,254 +0,0 @@ ---- -applyTo: "**/R/*.R,**/inst/qml/*.qml,**/po/**" -description: "gettext/gettextf/qsTr usage, formatting, plurals, Weblate workflow" ---- - -# Translation (i18n) Instructions - -## 1) Core Principle - -**ALL user-visible text must be wrapped for translation.** - -This module is translated into multiple languages via Weblate integration. - -## 2) R Code Translation - -### Use `gettext()` for static strings: -```r -# Single string -message <- gettext("Analysis complete") - -# Table titles -tab <- createJaspTable(title = gettext("Descriptive Statistics")) - -# Error messages -tab$setError(gettext("Insufficient observations")) -``` - -### Use `gettextf()` for dynamic strings: -```r -# Single placeholder -msg <- gettextf("Variable %s has insufficient data", varName) - -# Multiple placeholders - use numbered format for translators -msg <- gettextf("Number of factor levels is %1$s in %2$s", nLevels, varName) - -# Percentage signs must be doubled -label <- gettextf("%s%% CI for Mean Difference", 100 * alpha) -``` - -### Use `ngettext()` for plurals: -```r -msg <- ngettext(n, - "One observation removed", - "%d observations removed", - domain = "R-jaspEquivalenceTTests") -``` - -### Column overtitles with dynamic content: -```r -if (options$confidenceInterval) { - ciLabel <- gettextf("%s%% CI", 100 * options$confidenceIntervalLevel) - tab$addColumnInfo("lower", gettext("Lower"), overtitle = ciLabel) - tab$addColumnInfo("upper", gettext("Upper"), overtitle = ciLabel) -} -``` - -## 3) QML Translation - -### Wrap all visible strings with `qsTr()`: -```qml -CheckBox -{ - name: "descriptives" - label: qsTr("Descriptive statistics") - - CheckBox - { - name: "confidenceInterval" - label: qsTr("Confidence interval") - info: qsTr("Display confidence intervals for effect sizes") - } -} -``` - -### For groups and sections: -```qml -Group -{ - title: qsTr("Additional Statistics") - - CheckBox - { - label: qsTr("Effect size") - } -} - -Section -{ - title: qsTr("Advanced Options") - - DoubleField - { - label: qsTr("Prior scale") - } -} -``` - -### Radio buttons and dropdowns: -```qml -RadioButtonGroup -{ - name: "hypothesis" - title: qsTr("Alternative Hypothesis") - - RadioButton - { - value: "twoSided" - label: qsTr("Two-sided") - } - - RadioButton - { - value: "greater" - label: qsTr("Greater than") - } -} - -DropDown -{ - name: "effectSize" - label: qsTr("Effect Size") - values: [ - { label: qsTr("Cohen's d"), value: "cohen" }, - { label: qsTr("Glass' delta"), value: "glass" } - ] -} -``` - -## 4) Translation Rules - -### DO wrap for translation: -- ✅ Table/plot/container titles -- ✅ Column names and overtitles -- ✅ Error messages and warnings -- ✅ Footnotes and citations -- ✅ All QML labels, titles, and info text -- ✅ Help text and descriptions -- ✅ Button labels and tooltips - -### DON'T wrap for translation: -- ❌ Empty strings: `""` (NEVER mark for translation) -- ❌ Variable names (internal identifiers) -- ❌ Statistical symbols: `"β"`, `"p"`, `"t"`, `"df"` -- ❌ Mathematical expressions -- ❌ Code or syntax -- ❌ File paths - -### Format specifications: -```r -# CORRECT - use numbered placeholders for clarity -gettextf("Mean difference is %1$s with SE = %2$s", mean, se) - -# AVOID - unnamed placeholders are harder for translators -gettextf("Mean difference is %s with SE = %s", mean, se) -``` - -### Special characters: -```r -# Use UTF-8 escape sequences for non-ASCII -label <- gettext("Cram\u00E9r's V") # Cramér's V -symbol <- gettext("\u03B2") # β (beta) -``` - -### Percentage signs in format strings: -```r -# WRONG - single % will cause format error -label <- gettextf("%s% CI", 95) - -# CORRECT - double %% in format string -label <- gettextf("%s%% CI", 95) -``` - -## 5) Translation Workflow - -### Automated process: -1. Developers write code with `gettext()`/`gettextf()`/`qsTr()` -2. Translation extraction happens automatically -3. Weblate platform provides translation interface -4. Translators work on Weblate -5. Translation files synced back to repository automatically -6. `.github/workflows/translations.yml` handles automation - -### Translation files location: -``` -po/ # R translation files -inst/qml/translations/ # QML translation files (if exists) -``` - -### Manual updates (rare): -Usually handled automatically, but if needed: -```bash -# Update R translations (done by translation workflow) -# Don't manually edit .po files unless absolutely necessary -``` - -## 6) Testing Translations - -While we can't easily test all languages locally, ensure: -1. All user-visible strings are wrapped -2. Format strings use numbered placeholders -3. Percentage signs are doubled in format strings -4. No empty strings marked for translation -5. Context provided for ambiguous terms - -## 7) Common Mistakes to Avoid - -### ❌ WRONG: -```r -# Missing translation -tab <- createJaspTable(title = "Descriptive Statistics") - -# Empty string marked for translation -label <- gettext("") - -# Unnamed placeholders -msg <- gettextf("Found %s issues in %s", count, name) - -# Single % for percentage -label <- gettextf("%s% Confidence Interval", 95) -``` - -### ✅ CORRECT: -```r -# Proper translation -tab <- createJaspTable(title = gettext("Descriptive Statistics")) - -# No translation for empty string -label <- "" - -# Numbered placeholders for translators -msg <- gettextf("Found %1$s issues in %2$s", count, name) - -# Doubled %% for percentage -label <- gettextf("%s%% Confidence Interval", 95) -``` - -## 8) Translation Context - -For ambiguous terms, consider adding comments: -```r -# "Mean" as in average (not "mean" as in unkind) -columnTitle <- gettext("Mean") - -# "Scale" as in measurement scale (not fish scales) -fieldLabel <- qsTr("Scale variable") -``` - -## 9) Weblate Integration - -- Weblate repo: `jaspequivalencettests-qml` and `jaspequivalencettests-r` -- Automated workflow: `.github/workflows/translations.yml` -- Scheduled runs: Weekly on Saturday at 2:45 AM -- Manual trigger: `workflow_dispatch` available -- Translation updates automatically create commits/PRs diff --git a/.gitignore b/.gitignore index e4381df1d..6368fd524 100644 --- a/.gitignore +++ b/.gitignore @@ -70,5 +70,15 @@ Rproj.user renv/ _processedLockFile.lock +# AI files .vscode/ .positai +.agents/ +.claude/ +.codex/ +.github/instructions/ +.github/copilot-instructions.md +.mcp.json +AGENTS.md +codex.toml +MIGRATION.md diff --git a/.mcp.json b/.mcp.json deleted file mode 100644 index 0e17a8bbb..000000000 --- a/.mcp.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "mcpServers": { - "r-mcptools": { - "type": "stdio", - "command": "Rscript", - "args": ["-e", "source('.claude/mcp-server.R')"] - }, - "markitdown": { - "type": "stdio", - "command": "uvx", - "args": ["markitdown-mcp==0.0.1a4"] - } - } -} diff --git a/.vscode/mcp.json b/.vscode/mcp.json deleted file mode 100644 index 833da8387..000000000 --- a/.vscode/mcp.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "servers": { - "r-mcptools": { - "command": "Rscript", - "args": ["-e", "source('.claude/mcp-server.R')"] - }, - "markitdown": { - "command": "uvx", - "args": ["markitdown-mcp==0.0.1a4"] - } - } -} diff --git a/R/commonQualityControl.R b/R/commonQualityControl.R index 2ba236daf..b43a8f58f 100644 --- a/R/commonQualityControl.R +++ b/R/commonQualityControl.R @@ -644,7 +644,7 @@ KnownControlStats.RS <- function(N, sigma = 3) { return(list(LCL = LCLvector, UCL = UCLvector)) } -.controlChart <- function(dataset, plotType = c("xBar", "R", "I", "MR", "MMR", "s", "cusum", "ewma", "g", "t"), +.controlChart <- function(dataset, plotType = c("xBar", "R", "I", "MR", "MMR", "s", "cusum", "ewma", "g", "t", "p", "u"), ruleList = list(), stages = "", xBarSdType = c("r", "s", "pooled"), @@ -701,7 +701,7 @@ KnownControlStats.RS <- function(N, sigma = 3) { return(list(plotObject = plotObject, table = table, controlChartData = controlChartData)) } -.controlChart_calculations <- function(dataset, plotType = c("xBar", "R", "I", "MR", "MMR", "s", "cusum", "ewma", "g", "t"), +.controlChart_calculations <- function(dataset, plotType = c("xBar", "R", "I", "MR", "MMR", "s", "cusum", "ewma", "g", "t", "p", "u"), ruleList = list(), stages = "", xBarSdType = c("r", "s", "pooled"), @@ -952,6 +952,46 @@ KnownControlStats.RS <- function(N, sigma = 3) { center <- qweibull(p = .5, shape = shape, scale = scale) UCL <- qweibull(p = pnorm(3), shape = shape, scale = scale) LCL <- qweibull(p = pnorm(-3), shape = shape, scale = scale) + ### + ### Calculations for p chart + ### + } else if (plotType == "p") { + # expects exactly two columns: number of defectives and number of inspected units per sample + D <- dataCurrentStage[[1]] + n <- dataCurrentStage[[2]] + plotStatistic <- D / n + # phase2Mu is a proportion here; only evaluate it when phase 2 is requested, its default is "" + center <- if (phase2) as.numeric(phase2Mu) else sum(D, na.rm = TRUE) / sum(n, na.rm = TRUE) + se <- sqrt(center * (1 - center) / n) + # limits vary per sample and are clamped to the [0, 1] range of a proportion + UCL <- pmin(1, center + nSigmasControlLimits * se) + LCL <- pmax(0, center - nSigmasControlLimits * se) + # the p chart has no process std. dev.; this must be assigned because the returned list always + # contains "sd" = sigma and stats::sigma would silently be returned as a function otherwise + sigma <- NA_real_ + # NOTE: because LCL is clamped at 0, the 1-sigma/2-sigma zones that .nelsonLaws derives from the + # control limits are not valid here. Zone-based rules (4, 5, 6, 7, 9) are therefore stripped by + # .getRuleListSubgroupCharts(type = "p"). If they are ever wanted, compute them from the + # unclamped per-point limits instead. + ### + ### Calculations for u chart + ### + } else if (plotType == "u") { + # expects exactly two columns: number of defects and number of inspected units per sample + C <- dataCurrentStage[[1]] + n <- dataCurrentStage[[2]] + plotStatistic <- C / n + # phase2Mu is a defect rate here; only evaluate it when phase 2 is requested, its default is "" + center <- if (phase2) as.numeric(phase2Mu) else sum(C, na.rm = TRUE) / sum(n, na.rm = TRUE) + se <- sqrt(center / n) + # unlike a proportion, a defect rate has no upper bound, so only the lower limit is clamped + UCL <- center + nSigmasControlLimits * se + LCL <- pmax(0, center - nSigmasControlLimits * se) + # the u chart has no process std. dev.; this must be assigned because the returned list always + # contains "sd" = sigma and stats::sigma would silently be returned as a function otherwise + sigma <- NA_real_ + # NOTE: as on the p chart, the clamped LCL invalidates the zones .nelsonLaws derives from the + # limits, so zone rules are stripped by .getRuleListSubgroupCharts(type = "u"). } if (i != 1) { if (plotType == "cusum") { @@ -1065,7 +1105,7 @@ KnownControlStats.RS <- function(N, sigma = 3) { } .controlChart_table <- function(tableList, - plotType = c("xBar", "R", "I", "MR", "MMR", "s", "cusum", "ewma", "g", "t"), + plotType = c("xBar", "R", "I", "MR", "MMR", "s", "cusum", "ewma", "g", "t", "p", "u"), stages = "", tableLabels = "", nPoints = NA) { @@ -1080,7 +1120,9 @@ KnownControlStats.RS <- function(N, sigma = 3) { "cusum" = "cumulative sum", "ewma" = "exponentially weighted moving average", "g" = "g", - "t" = "t" + "t" = "t", + "p" = "p", + "u" = "u" ) table <- createJaspTable(title = gettextf("Test results for %1$s chart", tableTitle)) table$showSpecifiedColumnsOnly <- TRUE @@ -1166,7 +1208,7 @@ KnownControlStats.RS <- function(N, sigma = 3) { } .controlChart_plotting <- function(pointData, clData, stageLabels, clLabels, - plotType = c("xBar", "R", "I", "MR", "MMR", "s", "cusum", "ewma", "g", "t"), + plotType = c("xBar", "R", "I", "MR", "MMR", "s", "cusum", "ewma", "g", "t", "p", "u"), stages = "", phase2 = FALSE, warningLimits = FALSE, @@ -1205,7 +1247,9 @@ KnownControlStats.RS <- function(N, sigma = 3) { "MMR" = gettext("Moving range of subgroup mean"), "s" = gettext("Sample std. dev."), "cusum" = gettext("Cumulative sum"), - "ewma" = gettext("Exponentially weighted moving average")) + "ewma" = gettext("Exponentially weighted moving average"), + "p" = gettext("Proportion defective"), + "u" = gettext("Defects per unit")) } lineType <- if (phase2) "solid" else "dashed" # Create plot @@ -1711,7 +1755,7 @@ KnownControlStats.RS <- function(N, sigma = 3) { return(list) } -.getRuleListSubgroupCharts <- function(options, type = c("xBar", "R", "s")) { +.getRuleListSubgroupCharts <- function(options, type = c("xBar", "R", "s", "p", "u")) { ruleSet <- options[["testSet"]] if (ruleSet == "jaspDefault") { ruleList <- list("rule1" = list("enabled" = TRUE), @@ -1760,7 +1804,12 @@ KnownControlStats.RS <- function(N, sigma = 3) { ) } - if (type != "xBar") { # never apply rules other than 1,2,3 or 8 to s or R chart + # Never apply rules other than 1, 2, 3 or 8 to the s, R, p or u chart. Those charts are asymmetric + # around the center line, so the 1-sigma/2-sigma zones that .nelsonLaws derives from the control + # limits do not correspond to actual sigma multiples. On the p and u charts the lower limit is + # additionally clamped at 0, which rescales the lower zones by a different factor for every sample + # size. + if (type != "xBar") { ruleList[["rule4"]] <- NULL ruleList[["rule5"]] <- NULL ruleList[["rule6"]] <- NULL diff --git a/R/doeAnalysis.R b/R/doeAnalysis.R index bf63b2b7b..4e94f9020 100644 --- a/R/doeAnalysis.R +++ b/R/doeAnalysis.R @@ -1821,8 +1821,8 @@ get_levels <- function(var, num_levels, dataset) { return() } result <- jaspResults[[dep]][["doeResult"]]$object[["regression"]] - plot$plotObject <- jaspDescriptives::.plotMarginal(resid(result[["object"]]), NULL, binWidthType = options[["histogramBinWidthType"]], - numberOfBins = options[["histogramManualNumberOfBins"]]) + plot$plotObject <- jaspGraphs::jaspHistogram(resid(result[["object"]]), gettext("Residuals"), binWidthType = options[["histogramBinWidthType"]], + numberOfBins = options[["histogramManualNumberOfBins"]]) } } @@ -1897,7 +1897,8 @@ get_levels <- function(var, num_levels, dataset) { return() } plot <- createJaspPlot(title = gettext("Matrix residual plot"), width = 1000, height = 1000) - plot$dependOn(options = c("fourInOneResidualPlot", .doeAnalysisBaseDependencies())) + plot$dependOn(options = c("histogramBinWidthType", "histogramManualNumberOfBins", + "fourInOneResidualPlot", .doeAnalysisBaseDependencies())) plot$position <- 11 jaspResults[[dep]][["fourInOneResidualPlot"]] <- plot if (!ready || is.null(jaspResults[[dep]][["doeResult"]]) || jaspResults[[dep]]$getError()) { @@ -1907,7 +1908,8 @@ get_levels <- function(var, num_levels, dataset) { plotMat <- matrix(list(), 2, 2) plotMat[[1, 1]] <- .doeAnalysisPlotResidualsVsOrderPlotObject(result, dataset, options) plotMat[[2, 1]] <- .doeAnalysisPlotFittedVsResidualsPlotObject(result, options) - plotMat[[1, 2]] <- jaspDescriptives::.plotMarginal(resid(result[["object"]]), NULL) + plotMat[[1, 2]] <- jaspGraphs::jaspHistogram(resid(result[["object"]]), gettext("Residuals"), binWidthType = options[["histogramBinWidthType"]], + numberOfBins = options[["histogramManualNumberOfBins"]]) plotMat[[2, 2]] <- jaspGraphs::plotQQnorm(resid(result[["object"]]), abline = TRUE) + ggplot2::scale_x_continuous(expand = ggplot2::expansion(mult = 0.1), name = gettext("Theoretical quantiles")) + ggplot2::scale_y_continuous(expand = ggplot2::expansion(mult = 0.1), name = gettext("Observed quantiles")) diff --git a/R/processCapabilityAttributes.R b/R/processCapabilityAttributes.R new file mode 100644 index 000000000..8efcbd1e5 --- /dev/null +++ b/R/processCapabilityAttributes.R @@ -0,0 +1,860 @@ +# +# Copyright (C) 2013-2018 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# Attribute (count) capability analysis, in two distributions. +# +# Reached from processCapabilityStudies() when capabilityDataType == "attributes". Which distribution +# applies is decided by attributeDistribution: +# binomial - every inspected unit is pass/fail, the data are counts of defective units per sample, +# and capability is expressed as %Defective, PPM defective and Process Z. +# poisson - a unit can carry several defects, the data are counts of defects per sample, and +# capability is expressed as the mean number of defects per unit (DPU). +# The two share everything except the chart type, the centre-line option, the interval family, the +# summary rows and two validation rules. Stages are not supported here. + +# TRUE when the analysis runs in the Poisson (defects per unit) mode. +.qcAttributeIsPoisson <- function(options) options[["attributeDistribution"]] == "poisson" + +.qcAttributeCapability <- function(jaspResults, dataset, options) { + + numericColumns <- c(options[["attributeCounts"]], + if (options[["attributeSampleSizeType"]] == "variable") options[["attributeSampleSizeVariable"]]) + numericColumns <- numericColumns[numericColumns != ""] + labelColumn <- options[["attributeLabels"]] + labelColumn <- labelColumn[labelColumn != ""] + + ready <- options[["attributeCounts"]] != "" && + (options[["attributeSampleSizeType"]] == "constant" || options[["attributeSampleSizeVariable"]] != "") + + if (is.null(dataset) && ready) { + if (length(labelColumn) >= 1) { + dataset <- .readDataSetToEnd(columns.as.numeric = numericColumns, columns.as.factor = labelColumn) + } else { + dataset <- .readDataSetToEnd(columns.as.numeric = numericColumns) + } + } + + data <- NULL + if (ready) { + .hasErrors(dataset, type = c("infinity", "negativeValues", "observations"), + all.target = numericColumns, observations.amount = "< 2", exitAnalysisIfErrors = TRUE) + data <- .qcAttributeReadData(dataset, options) + .qcAttributeCheckErrors(dataset, data, options) + } + + if (options[["report"]]) + return(.qcAttributeReport(jaspResults, data, options, ready)) + + container <- .qcAttributeContainer(jaspResults, options) + .qcAttributeComputeState(container, data, options, ready) + + if (options[["attributeControlChart"]]) + .qcAttributeControlChart(container, options, ready) + if (options[["attributeCumulativePlot"]]) + .qcAttributeCumulativePlot(container, options, ready) + if (options[["attributeDistributionPlot"]]) + .qcAttributeDistributionPlot(container, options, ready) + if (.qcAttributeShowPanel(options, "rate")) + .qcAttributeRatePlot(container, options, ready, type = "rate") + if (.qcAttributeShowPanel(options, "histogram")) + .qcAttributeRatePlot(container, options, ready, type = "histogram") + if (options[["attributeSummaryTable"]]) + .qcAttributeSummaryTable(container, options, ready) +} + +# Data assembly and validation ---- + +.qcAttributeReadData <- function(dataset, options) { + counts <- as.numeric(dataset[[options[["attributeCounts"]]]]) + if (options[["attributeSampleSizeType"]] == "variable") { + sampleSize <- as.numeric(dataset[[options[["attributeSampleSizeVariable"]]]]) + } else { + sampleSize <- rep(as.numeric(options[["attributeSampleSizeValue"]]), length(counts)) + } + labels <- if (options[["attributeLabels"]] != "") as.character(dataset[[options[["attributeLabels"]]]]) else character(0) + + # Rows with missing values are kept rather than dropped: dropping them would renumber every + # following sample, so "Point 7" in the test results table would no longer be row 7 of the + # spreadsheet. They are blanked out instead and excluded from the totals. + incomplete <- is.na(counts) | is.na(sampleSize) | sampleSize == 0 + counts[incomplete] <- NA_real_ + sampleSize[incomplete] <- NA_real_ + + return(list(counts = counts, + sampleSize = sampleSize, + # a proportion defective in binomial mode, a defect rate in Poisson mode + rate = counts / sampleSize, + labels = labels, + nMissing = sum(incomplete), + index = seq_along(counts), + equalSizes = length(unique(sampleSize[!is.na(sampleSize)])) == 1)) +} + +.qcAttributeCheckErrors <- function(dataset, data, options) { + counts <- data[["counts"]] + sampleSize <- data[["sampleSize"]] + labels <- data[["labels"]] + complete <- !is.na(counts) & !is.na(sampleSize) + tolerance <- .Machine$double.eps^0.5 + poisson <- .qcAttributeIsPoisson(options) + + .hasErrors(dataset, exitAnalysisIfErrors = TRUE, custom = function() { + if (any(complete & abs(counts - round(counts)) > tolerance)) + return(if (poisson) gettext("The number of defects must contain whole numbers.") + else gettext("The number of defectives must contain whole numbers.")) + + if (poisson) { + # No sample-size rule applies here. A Poisson exposure may be fractional (2.5 square metres, + # 1.5 hours), so the binomial "positive whole number" rule is dropped; positivity is already + # guaranteed elsewhere, because a negative sample size is rejected by the negativeValues check + # in the orchestrator and a zero sample size is blanked out as a missing sample by + # .qcAttributeReadData. Nor is there an upper check on the counts: C > n is legal here, ten + # defects on five units being a defect rate of 2. + return() + } + + if (any(complete & (abs(sampleSize - round(sampleSize)) > tolerance | sampleSize < 1))) + return(gettext("The sample size must be a positive whole number.")) + + affected <- which(complete & counts > sampleSize) + if (length(affected) > 0) { + # the columns arrive as doubles, so %i would throw here; identify the sample by its label + # when one is assigned, otherwise by its row number + identifier <- if (length(labels) > 0) as.character(labels[affected[1]]) else as.character(affected[1]) + message <- gettextf("Sample %1$s has more defectives (%2$s) than inspected units (%3$s).", + identifier, as.integer(counts[affected[1]]), as.integer(sampleSize[affected[1]])) + if (length(affected) > 1) + message <- paste(message, sprintf(ngettext(length(affected), + "%i sample is affected in total.", + "%i samples are affected in total."), + length(affected))) + return(message) + } + }) +} + +# Statistics ---- + +# Confidence interval for a binomial proportion. Returns c(lower, upper) on the proportion scale. +.qcAttributeProportionCi <- function(nDefective, nInspected, ciLevel, method) { + alpha <- 1 - ciLevel + proportion <- nDefective / nInspected + + if (method == "exact") { + # Clopper-Pearson + lower <- if (nDefective == 0) 0 else qbeta(alpha / 2, nDefective, nInspected - nDefective + 1) + upper <- if (nDefective == nInspected) 1 else qbeta(1 - alpha / 2, nDefective + 1, nInspected - nDefective) + } else if (method == "wald") { + z <- qnorm(1 - alpha / 2) + halfWidth <- z * sqrt(proportion * (1 - proportion) / nInspected) + lower <- max(0, proportion - halfWidth) + upper <- min(1, proportion + halfWidth) + } else { + # Wilson score + z <- qnorm(1 - alpha / 2) + denominator <- 1 + z^2 / nInspected + centre <- (proportion + z^2 / (2 * nInspected)) / denominator + halfWidth <- z * sqrt(proportion * (1 - proportion) / nInspected + z^2 / (4 * nInspected^2)) / denominator + lower <- max(0, centre - halfWidth) + upper <- min(1, centre + halfWidth) + } + return(c(lower, upper)) +} + +# Confidence interval for a Poisson rate. Returns c(lower, upper) on the defects per unit scale. +# Unlike a proportion the rate is unbounded above, so only the lower bound is floored. +.qcAttributePoissonRateCi <- function(count, exposure, ciLevel, method) { + alpha <- 1 - ciLevel + rate <- count / exposure + + if (method == "exact") { + # Garwood, the Poisson analogue of Clopper-Pearson + lower <- if (count == 0) 0 else qgamma(alpha / 2, shape = count) / exposure + upper <- qgamma(1 - alpha / 2, shape = count + 1) / exposure + } else if (method == "wald") { + z <- qnorm(1 - alpha / 2) + halfWidth <- z * sqrt(rate / exposure) + lower <- max(0, rate - halfWidth) + upper <- rate + halfWidth + } else { + # score (Rao), the Poisson analogue of Wilson + z <- qnorm(1 - alpha / 2) + centre <- count + z^2 / 2 + spread <- z * sqrt(count + z^2 / 4) + lower <- max(0, (centre - spread) / exposure) + upper <- (centre + spread) / exposure + } + return(c(lower, upper)) +} + +# Dispatches to the interval family that matches the distribution. +.qcAttributeRateCi <- function(count, exposure, ciLevel, method, poisson) { + if (is.na(count) || is.na(exposure) || exposure <= 0) + return(c(NA_real_, NA_real_)) + if (poisson) + return(.qcAttributePoissonRateCi(count, exposure, ciLevel, method)) + return(.qcAttributeProportionCi(count, exposure, ciLevel, method)) +} + +.qcAttributeCountViolations <- function(violationTable) { + points <- c() + for (stageViolations in violationTable) { + tests <- stageViolations[names(stageViolations) != "stage"] + points <- c(points, unlist(tests, use.names = FALSE)) + } + points <- suppressWarnings(as.numeric(points)) + return(length(unique(points[!is.na(points)]))) +} + +.qcAttributeStatistics <- function(data, options) { + counts <- data[["counts"]] + sampleSize <- data[["sampleSize"]] + poisson <- .qcAttributeIsPoisson(options) + ciLevel <- options[["attributeCiLevel"]] # a CIField delivers a proportion in this module + ciMethod <- if (poisson) options[["poissonCiMethod"]] else options[["binomialCiMethod"]] + + totalCounts <- sum(counts, na.rm = TRUE) + totalExposure <- sum(sampleSize, na.rm = TRUE) + # the point estimate is always data based, also when a historical centre line is set: that value + # only moves the centre line of the chart, so keeping it out here keeps the estimate inside its + # own confidence interval + rateBar <- if (totalExposure > 0) totalCounts / totalExposure else NA_real_ + # the historical Poisson value is already a rate, the historical binomial value is a percentage + phase2 <- if (poisson) options[["poissonHistoricalDpu"]] else options[["binomialHistoricalProportion"]] + centre <- if (!phase2) rateBar + else if (poisson) options[["poissonHistoricalDpuValue"]] + else options[["binomialHistoricalProportionValue"]] / 100 + chartType <- if (poisson) "u" else "p" + + ci <- .qcAttributeRateCi(totalCounts, totalExposure, ciLevel, ciMethod, poisson) + + # running estimate with a band; blanked out samples contribute nothing but keep their row + cumulativeCounts <- cumsum(ifelse(is.na(counts), 0, counts)) + cumulativeExposure <- cumsum(ifelse(is.na(sampleSize), 0, sampleSize)) + cumulativeRate <- ifelse(cumulativeExposure > 0, cumulativeCounts / cumulativeExposure, NA_real_) + cumulativeCi <- vapply(data[["index"]], + function(i) .qcAttributeRateCi(cumulativeCounts[i], cumulativeExposure[i], + ciLevel, ciMethod, poisson), + numeric(2)) + + controlChartData <- .controlChart_calculations( + dataset = data.frame(counts = counts, sampleSize = sampleSize), + plotType = chartType, + ruleList = .getRuleListSubgroupCharts(options, type = chartType), + nSigmasControlLimits = options[["controlLimitsNumberOfSigmas"]], + phase2 = phase2, + phase2Mu = centre) + + statistics <- list( + data = data, + ciLevel = ciLevel, + ciLevelPercent = ciLevel * 100, + ciMethod = ciMethod, + chartType = chartType, + totalCounts = totalCounts, + totalExposure = totalExposure, + rateBar = rateBar, + rateCi = ci, + centre = centre, + cumulativeRate = cumulativeRate, + cumulativeLower = cumulativeCi[1, ], + cumulativeUpper = cumulativeCi[2, ], + expectedCounts = sampleSize * centre, + controlChartData = controlChartData, + nViolations = .qcAttributeCountViolations(controlChartData[["violationTable"]]) + ) + + if (poisson) { + # yield statistics, all monotone in the rate. P(no defect on a unit) = exp(-rate), so the share of + # conforming units follows from the rate alone. expm1 keeps the precision for the small rates of a + # capable process, which is exactly where Z is largest. + statistics[["percentUnits"]] <- 100 * -expm1(-rateBar) + statistics[["percentUnitsCi"]] <- 100 * -expm1(-ci) + statistics[["ppmUnits"]] <- 1e6 * -expm1(-rateBar) + statistics[["ppmUnitsCi"]] <- 1e6 * -expm1(-ci) + statistics[["processZ"]] <- qnorm(-expm1(-rateBar), lower.tail = FALSE) + # Z decreases in the rate, so the bounds swap + statistics[["processZCi"]] <- qnorm(-expm1(-rev(ci)), lower.tail = FALSE) + } else { + statistics[["percentDefective"]] <- 100 * rateBar + statistics[["percentDefectiveCi"]] <- 100 * ci + statistics[["ppm"]] <- 1e6 * rateBar + statistics[["ppmCi"]] <- 1e6 * ci + # upper-tail form avoids underflow for very capable processes; Z decreases in p, so bounds swap + statistics[["processZ"]] <- qnorm(rateBar, lower.tail = FALSE) + statistics[["processZCi"]] <- c(qnorm(ci[2], lower.tail = FALSE), qnorm(ci[1], lower.tail = FALSE)) + } + + return(statistics) +} + +# Scale and label helpers ---- + +# Poisson reports a rate throughout, binomial a percentage throughout. Only the optional yield rows +# of the Poisson summary table break that rule. +.qcAttributeRateScale <- function(options) if (.qcAttributeIsPoisson(options)) 1 else 100 + +.qcAttributeRateLabel <- function(options) { + if (.qcAttributeIsPoisson(options)) gettext("Defects per unit") else gettextf("Defective (%%)") +} + +.qcAttributeTargetValue <- function(options) { + if (.qcAttributeIsPoisson(options)) { + if (options[["poissonTarget"]]) options[["poissonTargetValue"]] else NULL + } else { + if (options[["binomialTarget"]]) options[["binomialTargetValue"]] else NULL + } +} + +# The overall level on the scale the supporting plots use. +.qcAttributeOverallLevel <- function(state, options) .qcAttributeRateScale(options) * state[["rateBar"]] + +# Container and state ---- + +.qcAttributeContainer <- function(jaspResults, options) { + if (!is.null(jaspResults[["attributeCapability"]])) + return(jaspResults[["attributeCapability"]]) + + title <- if (.qcAttributeIsPoisson(options)) gettext("Poisson capability analysis") else gettext("Binomial capability analysis") + container <- createJaspContainer(title) + container$dependOn(c(.qcAttributeOptionNames(), "report")) + container$position <- 1 + jaspResults[["attributeCapability"]] <- container + + return(container) +} + +.qcAttributeComputeState <- function(container, data, options, ready) { + if (!is.null(container[["attributeState"]])) + return() + + state <- createJaspState() + # the confidence interval feeds the summary table and the band of the cumulative plot, the rule + # settings feed the out-of-control footnote, so both belong to the cached statistics + state$dependOn(c("attributeCiLevel", "binomialCiMethod", "poissonCiMethod", "poissonYieldStatistics", + "controlLimitsNumberOfSigmas", .getDependenciesControlChartRules())) + container[["attributeState"]] <- state + + if (!ready) + return() + + state$object <- .qcAttributeStatistics(data, options) +} + +.qcAttributeGetState <- function(container) { + if (is.null(container[["attributeState"]])) + return(NULL) + return(container[["attributeState"]]$object) +} + +.qcAttributeXAxisTitle <- function(options) { + if (options[["attributeLabels"]] != "") options[["attributeLabels"]] else gettext("Sample") +} + +.qcAttributeAxisLabels <- function(state) { + labels <- state[["data"]][["labels"]] + if (length(labels) > 0) labels else "" +} + +# Control chart (p or u) ---- + +.qcAttributeControlChart <- function(container, options, ready) { + if (!is.null(container[["controlChart"]])) + return() + + poisson <- .qcAttributeIsPoisson(options) + chartName <- if (poisson) "u" else "p" + title <- if (poisson) gettext("u chart") else gettext("p chart") + + chartContainer <- createJaspContainer(title) + chartContainer$dependOn(c("attributeControlChart", "controlLimitsNumberOfSigmas", + .getDependenciesControlChartRules())) + chartContainer$position <- 1 + container[["controlChart"]] <- chartContainer + + plot <- createJaspPlot(title = title, width = 1200, height = 500) + plot$position <- 1 + chartContainer[["plot"]] <- plot + + state <- .qcAttributeGetState(container) + if (!ready || is.null(state)) { + emptyTable <- createJaspTable(title = gettextf("Test results for %1$s chart", chartName)) + emptyTable$showSpecifiedColumnsOnly <- TRUE + emptyTable$addColumnInfo(name = "noViolations", title = gettext("Tests"), type = "string") + emptyTable$position <- 2 + chartContainer[["table"]] <- emptyTable + return() + } + + plot$plotObject <- .qcAttributeControlChartPlotObject(state, options) + + table <- .controlChart_table(state[["controlChartData"]][["violationTable"]], plotType = chartName, + tableLabels = .qcAttributeAxisLabels(state), + nPoints = length(state[["controlChartData"]][["pointData"]][["plotStatistic"]])) + table$position <- 2 + chartContainer[["table"]] <- table +} + +.qcAttributeControlChartPlotObject <- function(state, options) { + controlChartData <- state[["controlChartData"]] + phase2 <- if (.qcAttributeIsPoisson(options)) options[["poissonHistoricalDpu"]] else options[["binomialHistoricalProportion"]] + return(.controlChart_plotting(pointData = controlChartData[["pointData"]], + clData = controlChartData[["clData"]], + stageLabels = controlChartData[["stageLabels"]], + clLabels = controlChartData[["clLabels"]], + plotType = state[["chartType"]], + phase2 = phase2, + xAxisLabels = .qcAttributeAxisLabels(state), + xAxisTitle = .qcAttributeXAxisTitle(options))) +} + +# Supporting plots ---- + +.qcAttributeCumulativePlot <- function(container, options, ready) { + if (!is.null(container[["cumulativePlot"]])) + return() + + title <- if (.qcAttributeIsPoisson(options)) gettext("Cumulative defects per unit") else gettextf("Cumulative defective (%%)") + plot <- createJaspPlot(title = title, width = 600, height = 400) + plot$position <- 2 + plot$dependOn(c("attributeCumulativePlot", "attributeCiLevel", "binomialCiMethod", "poissonCiMethod")) + container[["cumulativePlot"]] <- plot + + state <- .qcAttributeGetState(container) + if (!ready || is.null(state)) + return() + + plot$plotObject <- .qcAttributeCumulativePlotObject(state, options) +} + +.qcAttributeCumulativePlotObject <- function(state, options) { + scale <- .qcAttributeRateScale(options) + plotData <- data.frame(index = state[["data"]][["index"]], + estimate = scale * state[["cumulativeRate"]], + lower = scale * state[["cumulativeLower"]], + upper = scale * state[["cumulativeUpper"]]) + target <- .qcAttributeTargetValue(options) + overall <- .qcAttributeOverallLevel(state, options) + + yBreaks <- jaspGraphs::getPrettyAxisBreaks(na.omit(c(plotData$lower, plotData$upper, plotData$estimate, + overall, target))) + yLimits <- range(yBreaks) + yTitle <- if (.qcAttributeIsPoisson(options)) gettext("Cumulative defects per unit") else gettextf("Cumulative defective (%%)") + + # colours follow the continuous capability plots: grey/black for the data, red for the estimated + # process level, darkgreen for the target + plotObject <- ggplot2::ggplot() + + ggplot2::geom_ribbon(data = plotData, mapping = ggplot2::aes(x = index, ymin = lower, ymax = upper), + fill = "grey80", na.rm = TRUE) + + ggplot2::geom_hline(yintercept = overall, col = "red", linewidth = 1, na.rm = TRUE) + if (!is.null(target)) + plotObject <- plotObject + + ggplot2::geom_hline(yintercept = target, col = "darkgreen", linewidth = 1) + plotObject <- plotObject + + jaspGraphs::geom_line(plotData, mapping = ggplot2::aes(x = index, y = estimate), col = "black", na.rm = TRUE) + + jaspGraphs::geom_point(plotData, mapping = ggplot2::aes(x = index, y = estimate), size = 3, na.rm = TRUE) + + ggplot2::scale_y_continuous(name = yTitle, breaks = yBreaks, limits = yLimits) + + .qcAttributeSampleAxis(state, options) + + jaspGraphs::geom_rangeframe() + + jaspGraphs::themeJaspRaw() + + return(plotObject) +} + +.qcAttributeDistributionPlot <- function(container, options, ready) { + if (!is.null(container[["distributionPlot"]])) + return() + + title <- if (.qcAttributeIsPoisson(options)) gettext("Poisson plot") else gettext("Binomial plot") + plot <- createJaspPlot(title = title, width = 600, height = 400) + plot$position <- 3 + plot$dependOn("attributeDistributionPlot") + container[["distributionPlot"]] <- plot + + state <- .qcAttributeGetState(container) + if (!ready || is.null(state)) + return() + + plot$plotObject <- .qcAttributeDistributionPlotObject(state, options) +} + +.qcAttributeDistributionPlotObject <- function(state, options) { + plotData <- data.frame(expected = state[["expectedCounts"]], + observed = state[["data"]][["counts"]]) + poisson <- .qcAttributeIsPoisson(options) + xTitle <- if (poisson) gettext("Expected defects") else gettext("Expected defectives") + yTitle <- if (poisson) gettext("Observed defects") else gettext("Observed defectives") + + breaks <- jaspGraphs::getPrettyAxisBreaks(na.omit(c(plotData$expected, plotData$observed, 0))) + limits <- range(breaks) + + plotObject <- ggplot2::ggplot() + + # identity reference line, drawn like the other agreement diagonals in the module + ggplot2::geom_abline(intercept = 0, slope = 1, col = "gray", linetype = "dashed", linewidth = 1) + + jaspGraphs::geom_point(plotData, mapping = ggplot2::aes(x = expected, y = observed), size = 3, na.rm = TRUE) + + ggplot2::scale_x_continuous(name = xTitle, breaks = breaks, limits = limits) + + ggplot2::scale_y_continuous(name = yTitle, breaks = breaks, limits = limits) + + jaspGraphs::geom_rangeframe() + + jaspGraphs::themeJaspRaw() + + return(plotObject) +} + +# The two sample-size dependent panels are alternatives: the rate plot belongs to a variable sample +# size, the histogram to a constant one. QML hides the check box that does not apply, but a hidden +# check box keeps its value, so the rule is repeated here. Without it a histogram ticked under a +# constant sample size would come back when the user switches to a variable one. +.qcAttributeShowPanel <- function(options, type = c("rate", "histogram")) { + type <- match.arg(type) + if (type == "rate") + return(options[["attributeRatePlot"]] && options[["attributeSampleSizeType"]] == "variable") + return(options[["attributeHistogram"]] && options[["attributeSampleSizeType"]] == "constant") +} + +# Covers both sample-size dependent panels. +.qcAttributeRatePlot <- function(container, options, ready, type = c("rate", "histogram")) { + type <- match.arg(type) + poisson <- .qcAttributeIsPoisson(options) + elementKey <- if (type == "rate") "ratePlot" else "histogram" + if (!is.null(container[[elementKey]])) + return() + + title <- if (type == "rate") { + if (poisson) gettext("Rate of defects") else gettext("Rate of defectives") + } else { + if (poisson) gettext("Distribution of defects per unit") else gettextf("Distribution of defective (%%)") + } + plot <- createJaspPlot(title = title, width = 600, height = 400) + plot$position <- 4 + plot$dependOn(if (type == "rate") "attributeRatePlot" else c("attributeHistogram", "attributeHistogramBinNumber")) + container[[elementKey]] <- plot + + state <- .qcAttributeGetState(container) + if (!ready || is.null(state)) + return() + + if (type == "rate") { + plot$plotObject <- .qcAttributeRatePlotObject(state, options) + # the panel is chosen in QML from attributeSampleSizeType, so an assigned sample size column that + # happens to be constant is reported rather than silently swapped for the histogram + if (isTRUE(state[["data"]][["equalSizes"]])) { + plot$title <- if (poisson) gettext("Rate of defects (constant sample size)") else gettext("Rate of defectives (constant sample size)") + note <- createJaspHtml(paste0("", gettext("Note."), " ", + gettext("The assigned sample size column is constant, so all samples share a single x value.")), + elementType = "p") + note$position <- 5 + note$dependOn("attributeRatePlot") + container[["ratePlotNote"]] <- note + } + } else { + plot$plotObject <- .qcAttributeHistogramPlotObject(state, options) + } +} + +.qcAttributeRatePlotObject <- function(state, options) { + scale <- .qcAttributeRateScale(options) + plotData <- data.frame(sampleSize = state[["data"]][["sampleSize"]], + level = scale * state[["data"]][["rate"]]) + overall <- .qcAttributeOverallLevel(state, options) + + xBreaks <- jaspGraphs::getPrettyAxisBreaks(na.omit(plotData$sampleSize)) + yBreaks <- jaspGraphs::getPrettyAxisBreaks(na.omit(c(plotData$level, overall))) + + plotObject <- ggplot2::ggplot() + + ggplot2::geom_hline(yintercept = overall, col = "red", linewidth = 1, na.rm = TRUE) + + jaspGraphs::geom_point(plotData, mapping = ggplot2::aes(x = sampleSize, y = level), size = 3, na.rm = TRUE) + + ggplot2::scale_x_continuous(name = gettext("Sample size"), breaks = xBreaks, limits = range(xBreaks)) + + ggplot2::scale_y_continuous(name = .qcAttributeRateLabel(options), breaks = yBreaks, limits = range(yBreaks)) + + jaspGraphs::geom_rangeframe() + + jaspGraphs::themeJaspRaw() + + return(plotObject) +} + +.qcAttributeHistogramPlotObject <- function(state, options) { + scale <- .qcAttributeRateScale(options) + level <- na.omit(scale * state[["data"]][["rate"]]) + plotData <- data.frame(level = as.numeric(level)) + target <- .qcAttributeTargetValue(options) + + # breaks is a suggestion: hist() rounds the boundaries to readable values, as in the histograms of + # the continuous path + histogram <- hist(plotData$level, plot = FALSE, breaks = options[["attributeHistogramBinNumber"]]) + binWidth <- histogram$breaks[2] - histogram$breaks[1] + # the target is included in the breaks so its line stays inside the panel when it falls outside the data + xBreaks <- jaspGraphs::getPrettyAxisBreaks(c(histogram$breaks, plotData$level, target), min.n = 4) + yBreaks <- jaspGraphs::getPrettyAxisBreaks(c(0, histogram$counts)) + + plotObject <- ggplot2::ggplot() + + ggplot2::geom_histogram(data = plotData, mapping = ggplot2::aes(x = level), fill = "grey", col = "black", + linewidth = .7, binwidth = binWidth, center = binWidth / 2, na.rm = TRUE) + # darkgreen for the target as in the other capability plots, dashed and drawn over the bars + if (!is.null(target)) + plotObject <- plotObject + + ggplot2::geom_vline(xintercept = target, col = "darkgreen", linetype = "dashed", linewidth = 1) + plotObject <- plotObject + + ggplot2::scale_x_continuous(name = .qcAttributeRateLabel(options), breaks = xBreaks, limits = range(xBreaks)) + + ggplot2::scale_y_continuous(name = gettext("Count"), breaks = yBreaks, limits = range(yBreaks)) + + jaspGraphs::geom_rangeframe() + + jaspGraphs::themeJaspRaw() + + return(plotObject) +} + +.qcAttributeSampleAxis <- function(state, options) { + index <- state[["data"]][["index"]] + xBreaks <- unique(as.integer(jaspGraphs::getPrettyAxisBreaks(index))) + xBreaks <- xBreaks[xBreaks >= 1 & xBreaks <= max(index)] + labels <- state[["data"]][["labels"]] + xLabels <- if (length(labels) > 0) labels[xBreaks] else xBreaks + + return(ggplot2::scale_x_continuous(name = .qcAttributeXAxisTitle(options), breaks = xBreaks, + limits = c(min(index) - .5, max(index) + .5), labels = xLabels)) +} + +# Summary table ---- + +.qcAttributeSummaryTable <- function(container, options, ready) { + if (!is.null(container[["summaryTable"]])) + return() + + table <- createJaspTable(title = gettext("Summary statistics")) + table$position <- 6 + table$dependOn(c("attributeSummaryTable", "attributeCiLevel", "binomialCiMethod", "poissonCiMethod", + "poissonYieldStatistics", "controlLimitsNumberOfSigmas", + .getDependenciesControlChartRules())) + table$showSpecifiedColumnsOnly <- TRUE + + ciTitle <- gettextf("%s%% CI", options[["attributeCiLevel"]] * 100) + table$addColumnInfo(name = "statistic", title = "", type = "string") + table$addColumnInfo(name = "value", title = gettext("Value"), type = "number") + table$addColumnInfo(name = "ciLower", title = gettext("Lower"), type = "number", overtitle = ciTitle) + table$addColumnInfo(name = "ciUpper", title = gettext("Upper"), type = "number", overtitle = ciTitle) + + container[["summaryTable"]] <- table + + state <- .qcAttributeGetState(container) + if (!ready || is.null(state)) + return() + + table$setData(.qcAttributeSummaryDataframe(state, options)) + for (footnote in .qcAttributeSummaryFootnotes(state, options)) + table$addFootnote(footnote) +} + +.qcAttributeSummaryDataframe <- function(state, options, formatNumbers = FALSE) { + if (.qcAttributeIsPoisson(options)) { + # Minitab's Poisson capability summary is the mean DPU and its interval, and nothing else + tableDf <- data.frame(statistic = gettext("Mean DPU"), + value = state[["rateBar"]], + ciLower = state[["rateCi"]][1], + ciUpper = state[["rateCi"]][2], + stringsAsFactors = FALSE) + + if (options[["poissonYieldStatistics"]]) + tableDf <- rbind(tableDf, + data.frame(statistic = c(gettextf("Defective units (%%)"), gettext("PPM defective"), + gettext("Process Z")), + value = c(state[["percentUnits"]], state[["ppmUnits"]], state[["processZ"]]), + ciLower = c(state[["percentUnitsCi"]][1], state[["ppmUnitsCi"]][1], + state[["processZCi"]][1]), + ciUpper = c(state[["percentUnitsCi"]][2], state[["ppmUnitsCi"]][2], + state[["processZCi"]][2]), + stringsAsFactors = FALSE)) + + if (options[["poissonTarget"]]) + tableDf <- rbind(tableDf, data.frame(statistic = gettext("Target DPU"), + value = options[["poissonTargetValue"]], + ciLower = NA_real_, + ciUpper = NA_real_, + stringsAsFactors = FALSE)) + } else { + tableDf <- data.frame(statistic = c(gettextf("Defective (%%)"), gettext("PPM defective"), gettext("Process Z")), + value = c(state[["percentDefective"]], state[["ppm"]], state[["processZ"]]), + ciLower = c(state[["percentDefectiveCi"]][1], state[["ppmCi"]][1], state[["processZCi"]][1]), + ciUpper = c(state[["percentDefectiveCi"]][2], state[["ppmCi"]][2], state[["processZCi"]][2]), + stringsAsFactors = FALSE) + + if (options[["binomialTarget"]]) + tableDf <- rbind(tableDf, data.frame(statistic = gettextf("Target defective (%%)"), + value = options[["binomialTargetValue"]], + ciLower = NA_real_, + ciUpper = NA_real_, + stringsAsFactors = FALSE)) + } + + if (formatNumbers) { + for (column in c("value", "ciLower", "ciUpper")) + tableDf[[column]] <- .pcTableFormatNumbers(tableDf[[column]]) + colnames(tableDf) <- c(gettext("Statistic"), gettext("Value"), + gettextf("Lower %s%% CI", state[["ciLevelPercent"]]), + gettextf("Upper %s%% CI", state[["ciLevelPercent"]])) + } + + return(tableDf) +} + +.qcAttributeSummaryFootnotes <- function(state, options) { + footnotes <- c() + poisson <- .qcAttributeIsPoisson(options) + + # the violation count is computed with the statistics, so this warning also shows when the control + # chart itself is switched off + if (state[["nViolations"]] > 0) + footnotes <- c(footnotes, sprintf(ngettext(state[["nViolations"]], + "The process is not in control (%i point fails the selected tests); the capability estimate may not be representative.", + "The process is not in control (%i points fail the selected tests); the capability estimate may not be representative."), + state[["nViolations"]])) + + footnotes <- c(footnotes, if (poisson) + switch(options[["poissonCiMethod"]], + "exact" = gettext("Confidence intervals are exact (Garwood)."), + "wald" = gettext("Confidence intervals use the Wald (normal approximation) method."), + "score" = gettext("Confidence intervals use the score method.")) + else + switch(options[["binomialCiMethod"]], + "exact" = gettext("Confidence intervals are exact (Clopper-Pearson)."), + "wald" = gettext("Confidence intervals use the Wald (normal approximation) method."), + "wilson" = gettext("Confidence intervals use the Wilson score method."))) + + if (poisson) { + if (options[["poissonHistoricalDpu"]]) + footnotes <- c(footnotes, gettextf("The control chart centre line uses a historical defect rate of %s defects per unit; the statistics in this table are estimated from the observed data.", + options[["poissonHistoricalDpuValue"]])) + if (options[["poissonYieldStatistics"]]) + footnotes <- c(footnotes, gettext("Yield statistics assume that a unit is conforming when it carries no defect.")) + } else if (options[["binomialHistoricalProportion"]]) { + footnotes <- c(footnotes, gettextf("The control chart centre line uses a historical proportion defective of %s%%; the statistics in this table are estimated from the observed data.", + options[["binomialHistoricalProportionValue"]])) + } + + nMissing <- state[["data"]][["nMissing"]] + if (nMissing > 0) + footnotes <- c(footnotes, sprintf(ngettext(nMissing, + "%i sample with missing values was excluded from the statistics.", + "%i samples with missing values were excluded from the statistics."), + nMissing)) + + if (poisson) { + # a defect rate is unbounded above, so there is no "everything defective" degenerate case + if (isTRUE(state[["totalCounts"]] == 0)) + footnotes <- c(footnotes, gettext("No defects were observed. The mean DPU is zero and only its upper confidence bound is informative.")) + } else if (isTRUE(state[["totalCounts"]] == 0)) { + footnotes <- c(footnotes, gettext("No defectives were observed. The Process Z is therefore unbounded and only its confidence bound is informative.")) + } else if (isTRUE(state[["totalCounts"]] == state[["totalExposure"]])) { + footnotes <- c(footnotes, gettext("All inspected units were defective. The Process Z is therefore unbounded and only its confidence bound is informative.")) + } + + return(footnotes) +} + +# Report ---- + +.qcAttributeReport <- function(jaspResults, data, options, ready) { + # the attribute report draws its own panels, so it counts them itself instead of reusing the + # element count of the continuous path + nSupportingPlots <- sum(options[["attributeCumulativePlot"]], options[["attributeDistributionPlot"]], + .qcAttributeShowPanel(options, "rate"), .qcAttributeShowPanel(options, "histogram")) + nElements <- sum(options[["reportProcessStability"]], + options[["reportProcessCapabilityPlot"]] * nSupportingPlots, + options[["reportProcessCapabilityTables"]], + options[["reportMetaData"]]) + plotHeight <- max(1, ceiling(nElements / 2)) * 500 + + reportPlot <- createJaspPlot(title = gettext("Process Capability Report"), width = 1250, height = plotHeight) + jaspResults[["report"]] <- reportPlot + # the report element key is shared with the continuous path, so capabilityDataType must be part of + # the dependencies (it is, through .qcAttributeOptionNames) + jaspResults[["report"]]$dependOn(c( + .qcAttributeOptionNames(), .qcReportOptionNames(), + "attributeControlChart", "attributeCumulativePlot", "attributeDistributionPlot", + "attributeRatePlot", "attributeHistogram", "attributeHistogramBinNumber", "attributeSummaryTable", + "attributeCiLevel", "binomialCiMethod", "poissonCiMethod", "poissonYieldStatistics", + "controlLimitsNumberOfSigmas", .getDependenciesControlChartRules() + )) + + if (!options[["reportProcessStability"]] && !options[["reportProcessCapabilityPlot"]] && + !options[["reportProcessCapabilityTables"]]) { + reportPlot$setError(gettext("No report components selected.")) + return() + } + + if (!ready) + return() + + state <- .qcAttributeStatistics(data, options) + + title <- "" + if (options[["reportTitle"]]) + title <- if (options[["reportTitleText"]] == "") gettext("Process Capability Report") else options[["reportTitleText"]] + + text <- NULL + if (options[["reportMetaData"]]) { + text <- c() + text <- if (options[["reportLocation"]]) c(text, gettextf("Location: %s", options[["reportLocationText"]])) else text + text <- if (options[["reportLine"]]) c(text, gettextf("Line: %s", options[["reportLineText"]])) else text + text <- if (options[["reportMachine"]]) c(text, gettextf("Machine: %s", options[["reportMachineText"]])) else text + text <- if (options[["reportVariable"]]) c(text, gettextf("Variable: %s", options[["reportVariableText"]])) else text + text <- if (options[["reportProcess"]]) c(text, gettextf("Process: %s", options[["reportProcessText"]])) else text + text <- if (options[["reportDate"]]) c(text, gettextf("Date: %s", options[["reportDateText"]])) else text + text <- if (options[["reportReportedBy"]]) c(text, gettextf("Reported by: %s", options[["reportReportedByText"]])) else text + text <- if (options[["reportConclusion"]]) c(text, gettextf("Conclusion: %s", options[["reportConclusionText"]])) else text + } + + plots <- list() + if (options[["reportProcessStability"]]) + plots[[length(plots) + 1]] <- .qcAttributeControlChartPlotObject(state, options) + if (options[["reportProcessCapabilityPlot"]]) { + if (options[["attributeCumulativePlot"]]) + plots[[length(plots) + 1]] <- .qcAttributeCumulativePlotObject(state, options) + if (options[["attributeDistributionPlot"]]) + plots[[length(plots) + 1]] <- .qcAttributeDistributionPlotObject(state, options) + if (.qcAttributeShowPanel(options, "rate")) + plots[[length(plots) + 1]] <- .qcAttributeRatePlotObject(state, options) + if (.qcAttributeShowPanel(options, "histogram")) + plots[[length(plots) + 1]] <- .qcAttributeHistogramPlotObject(state, options) + } + # .qcReport cannot lay out an empty plot list + if (length(plots) == 0) + plots <- list(ggplot2::ggplot() + ggplot2::theme_void()) + + tables <- list() + tableTitles <- "" + if (options[["reportProcessCapabilityTables"]]) { + tables[[1]] <- .qcAttributeSummaryDataframe(state, options, formatNumbers = TRUE) + tableTitles <- list(gettext("Summary statistics")) + } + + reportPlot$plotObject <- .qcReport(text = text, plots = plots, tables = tables, textMaxRows = 8, + tableTitles = tableTitles, reportTitle = title, tableSize = 6) +} + +# Dependencies ---- + +# Both distributions' options are listed unconditionally: the inactive set never changes while the +# user works in the other mode, so the over-inclusion costs nothing and a conditional vector would be +# one more thing to get wrong. +.qcAttributeOptionNames <- function() { + dependencies <- c("capabilityDataType", "attributeDistribution", + "attributeCounts", "attributeSampleSizeType", "attributeSampleSizeValue", + "attributeSampleSizeVariable", "attributeLabels", + "binomialHistoricalProportion", "binomialHistoricalProportionValue", + "binomialTarget", "binomialTargetValue", + "poissonHistoricalDpu", "poissonHistoricalDpuValue", + "poissonTarget", "poissonTargetValue") + return(dependencies) +} diff --git a/R/processCapabilityStudies.R b/R/processCapabilityStudies.R index 78972f622..f8957177b 100644 --- a/R/processCapabilityStudies.R +++ b/R/processCapabilityStudies.R @@ -17,6 +17,12 @@ #' @export processCapabilityStudies <- function(jaspResults, dataset, options) { + # Attribute (count) data has its own data entry, charts and statistics and shares nothing with the + # continuous pipeline below, so it branches out before any of it. Which of the two attribute + # analyses runs, binomial or Poisson, is decided there by attributeDistribution. + if (options[["capabilityDataType"]] == "attributes") + return(.qcAttributeCapability(jaspResults, dataset, options)) + wideFormat <- options[["dataFormat"]] == "wideFormat" # In wide format we have one subgroup per row, else we need a either a grouping variable or later specify subgroup size manually if (wideFormat) { @@ -127,8 +133,8 @@ processCapabilityStudies <- function(jaspResults, dataset, options) { jaspResults[["zeroWarning"]] <- createJaspHtml(text = gettext("All zero values have been replaced with a value equal to one-half of the smallest data point."), elementType = "p", title = "Zero values found in non-normal capability study:", position = 1) - jaspResults[["zeroWarning"]]$dependOn(c("measurementLongFormat", "measurementsWideFormat", "capabilityStudyType", - "nullDistribution")) + jaspResults[["zeroWarning"]]$dependOn(c("capabilityDataType", "measurementLongFormat", "measurementsWideFormat", + "capabilityStudyType", "nullDistribution")) } } @@ -3882,7 +3888,7 @@ processCapabilityStudies <- function(jaspResults, dataset, options) { } .qcDataOptionNames <- function() { - dependencies <- c("dataFormat", + dependencies <- c("capabilityDataType", "dataFormat", "measurementLongFormat", "subgroup","stagesLongFormat", "measurementsWideFormat", "stagesWideFormat", "subgroupSizeType", "groupingVariable", "groupingVariableMethod", diff --git a/inst/help/attributesCharts.md b/inst/help/attributesCharts.md index 7012ff81a..3d4a91e31 100644 --- a/inst/help/attributesCharts.md +++ b/inst/help/attributesCharts.md @@ -38,6 +38,18 @@ Defects charts: Defects charts are used for products that have multiple defects X-mR chart, which charts the process values (individuals) and moving range (mR) over time. +### Relation to *Process Capability Studies* +The *Process Capability Studies* analysis also produces p and u charts, when its data type is set to "Pass/fail counts (attributes)". Its count type chooses between them: "Defective units (binomial)" gives a p chart with %Defective, PPM defective and Process Z, and "Defects per unit (Poisson)" gives a u chart with the mean defects per unit (DPU). Use that analysis when you want capability statistics alongside the chart, and this one when you want np, c or Laney charts. + +The two **p charts** do not have to agree: +- This analysis replaces the stepped control limits with constant limits computed from the mean sample size whenever min(n)/max(n) is at least 0.75; *Process Capability Studies* always computes the limits from the individual sample size, so its limits step whenever the sample size changes. +- The two use different out-of-control rule engines, so they can flag different points. + +The two **u charts** compute the same limits, both using the per-sample formula. They can still differ in three ways: +- The rule engines differ, so they can flag different points. +- The number of standard deviations is fixed at 3 here and configurable in *Process Capability Studies*. +- This analysis rejects a sample whose number of defects exceeds the sample size. That restriction is correct for the p and np charts, where each unit is either good or defective, but not for the u and c charts: a single unit can carry several defects, so ten defects on five inspected units is a legitimate rate of 2 defects per unit. *Process Capability Studies* accepts such samples in its Poisson mode. + ### Out-of-control Signals ------- diff --git a/inst/help/processCapabilityStudies.md b/inst/help/processCapabilityStudies.md index 4172d7a25..221666c90 100644 --- a/inst/help/processCapabilityStudies.md +++ b/inst/help/processCapabilityStudies.md @@ -7,6 +7,18 @@ Rational subgroup: "A subgroup gathered in such a manner as to give the maximum ## Input ------- +### Data Type +- **Measurement data (variables)**: each observation is a continuous measurement that is compared against specification limits. This is the analysis described in the rest of this page and produces Cp/Cpk/Pp/Ppk. +- **Pass/fail counts (attributes)**: the data are counts per sample rather than measurements. There are no specification limits. See "Attribute Capability Analysis" below. + +### Count Type +In attributes mode a second choice decides how the counts are interpreted. The distinction is between defective *units* and *defects*, and it is the most common source of confusion between the two modes: + +- **Defective units (binomial)**: every inspected unit is classified as either good or defective, so the count can never exceed the number of units inspected. Capability is expressed as %Defective, PPM defective and Process Z. +- **Defects per unit (Poisson)**: a single unit can carry several defects, so the count *can* exceed the number of units inspected. Ten defects found on five inspected units is perfectly legal and means a rate of 2 defects per unit. Capability is expressed as DPU (defects per unit). + +Variables that were assigned in the other data type stay assigned when you switch, but they are not used by the active analysis. + ### Data Format Data can be in the form of all observations in one column ("Single column") or across rows with a subgroup index ("Across rows"). @@ -123,6 +135,114 @@ The size of the subgroups is relevant for the calculation of the process varianc +## Attribute Capability Analysis +------- +Selected with **Data type → Pass/fail counts (attributes)**. The data are counts per sample rather than measurements, so there are no specification limits and capability is not expressed as Cp/Cpk/Pp/Ppk. The **Count type** decides which of the two analyses below runs. + +Stages are not supported in this mode. + +### Scale conventions of the two modes +The two modes use opposite conventions, so they are stated here side by side: + +| | binomial | Poisson | +|---|---|---| +| reported throughout | percent | DPU (a rate) | +| exception | the y-axis of the p chart is a proportion, following the usual SPC convention | the optional yield rows of the summary table are in percent and PPM | +| historical value | a percentage | a rate | +| target | a percentage | a rate | + +### Binomial Capability Analysis +Every inspected unit is classified as either good or defective, and the data consist of the number of defective units $D_i$ found in a sample of $n_i$ inspected units. Capability is expressed as the percentage of defective units, the equivalent number of defectives per million and the corresponding sigma level. Because each unit is either good or defective, $D_i$ can never exceed $n_i$; a sample where it does is reported as an error. + +#### Assignment Box +- **Defectives**: the number of defective units found in each sample. +- **Sample size (Total)**: the number of units inspected in each sample. Only used when the sample size is set to "Variable". +- **Timestamp (optional)**: labels for the samples, used on the x-axis of the p chart and in the test results table. + +#### Options +- **Sample size**: choose "Constant" and enter the number of units inspected in every sample, or choose "Variable" and assign a column holding the sample size. This choice also decides which of the two sample-size dependent panels is offered ("Rate of defectives" for variable sizes, "Distribution of defective (%)" for a constant size), so the label of the check box always matches what is drawn. +- **Historical proportion defective (%)**: use a known proportion as the centre line of the p chart instead of estimating it from the data. This changes the chart only. The summary statistics are always estimated from the observed data, so that the reported estimate stays inside its own confidence interval. +- **Target defective (%)**: a target percentage, reported in the summary table and drawn as a reference line in the cumulative plot and in the histogram. +- **Confidence interval** and **Interval method**: the level and the method used for the intervals in the summary table and for the band of the cumulative plot. + +#### Output +- **p chart**: the observed proportion defective $\hat{p}_i = D_i / n_i$ per sample with the centre line at $\bar{p} = \sum D_i / \sum n_i$ and control limits at $\bar{p} \pm k\sqrt{\bar{p}(1-\bar{p})/n_i}$, where $k$ is the number of standard deviations set under Advanced options. Because the limits depend on $n_i$, they step up and down whenever the number of inspected units changes: a smaller sample gives a less precise estimate and therefore wider limits. Limits are clamped to the interval $[0, 1]$. Only the run based tests (beyond limit, shift, trend, oscillation) are applied; the zone based tests are not, because the zones of a clamped, asymmetric chart do not correspond to actual sigma multiples. +- **Cumulative defective (%)**: the running estimate $\sum_{j \le i} D_j / \sum_{j \le i} n_j$ with a confidence band, the overall estimate as a horizontal line and, if set, the target. The band shows whether enough samples were collected for the estimate to settle. +- **Binomial plot**: the observed number of defectives against the expected number, with the diagonal $y = x$ as reference. The expected number is $n_i$ times the centre line proportion of the p chart, that is $n_i \bar{p}$, or $n_i$ times the historical proportion when one is set. Points scattering around the diagonal support the binomial assumption. +- **Rate of defectives**: the percentage defective against the sample size. A trend indicates that the percentage defective depends on how many units were inspected. +- **Distribution of defective (%)**: a histogram of the percentage defective across samples. If a target is set, it is drawn as a dashed vertical line, so a distribution sitting to the right of the line marks samples worse than the target. **Number of bins** sets the suggested number of bins; the boundaries are rounded to readable values, so the histogram can end up with a slightly different number of bins. +- **Summary statistics**: + - **Defective (%)** $= 100\,\bar{p}$ + - **PPM defective** $= 10^6\,\bar{p}$ + - **Process Z** $= \Phi^{-1}(1 - \bar{p})$, the standard normal quantile corresponding to the estimated proportion defective. Larger is better. It is unbounded when no defectives, or only defectives, were observed; in that case only the confidence bound is informative. + - Confidence intervals are computed on $\bar{p}$ and transformed to the PPM and Z scale. Because Z decreases in $p$, the upper bound of $p$ gives the lower bound of Z. Three methods are available: **exact** (Clopper-Pearson, the default and the most conservative), **Wald** (normal approximation, unreliable for small counts) and **Wilson** score. + - A footnote warns when the p chart shows out-of-control points, because a capability estimate from an unstable process is not representative of future output. + +#### Assumptions +- Units are inspected independently of one another. +- The probability that a unit is defective is constant within a sample. +- The number of inspected units per sample is known. +- The process is in statistical control. If the p chart flags points, the capability estimate describes the observed data but does not predict future output. + +### Poisson Capability Analysis +A single inspected unit can carry any number of defects, and the data consist of the total number of defects $C_i$ found in a sample of $n_i$ inspected units. Capability is expressed as **DPU**, the mean number of defects per unit. + +Because a unit can carry several defects, **$C_i$ may exceed $n_i$**. Ten defects on five inspected units is a DPU of 2 and is analysed without complaint; only a negative or non-integer count is rejected. + +#### Assignment Box +- **Defects**: the total number of defects found in each sample. +- **Sample size (Total)**: the number of units inspected in each sample, or the size of the inspected unit. Only used when the sample size is set to "Variable". +- **Timestamp (optional)**: labels for the samples, used on the x-axis of the u chart and in the test results table. + +The inspected amount need not be a whole number: 2.5 square metres of sheet or 1.5 hours of operation are valid exposures. A **fractional sample size is accepted when it is assigned as a column**, but the *constant* sample size field takes whole numbers only, because the same field serves the binomial mode where a fractional number of inspected units is meaningless. Assign a column when the constant exposure is fractional. A sample with a sample size of zero carries no information and is excluded as a missing sample; it keeps its row, so the point numbering of the chart does not shift. + +#### Options +- **Sample size**: as in the binomial mode, and it likewise decides which of the two sample-size dependent panels is offered. +- **Historical defects per unit**: use a known defect rate as the centre line of the u chart instead of estimating it from the data. This is a rate, not a percentage. It changes the chart only; the summary statistics are always estimated from the observed data, so that the reported estimate stays inside its own confidence interval. +- **Target defects per unit**: a target rate, reported in the summary table and drawn as a reference line in the cumulative plot and in the histogram. +- **Confidence interval** and **Interval method**: the level and the method used for the interval on the mean DPU and for the band of the cumulative plot. + +#### Output +- **u chart**: the observed defect rate $u_i = C_i / n_i$ per sample with the centre line at $\bar{u} = \sum C_i / \sum n_i$ and control limits at $\bar{u} \pm k\sqrt{\bar{u}/n_i}$, where $k$ is the number of standard deviations set under Advanced options. As on the p chart the limits step whenever $n_i$ changes. **Only the lower limit is clamped, at 0**: a defect rate has no upper bound, so there is no upper clamp. Only the run based tests are applied, for the same reason as on the p chart. When every sample inspects exactly one unit ($n \equiv 1$) the u chart is a c chart. +- **Cumulative defects per unit**: the running estimate $\sum_{j \le i} C_j / \sum_{j \le i} n_j$ with a confidence band, the overall estimate as a horizontal line and, if set, the target. +- **Poisson plot**: the observed number of defects against the expected number $n_i \bar{u}$, with the diagonal $y = x$ as reference. Points scattering around the diagonal support the Poisson assumption; a systematic spread that grows faster than the diagonal suggests overdispersion, for which a Laney u′ chart in *Control Charts for Attributes* is more appropriate. +- **Rate of defects**: the defect rate against the sample size. A trend indicates that the rate depends on how much was inspected. +- **Distribution of defects per unit**: a histogram of the defect rate across samples, with the target as a dashed vertical line if one is set. +- **Summary statistics**: + - **Mean DPU** $= \bar{u} = \sum C_i / \sum n_i$, with a confidence interval. This is the whole analysis by default, matching Minitab's Poisson capability summary. + - Writing $C = \sum C_i$, $N = \sum n_i$, $\alpha = 1 - \text{level}$ and $z = \Phi^{-1}(1 - \alpha/2)$, three interval methods are available: + - **Exact (Garwood)**, the default and the Poisson analogue of Clopper-Pearson: lower $= F^{-1}_{\Gamma}(\alpha/2;\, C)/N$ and upper $= F^{-1}_{\Gamma}(1-\alpha/2;\, C+1)/N$, with the lower bound taken as 0 when $C = 0$. + - **Wald**, the normal approximation: $\bar{u} \pm z\sqrt{\bar{u}/N}$, floored at 0. Unreliable for small counts. + - **Score** (Rao), the Poisson analogue of Wilson: $\left(C + z^2/2 \pm z\sqrt{C + z^2/4}\right)/N$, floored at 0. + - **Yield statistics** (optional, off by default) add three derived rows. They rest on an assumption that the Poisson model itself does not make, namely that **a unit is conforming exactly when it carries no defect**; a footnote states this whenever they are shown. With that assumption the probability that a unit is free of defects is $e^{-\bar{u}}$, so: + - **Defective units (%)** $= 100\left(1 - e^{-\bar{u}}\right)$ + - **PPM defective** $= 10^6\left(1 - e^{-\bar{u}}\right)$ + - **Process Z** $= \Phi^{-1}\!\left(e^{-\bar{u}}\right)$ + All three are monotone in $\bar{u}$, so the DPU interval carries over directly. The percentage and PPM keep the order of the bounds; Z reverses it, because Z decreases as the defect rate rises. + - A footnote warns when the u chart shows out-of-control points. + - When no defects at all were observed the mean DPU is exactly zero, the control limits collapse onto the centre line and nothing is flagged. A footnote points out that only the upper confidence bound is informative. There is no corresponding upper degenerate case, because a defect rate is unbounded above. + +#### Assumptions +- Defects occur independently of one another. +- The defect rate is constant within a sample. +- The inspected amount per sample is known. +- The process is in statistical control. If the u chart flags points, the capability estimate describes the observed data but does not predict future output. + +### Relation to *Control Charts for Attributes* +That analysis also produces p and u charts, and the results do not have to agree. + +For the **p chart** the limits themselves can differ: +- This analysis always draws stepped control limits computed from the individual sample size $n_i$. +- *Control Charts for Attributes* replaces the stepped limits with constant limits computed from the mean sample size whenever $\min(n)/\max(n) \ge 0.75$. + +For the **u chart** the limits agree: both analyses compute $\bar{u} \pm 3\sqrt{\bar{u}/n_i}$ per sample. What differs is: +- the out-of-control rule engine and the styling, so the two can flag different points; +- the number of standard deviations, which is fixed at 3 in *Control Charts for Attributes* and configurable here under Advanced options; +- *Control Charts for Attributes* additionally offers the Laney u′ chart for overdispersed data, which this analysis does not; +- *Control Charts for Attributes* rejects a sample whose count exceeds the sample size even on a u chart, which this analysis correctly allows. + +Use this analysis when you want capability statistics alongside the chart, and *Control Charts for Attributes* when you want np, c or Laney charts. + ## References ------- 1. Automotive Industry Action Group, *Statistical Process Control - Reference Manual* (July 2005, 2nd Edition) diff --git a/inst/qml/processCapabilityStudies.qml b/inst/qml/processCapabilityStudies.qml index a4fb8eb1a..9b13f12fa 100644 --- a/inst/qml/processCapabilityStudies.qml +++ b/inst/qml/processCapabilityStudies.qml @@ -22,11 +22,41 @@ Form { columns: 2 + DropDown + { + name: "capabilityDataType" + id: capabilityDataType + label: qsTr("Data type") + info: qsTr("Choose measurement data (continuous values compared against specification limits) or counts of defective units or defects per sample.") + indexDefaultValue: 0 + values: + [ + {label: qsTr("Measurement data (variables)"), value: "variables"}, + {label: qsTr("Pass/fail counts (attributes)"), value: "attributes"}, + ] + } + + DropDown + { + name: "attributeDistribution" + id: attributeDistribution + label: qsTr("Count type") + info: qsTr("Choose whether each inspected unit is classified as good or defective (binomial), or whether the number of defects found on the inspected units is counted, so that one unit can carry several defects (Poisson).") + visible: capabilityDataType.currentValue == "attributes" + indexDefaultValue: 0 + values: + [ + {label: qsTr("Defective units (binomial)"), value: "binomial"}, + {label: qsTr("Defects per unit (Poisson)"), value: "poisson"}, + ] + } + DropDown { name: "dataFormat" label: qsTr("Data format") id: dataFormat + visible: capabilityDataType.currentValue == "variables" indexDefaultValue: 0 values: [ @@ -43,7 +73,7 @@ Form VariablesForm { id: variablesFormLongFormat - visible: dataFormat.currentValue == "longFormat" + visible: capabilityDataType.currentValue == "variables" && dataFormat.currentValue == "longFormat" AvailableVariablesList { @@ -82,7 +112,7 @@ Form VariablesForm { id: variablesFormWideFormat - visible: dataFormat.currentValue == "wideFormat" + visible: capabilityDataType.currentValue == "variables" && dataFormat.currentValue == "wideFormat" AvailableVariablesList { @@ -116,9 +146,52 @@ Form } } + VariablesForm + { + id: variablesFormAttributes + visible: capabilityDataType.currentValue == "attributes" + + AvailableVariablesList + { + name: "variablesFormAttributes" + } + + AssignedVariablesList + { + name: "attributeCounts" + title: attributeDistribution.currentValue == "poisson" ? qsTr("Defects") : qsTr("Defectives") + id: attributeCounts + info: qsTr("For defective units (binomial), the number of defective units found in each inspected sample. For defects per unit (Poisson), the total number of defects found in each inspected sample, which may exceed the number of units inspected.") + allowedColumns: ["scale"] + singleVariable: true + } + + AssignedVariablesList + { + name: "attributeSampleSizeVariable" + title: qsTr("Sample size (Total)") + id: attributeSampleSizeVariable + info: qsTr("The number of units inspected in each sample, or the size of the inspected unit. Only used when the sample size varies between samples. For defects per unit (Poisson) the inspected amount may be fractional, for example 2.5 square metres.") + allowedColumns: ["scale"] + singleVariable: true + enabled: attributeSampleSizeType.value == "variable" + } + + AssignedVariablesList + { + name: "attributeLabels" + title: qsTr("Timestamp (optional)") + id: attributeLabels + info: qsTr("Optional labels for the samples, used on the x-axis of the control chart and in the test results table.") + singleVariable: true + allowedColumns: ["nominal"] + } + } + Group { columns: 2 + visible: capabilityDataType.currentValue == "variables" RadioButtonGroup { @@ -201,6 +274,8 @@ Form ColumnLayout { + visible: capabilityDataType.currentValue == "variables" + Group { title: qsTr("Transform data") @@ -614,6 +689,7 @@ Form ColumnLayout { + visible: capabilityDataType.currentValue == "variables" Group { @@ -729,6 +805,241 @@ Form } } } + + ColumnLayout + { + visible: capabilityDataType.currentValue == "attributes" + + Group + { + title: attributeDistribution.currentValue == "poisson" ? qsTr("Poisson capability") : qsTr("Binomial capability") + + RadioButtonGroup + { + name: "attributeSampleSizeType" + id: attributeSampleSizeType + title: qsTr("Sample size") + info: qsTr("Whether every sample contains the same number of inspected units, or the number of inspected units is given by a column in the data. A constant sample size must be a whole number; assign a column when the inspected amount is fractional.") + + RadioButton + { + value: "constant" + label: qsTr("Constant") + checked: true + childrenOnSameRow: true + + IntegerField + { + name: "attributeSampleSizeValue" + id: attributeSampleSizeValue + defaultValue: 50 + min: 1 + fieldWidth: 50 + } + } + + RadioButton + { + value: "variable" + label: qsTr("Variable (assign a column)") + } + } + + CheckBox + { + name: "binomialHistoricalProportion" + label: qsTr("Historical proportion defective (%)") + id: binomialHistoricalProportion + visible: attributeDistribution.currentValue == "binomial" + info: qsTr("Use a known proportion defective as the centre line of the p chart instead of estimating it from the data. The summary statistics are always estimated from the observed data.") + childrenOnSameRow: true + + DoubleField + { + name: "binomialHistoricalProportionValue" + id: binomialHistoricalProportionValue + defaultValue: 5 + min: 0 + max: 100 + decimals: 6 + negativeValues: false + } + } + + CheckBox + { + name: "poissonHistoricalDpu" + label: qsTr("Historical defects per unit") + id: poissonHistoricalDpu + visible: attributeDistribution.currentValue == "poisson" + info: qsTr("Use a known defect rate as the centre line of the u chart instead of estimating it from the data. The summary statistics are always estimated from the observed data.") + childrenOnSameRow: true + + DoubleField + { + name: "poissonHistoricalDpuValue" + id: poissonHistoricalDpuValue + defaultValue: 1 + min: 0 + decimals: 6 + negativeValues: false + } + } + + CheckBox + { + name: "binomialTarget" + label: qsTr("Target defective (%)") + id: binomialTarget + visible: attributeDistribution.currentValue == "binomial" + info: qsTr("Target percentage of defective units. Reported in the summary table and drawn as a reference line in the cumulative plot and the histogram.") + childrenOnSameRow: true + + DoubleField + { + name: "binomialTargetValue" + id: binomialTargetValue + defaultValue: 0 + min: 0 + max: 100 + decimals: 6 + negativeValues: false + } + } + + CheckBox + { + name: "poissonTarget" + label: qsTr("Target defects per unit") + id: poissonTarget + visible: attributeDistribution.currentValue == "poisson" + info: qsTr("Target number of defects per unit. Reported in the summary table and drawn as a reference line in the cumulative plot and the histogram.") + childrenOnSameRow: true + + DoubleField + { + name: "poissonTargetValue" + id: poissonTargetValue + defaultValue: 0 + min: 0 + decimals: 6 + negativeValues: false + } + } + } + + Group + { + title: qsTr("Output") + + CheckBox + { + name: "attributeControlChart" + label: attributeDistribution.currentValue == "poisson" ? qsTr("u chart") : qsTr("p chart") + info: qsTr("Control chart of the proportion defective (binomial) or of the defects per unit (Poisson) per sample. The control limits follow the sample size, so they step whenever the number of inspected units changes.") + checked: true + } + + CheckBox + { + name: "attributeCumulativePlot" + label: attributeDistribution.currentValue == "poisson" ? qsTr("Cumulative defects per unit") : qsTr("Cumulative defective (%)") + info: qsTr("Running estimate of the percentage defective (binomial) or of the defects per unit (Poisson) as samples accumulate, with a confidence band. Use it to judge whether enough samples were collected for the estimate to settle.") + checked: true + } + + CheckBox + { + name: "attributeDistributionPlot" + label: attributeDistribution.currentValue == "poisson" ? qsTr("Poisson plot") : qsTr("Binomial plot") + info: qsTr("Observed against expected number of defectives (binomial) or defects (Poisson) per sample. The points scatter around the diagonal when the assumed distribution holds.") + checked: false + } + + CheckBox + { + name: "attributeRatePlot" + label: attributeDistribution.currentValue == "poisson" ? qsTr("Rate of defects") : qsTr("Rate of defectives") + info: qsTr("Percentage defective (binomial) or defects per unit (Poisson) against sample size. Use it to detect a rate that drifts with the number of units inspected.") + checked: false + visible: attributeSampleSizeType.value == "variable" + } + + CheckBox + { + name: "attributeHistogram" + label: attributeDistribution.currentValue == "poisson" ? qsTr("Distribution of defects per unit") : qsTr("Distribution of defective (%)") + info: qsTr("Histogram of the percentage defective (binomial) or of the defects per unit (Poisson) across the samples, with the target as a dashed line if one is set.") + checked: false + visible: attributeSampleSizeType.value == "constant" + + DoubleField + { + name: "attributeHistogramBinNumber" + label: qsTr("Number of bins") + info: qsTr("Suggested number of bins. The bin boundaries are rounded to readable values, so the histogram can end up with a slightly different number of bins.") + defaultValue: 10 + min: 3; + max: 10000; + } + } + + CheckBox + { + name: "attributeSummaryTable" + label: qsTr("Summary statistics") + info: qsTr("Table of %Defective, PPM defective and Process Z (binomial), or of the mean defects per unit (Poisson), with confidence intervals.") + checked: true + + CheckBox + { + name: "poissonYieldStatistics" + label: qsTr("Yield statistics") + info: qsTr("Add the percentage of defective units, the PPM defective and the Process Z derived from the mean defects per unit. These assume that a unit is conforming when it carries no defect, which the Poisson model itself does not imply.") + checked: false + visible: attributeDistribution.currentValue == "poisson" + } + } + + CIField + { + name: "attributeCiLevel" + label: qsTr("Confidence interval") + info: qsTr("Confidence level for the intervals in the summary table and for the band of the cumulative plot.") + defaultValue: 95 + } + + DropDown + { + name: "binomialCiMethod" + label: qsTr("Interval method") + visible: attributeDistribution.currentValue == "binomial" + info: qsTr("Method used to compute the confidence interval for the proportion defective.") + indexDefaultValue: 0 + values: + [ + {label: qsTr("Exact (Clopper-Pearson)"), value: "exact"}, + {label: qsTr("Wald"), value: "wald"}, + {label: qsTr("Wilson score"), value: "wilson"} + ] + } + + DropDown + { + name: "poissonCiMethod" + label: qsTr("Interval method") + visible: attributeDistribution.currentValue == "poisson" + info: qsTr("Method used to compute the confidence interval for the mean defects per unit.") + indexDefaultValue: 0 + values: + [ + {label: qsTr("Exact (Garwood)"), value: "exact"}, + {label: qsTr("Wald"), value: "wald"}, + {label: qsTr("Score"), value: "score"} + ] + } + } + } } @@ -903,28 +1214,31 @@ Form CheckBox { name: "reportProcessStability" - label: qsTr("Show stability of process charts") + label: capabilityDataType.currentValue != "attributes" ? qsTr("Show stability of process charts") + : attributeDistribution.currentValue == "poisson" ? qsTr("Show u chart") : qsTr("Show p chart") checked: true } - + CheckBox { name: "reportProcessCapabilityPlot" - label: qsTr("Show process capability plot") + label: capabilityDataType.currentValue != "attributes" ? qsTr("Show process capability plot") + : attributeDistribution.currentValue == "poisson" ? qsTr("Show Poisson capability plots") : qsTr("Show binomial capability plots") checked: true } - + CheckBox { name: "reportProbabilityPlot" label: qsTr("Show probability plot") checked: true + visible: capabilityDataType.currentValue == "variables" } - + CheckBox { name: "reportProcessCapabilityTables" - label: qsTr("Show process capability tables") + label: capabilityDataType.currentValue == "attributes" ? qsTr("Show summary statistics table") : qsTr("Show process capability tables") checked: true } @@ -946,6 +1260,7 @@ Form { name: "probabilityPlotRankMethod" label: qsTr("Rank method for probability plot") + visible: capabilityDataType.currentValue == "variables" indexDefaultValue: 0 values: [ @@ -961,7 +1276,8 @@ Form name: "histogramBinBoundaryDirection" id: histogramBinBoundaryDirection label: qsTr("Histogram bin boundaries") - values: + visible: capabilityDataType.currentValue == "variables" + values: [ { label: qsTr("Left open"), value: "left"}, { label: qsTr("Right open"), value: "right"} @@ -974,7 +1290,8 @@ Form name: "nullDistribution" id: nullDistribution label: qsTr("Null distribution for probability plot") - values: + visible: capabilityDataType.currentValue == "variables" + values: [ { label: qsTr("Normal"), value: "normal" }, { label: qsTr("Log-normal"), value: "lognormal" }, @@ -1006,6 +1323,7 @@ Form { title: "" columns: 2 + visible: capabilityDataType.currentValue == "variables" DropDown { diff --git a/renv.lock b/renv.lock index 3107dd11f..7c1d6b943 100644 --- a/renv.lock +++ b/renv.lock @@ -4106,70 +4106,8 @@ }, "igraph": { "Package": "igraph", - "Version": "2.3.1", + "Version": "2.3.3", "Source": "Repository", - "Title": "Network Analysis and Visualization", - "Authors@R": "c( person(\"Gábor\", \"Csárdi\", , \"csardi.gabor@gmail.com\", role = \"aut\", comment = c(ORCID = \"0000-0001-7098-9676\")), person(\"Tamás\", \"Nepusz\", , \"ntamas@gmail.com\", role = \"aut\", comment = c(ORCID = \"0000-0002-1451-338X\")), person(\"Vincent\", \"Traag\", role = \"aut\", comment = c(ORCID = \"0000-0003-3170-3879\")), person(\"Szabolcs\", \"Horvát\", , \"szhorvat@gmail.com\", role = \"aut\", comment = c(ORCID = \"0000-0002-3100-523X\")), person(\"Fabio\", \"Zanini\", , \"fabio.zanini@unsw.edu.au\", role = \"aut\", comment = c(ORCID = \"0000-0001-7097-8539\")), person(\"Daniel\", \"Noom\", role = \"aut\"), person(\"Kirill\", \"Müller\", , \"kirill@cynkra.com\", role = c(\"aut\", \"cre\"), comment = c(ORCID = \"0000-0002-1416-3412\")), person(\"Michael\", \"Antonov\", role = \"ctb\"), person(\"Chan Zuckerberg Initiative\", role = \"fnd\", comment = c(ROR = \"02qenvm24\")), person(\"David\", \"Schoch\", , \"david.schoch@cynkra.com\", role = \"aut\", comment = c(ORCID = \"0000-0003-2952-4812\")), person(\"Maëlle\", \"Salmon\", , \"maelle@cynkra.com\", role = \"aut\", comment = c(ORCID = \"0000-0002-2815-0399\")), person(\"R Consortium\", role = \"fnd\", comment = c(ROR = \"01z833950\")) )", - "Description": "Routines for simple graphs and network analysis. It can handle large graphs very well and provides functions for generating random and regular graphs, graph visualization, centrality methods and much more.", - "License": "GPL (>= 2)", - "URL": "https://r.igraph.org/, https://igraph.org/, https://igraph.discourse.group/", - "BugReports": "https://github.com/igraph/rigraph/issues", - "Depends": [ - "methods", - "R (>= 3.5.0)" - ], - "Imports": [ - "cli", - "graphics", - "grDevices", - "lifecycle", - "magrittr", - "Matrix", - "pkgconfig (>= 2.0.0)", - "rlang (>= 1.1.0)", - "stats", - "utils", - "vctrs" - ], - "Suggests": [ - "ape (>= 5.7-0.1)", - "callr", - "decor", - "digest", - "igraphdata", - "knitr", - "rgl (>= 1.3.14)", - "rmarkdown", - "scales", - "stats4", - "tcltk", - "testthat", - "vdiffr", - "withr" - ], - "Enhances": [ - "graph" - ], - "LinkingTo": [ - "cpp11 (>= 0.5.0)" - ], - "VignetteBuilder": "knitr", - "Config/build/compilation-database": "false", - "Config/build/never-clean": "true", - "Config/comment/compilation-database": "Generate manually with pkgload:::generate_db() for faster pkgload::load_all()", - "Config/Needs/build": "devtools, irlba, pkgconfig", - "Config/Needs/coverage": "covr", - "Config/Needs/roxygen2": "r-lib/roxygen2, igraph/igraph.r2cdocs, moodymudskipper/devtag", - "Config/Needs/website": "here, readr, tibble, xmlparsedata, xml2", - "Config/testthat/edition": "3", - "Config/testthat/parallel": "true", - "Config/testthat/start-first": "aaa-auto, vs-es, scan, vs-operators, weakref, watts.strogatz.game", - "Encoding": "UTF-8", - "RoxygenNote": "7.3.3.9000", - "SystemRequirements": "libxml2 (optional), glpk (>= 4.57, optional)", - "NeedsCompilation": "yes", - "Author": "Gábor Csárdi [aut] (ORCID: ), Tamás Nepusz [aut] (ORCID: ), Vincent Traag [aut] (ORCID: ), Szabolcs Horvát [aut] (ORCID: ), Fabio Zanini [aut] (ORCID: ), Daniel Noom [aut], Kirill Müller [aut, cre] (ORCID: ), Michael Antonov [ctb], Chan Zuckerberg Initiative [fnd] (ROR: ), David Schoch [aut] (ORCID: ), Maëlle Salmon [aut] (ORCID: ), R Consortium [fnd] (ROR: )", - "Maintainer": "Kirill Müller ", "Repository": "CRAN" }, "irr": { @@ -5042,9 +4980,6 @@ "Package": "mgcv", "Version": "1.9-4", "Source": "Repository", - "Authors@R": "person(given = \"Simon\", family = \"Wood\", role = c(\"aut\", \"cre\"), email = \"simon.wood@r-project.org\")", - "Title": "Mixed GAM Computation Vehicle with Automatic Smoothness Estimation", - "Description": "Generalized additive (mixed) models, some of their extensions and other generalized ridge regression with multiple smoothing parameter estimation by (Restricted) Marginal Likelihood, Cross Validation and similar, or using iterated nested Laplace approximation for fully Bayesian inference. See Wood (2025) for an overview. Includes a gam() function, a wide variety of smoothers, 'JAGS' support and distributions beyond the exponential family.", "Priority": "recommended", "Depends": [ "R (>= 4.4.0)", @@ -5058,18 +4993,24 @@ "splines", "utils" ], + "LinkingTo": [ + null + ], "Suggests": [ "parallel", "survival", "MASS" ], - "LazyLoad": "yes", - "ByteCompile": "yes", + "Enhances": [ + null + ], "License": "GPL (>= 2)", + "License_is_FOSS": null, + "License_restricts_use": null, + "OS_type": null, "NeedsCompilation": "yes", - "Author": "Simon Wood [aut, cre]", - "Maintainer": "Simon Wood ", - "Repository": "CRAN" + "Repository": "RSPM", + "Type": "source" }, "microbenchmark": { "Package": "microbenchmark", diff --git a/tests/testthat/_snaps/doeAnalysis/histogram-of-residuals27.svg b/tests/testthat/_snaps/doeAnalysis/histogram-of-residuals27.svg index 3c964c0fc..e5505710f 100644 --- a/tests/testthat/_snaps/doeAnalysis/histogram-of-residuals27.svg +++ b/tests/testthat/_snaps/doeAnalysis/histogram-of-residuals27.svg @@ -21,55 +21,55 @@ - - + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - -0 -2 -4 -6 -8 - - - - - - - - - - - - --15 --10 --5 -0 -5 -10 -Counts -histogram-of-residuals27 + +0 +2 +4 +6 +8 + + + + + + + + + + + + +-15 +-10 +-5 +0 +5 +10 +Residuals +Counts +histogram-of-residuals27 diff --git a/tests/testthat/_snaps/doeAnalysis/matrix-residual-plot27-subplot-3.svg b/tests/testthat/_snaps/doeAnalysis/matrix-residual-plot27-subplot-3.svg index d210078c6..ea602103c 100644 --- a/tests/testthat/_snaps/doeAnalysis/matrix-residual-plot27-subplot-3.svg +++ b/tests/testthat/_snaps/doeAnalysis/matrix-residual-plot27-subplot-3.svg @@ -21,55 +21,55 @@ - - + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - -0 -2 -4 -6 -8 - - - - - - - - - - - - --15 --10 --5 -0 -5 -10 -Counts + +0 +2 +4 +6 +8 + + + + + + + + + + + + +-15 +-10 +-5 +0 +5 +10 +Residuals +Counts matrix-residual-plot27-subplot-3 diff --git a/tests/testthat/_snaps/processCapabilityBinomial/binomial-cumulative.svg b/tests/testthat/_snaps/processCapabilityBinomial/binomial-cumulative.svg new file mode 100644 index 000000000..88e07e465 --- /dev/null +++ b/tests/testthat/_snaps/processCapabilityBinomial/binomial-cumulative.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 +5 +10 +15 +20 + + + + + + + + + + + +5 +10 +15 +20 +25 +Sample +Cumulative defective (%) +binomial-cumulative + + diff --git a/tests/testthat/_snaps/processCapabilityBinomial/binomial-distribution.svg b/tests/testthat/_snaps/processCapabilityBinomial/binomial-distribution.svg new file mode 100644 index 000000000..f8630d202 --- /dev/null +++ b/tests/testthat/_snaps/processCapabilityBinomial/binomial-distribution.svg @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 +2 +4 +6 +8 +10 +12 + + + + + + + + + + + + + + + +0 +2 +4 +6 +8 +10 +12 +Expected defectives +Observed defectives +binomial-distribution + + diff --git a/tests/testthat/_snaps/processCapabilityBinomial/binomial-histogram-target.svg b/tests/testthat/_snaps/processCapabilityBinomial/binomial-histogram-target.svg new file mode 100644 index 000000000..003e9ddc2 --- /dev/null +++ b/tests/testthat/_snaps/processCapabilityBinomial/binomial-histogram-target.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 +2 +4 +6 +8 +10 +12 +14 + + + + + + + + + + + + + + + +0 +5 +10 +15 +20 +25 +Defective (%) +Count +binomial-histogram-target + + diff --git a/tests/testthat/_snaps/processCapabilityBinomial/binomial-histogram.svg b/tests/testthat/_snaps/processCapabilityBinomial/binomial-histogram.svg new file mode 100644 index 000000000..c1a735fbb --- /dev/null +++ b/tests/testthat/_snaps/processCapabilityBinomial/binomial-histogram.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 +2 +4 +6 +8 +10 +12 +14 + + + + + + + + + + + + + + + +0 +5 +10 +15 +20 +25 +Defective (%) +Count +binomial-histogram + + diff --git a/tests/testthat/_snaps/processCapabilityBinomial/binomial-p-chart.svg b/tests/testthat/_snaps/processCapabilityBinomial/binomial-p-chart.svg new file mode 100644 index 000000000..14ddf29e9 --- /dev/null +++ b/tests/testthat/_snaps/processCapabilityBinomial/binomial-p-chart.svg @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +CL = 0.07 + +LCL = 0 + +UCL = 0.16 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0.00 +0.05 +0.10 +0.15 +0.20 +0.25 + + + + + + + + + + + + + +1 +5 +10 +15 +20 +25 +Sample +Proportion defective +binomial-p-chart + + diff --git a/tests/testthat/_snaps/processCapabilityBinomial/binomial-rate.svg b/tests/testthat/_snaps/processCapabilityBinomial/binomial-rate.svg new file mode 100644 index 000000000..033d6be37 --- /dev/null +++ b/tests/testthat/_snaps/processCapabilityBinomial/binomial-rate.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 +5 +10 +15 +20 +25 + + + + + + + + + + + + + + +40 +45 +50 +55 +60 +65 +70 +Sample size +Defective (%) +binomial-rate + + diff --git a/tests/testthat/_snaps/processCapabilityBinomial/binomial-report-subplot-1.svg b/tests/testthat/_snaps/processCapabilityBinomial/binomial-report-subplot-1.svg new file mode 100644 index 000000000..8f43f4e8f --- /dev/null +++ b/tests/testthat/_snaps/processCapabilityBinomial/binomial-report-subplot-1.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + +binomial-report-subplot-1 + + diff --git a/tests/testthat/_snaps/processCapabilityBinomial/binomial-report-subplot-2.svg b/tests/testthat/_snaps/processCapabilityBinomial/binomial-report-subplot-2.svg new file mode 100644 index 000000000..d9a92aa72 --- /dev/null +++ b/tests/testthat/_snaps/processCapabilityBinomial/binomial-report-subplot-2.svg @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +CL = 0.07 + +LCL = 0 + +UCL = 0.16 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0.00 +0.05 +0.10 +0.15 +0.20 +0.25 + + + + + + + + + + + + + +1 +5 +10 +15 +20 +25 +Sample +Proportion defective +binomial-report-subplot-2 + + diff --git a/tests/testthat/_snaps/processCapabilityBinomial/binomial-report-subplot-3.svg b/tests/testthat/_snaps/processCapabilityBinomial/binomial-report-subplot-3.svg new file mode 100644 index 000000000..739812f85 --- /dev/null +++ b/tests/testthat/_snaps/processCapabilityBinomial/binomial-report-subplot-3.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 +5 +10 +15 +20 + + + + + + + + + + + +5 +10 +15 +20 +25 +Sample +Cumulative defective (%) +binomial-report-subplot-3 + + diff --git a/tests/testthat/_snaps/processCapabilityBinomial/binomial-report-subplot-4.svg b/tests/testthat/_snaps/processCapabilityBinomial/binomial-report-subplot-4.svg new file mode 100644 index 000000000..6de162abc --- /dev/null +++ b/tests/testthat/_snaps/processCapabilityBinomial/binomial-report-subplot-4.svg @@ -0,0 +1,274 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Statistic + + + + +Defective (%) + + + + +Value + + + + +PPM defective + + + + +Lower 95% CI + + + + +Process Z + + + + +Upper 95% CI + + + + +6.84 + + + + +68401.49 + + + + +1.49 + + + + +5.55 + + + + +55494.13 + + + + +1.38 + + + + +8.32 + + + + +83231.08 + + + + +1.59 + + +Summary statistics + + +binomial-report-subplot-4 + + diff --git a/tests/testthat/_snaps/processCapabilityBinomial/binomial-report-subplot-5.svg b/tests/testthat/_snaps/processCapabilityBinomial/binomial-report-subplot-5.svg new file mode 100644 index 000000000..da47cc739 --- /dev/null +++ b/tests/testthat/_snaps/processCapabilityBinomial/binomial-report-subplot-5.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + +Binomial capability + + +binomial-report-subplot-5 + + diff --git a/tests/testthat/_snaps/processCapabilityBinomial/binomial-report-subplot-6.svg b/tests/testthat/_snaps/processCapabilityBinomial/binomial-report-subplot-6.svg new file mode 100644 index 000000000..ed01e7280 --- /dev/null +++ b/tests/testthat/_snaps/processCapabilityBinomial/binomial-report-subplot-6.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + +Location: Amsterdam +Line: Line 1 +Machine: Machine 1 +Variable: Defectives +Process: Process 1 +Date: 2026-01-01 +Reported by: JASP +Conclusion: In control + + +binomial-report-subplot-6 + + diff --git a/tests/testthat/_snaps/processCapabilityBinomial/binomial-report-subplot-7.svg b/tests/testthat/_snaps/processCapabilityBinomial/binomial-report-subplot-7.svg new file mode 100644 index 000000000..b4ffe688b --- /dev/null +++ b/tests/testthat/_snaps/processCapabilityBinomial/binomial-report-subplot-7.svg @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 +2 +4 +6 +8 +10 +12 + + + + + + + + + + + + + + + +0 +2 +4 +6 +8 +10 +12 +Expected defectives +Observed defectives +binomial-report-subplot-7 + + diff --git a/tests/testthat/_snaps/processCapabilityBinomial/binomial-report-subplot-8.svg b/tests/testthat/_snaps/processCapabilityBinomial/binomial-report-subplot-8.svg new file mode 100644 index 000000000..ff57625f7 --- /dev/null +++ b/tests/testthat/_snaps/processCapabilityBinomial/binomial-report-subplot-8.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + +binomial-report-subplot-8 + + diff --git a/tests/testthat/_snaps/processCapabilityBinomial/reference_plotobject/binomial-cumulative.rds b/tests/testthat/_snaps/processCapabilityBinomial/reference_plotobject/binomial-cumulative.rds new file mode 100644 index 000000000..de0815039 Binary files /dev/null and b/tests/testthat/_snaps/processCapabilityBinomial/reference_plotobject/binomial-cumulative.rds differ diff --git a/tests/testthat/_snaps/processCapabilityBinomial/reference_plotobject/binomial-distribution.rds b/tests/testthat/_snaps/processCapabilityBinomial/reference_plotobject/binomial-distribution.rds new file mode 100644 index 000000000..16f934c31 Binary files /dev/null and b/tests/testthat/_snaps/processCapabilityBinomial/reference_plotobject/binomial-distribution.rds differ diff --git a/tests/testthat/_snaps/processCapabilityBinomial/reference_plotobject/binomial-histogram.rds b/tests/testthat/_snaps/processCapabilityBinomial/reference_plotobject/binomial-histogram.rds new file mode 100644 index 000000000..c7d6e139e Binary files /dev/null and b/tests/testthat/_snaps/processCapabilityBinomial/reference_plotobject/binomial-histogram.rds differ diff --git a/tests/testthat/_snaps/processCapabilityBinomial/reference_plotobject/binomial-rate.rds b/tests/testthat/_snaps/processCapabilityBinomial/reference_plotobject/binomial-rate.rds new file mode 100644 index 000000000..7288277dd Binary files /dev/null and b/tests/testthat/_snaps/processCapabilityBinomial/reference_plotobject/binomial-rate.rds differ diff --git a/tests/testthat/_snaps/processCapabilityBinomial/reference_plotobject/binomial-report-subplot-3.rds b/tests/testthat/_snaps/processCapabilityBinomial/reference_plotobject/binomial-report-subplot-3.rds new file mode 100644 index 000000000..de0815039 Binary files /dev/null and b/tests/testthat/_snaps/processCapabilityBinomial/reference_plotobject/binomial-report-subplot-3.rds differ diff --git a/tests/testthat/_snaps/processCapabilityBinomial/reference_plotobject/binomial-report-subplot-7.rds b/tests/testthat/_snaps/processCapabilityBinomial/reference_plotobject/binomial-report-subplot-7.rds new file mode 100644 index 000000000..16f934c31 Binary files /dev/null and b/tests/testthat/_snaps/processCapabilityBinomial/reference_plotobject/binomial-report-subplot-7.rds differ diff --git a/tests/testthat/_snaps/processCapabilityPoisson/poisson-cumulative.svg b/tests/testthat/_snaps/processCapabilityPoisson/poisson-cumulative.svg new file mode 100644 index 000000000..c83b86ea5 --- /dev/null +++ b/tests/testthat/_snaps/processCapabilityPoisson/poisson-cumulative.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0.0 +0.5 +1.0 +1.5 +2.0 + + + + + + + + + + + +5 +10 +15 +20 +25 +Sample +Cumulative defects per unit +poisson-cumulative + + diff --git a/tests/testthat/_snaps/processCapabilityPoisson/poisson-distribution.svg b/tests/testthat/_snaps/processCapabilityPoisson/poisson-distribution.svg new file mode 100644 index 000000000..e9bbf0cf4 --- /dev/null +++ b/tests/testthat/_snaps/processCapabilityPoisson/poisson-distribution.svg @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 +5 +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + +0 +5 +10 +15 +20 +25 +30 +Expected defects +Observed defects +poisson-distribution + + diff --git a/tests/testthat/_snaps/processCapabilityPoisson/poisson-histogram-target.svg b/tests/testthat/_snaps/processCapabilityPoisson/poisson-histogram-target.svg new file mode 100644 index 000000000..11e830a01 --- /dev/null +++ b/tests/testthat/_snaps/processCapabilityPoisson/poisson-histogram-target.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 +1 +2 +3 +4 +5 +6 + + + + + + + + + + + + + + +0.5 +1.0 +1.5 +2.0 +2.5 +3.0 +Defects per unit +Count +poisson-histogram-target + + diff --git a/tests/testthat/_snaps/processCapabilityPoisson/poisson-histogram.svg b/tests/testthat/_snaps/processCapabilityPoisson/poisson-histogram.svg new file mode 100644 index 000000000..457aebf0e --- /dev/null +++ b/tests/testthat/_snaps/processCapabilityPoisson/poisson-histogram.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 +1 +2 +3 +4 +5 +6 + + + + + + + + + + + + + + +0.5 +1.0 +1.5 +2.0 +2.5 +3.0 +Defects per unit +Count +poisson-histogram + + diff --git a/tests/testthat/_snaps/processCapabilityPoisson/poisson-rate.svg b/tests/testthat/_snaps/processCapabilityPoisson/poisson-rate.svg new file mode 100644 index 000000000..d6d806e71 --- /dev/null +++ b/tests/testthat/_snaps/processCapabilityPoisson/poisson-rate.svg @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0.5 +1.0 +1.5 +2.0 +2.5 +3.0 + + + + + + + + + + + + + + + +8 +9 +10 +11 +12 +13 +14 +15 +Sample size +Defects per unit +poisson-rate + + diff --git a/tests/testthat/_snaps/processCapabilityPoisson/poisson-report-subplot-1.svg b/tests/testthat/_snaps/processCapabilityPoisson/poisson-report-subplot-1.svg new file mode 100644 index 000000000..55b66dbd1 --- /dev/null +++ b/tests/testthat/_snaps/processCapabilityPoisson/poisson-report-subplot-1.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + +poisson-report-subplot-1 + + diff --git a/tests/testthat/_snaps/processCapabilityPoisson/poisson-report-subplot-2.svg b/tests/testthat/_snaps/processCapabilityPoisson/poisson-report-subplot-2.svg new file mode 100644 index 000000000..879ac4755 --- /dev/null +++ b/tests/testthat/_snaps/processCapabilityPoisson/poisson-report-subplot-2.svg @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +CL = 1.22 + +LCL = 0.12 + +UCL = 2.32 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0.0 +0.5 +1.0 +1.5 +2.0 +2.5 +3.0 + + + + + + + + + + + + + + +1 +5 +10 +15 +20 +25 +Sample +Defects per unit +poisson-report-subplot-2 + + diff --git a/tests/testthat/_snaps/processCapabilityPoisson/poisson-report-subplot-3.svg b/tests/testthat/_snaps/processCapabilityPoisson/poisson-report-subplot-3.svg new file mode 100644 index 000000000..40985e368 --- /dev/null +++ b/tests/testthat/_snaps/processCapabilityPoisson/poisson-report-subplot-3.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0.0 +0.5 +1.0 +1.5 +2.0 + + + + + + + + + + + +5 +10 +15 +20 +25 +Sample +Cumulative defects per unit +poisson-report-subplot-3 + + diff --git a/tests/testthat/_snaps/processCapabilityPoisson/poisson-report-subplot-4.svg b/tests/testthat/_snaps/processCapabilityPoisson/poisson-report-subplot-4.svg new file mode 100644 index 000000000..a7ef38109 --- /dev/null +++ b/tests/testthat/_snaps/processCapabilityPoisson/poisson-report-subplot-4.svg @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Statistic + + + + +Mean DPU + + + + +Value + + + + +1.22 + + + + +Lower 95% CI + + + + +1.09 + + + + +Upper 95% CI + + + + +1.36 + + +Summary statistics + + +poisson-report-subplot-4 + + diff --git a/tests/testthat/_snaps/processCapabilityPoisson/poisson-report-subplot-5.svg b/tests/testthat/_snaps/processCapabilityPoisson/poisson-report-subplot-5.svg new file mode 100644 index 000000000..d4caa812f --- /dev/null +++ b/tests/testthat/_snaps/processCapabilityPoisson/poisson-report-subplot-5.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + +Poisson capability + + +poisson-report-subplot-5 + + diff --git a/tests/testthat/_snaps/processCapabilityPoisson/poisson-report-subplot-6.svg b/tests/testthat/_snaps/processCapabilityPoisson/poisson-report-subplot-6.svg new file mode 100644 index 000000000..58ea9f8a1 --- /dev/null +++ b/tests/testthat/_snaps/processCapabilityPoisson/poisson-report-subplot-6.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + +Location: Amsterdam +Line: Line 1 +Machine: Machine 1 +Variable: Defects +Process: Process 1 +Date: 2026-01-01 +Reported by: JASP +Conclusion: In control + + +poisson-report-subplot-6 + + diff --git a/tests/testthat/_snaps/processCapabilityPoisson/poisson-report-subplot-7.svg b/tests/testthat/_snaps/processCapabilityPoisson/poisson-report-subplot-7.svg new file mode 100644 index 000000000..c3eafc7be --- /dev/null +++ b/tests/testthat/_snaps/processCapabilityPoisson/poisson-report-subplot-7.svg @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 +5 +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + +0 +5 +10 +15 +20 +25 +30 +Expected defects +Observed defects +poisson-report-subplot-7 + + diff --git a/tests/testthat/_snaps/processCapabilityPoisson/poisson-report-subplot-8.svg b/tests/testthat/_snaps/processCapabilityPoisson/poisson-report-subplot-8.svg new file mode 100644 index 000000000..88c3a17d6 --- /dev/null +++ b/tests/testthat/_snaps/processCapabilityPoisson/poisson-report-subplot-8.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + +poisson-report-subplot-8 + + diff --git a/tests/testthat/_snaps/processCapabilityPoisson/poisson-u-chart.svg b/tests/testthat/_snaps/processCapabilityPoisson/poisson-u-chart.svg new file mode 100644 index 000000000..651264de8 --- /dev/null +++ b/tests/testthat/_snaps/processCapabilityPoisson/poisson-u-chart.svg @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +CL = 1.22 + +LCL = 0.12 + +UCL = 2.32 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0.0 +0.5 +1.0 +1.5 +2.0 +2.5 +3.0 + + + + + + + + + + + + + + +1 +5 +10 +15 +20 +25 +Sample +Defects per unit +poisson-u-chart + + diff --git a/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-cumulative.rds b/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-cumulative.rds new file mode 100644 index 000000000..626c5b92e Binary files /dev/null and b/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-cumulative.rds differ diff --git a/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-distribution.rds b/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-distribution.rds new file mode 100644 index 000000000..86028f525 Binary files /dev/null and b/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-distribution.rds differ diff --git a/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-histogram-target.rds b/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-histogram-target.rds new file mode 100644 index 000000000..09c83d0a6 Binary files /dev/null and b/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-histogram-target.rds differ diff --git a/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-histogram.rds b/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-histogram.rds new file mode 100644 index 000000000..5e99f1404 Binary files /dev/null and b/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-histogram.rds differ diff --git a/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-rate.rds b/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-rate.rds new file mode 100644 index 000000000..b3cdc7ec1 Binary files /dev/null and b/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-rate.rds differ diff --git a/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-report-subplot-1.rds b/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-report-subplot-1.rds new file mode 100644 index 000000000..8691e4caa Binary files /dev/null and b/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-report-subplot-1.rds differ diff --git a/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-report-subplot-2.rds b/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-report-subplot-2.rds new file mode 100644 index 000000000..cf4798453 Binary files /dev/null and b/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-report-subplot-2.rds differ diff --git a/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-report-subplot-3.rds b/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-report-subplot-3.rds new file mode 100644 index 000000000..626c5b92e Binary files /dev/null and b/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-report-subplot-3.rds differ diff --git a/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-report-subplot-4.rds b/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-report-subplot-4.rds new file mode 100644 index 000000000..d72432942 Binary files /dev/null and b/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-report-subplot-4.rds differ diff --git a/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-report-subplot-5.rds b/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-report-subplot-5.rds new file mode 100644 index 000000000..17c385c06 Binary files /dev/null and b/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-report-subplot-5.rds differ diff --git a/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-report-subplot-6.rds b/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-report-subplot-6.rds new file mode 100644 index 000000000..cfb259445 Binary files /dev/null and b/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-report-subplot-6.rds differ diff --git a/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-report-subplot-7.rds b/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-report-subplot-7.rds new file mode 100644 index 000000000..86028f525 Binary files /dev/null and b/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-report-subplot-7.rds differ diff --git a/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-report-subplot-8.rds b/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-report-subplot-8.rds new file mode 100644 index 000000000..7e7e2b7f0 Binary files /dev/null and b/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-report-subplot-8.rds differ diff --git a/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-u-chart.rds b/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-u-chart.rds new file mode 100644 index 000000000..cf4798453 Binary files /dev/null and b/tests/testthat/_snaps/processCapabilityPoisson/reference_plotobject/poisson-u-chart.rds differ diff --git a/tests/testthat/_snaps/reference_plotobject/poisson-cumulative.rds b/tests/testthat/_snaps/reference_plotobject/poisson-cumulative.rds new file mode 100644 index 000000000..626c5b92e Binary files /dev/null and b/tests/testthat/_snaps/reference_plotobject/poisson-cumulative.rds differ diff --git a/tests/testthat/_snaps/reference_plotobject/poisson-distribution.rds b/tests/testthat/_snaps/reference_plotobject/poisson-distribution.rds new file mode 100644 index 000000000..86028f525 Binary files /dev/null and b/tests/testthat/_snaps/reference_plotobject/poisson-distribution.rds differ diff --git a/tests/testthat/_snaps/reference_plotobject/poisson-histogram-target.rds b/tests/testthat/_snaps/reference_plotobject/poisson-histogram-target.rds new file mode 100644 index 000000000..09c83d0a6 Binary files /dev/null and b/tests/testthat/_snaps/reference_plotobject/poisson-histogram-target.rds differ diff --git a/tests/testthat/_snaps/reference_plotobject/poisson-histogram.rds b/tests/testthat/_snaps/reference_plotobject/poisson-histogram.rds new file mode 100644 index 000000000..5e99f1404 Binary files /dev/null and b/tests/testthat/_snaps/reference_plotobject/poisson-histogram.rds differ diff --git a/tests/testthat/_snaps/reference_plotobject/poisson-rate.rds b/tests/testthat/_snaps/reference_plotobject/poisson-rate.rds new file mode 100644 index 000000000..b3cdc7ec1 Binary files /dev/null and b/tests/testthat/_snaps/reference_plotobject/poisson-rate.rds differ diff --git a/tests/testthat/_snaps/reference_plotobject/poisson-report-subplot-1.rds b/tests/testthat/_snaps/reference_plotobject/poisson-report-subplot-1.rds new file mode 100644 index 000000000..8691e4caa Binary files /dev/null and b/tests/testthat/_snaps/reference_plotobject/poisson-report-subplot-1.rds differ diff --git a/tests/testthat/_snaps/reference_plotobject/poisson-report-subplot-2.rds b/tests/testthat/_snaps/reference_plotobject/poisson-report-subplot-2.rds new file mode 100644 index 000000000..cf4798453 Binary files /dev/null and b/tests/testthat/_snaps/reference_plotobject/poisson-report-subplot-2.rds differ diff --git a/tests/testthat/_snaps/reference_plotobject/poisson-report-subplot-3.rds b/tests/testthat/_snaps/reference_plotobject/poisson-report-subplot-3.rds new file mode 100644 index 000000000..626c5b92e Binary files /dev/null and b/tests/testthat/_snaps/reference_plotobject/poisson-report-subplot-3.rds differ diff --git a/tests/testthat/_snaps/reference_plotobject/poisson-report-subplot-4.rds b/tests/testthat/_snaps/reference_plotobject/poisson-report-subplot-4.rds new file mode 100644 index 000000000..d72432942 Binary files /dev/null and b/tests/testthat/_snaps/reference_plotobject/poisson-report-subplot-4.rds differ diff --git a/tests/testthat/_snaps/reference_plotobject/poisson-report-subplot-5.rds b/tests/testthat/_snaps/reference_plotobject/poisson-report-subplot-5.rds new file mode 100644 index 000000000..17c385c06 Binary files /dev/null and b/tests/testthat/_snaps/reference_plotobject/poisson-report-subplot-5.rds differ diff --git a/tests/testthat/_snaps/reference_plotobject/poisson-report-subplot-6.rds b/tests/testthat/_snaps/reference_plotobject/poisson-report-subplot-6.rds new file mode 100644 index 000000000..cfb259445 Binary files /dev/null and b/tests/testthat/_snaps/reference_plotobject/poisson-report-subplot-6.rds differ diff --git a/tests/testthat/_snaps/reference_plotobject/poisson-report-subplot-7.rds b/tests/testthat/_snaps/reference_plotobject/poisson-report-subplot-7.rds new file mode 100644 index 000000000..86028f525 Binary files /dev/null and b/tests/testthat/_snaps/reference_plotobject/poisson-report-subplot-7.rds differ diff --git a/tests/testthat/_snaps/reference_plotobject/poisson-report-subplot-8.rds b/tests/testthat/_snaps/reference_plotobject/poisson-report-subplot-8.rds new file mode 100644 index 000000000..7e7e2b7f0 Binary files /dev/null and b/tests/testthat/_snaps/reference_plotobject/poisson-report-subplot-8.rds differ diff --git a/tests/testthat/_snaps/reference_plotobject/poisson-u-chart.rds b/tests/testthat/_snaps/reference_plotobject/poisson-u-chart.rds new file mode 100644 index 000000000..cf4798453 Binary files /dev/null and b/tests/testthat/_snaps/reference_plotobject/poisson-u-chart.rds differ diff --git a/tests/testthat/datasets/processCapabilityStudy/binomialCapability.csv b/tests/testthat/datasets/processCapabilityStudy/binomialCapability.csv new file mode 100644 index 000000000..4b2390b61 --- /dev/null +++ b/tests/testthat/datasets/processCapabilityStudy/binomialCapability.csv @@ -0,0 +1,26 @@ +Defectives,SampleSize,Day +3,50,Day 1 +4,60,Day 2 +2,45,Day 3 +3,55,Day 4 +4,50,Day 5 +5,70,Day 6 +2,40,Day 7 +3,55,Day 8 +4,60,Day 9 +4,50,Day 10 +3,45,Day 11 +4,65,Day 12 +3,55,Day 13 +12,50,Day 14 +4,60,Day 15 +2,45,Day 16 +3,55,Day 17 +2,40,Day 18 +3,50,Day 19 +4,60,Day 20 +5,70,Day 21 +2,45,Day 22 +4,55,Day 23 +3,50,Day 24 +4,65,Day 25 diff --git a/tests/testthat/datasets/processCapabilityStudy/binomialCapabilityEdgeCases.csv b/tests/testthat/datasets/processCapabilityStudy/binomialCapabilityEdgeCases.csv new file mode 100644 index 000000000..9aef9d27d --- /dev/null +++ b/tests/testthat/datasets/processCapabilityStudy/binomialCapabilityEdgeCases.csv @@ -0,0 +1,9 @@ +Defectives,SampleSize,DefectivesFractional,DefectivesZero,SampleSizeInvalid +3,50,3.5,0,0.5 +4,60,4,0,60 +2,45,2,0,45 +3,55,3,0,55 +4,50,4,0,50 +5,70,5,0,70 +2,40,2,0,40 +3,55,3,0,55 diff --git a/tests/testthat/datasets/processCapabilityStudy/binomialCapabilityMissing.csv b/tests/testthat/datasets/processCapabilityStudy/binomialCapabilityMissing.csv new file mode 100644 index 000000000..ce2ea807c --- /dev/null +++ b/tests/testthat/datasets/processCapabilityStudy/binomialCapabilityMissing.csv @@ -0,0 +1,13 @@ +Defectives,SampleSize,Day +3,50,Day 1 +4,50,Day 2 +,50,Day 3 +3,50,Day 4 +4,50,Day 5 +2,50,Day 6 +15,50,Day 7 +3,50,Day 8 +4,50,Day 9 +2,50,Day 10 +3,50,Day 11 +4,50,Day 12 diff --git a/tests/testthat/datasets/processCapabilityStudy/poissonCapability.csv b/tests/testthat/datasets/processCapabilityStudy/poissonCapability.csv new file mode 100644 index 000000000..588ac007c --- /dev/null +++ b/tests/testthat/datasets/processCapabilityStudy/poissonCapability.csv @@ -0,0 +1,26 @@ +Defects,SampleSize,Day +8,10,Day 1 +18,12,Day 2 +13,8,Day 3 +22,15,Day 4 +12,10,Day 5 +12,14,Day 6 +9,9,Day 7 +14,11,Day 8 +16,13,Day 9 +9,10,Day 10 +18,12,Day 11 +10,8,Day 12 +11,15,Day 13 +29,10,Day 14 +13,14,Day 15 +10,9,Day 16 +8,11,Day 17 +17,13,Day 18 +12,10,Day 19 +16,12,Day 20 +11,8,Day 21 +17,15,Day 22 +13,10,Day 23 +15,14,Day 24 +11,9,Day 25 diff --git a/tests/testthat/datasets/processCapabilityStudy/poissonCapabilityEdgeCases.csv b/tests/testthat/datasets/processCapabilityStudy/poissonCapabilityEdgeCases.csv new file mode 100644 index 000000000..e2b9e54f9 --- /dev/null +++ b/tests/testthat/datasets/processCapabilityStudy/poissonCapabilityEdgeCases.csv @@ -0,0 +1,9 @@ +Defects,SampleSize,DefectsFractional,DefectsZero,SampleSizeZero,SampleSizeFractional +11,10,3.5,0,0,2.5 +13,12,13,0,12,3.5 +9,8,9,0,8,4.5 +16,15,16,0,15,2.5 +11,10,11,0,10,3.5 +15,14,15,0,14,4.5 +10,9,10,0,9,2.5 +12,11,12,0,11,3.5 diff --git a/tests/testthat/datasets/processCapabilityStudy/poissonCapabilityMissing.csv b/tests/testthat/datasets/processCapabilityStudy/poissonCapabilityMissing.csv new file mode 100644 index 000000000..948f189ae --- /dev/null +++ b/tests/testthat/datasets/processCapabilityStudy/poissonCapabilityMissing.csv @@ -0,0 +1,13 @@ +Defects,SampleSize,Day +11,10,Day 1 +12,10,Day 2 +,10,Day 3 +11,10,Day 4 +12,10,Day 5 +10,10,Day 6 +30,10,Day 7 +11,10,Day 8 +12,10,Day 9 +10,10,Day 10 +11,10,Day 11 +12,10,Day 12 diff --git a/tests/testthat/test-example-ProcessCapabilityStudyLongFormat.R b/tests/testthat/test-example-ProcessCapabilityStudyLongFormat.R index 1b8393d96..c5994b728 100644 --- a/tests/testthat/test-example-ProcessCapabilityStudyLongFormat.R +++ b/tests/testthat/test-example-ProcessCapabilityStudyLongFormat.R @@ -7,7 +7,11 @@ test_that("processCapabilityStudies results match", { # Load from JASP example file jaspFile <- testthat::test_path("..", "..", "examples", "ProcessCapabilityStudyLongFormat.jasp") - opts <- jaspTools::analysisOptions(jaspFile) + # A .jasp file only stores the options that existed when it was saved. JASP Desktop fills the rest + # in from the QML defaults; jaspTools does not, so options added after the file was saved have to + # be merged in here or the analysis is called with an incomplete options list. + opts <- modifyList(jaspTools::analysisOptions("processCapabilityStudies"), + jaspTools::analysisOptions(jaspFile)) dataset <- jaspTools::extractDatasetFromJASPFile(jaspFile) # Encode and run analysis diff --git a/tests/testthat/test-example-ProcessCapabilityStudyWideFormat.R b/tests/testthat/test-example-ProcessCapabilityStudyWideFormat.R index 9a12f89e1..fb4b811c0 100644 --- a/tests/testthat/test-example-ProcessCapabilityStudyWideFormat.R +++ b/tests/testthat/test-example-ProcessCapabilityStudyWideFormat.R @@ -7,7 +7,11 @@ test_that("processCapabilityStudies results match", { # Load from JASP example file jaspFile <- testthat::test_path("..", "..", "examples", "ProcessCapabilityStudyWideFormat.jasp") - opts <- jaspTools::analysisOptions(jaspFile) + # A .jasp file only stores the options that existed when it was saved. JASP Desktop fills the rest + # in from the QML defaults; jaspTools does not, so options added after the file was saved have to + # be merged in here or the analysis is called with an incomplete options list. + opts <- modifyList(jaspTools::analysisOptions("processCapabilityStudies"), + jaspTools::analysisOptions(jaspFile)) dataset <- jaspTools::extractDatasetFromJASPFile(jaspFile) # Encode and run analysis diff --git a/tests/testthat/test-processCapabilityBinomial.R b/tests/testthat/test-processCapabilityBinomial.R new file mode 100644 index 000000000..ec3ee5a32 --- /dev/null +++ b/tests/testthat/test-processCapabilityBinomial.R @@ -0,0 +1,352 @@ +context("[Quality Control] Binomial Capability Analysis") +.numDecimals <- 2 + +# Shared option scaffolding #### + +binomialOptions <- function(...) { + options <- analysisOptions("processCapabilityStudies") + options$capabilityDataType <- "attributes" + options$attributeCounts <- "Defectives" + options$testSet <- "jaspDefault" + overrides <- list(...) + for (name in names(overrides)) + options[[name]] <- overrides[[name]] + return(options) +} + +binomialDataset <- function() testthat::test_path("datasets/processCapabilityStudy/binomialCapability.csv") +binomialMissingData <- function() testthat::test_path("datasets/processCapabilityStudy/binomialCapabilityMissing.csv") +binomialEdgeData <- function() testthat::test_path("datasets/processCapabilityStudy/binomialCapabilityEdgeCases.csv") + +binomialCollection <- function(results) results[["results"]][["attributeCapability"]][["collection"]] + +binomialTable <- function(results, key) + binomialCollection(results)[[paste0("attributeCapability_", key)]][["data"]] + +binomialFootnotes <- function(results, key) + binomialCollection(results)[[paste0("attributeCapability_", key)]][["footnotes"]] + +binomialPlot <- function(results, key) { + plotName <- binomialCollection(results)[[paste0("attributeCapability_", key)]][["data"]] + return(results[["state"]][["figures"]][[plotName]][["obj"]]) +} + +binomialChartElement <- function(results, key) + binomialCollection(results)[["attributeCapability_controlChart"]][["collection"]][[paste0("attributeCapability_controlChart_", key)]] + +# 1. Summary table, constant sample size #### + +options <- binomialOptions(attributeSampleSizeType = "constant", attributeSampleSizeValue = 50) +set.seed(1) +resultsConstant <- runAnalysis("processCapabilityStudies", binomialDataset(), options) + +test_that("B1 Summary statistics table with a constant sample size", { + # 92 defectives out of 25 * 50 = 1250 inspected units, exact (Clopper-Pearson) interval + table <- binomialTable(resultsConstant, "summaryTable") + jaspTools::expect_equal_tables(table, + list(5.9741250430, 8.9502194335, "Defective (%)", 7.36, + 59741.2504301, 89502.1943348, "PPM defective", 73600, + 1.3438268382, 1.5569493940, "Process Z", 1.4494928344)) +}) + +# 2. Summary table, variable sample size #### + +options <- binomialOptions(attributeSampleSizeType = "variable", attributeSampleSizeVariable = "SampleSize") +set.seed(1) +resultsVariable <- runAnalysis("processCapabilityStudies", binomialDataset(), options) + +test_that("B2 Summary statistics table with a variable sample size", { + # 92 defectives out of 1345 inspected units + table <- binomialTable(resultsVariable, "summaryTable") + jaspTools::expect_equal_tables(table, + list(5.5494128152, 8.3231084788, "Defective (%)", 6.8401486989, + 55494.1281507, 83231.0847878, "PPM defective", 68401.4869888, + 1.3836613625, 1.5937668887, "Process Z", 1.4878025463)) +}) + +# 3. Confidence interval methods #### + +test_that("B3 Wald interval bounds", { + options <- binomialOptions(attributeSampleSizeType = "constant", attributeSampleSizeValue = 50, + binomialCiMethod = "wald") + set.seed(1) + results <- runAnalysis("processCapabilityStudies", binomialDataset(), options) + table <- binomialTable(results, "summaryTable") + jaspTools::expect_equal_tables(table, + list(5.9124576955, 8.8075423045, "Defective (%)", 7.36, + 59124.5769546, 88075.4230454, "PPM defective", 73600, + 1.3527020162, 1.5621648709, "Process Z", 1.4494928344)) +}) + +test_that("B3 Wilson interval bounds", { + options <- binomialOptions(attributeSampleSizeType = "constant", attributeSampleSizeValue = 50, + binomialCiMethod = "wilson") + set.seed(1) + results <- runAnalysis("processCapabilityStudies", binomialDataset(), options) + table <- binomialTable(results, "summaryTable") + jaspTools::expect_equal_tables(table, + list(6.0394232282, 8.9418535113, "Defective (%)", 7.36, + 60394.2322820, 89418.5351132, "PPM defective", 73600, + 1.3443443194, 1.5514726173, "Process Z", 1.4494928344)) +}) + +test_that("B3 The CI level is read as a proportion, not as a percentage", { + # regression guard: a CIField delivers 0.95, so alpha must be 1 - 0.95 and the overtitle "95% CI" + schema <- binomialCollection(resultsConstant)[["attributeCapability_summaryTable"]][["schema"]][["fields"]] + overtitles <- unique(unlist(lapply(schema, function(field) field[["overTitle"]]))) + expect_true("95% CI" %in% overtitles) +}) + +# 4. Historical proportion affects the chart only #### + +test_that("B4 A historical proportion moves the centre line but not the statistics", { + options <- binomialOptions(attributeSampleSizeType = "constant", attributeSampleSizeValue = 50, + binomialHistoricalProportion = TRUE, binomialHistoricalProportionValue = 2) + set.seed(1) + results <- runAnalysis("processCapabilityStudies", binomialDataset(), options) + + # the point estimate and its interval stay data based + table <- binomialTable(results, "summaryTable") + jaspTools::expect_equal_tables(table, + list(5.9741250430, 8.9502194335, "Defective (%)", 7.36, + 59741.2504301, 89502.1943348, "PPM defective", 73600, + 1.3438268382, 1.5569493940, "Process Z", 1.4494928344)) + + footnotes <- unlist(lapply(binomialFootnotes(results, "summaryTable"), `[[`, "text")) + expect_true(any(grepl("historical proportion defective of 2%", footnotes, fixed = TRUE))) +}) + +# 5. p chart #### + +test_that("B5 p chart plot", { + plotObject <- binomialChartElement(resultsVariable, "plot")[["data"]] + testPlot <- resultsVariable[["state"]][["figures"]][[plotObject]][["obj"]] + jaspTools::expect_equal_plots(testPlot, "binomial-p-chart") +}) + +test_that("B5 p chart test results table", { + table <- binomialChartElement(resultsVariable, "table")[["data"]] + jaspTools::expect_equal_tables(table, list("Point 14")) +}) + +# 6. Supporting plots #### + +test_that("B6 Cumulative defective plot", { + plotObject <- binomialPlot(resultsVariable, "cumulativePlot") + jaspTools::expect_equal_plots(plotObject, "binomial-cumulative") +}) + +test_that("B6 Binomial plot and rate plot", { + options <- binomialOptions(attributeSampleSizeType = "variable", attributeSampleSizeVariable = "SampleSize", + attributeDistributionPlot = TRUE, attributeRatePlot = TRUE) + set.seed(1) + results <- runAnalysis("processCapabilityStudies", binomialDataset(), options) + jaspTools::expect_equal_plots(binomialPlot(results, "distributionPlot"), "binomial-distribution") + jaspTools::expect_equal_plots(binomialPlot(results, "ratePlot"), "binomial-rate") +}) + +test_that("B6 Histogram of the percentage defective", { + options <- binomialOptions(attributeSampleSizeType = "constant", attributeSampleSizeValue = 50, + attributeHistogram = TRUE) + set.seed(1) + results <- runAnalysis("processCapabilityStudies", binomialDataset(), options) + jaspTools::expect_equal_plots(binomialPlot(results, "histogram"), "binomial-histogram") +}) + +test_that("B6 Histogram with a target", { + # the target is drawn as a dashed line and, being outside the observed range, also widens the x axis + options <- binomialOptions(attributeSampleSizeType = "constant", attributeSampleSizeValue = 50, + attributeHistogram = TRUE, binomialTarget = TRUE, binomialTargetValue = 2) + set.seed(1) + results <- runAnalysis("processCapabilityStudies", binomialDataset(), options) + jaspTools::expect_equal_plots(binomialPlot(results, "histogram"), "binomial-histogram-target") +}) + +test_that("B6 The number of bins of the histogram follows the GUI", { + binWidth <- function(nBins) { + options <- binomialOptions(attributeSampleSizeType = "constant", attributeSampleSizeValue = 50, + attributeHistogram = TRUE, attributeHistogramBinNumber = nBins) + set.seed(1) + results <- runAnalysis("processCapabilityStudies", binomialDataset(), options) + layer <- ggplot2::layer_data(binomialPlot(results, "histogram"), 1) + # the spacing of the bin centres, which survives the bars clipped by the axis limits + return(min(diff(sort(layer[["x"]])))) + } + # hist() rounds the boundaries, so the option controls the bin width rather than an exact count + expect_lt(binWidth(25), binWidth(10)) +}) + +# 7. Element keys follow the QML gating #### + +test_that("B7 The two sample-size dependent panels are separate elements", { + # QML only offers the histogram for a constant sample size and the rate plot for a variable one, + # so each has its own element key and neither can be silently substituted for the other + options <- binomialOptions(attributeSampleSizeType = "constant", attributeSampleSizeValue = 50, + attributeHistogram = TRUE) + set.seed(1) + constant <- runAnalysis("processCapabilityStudies", binomialDataset(), options) + expect_true("attributeCapability_histogram" %in% names(binomialCollection(constant))) + expect_false("attributeCapability_ratePlot" %in% names(binomialCollection(constant))) + + options <- binomialOptions(attributeSampleSizeType = "variable", attributeSampleSizeVariable = "SampleSize", + attributeRatePlot = TRUE) + set.seed(1) + variable <- runAnalysis("processCapabilityStudies", binomialDataset(), options) + expect_true("attributeCapability_ratePlot" %in% names(binomialCollection(variable))) + expect_false("attributeCapability_histogram" %in% names(binomialCollection(variable))) +}) + +test_that("B7 A panel that does not match the sample size type is dropped", { + # QML hides the check box that does not apply but keeps its value, so a histogram ticked under a + # constant sample size must not survive the switch to a variable one, and vice versa + options <- binomialOptions(attributeSampleSizeType = "variable", attributeSampleSizeVariable = "SampleSize", + attributeHistogram = TRUE, attributeRatePlot = TRUE) + set.seed(1) + variable <- runAnalysis("processCapabilityStudies", binomialDataset(), options) + expect_false("attributeCapability_histogram" %in% names(binomialCollection(variable))) + expect_true("attributeCapability_ratePlot" %in% names(binomialCollection(variable))) + + options <- binomialOptions(attributeSampleSizeType = "constant", attributeSampleSizeValue = 50, + attributeHistogram = TRUE, attributeRatePlot = TRUE) + set.seed(1) + constant <- runAnalysis("processCapabilityStudies", binomialDataset(), options) + expect_false("attributeCapability_ratePlot" %in% names(binomialCollection(constant))) + expect_true("attributeCapability_histogram" %in% names(binomialCollection(constant))) +}) + +# 8. Error paths #### + +test_that("B8 More defectives than inspected units is reported for a single sample", { + options <- binomialOptions(attributeSampleSizeType = "constant", attributeSampleSizeValue = 5) + set.seed(1) + results <- runAnalysis("processCapabilityStudies", binomialDataset(), options) + # regression guard: %i throws on the doubles that come out of the data reader, %s must be used + expect_match(results[["results"]][["errorMessage"]], + "Sample 14 has more defectives (12) than inspected units (5).", fixed = TRUE) +}) + +test_that("B8 More defectives than inspected units reports the number of affected samples", { + options <- binomialOptions(attributeSampleSizeType = "constant", attributeSampleSizeValue = 2) + set.seed(1) + results <- runAnalysis("processCapabilityStudies", binomialDataset(), options) + expect_match(results[["results"]][["errorMessage"]], "samples are affected in total", fixed = TRUE) +}) + +test_that("B8 Non-integer defectives are rejected", { + options <- binomialOptions(attributeCounts = "DefectivesFractional", + attributeSampleSizeType = "variable", attributeSampleSizeVariable = "SampleSize") + set.seed(1) + results <- runAnalysis("processCapabilityStudies", binomialEdgeData(), options) + expect_match(results[["results"]][["errorMessage"]], + "The number of defectives must contain whole numbers.", fixed = TRUE) +}) + +test_that("B8 A sample size below one is rejected", { + options <- binomialOptions(attributeSampleSizeType = "variable", + attributeSampleSizeVariable = "SampleSizeInvalid") + set.seed(1) + results <- runAnalysis("processCapabilityStudies", binomialEdgeData(), options) + expect_match(results[["results"]][["errorMessage"]], + "The sample size must be a positive whole number.", fixed = TRUE) +}) + +# 9. Degenerate process #### + +test_that("B9 Zero observed defectives does not crash and is flagged", { + options <- binomialOptions(attributeCounts = "DefectivesZero", + attributeSampleSizeType = "variable", attributeSampleSizeVariable = "SampleSize") + set.seed(1) + results <- runAnalysis("processCapabilityStudies", binomialEdgeData(), options) + expect_equal(results[["status"]], "complete") + footnotes <- unlist(lapply(binomialFootnotes(results, "summaryTable"), `[[`, "text")) + expect_true(any(grepl("No defectives were observed", footnotes, fixed = TRUE))) +}) + +# 10. Missing values keep the sample numbering #### + +test_that("B10 A missing sample does not shift the point numbers", { + options <- binomialOptions(attributeSampleSizeType = "constant", attributeSampleSizeValue = 50) + set.seed(1) + results <- runAnalysis("processCapabilityStudies", binomialMissingData(), options) + # row 3 is missing and row 7 is out of control; the violation must stay "Point 7" + table <- binomialChartElement(results, "table")[["data"]] + jaspTools::expect_equal_tables(table, list("Point 7")) + + footnotes <- unlist(lapply(binomialFootnotes(results, "summaryTable"), `[[`, "text")) + expect_true(any(grepl("1 sample with missing values", footnotes, fixed = TRUE))) +}) + +# 11. Zone based rules stay off on a clamped p chart #### + +test_that("B11 Rules 4, 5, 6, 7 and 9 do not fire when the lower limit is clamped", { + options <- binomialOptions(attributeSampleSizeType = "variable", attributeSampleSizeVariable = "SampleSize", + testSet = "nelsonLaws") + set.seed(1) + results <- runAnalysis("processCapabilityStudies", binomialDataset(), options) + schema <- binomialChartElement(results, "table")[["schema"]][["fields"]] + columns <- unlist(lapply(schema, `[[`, "name")) + expect_false(any(c("test4", "test5", "test6", "test7", "test9") %in% columns)) +}) + +# 12. Out-of-control footnote #### + +test_that("B12 An out-of-control point is flagged on the summary table", { + footnotes <- unlist(lapply(binomialFootnotes(resultsVariable, "summaryTable"), `[[`, "text")) + expect_true(any(grepl("The process is not in control", footnotes, fixed = TRUE))) +}) + +test_that("B12 A stable process carries no out-of-control footnote", { + options <- binomialOptions(attributeCounts = "Defectives", + attributeSampleSizeType = "variable", attributeSampleSizeVariable = "SampleSize") + set.seed(1) + results <- runAnalysis("processCapabilityStudies", binomialEdgeData(), options) + footnotes <- unlist(lapply(binomialFootnotes(results, "summaryTable"), `[[`, "text")) + expect_false(any(grepl("The process is not in control", footnotes, fixed = TRUE))) +}) + +# 13. Empty state #### + +test_that("B13 Without an assigned variable the analysis renders empty output", { + options <- binomialOptions(attributeCounts = "") + set.seed(1) + results <- runAnalysis("processCapabilityStudies", binomialDataset(), options) + expect_equal(results[["status"]], "complete") + + summaryTable <- binomialCollection(results)[["attributeCapability_summaryTable"]] + expect_equal(length(summaryTable[["data"]]), 0) + expect_equal(length(summaryTable[["schema"]][["fields"]]), 4) + expect_null(summaryTable[["error"]]) +}) + +# 14. Report #### + +test_that("B14 Report", { + options <- binomialOptions(attributeSampleSizeType = "variable", attributeSampleSizeVariable = "SampleSize", + report = TRUE, attributeDistributionPlot = TRUE) + options$reportTitleText <- "Binomial capability" + options$reportLocationText <- "Amsterdam" + options$reportLineText <- "Line 1" + options$reportMachineText <- "Machine 1" + options$reportVariableText <- "Defectives" + options$reportProcessText <- "Process 1" + options$reportDateText <- "2026-01-01" + options$reportReportedByText <- "JASP" + options$reportConclusionText <- "In control" + set.seed(1) + results <- runAnalysis("processCapabilityStudies", binomialDataset(), options) + plotName <- results[["results"]][["report"]][["data"]] + testPlot <- results[["state"]][["figures"]][[plotName]][["obj"]] + jaspTools::expect_equal_plots(testPlot, "binomial-report") +}) + +test_that("B14 Report without components selected shows an error", { + options <- binomialOptions(attributeSampleSizeType = "variable", attributeSampleSizeVariable = "SampleSize", + report = TRUE) + options$reportProcessStability <- FALSE + options$reportProcessCapabilityPlot <- FALSE + options$reportProcessCapabilityTables <- FALSE + set.seed(1) + results <- runAnalysis("processCapabilityStudies", binomialDataset(), options) + expect_match(results[["results"]][["report"]][["error"]][["errorMessage"]], + "No report components selected.", fixed = TRUE) +}) diff --git a/tests/testthat/test-processCapabilityPoisson.R b/tests/testthat/test-processCapabilityPoisson.R new file mode 100644 index 000000000..fde47cab4 --- /dev/null +++ b/tests/testthat/test-processCapabilityPoisson.R @@ -0,0 +1,410 @@ +context("[Quality Control] Poisson Capability Analysis") +.numDecimals <- 2 + +# Shared option scaffolding #### + +poissonOptions <- function(...) { + options <- analysisOptions("processCapabilityStudies") + options$capabilityDataType <- "attributes" + options$attributeDistribution <- "poisson" + options$attributeCounts <- "Defects" + options$testSet <- "jaspDefault" + # GUI defaults of the Poisson controls, set explicitly rather than relying on analysisOptions() + # deriving them from the QML, because the controls carry visibility bindings + options$poissonHistoricalDpu <- FALSE + options$poissonHistoricalDpuValue <- 1 + options$poissonTarget <- FALSE + options$poissonTargetValue <- 0 + options$poissonCiMethod <- "exact" + options$poissonYieldStatistics <- FALSE + overrides <- list(...) + for (name in names(overrides)) + options[[name]] <- overrides[[name]] + return(options) +} + +poissonDataset <- function() testthat::test_path("datasets/processCapabilityStudy/poissonCapability.csv") +poissonMissingData <- function() testthat::test_path("datasets/processCapabilityStudy/poissonCapabilityMissing.csv") +poissonEdgeData <- function() testthat::test_path("datasets/processCapabilityStudy/poissonCapabilityEdgeCases.csv") + +poissonCollection <- function(results) results[["results"]][["attributeCapability"]][["collection"]] + +poissonTable <- function(results, key) + poissonCollection(results)[[paste0("attributeCapability_", key)]][["data"]] + +poissonFootnotes <- function(results, key) + unlist(lapply(poissonCollection(results)[[paste0("attributeCapability_", key)]][["footnotes"]], `[[`, "text")) + +poissonPlot <- function(results, key) { + plotName <- poissonCollection(results)[[paste0("attributeCapability_", key)]][["data"]] + return(results[["state"]][["figures"]][[plotName]][["obj"]]) +} + +poissonChartElement <- function(results, key) + poissonCollection(results)[["attributeCapability_controlChart"]][["collection"]][[paste0("attributeCapability_controlChart_", key)]] + +# 1. Summary table, constant sample size #### + +options <- poissonOptions(attributeSampleSizeType = "constant", attributeSampleSizeValue = 10) +set.seed(1) +resultsConstant <- runAnalysis("processCapabilityStudies", poissonDataset(), options) + +test_that("P1 Summary statistics table with a constant sample size", { + # 344 defects over 25 * 10 = 250 inspected units, exact (Garwood) interval + table <- poissonTable(resultsConstant, "summaryTable") + jaspTools::expect_equal_tables(table, + list(1.2344172176, 1.5293696715, "Mean DPU", 1.376)) +}) + +# 2. Summary table, variable sample size #### + +options <- poissonOptions(attributeSampleSizeType = "variable", attributeSampleSizeVariable = "SampleSize") +set.seed(1) +resultsVariable <- runAnalysis("processCapabilityStudies", poissonDataset(), options) + +test_that("P2 Summary statistics table with a variable sample size", { + # 344 defects over 282 inspected units + table <- poissonTable(resultsVariable, "summaryTable") + jaspTools::expect_equal_tables(table, + list(1.0943415050, 1.3558241768, "Mean DPU", 1.2198581560)) +}) + +# 3. Confidence interval methods #### + +test_that("P3 Wald interval bounds", { + options <- poissonOptions(attributeSampleSizeType = "constant", attributeSampleSizeValue = 10, + poissonCiMethod = "wald") + set.seed(1) + results <- runAnalysis("processCapabilityStudies", poissonDataset(), options) + jaspTools::expect_equal_tables(poissonTable(results, "summaryTable"), + list(1.2305923339, 1.5214076661, "Mean DPU", 1.376)) +}) + +test_that("P3 Score interval bounds", { + options <- poissonOptions(attributeSampleSizeType = "constant", attributeSampleSizeValue = 10, + poissonCiMethod = "score") + set.seed(1) + results <- runAnalysis("processCapabilityStudies", poissonDataset(), options) + jaspTools::expect_equal_tables(poissonTable(results, "summaryTable"), + list(1.2380724215, 1.5292934137, "Mean DPU", 1.376)) +}) + +test_that("P3 The three interval methods are each named in a footnote", { + method <- function(m) { + options <- poissonOptions(attributeSampleSizeType = "constant", attributeSampleSizeValue = 10, + poissonCiMethod = m) + set.seed(1) + poissonFootnotes(runAnalysis("processCapabilityStudies", poissonDataset(), options), "summaryTable") + } + expect_true(any(grepl("exact (Garwood)", method("exact"), fixed = TRUE))) + expect_true(any(grepl("Wald", method("wald"), fixed = TRUE))) + expect_true(any(grepl("score method", method("score"), fixed = TRUE))) +}) + +# 4. The CI level is a proportion, not a percentage #### + +test_that("P4 The CI level is read as a proportion", { + # regression guard: a CIField delivers 0.95, so alpha must be 1 - 0.95 and the overtitle "95% CI" + schema <- poissonCollection(resultsConstant)[["attributeCapability_summaryTable"]][["schema"]][["fields"]] + overtitles <- unique(unlist(lapply(schema, function(field) field[["overTitle"]]))) + expect_true("95% CI" %in% overtitles) +}) + +# 5. Yield statistics are opt-in #### + +test_that("P5 Without the check box only the mean DPU is reported", { + table <- poissonTable(resultsConstant, "summaryTable") + expect_equal(length(table), 1) +}) + +test_that("P5 The yield rows are added and the Z bounds are the swap of the DPU bounds", { + options <- poissonOptions(attributeSampleSizeType = "constant", attributeSampleSizeValue = 10, + poissonYieldStatistics = TRUE) + set.seed(1) + results <- runAnalysis("processCapabilityStudies", poissonDataset(), options) + table <- poissonTable(results, "summaryTable") + jaspTools::expect_equal_tables(table, + list(1.2344172176, 1.5293696715, "Mean DPU", 1.376, + 70.8995694867, 78.3327801053, "Defective units (%)", 74.7413117413, + 708995.694867111, 783327.801052881, "PPM defective", 747413.117413390, + -0.7834815262, -0.5504531384, "Process Z", -0.6663713580)) + + # Z decreases in the rate, so the upper DPU bound produces the lower Z bound + zLower <- qnorm(-expm1(-1.5293696715), lower.tail = FALSE) + zUpper <- qnorm(-expm1(-1.2344172176), lower.tail = FALSE) + expect_equal(table[[4]][["ciLower"]], zLower, tolerance = 1e-8) + expect_equal(table[[4]][["ciUpper"]], zUpper, tolerance = 1e-8) + + expect_true(any(grepl("a unit is conforming when it carries no defect", + poissonFootnotes(results, "summaryTable"), fixed = TRUE))) +}) + +# 6. Historical DPU affects the chart only, and is not divided by 100 #### + +test_that("P6 A historical DPU moves the centre line but not the statistics", { + options <- poissonOptions(attributeSampleSizeType = "constant", attributeSampleSizeValue = 10, + poissonHistoricalDpu = TRUE, poissonHistoricalDpuValue = 2) + set.seed(1) + results <- runAnalysis("processCapabilityStudies", poissonDataset(), options) + + # the point estimate and its interval stay data based + jaspTools::expect_equal_tables(poissonTable(results, "summaryTable"), + list(1.2344172176, 1.5293696715, "Mean DPU", 1.376)) + + expect_true(any(grepl("historical defect rate of 2 defects per unit", + poissonFootnotes(results, "summaryTable"), fixed = TRUE))) + + # regression guard for X3: the Poisson value is a rate, so it must not be divided by 100. + # Layer 1 of the control chart is the centre line step. + plotObject <- poissonChartElement(results, "plot")[["data"]] + testPlot <- results[["state"]][["figures"]][[plotObject]][["obj"]] + centre <- unique(na.omit(ggplot2::ggplot_build(testPlot)$data[[1]][["y"]])) + expect_equal(as.numeric(centre), 2, tolerance = 1e-8) +}) + +# 7. u chart #### + +test_that("P7 u chart plot", { + plotObject <- poissonChartElement(resultsVariable, "plot")[["data"]] + testPlot <- resultsVariable[["state"]][["figures"]][[plotObject]][["obj"]] + jaspTools::expect_equal_plots(testPlot, "poisson-u-chart") +}) + +test_that("P7 u chart test results table", { + table <- poissonChartElement(resultsVariable, "table")[["data"]] + jaspTools::expect_equal_tables(table, list("Point 14")) +}) + +test_that("P7 The test results table is titled for the u chart", { + expect_match(poissonChartElement(resultsVariable, "table")[["title"]], "u chart", fixed = TRUE) +}) + +# 8. Supporting plots, all on the DPU scale #### + +test_that("P8 Cumulative defects per unit plot", { + jaspTools::expect_equal_plots(poissonPlot(resultsVariable, "cumulativePlot"), "poisson-cumulative") +}) + +test_that("P8 Poisson plot and rate plot", { + options <- poissonOptions(attributeSampleSizeType = "variable", attributeSampleSizeVariable = "SampleSize", + attributeDistributionPlot = TRUE, attributeRatePlot = TRUE) + set.seed(1) + results <- runAnalysis("processCapabilityStudies", poissonDataset(), options) + jaspTools::expect_equal_plots(poissonPlot(results, "distributionPlot"), "poisson-distribution") + jaspTools::expect_equal_plots(poissonPlot(results, "ratePlot"), "poisson-rate") + + # P7 scale convention: the rate plot is in DPU, not in percent + ratePlotData <- ggplot2::layer_data(poissonPlot(results, "ratePlot"), 2) + expect_equal(max(ratePlotData[["y"]]), 2.9, tolerance = 1e-8) +}) + +test_that("P8 Histogram of the defects per unit", { + options <- poissonOptions(attributeSampleSizeType = "constant", attributeSampleSizeValue = 10, + attributeHistogram = TRUE) + set.seed(1) + results <- runAnalysis("processCapabilityStudies", poissonDataset(), options) + jaspTools::expect_equal_plots(poissonPlot(results, "histogram"), "poisson-histogram") +}) + +test_that("P8 Histogram with a target in DPU", { + options <- poissonOptions(attributeSampleSizeType = "constant", attributeSampleSizeValue = 10, + attributeHistogram = TRUE, poissonTarget = TRUE, poissonTargetValue = 0.5) + set.seed(1) + results <- runAnalysis("processCapabilityStudies", poissonDataset(), options) + jaspTools::expect_equal_plots(poissonPlot(results, "histogram"), "poisson-histogram-target") + + # the target is tabulated in DPU and carries a value but no interval + table <- poissonTable(results, "summaryTable") + expect_equal(vapply(table, `[[`, character(1), "statistic"), c("Mean DPU", "Target DPU")) + expect_equal(table[[2]][["value"]], 0.5, tolerance = 1e-8) +}) + +# 9. Defects may exceed the sample size #### + +test_that("P9 More defects than inspected units analyses cleanly and gives a DPU above one", { + # 18 of the 25 samples carry more defects than inspected units, which is legal for a defect rate + # and must not raise the binomial "more defectives than inspected units" error + expect_equal(resultsVariable[["status"]], "complete") + expect_null(resultsVariable[["results"]][["errorMessage"]]) + expect_gt(poissonTable(resultsVariable, "summaryTable")[[1]][["value"]], 1) +}) + +# 10. Validation differs from the binomial mode #### + +test_that("P10 A fractional sample size column is accepted", { + options <- poissonOptions(attributeSampleSizeType = "variable", + attributeSampleSizeVariable = "SampleSizeFractional") + set.seed(1) + results <- runAnalysis("processCapabilityStudies", poissonEdgeData(), options) + expect_equal(results[["status"]], "complete") + expect_null(results[["results"]][["errorMessage"]]) +}) + +test_that("P10 Non-integer defects are rejected", { + options <- poissonOptions(attributeCounts = "DefectsFractional", + attributeSampleSizeType = "variable", attributeSampleSizeVariable = "SampleSize") + set.seed(1) + results <- runAnalysis("processCapabilityStudies", poissonEdgeData(), options) + expect_match(results[["results"]][["errorMessage"]], + "The number of defects must contain whole numbers.", fixed = TRUE) +}) + +test_that("P10 A zero sample size is excluded as a missing sample", { + # the shared reader blanks a sample with nothing inspected rather than rejecting it, as in the + # binomial mode; the sample keeps its row so the point numbering does not shift + options <- poissonOptions(attributeSampleSizeType = "variable", + attributeSampleSizeVariable = "SampleSizeZero") + set.seed(1) + results <- runAnalysis("processCapabilityStudies", poissonEdgeData(), options) + expect_equal(results[["status"]], "complete") + expect_true(any(grepl("1 sample with missing values", poissonFootnotes(results, "summaryTable"), fixed = TRUE))) +}) + +# 11. Degenerate zero-defect process #### + +test_that("P11 Zero observed defects renders, reports no violations and is flagged", { + options <- poissonOptions(attributeCounts = "DefectsZero", + attributeSampleSizeType = "variable", attributeSampleSizeVariable = "SampleSize") + set.seed(1) + results <- runAnalysis("processCapabilityStudies", poissonEdgeData(), options) + expect_equal(results[["status"]], "complete") + + # every statistic equals the collapsed centre line, and .nelsonLaws uses strict comparisons, so + # nothing may be flagged + jaspTools::expect_equal_tables(poissonChartElement(results, "table")[["data"]], + list("No test violations occurred.")) + + footnotes <- poissonFootnotes(results, "summaryTable") + expect_true(any(grepl("No defects were observed", footnotes, fixed = TRUE))) + expect_false(any(grepl("The process is not in control", footnotes, fixed = TRUE))) + + # only the upper bound is informative + jaspTools::expect_equal_tables(poissonTable(results, "summaryTable"), + list(0, 0.0414480837541, "Mean DPU", 0)) +}) + +# 12. The u chart limits agree with qcc #### + +test_that("P12 The u chart limits match qcc at three sigma", { + data <- read.csv(poissonDataset()) + reference <- qcc::qcc(data$Defects, sizes = data$SampleSize, type = "u", plot = FALSE) + + plotObject <- poissonChartElement(resultsVariable, "plot")[["data"]] + testPlot <- resultsVariable[["state"]][["figures"]][[plotObject]][["obj"]] + built <- ggplot2::ggplot_build(testPlot)$data + # layers 1 to 3 of .controlChart_plotting are the centre, UCL and LCL step lines + expect_equal(sort(unique(round(built[[2]][["y"]], 8))), + sort(unique(round(reference$limits[, "UCL"], 8)))) + expect_equal(sort(unique(round(built[[3]][["y"]], 8))), + sort(unique(round(reference$limits[, "LCL"], 8)))) + # the lower limit is clamped at zero but the upper one is not + expect_gte(min(built[[3]][["y"]]), 0) +}) + +# 13. Missing values keep the sample numbering #### + +test_that("P13 A missing sample does not shift the point numbers", { + options <- poissonOptions(attributeSampleSizeType = "constant", attributeSampleSizeValue = 10) + set.seed(1) + results <- runAnalysis("processCapabilityStudies", poissonMissingData(), options) + # row 3 is missing and row 7 is out of control; the violation must stay "Point 7" + jaspTools::expect_equal_tables(poissonChartElement(results, "table")[["data"]], list("Point 7")) + expect_true(any(grepl("1 sample with missing values", poissonFootnotes(results, "summaryTable"), fixed = TRUE))) +}) + +# 14. Zone based rules stay off on a clamped u chart #### + +test_that("P14 Rules 4, 5, 6, 7 and 9 do not fire on the u chart", { + options <- poissonOptions(attributeSampleSizeType = "variable", attributeSampleSizeVariable = "SampleSize", + testSet = "nelsonLaws") + set.seed(1) + results <- runAnalysis("processCapabilityStudies", poissonDataset(), options) + schema <- poissonChartElement(results, "table")[["schema"]][["fields"]] + columns <- unlist(lapply(schema, `[[`, "name")) + expect_false(any(c("test4", "test5", "test6", "test7", "test9") %in% columns)) +}) + +# 15. Element keys follow the sample-size type #### + +test_that("P15 The two sample-size dependent panels are separate elements", { + options <- poissonOptions(attributeSampleSizeType = "constant", attributeSampleSizeValue = 10, + attributeHistogram = TRUE, attributeRatePlot = TRUE) + set.seed(1) + constant <- runAnalysis("processCapabilityStudies", poissonDataset(), options) + expect_true("attributeCapability_histogram" %in% names(poissonCollection(constant))) + expect_false("attributeCapability_ratePlot" %in% names(poissonCollection(constant))) + + options <- poissonOptions(attributeSampleSizeType = "variable", attributeSampleSizeVariable = "SampleSize", + attributeHistogram = TRUE, attributeRatePlot = TRUE) + set.seed(1) + variable <- runAnalysis("processCapabilityStudies", poissonDataset(), options) + expect_true("attributeCapability_ratePlot" %in% names(poissonCollection(variable))) + expect_false("attributeCapability_histogram" %in% names(poissonCollection(variable))) +}) + +# 16. Empty state #### + +test_that("P16 Without an assigned variable the analysis renders empty output", { + options <- poissonOptions(attributeCounts = "") + set.seed(1) + results <- runAnalysis("processCapabilityStudies", poissonDataset(), options) + expect_equal(results[["status"]], "complete") + + summaryTable <- poissonCollection(results)[["attributeCapability_summaryTable"]] + expect_equal(length(summaryTable[["data"]]), 0) + expect_equal(length(summaryTable[["schema"]][["fields"]]), 4) + expect_null(summaryTable[["error"]]) +}) + +# 17. Report #### + +test_that("P17 Report", { + options <- poissonOptions(attributeSampleSizeType = "variable", attributeSampleSizeVariable = "SampleSize", + report = TRUE, attributeDistributionPlot = TRUE) + options$reportTitleText <- "Poisson capability" + options$reportLocationText <- "Amsterdam" + options$reportLineText <- "Line 1" + options$reportMachineText <- "Machine 1" + options$reportVariableText <- "Defects" + options$reportProcessText <- "Process 1" + options$reportDateText <- "2026-01-01" + options$reportReportedByText <- "JASP" + options$reportConclusionText <- "In control" + set.seed(1) + results <- runAnalysis("processCapabilityStudies", poissonDataset(), options) + plotName <- results[["results"]][["report"]][["data"]] + testPlot <- results[["state"]][["figures"]][[plotName]][["obj"]] + jaspTools::expect_equal_plots(testPlot, "poisson-report") +}) + +test_that("P17 Report without components selected shows an error", { + options <- poissonOptions(attributeSampleSizeType = "variable", attributeSampleSizeVariable = "SampleSize", + report = TRUE) + options$reportProcessStability <- FALSE + options$reportProcessCapabilityPlot <- FALSE + options$reportProcessCapabilityTables <- FALSE + set.seed(1) + results <- runAnalysis("processCapabilityStudies", poissonDataset(), options) + expect_match(results[["results"]][["report"]][["error"]][["errorMessage"]], + "No report components selected.", fixed = TRUE) +}) + +# 18. The shared branch is fully switched by attributeDistribution #### + +test_that("P18 Switching to the binomial distribution produces the binomial output", { + # same fixture and a sample size large enough for the counts to be valid defective counts; only + # attributeDistribution differs, so any Poisson row surviving here would prove a missed branch + options <- poissonOptions(attributeDistribution = "binomial", + attributeSampleSizeType = "constant", attributeSampleSizeValue = 50) + set.seed(1) + results <- runAnalysis("processCapabilityStudies", poissonDataset(), options) + expect_equal(results[["status"]], "complete") + + statistics <- vapply(poissonTable(results, "summaryTable"), `[[`, character(1), "statistic") + expect_equal(statistics, c("Defective (%)", "PPM defective", "Process Z")) + expect_false("Mean DPU" %in% statistics) + + # the p chart, not the u chart, and the binomial interval footnote + expect_match(poissonChartElement(results, "table")[["title"]], "p chart", fixed = TRUE) + expect_true(any(grepl("Clopper-Pearson", poissonFootnotes(results, "summaryTable"), fixed = TRUE))) +})