jhelvy / surveydown

An attempt to build a markdown-based survey platform using Quarto & Shiny
12 stars 1 forks source link

Control UI #7

Closed jhelvy closed 1 month ago

jhelvy commented 1 month ago

I'm debating about how best to allow users to define some general survey control logic, namely conditionally skipping to pages in the survey and conditionally displaying questions, both based on question responses.

Right now we've come up with what I'm calling version 1, but I'm considering a version 2. I describe them below.

Version 1

sd_server(
  input = input,
  session = session,
  skip_if = tibble::tribble(
    ~question_id,  ~question_value, ~target,
    "skip_to_end", "end",           "end"
  ),
  show_if = tibble::tribble(
    ~question_id,  ~question_value, ~target,
    "penguins",    "other",         "penguins_other"
  )
)

In this version, the user provides a data frame / tibble to skip_if and a separate data frame to show_if. These can be defined inside the qmd file like this, or in separate csv files that are then read in. The header of these data frames must not be changed.

More complex skip logic (with custom functions) is handled with additional skip_if_complex and show_if_complex arguments, like this:

sd_server(
  input = input,
  session = session,
  skip_if_complex = list(
    list(
      condition = function(input) input$skip_to_end == "end",
      target = "end"
    )
  ),
  show_if_complex = list(
    list(
      condition = function(input) input$penguins == "other",
      target = "penguins_other"
    )
  )
)

Version 2

Version 2 combines the skip_if and show_if arguments into a single control argument, something like this:

sd_server(
  input = input,
  session = session,
  control = tibble::tribble(
    ~question_id,  ~question_value, ~target,          ~type,
    "skip_to_end", "end",           "end",            "skip",
    "penguins",    "other",         "penguins_other", "show"
  )
)

Again, the user can define this inside the qmd file like this, or in a separate csv file that is then read in.

More complex skip logic (with custom functions) could be handled with just one additional control_custom argument, like this:

sd_server(
  input = input,
  session = session,
  control_custom = list(
    list(
      condition = function(input) input$skip_to_end == "end",
      target = "end",
      type = "skip"
    ),
    list(
      condition = function(input) input$penguins == "other",
      target = "penguins_other",
      type = "show"
    )
  )
)

Tradeoffs

Between these, I'm leaning towards Version 2. It just feels more concise. A user could have a single separate control.csv file, for example, that they keep in the root directory, and then their server code would be super parsimonious, like this:

sd_server(
  input = input,
  session = session,
  control = readr::read_csv('control.csv')
)
warnes commented 1 month ago

You may want to consider how existing systems (e.g. Qualtrics, whose file format is described at https://gist.github.com/ctesta01/d4255959dace01431fb90618d1e8c241) handle control.

pingfan-hu commented 1 month ago

I personally prefer version 1, with skip and show logics in 2 separate tibbles. For version 2, It's surely tidy in one single tibble, but the users may mess up with numbers of conditions. For version 2, I expect users to group skip and show logics like this:

sd_server(
  input = input,
  session = session,
  control = tibble::tribble(
    ~question_id,  ~question_value, ~target,   ~type,
    "skip_q1",     "skip_v1",       "skip_t1", "skip",
    "skip_q2",     "skip_v2",       "skip_t2", "skip",
    "skip_q3",     "skip_v3",       "skip_t3", "skip",
    "show_q1",     "show_v1",       "show_t1", "show",
    "show_q2",     "show_v2",       "show_t2", "show",
    "show_q3",     "show_v3",       "show_t3", "show"
  )
)

If users mix skip and show logics, the code will still run, but it would be hard to manage, so I would prefer to intentionally separate them to avoid possible maintaining problems. Above is merely my initial thoughts. We could talk more about it if needed.

jhelvy commented 1 month ago

@pingfan-hu Good points. And I suppose if a user wanted the server code to be parsimonious, it'd be easy enough to just have two csv files and read them in like this:

sd_server(
  input = input,
  session = session,
  skip_if = readr::read_csv("skip_if.csv"),
  show_if = readr::read_csv("show_if.csv")
)
jhelvy commented 1 month ago

At a minimum, for version 1 I think skip_if_custom and show_if_custom are better argument names than skip_if_complex and show_if_complex for allowing users to define more custom skip and show logic.

pingfan-hu commented 1 month ago

Yes I agree, like, don't emphasize "complex" even though it's true. Users may panic.

jhelvy commented 1 month ago

Closing this because I believe we've agreed on option 1, at least for now, and the change from complex to custom was made now.