Skip to content

Commit

Permalink
productive procrastination? second pass at timeline.md docs for dynam…
Browse files Browse the repository at this point in the history
…ic node add/remove
  • Loading branch information
cherriechang committed Nov 6, 2024
1 parent 3b0de40 commit 02697e7
Showing 1 changed file with 47 additions and 33 deletions.
80 changes: 47 additions & 33 deletions docs/overview/timeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,10 @@ jsPsych.run([pre_if_trial, if_node, after_if_trial]);

## Modifying timelines at runtime

Although this functionality can also be achieved through a combination of the `conditional_function` and the use of dynamic variables in the `stimulus` parameter, our timeline implementation allows you to dynamically insert or remove trials and nested timelines during runtime. For example, you may have a branching point in your experiment where the participant is given 4 choices, each leading to a different timeline:
Although this functionality can also be achieved through a combination of the `conditional_function` and the use of dynamic variables in the `stimulus` parameter, our timeline implementation allows you to dynamically insert or remove trials and nested timelines during runtime.

### Inserting timeline nodes at runtime
For example, you may have a branching point in your experiment where the participant is given 3 choices, each leading to a different timeline:

```javascript
const jspsych = initJsPsych();
Expand All @@ -482,23 +485,11 @@ const welcome_trial = {

const choice_trial = {
type: jsPsychHtmlKeyboardResponse,
stimulus: 'Press 1 if you are a new participant. Press 2 for inquiries about an existing experiment run. Press 3 for Spanish. Press 4 to see the welcome page and these options again (in case of stupidity-induced blindness). Press 5 to exit.'
}

const start_node = {
timeline: [welcome_trial, choice_trial],
loop_function: function(data){
if(jsPsych.pluginAPI.compareKeys(data.values()[0].response, '4')){
return true;
} else {
return false;
}
}
stimulus: 'Press 1 if you are a new participant. Press 2 for inquiries about an existing experiment run. Press 3 for Spanish.',
choices: ['1','2','3']
}

timeline.push(start_node);
```
This would be trickier to implement with the `conditional_function` since it can only handle 2 branches -- case when `True` or case when `False`. Instead, you can modify the timeline by dynamically adding or removing nodes according to the chosen condition at the end of the choice trial:
This would be trickier to implement with the `conditional_function` since it can only handle 2 branches -- case when `True` or case when `False`. Instead, you can modify the timeline by modifying `choice_trial` to dynamically adding a timeline at the end of the choice trial according to the chosen condition:

```javascript
const b1_t1 = {...};
Expand All @@ -511,45 +502,68 @@ const branch_1 = [b1_t1, b1_t2, b1_t3];
const branch_2 = [b2_t1, b2_t2, b2_t3];
const branch_3 = [b3_t1, b3_t2, b3_t3];

const choice_confirm_trial = {
const choice_trial = {
type: jsPsychHtmlKeyboardResponse,
stimulus: () => {
const choice = jsPsych.data.get().last(1).values()[0];
return `You have chosen ${choice}.`
stimulus: 'Press 1 if you are a new participant. Press 2 for inquiries about an existing experiment run. Press 3 for Spanish.',
choices: ['1','2','3'],
on_finish: (data) => {
switch(data.response) {
case '1':
timeline.push(branch_1);
break;
case '2':
timeline.push(branch_2);
break;
case '3':
timeline.push(branch_3);
break;
}
}
on_finish: () => {
switch(choice) {
case 1:
}
timeline.push(start_trial, choice_trial);
```
During runtime, choices 1, 2 and 3 will dynamically add a different (nested) timeline, `branch_1`, `branch_2` and `branch_3` respectively, to the end of the main timeline.

### Removing timeline nodes at runtime

You can also remove upcoming timeline nodes from the timeline at runtime. To demonstrate this, we can modify the above example by adding a 4th choice and extra trial at the end of the timeline:

```javascript
const choice_trial = {
type: jsPsychHtmlKeyboardResponse,
stimulus: 'Press 1 if you are a new participant. Press 2 for inquiries about an existing experiment run. Press 3 for Spanish. Press 4 to exit.',
choices: ['1','2','3', '4'],
on_finish: (data) => {
switch(data.response) {
case '1':
timeline.push(branch_1);
break;
case 2:
case '2':
timeline.push(branch_2);
break;
case 3:
case '3':
timeline.push(branch_3);
break;
case 5:
case '4':
timeline.pop();
break;
default:
break;
}
}
}

const end_trial = {
type: jsPsychHtmlKeyboardResponse,
stimulus: 'End of experiment.'
type: JsPsychHtmlKeyboardResponse,
stimulus: 'End of experiment'
}

timeline.push(choice_confirm_trial, end_trial);
timeline.push(start_trial, choice_trial, end_trial)
```
During runtime, choices 1, 2 and 3 will dynamically add a different (nested) timeline, `branch_1`, `branch_2` and `branch_3` respectively, to the end of the main timeline. Choice 4 is caught at `start_node`, where the `loop_function` will resolve to `True` and repeat the welcome and choice trials. Finally, choice 5 removes the `end_trial` that was pushed to the timeline before runtime.
Now, if 1, 2 or 3 were chosen during runtime, the `end_trial` will run after the dynamically added timeline corresponding to the choice has been run; but if 4 was chosen, `end_trial` will be removed at runtime, and the timeline will terminate.

### Exception cases for adding/removing timeline nodes dynamically
Adding or removing timeline nodes work as expected when the addition/removal occurs at a future point in the timeline relative to the current executing node, but not if it occurs before the current node. The example above works as expected becaues all added/removed nodes occur at the end of the timeline via `push()` and `pop()`, but if the branches were inserted at a point in the timeline that has already been executed, e.g. `timeline.splice(0, 0, branch_1)`, then `branch_1` will not be executed.

In the case of a looping timeline, adding a timeline node at a point before the current node will cause the current node to be executed again. For example, `start_node` is currently a looping timeline of this form `welcome`->`choice`. If we added an `on_finish` function to `choice_trial` that adds a timeline node to the start of the timeline, e.g. `hello`->`welcome`->`choice`, the `choice` trial will appear again as the next trial immediately after (and cause an infinite loop!). Similarly, if we tried to remove the `hello` trial from a looping timeline of the form `hello`->`welcome`->`choice` while executing the `welcome` trial, the `choice` trial will not be executed.
In the case of a looping timeline, adding a timeline node at a point before the current node will cause the current node to be executed again; and removing a timeline node at a point before the current node will cause the next node to be skipped.

## Timeline start and finish functions

Expand Down

0 comments on commit 02697e7

Please sign in to comment.