Simultaneously sampling from two variables in jsPsych

I am using jsPsych to create an experiment and I am struggling to sample from two variables simultaneously. Specifically, in each trial, I would like to present a primeWord and a targetWord by randomly sampling each of them from its own variable.

I have looked into several resources—such as sampling without replacement, custom sampling and position indices—but to no avail. I’m a beginner at this, so it’s possible that one of these resources was relevant (especially the last one, I think).

In addition to the parallel sampling, I wonder how I could save the same trial index in the data of both primeWord and targetWord.

Solution

Finally I solved this using position indices in a for-of loop (see below: For loop that creates trial information iteratively over trials (3 steps)).

<!DOCTYPE html>
<html>

  <head>

    <!-- jsPsych plugins -->
    <script src="../jspsych.js"></script>
    <script src="../plugins/jspsych-html-keyboard-response.js"></script>
    <script src="../plugins/jspsych-html-button-response.js"></script>

    <!-- CSS -->
    <link rel="stylesheet" href="../css/jspsych.css">

    <style>
      body.jspsych-display-element {
        color: #ececec;
        background-color: #2b2b2b;
      }

      #jspsych-html-keyboard-response-stimulus {
        font-size: 32px;
      }

      .fas,
      .far {
        color: #b6b6b6;
      }

    </style>

  </head>


  <!-- Beginning of the script that contains the core of the experiment -->
  <script>
    /* Create empty timeline object, which will be sequentially filled in using timeline.push() */
    var timeline = [];

    var instructions = {
      type: 'html-button-response',
      stimulus: ["<p>Each screen will show a word in lower case, such as 'target'. Press <b>F</b> if the word is primarily abstract</p>" +
        '<p>or <b>J</b> if it is primarily concrete. Each word is presented for up to five seconds.</p>'
      ],
      choices: ['Ready to start']
    }
    /* Add instructions to the timeline */
    timeline.push(instructions)


    /* Stimuli */

    /* Begin with a general list of words, and randomly split the 
    list into a set of prime words and a set of target words. */

    var allWords = [{
        word: 'word 1',
        correct_response: 'abstract'
      },
      {
        word: 'word 2',
        correct_response: 'abstract'
      },
      {
        word: 'word 3',
        correct_response: 'abstract'
      },
      {
        word: 'word 4',
        correct_response: 'abstract'
      },
      {
        word: 'word 5',
        correct_response: 'abstract'
      },
      {
        word: 'word 6',
        correct_response: 'concrete'
      },
      {
        word: 'word 7',
        correct_response: 'concrete'
      },
      {
        word: 'word 8',
        correct_response: 'concrete'
      },
      {
        word: 'word 9',
        correct_response: 'concrete'
      },
      {
        word: 'word 10',
        correct_response: 'concrete'
      }

    ]

    /* Shuffle all words */
    var shuffled_allWords = allWords.sort(function() {
      return 0.5 - Math.random()
    });

    /* Split up the list into two sets */
    var midpoint = Math.floor(shuffled_allWords.length / 2);

    /* Set number of trials per participant (must be smaller than half of all words) */
    var number_of_trials = 4;

    /* Create the set of prime words */
    var primeWords = shuffled_allWords.slice(0, number_of_trials);

    /* Make prime words uppercase, as in Hutchison et al.
    (2013; https://doi.org/10.3758/s13428-012-0304-z) */

    var primeWords = primeWords.map(item => ({
      ...item,
      word: item.word.toUpperCase()
    }))

    /* Create the set of target words */
    var targetWords = shuffled_allWords.slice(midpoint, midpoint + number_of_trials);


    /* Next, create set of interstimulus intervals from a range between 60 and 1200 ms. 
    First, the range is split into as many integers as the number of trials, equally 
    for all participants. Afterwards, the list is shuffled within participants. */

    function makeArr(startValue, stopValue, cardinality) {
      var arr = [];
      var step = (stopValue - startValue) / (cardinality - 1);
      for (var i = 0; i < cardinality; i++) {
        arr.push(startValue + (step * i));
      }
      return arr;
    }

    ordered_interstimulus_intervals = makeArr(60, 1200, number_of_trials);

    interstimulus_interval =
      ordered_interstimulus_intervals.sort(function() {
        return 0.5 - Math.random()
      });


    /* For loop that creates trial information iteratively over trials (3 steps) */

    /* 1. Enable function to create iterable range, to be used in the for loop below */
    const Range = (start, end) => ({
      *[Symbol.iterator]() {
        while (start < end)
          yield start++;
      }
    })

    /* 2. Initialise stimuli array */
    stimuli = [];

    /* 3. Run loop */
    for (const i of Range(0, number_of_trials)) {
      stimuli.push({
        primeWord: primeWords[i].word,
        targetWord: targetWords[i].word,
        interstimulus_interval: interstimulus_interval[i],
        correct_response: targetWords[i].correct_response,
        trial: i + 1 /* 1 is added because, otherwise, trials would else trials would start from 0 */
      })
    }


    /* Trial content: fixation, primeWord, interstimulus interval, targetWord, feedback.
    This constitutes a unique trial in the semantic priming paradigm. Yet, beware that 
    jsPsych provides a 'trial_index' value in the output of the task. That index is 
    assigned to each part of every trial. Thus, in the present experiment, there are 
    five trial_index values per trial--namely, one for each part listed above. */

    /* Fixation cross */
    var fixation = {
      type: 'html-keyboard-response',
      stimulus: '+',
      response_ends_trial: false,
      trial_duration: function() {
        /* Set fixations with a varying duration to boost participants' attention */
        return jsPsych.randomization.sampleWithoutReplacement([400, 450, 500, 550, 600], 1)[0];
      },
      post_trial_gap: 0,
      data: {
        trial: jsPsych.timelineVariable('trial')
      },
      css_classes: ['stimulus'],
      /* Computation run at the end of each trial */
      on_finish: function(data) {
        /* Log key presses, if any, by writing 1 into fixation_keypresses (else, write 0) */
        if (data.key_press == null) {
          var fixation_keypresses = 0;
        } else {
          var fixation_keypresses = 1;
        };
        data.fixation_keypresses = fixation_keypresses
      }
    };

    var primeWord = {
      type: 'html-keyboard-response',
      stimulus: jsPsych.timelineVariable('primeWord'),
      response_ends_trial: false,
      trial_duration: 150,
      post_trial_gap: 0,
      data: {
        position: 'prime',
        trial: jsPsych.timelineVariable('trial')
      },
      css_classes: ['stimulus'],
      /* Computation run at the end of each trial */
      on_finish: function(data) {
        /* Log key presses, if any, by writing 1 into primeWord_keypresses (else, write 0) */
        if (data.key_press == null) {
          var primeWord_keypresses = 0;
        } else {
          var primeWord_keypresses = 1;
        };
        data.primeWord_keypresses = primeWord_keypresses
      }
    };

    var interstimulus_interval = {
      type: 'html-keyboard-response',
      stimulus: ' ',
      response_ends_trial: false,
      trial_duration: jsPsych.timelineVariable('interstimulus_interval'),
      post_trial_gap: 0,
      data: {
        interstimulus_interval: jsPsych.timelineVariable('interstimulus_interval'),
        trial: jsPsych.timelineVariable('trial')
      },
      css_classes: ['stimulus'],
      /* Computation run at the end of each trial */
      on_finish: function(data) {
        /* Log key presses, if any, by writing 1 into interstimulus_interval_keypresses (else, write 0) */
        if (data.key_press == null) {
          var interstimulus_interval_keypresses = 0;
        } else {
          var interstimulus_interval_keypresses = 1;
        };
        data.interstimulus_interval_keypresses = interstimulus_interval_keypresses
      }
    };

    var targetWord = {
      type: 'html-keyboard-response',
      stimulus: jsPsych.timelineVariable('targetWord'),
      choices: ['f', 'j'],
      trial_duration: 3000,
      post_trial_gap: 0,
      css_classes: ['stimulus'],
      data: {
        position: 'target',
        trial: jsPsych.timelineVariable('trial'),
        correct_response: jsPsych.timelineVariable('correct_response')
      },
      /* Computation run at the end of each trial */
      on_finish: function(data) {
        if (data.key_press !== null) {
          /* Label correct responses */
          if (data.correct_response == 'abstract' && data.key_press == jsPsych.pluginAPI.convertKeyCharacterToKeyCode('f') ||
            data.correct_response == 'concrete' && data.key_press == jsPsych.pluginAPI.convertKeyCharacterToKeyCode('j')) {
            var accuracy = 'correct';
            /* Label incorrect responses */
          } else if (data.correct_response == 'abstract' && data.key_press == jsPsych.pluginAPI.convertKeyCharacterToKeyCode('j') ||
            data.correct_response == 'concrete' && data.key_press == jsPsych.pluginAPI.convertKeyCharacterToKeyCode('f')) {
            var accuracy = 'incorrect';
          }
          /* Label unanswered trials */
        } else {
          var accuracy = 'unanswered';
        };
        data.accuracy = accuracy;
        /* Count up premature responses per trial. The command 'last(4)' is used below
        to consider only the current part of the 'trial' (i.e., targetWord) and the 
        three previous parts (i.e., interstimulus_interval, primeWord and fixation). 
        Notice that the response entered in this part (targetWord) is not added into 
        the sum, as it is appropriate to respond to the target word. */
        data.premature_responses =
          jsPsych.data.get().last(4).filter('fixation_keypresses' == 1).select('fixation_keypresses').sum() +
          jsPsych.data.get().last(4).filter('primeWord_keypresses' == 1).select('primeWord_keypresses').sum() +
          jsPsych.data.get().last(4).filter('interstimulus_interval_keypresses' == 1).select('interstimulus_interval_keypresses').sum();
      }
    };

    feedback = {
      type: 'html-keyboard-response',
      stimulus: function() {
        var last_trial_accuracy = jsPsych.data.getLastTrialData().values()[0].accuracy;
        if (last_trial_accuracy == 'incorrect') {
          return '<p style="color:red; font-face:bold;">X</p>';
        } else if (last_trial_accuracy == 'unanswered') {
          return '<p style="color:red; font-face:bold;">0</p>'
        } else {
          return ''
        }
      },
      choices: jsPsych.NO_KEYS,
      trial_duration: function() {
        var last_trial_accuracy = jsPsych.data.getLastTrialData().values()[0].accuracy;
        if (last_trial_accuracy == 'correct') {
          return 0
        } else {
          return 800
        }
      }
    };

    var main_procedure = {
      timeline: [fixation, primeWord, interstimulus_interval, targetWord, feedback],
      timeline_variables: stimuli
    };
    timeline.push(main_procedure);


    var debrief = {
      type: 'html-keyboard-response',
      choices: ['c'],
      stimulus: function() {
        var total_correct = jsPsych.data.get().filter({
          accuracy: 'correct'
        }).count();
        var total_incorrect = jsPsych.data.get().filter({
          accuracy: 'incorrect'
        }).count();
        var total_unanswered = jsPsych.data.get().filter({
          accuracy: 'unanswered'
        }).count();
        var accuracy_rate = Math.round(total_correct / (total_correct + total_incorrect + total_unanswered) * 100) + "%";
        var message = "<div style='font-size:20px;'><p>All done!</p>" +
          "<p>Your accuracy rate was " + accuracy_rate + " (" + total_correct + " correct trials, " + total_incorrect +
          " incorrect and " + total_unanswered + " unanswered).</p>" +
          "<p>Press C to see the entire set of data generated by this experiment.</p></div>";
        return message;
      }
    }
    /* Add debrief to the timeline */
    timeline.push(debrief);


    /* Initialize experiment by incorporating the timeline
    and setting the data to be displayed at the end. */
    jsPsych.init({
      timeline: timeline,
      on_finish: function() {
        jsPsych.data.displayData();
      },
      default_iti: function() {
        /* Use varying intertrial intervals to reduce habituation effects */
        return jsPsych.randomization.sampleWithoutReplacement([1300, 1400, 1500, 1600, 1700], 1)[0];
      }
    });

  </script>

</html>
comments powered by Disqus