Skip to content

Tutorial: A simple experiment

The tutorial below starts with a simple lexical-decision task written in plain jsPsych 7 and explains how to modify this code to run in Pushkin. This is a recommended tutorial for learning the ropes, but a more complete lexical-decision experiment template is available to install through the Pushkin CLI.

If you are not familiar with jsPsych, please consult their documentation. At a minimum, you should understand the basics of timelines and plugins in jsPsych.

Note

As of v3.6 of pushkin-cli, the procedures described here for moving the timeline and importing plugins can be automated if you choose to import a jsPsych experiment during pushkin install experiment and select the basic template (v5+). You can still do these tasks manually if you choose. You may also need to do parts of these procedures in the course of modifying one of the other experiment templates.

Initial jsPsych code

We will start with a simple lexical-decision experiment. The code has been adapted from the experiment here in order to be compatible with jsPsych 7. If you save the code below as a .html file, you should be able to run it as a standalone jsPsych experiment:

lexical-decision.html
<!DOCTYPE html>
<html>
  <head>
    <title>My experiment</title>
    <script src="https://unpkg.com/jspsych@7.3.3"></script>
    <script src="https://unpkg.com/@jspsych/plugin-html-keyboard-response@1.1.2"></script>
    <link
      href="https://unpkg.com/jspsych@7.3.3/css/jspsych.css"
      rel="stylesheet"
      type="text/css"
    />
    <style>
      .fixation {
        border: 2px solid black;
        height: 100px;
        width: 200px;
        font-size: 24px;
        position: relative;
        margin: auto;
      }
      .fixation p {
        width: 100%;
        position: absolute;
        margin: 0.25em;
      }
      .fixation p.top {
        top: 0px;
      }
      .fixation p.bottom {
        bottom: 0px;
      }
      .correct {
        border-color: green;
      }
      .incorrect {
        border-color: red;
      }
    </style>
  </head>
  <body></body>
  <script>
    const stimArray = [
      { word_1: 'SOCKS', word_2: 'SHOE', both_words: true, related: true },
      { word_1: 'SLOW', word_2: 'FAST', both_words: true, related: true },
      { word_1: 'QUEEN', word_2: 'KING', both_words: true, related: true },
      { word_1: 'LEAF', word_2: 'TREE', both_words: true, related: true },

      { word_1: 'SOCKS', word_2: 'TREE', both_words: true, related: false },
      { word_1: 'SLOW', word_2: 'SHOE', both_words: true, related: false },
      { word_1: 'QUEEN', word_2: 'FAST', both_words: true, related: false },
      { word_1: 'LEAF', word_2: 'KING', both_words: true, related: false },

      { word_1: 'AGAIN', word_2: 'PLAW', both_words: false, related: false },
      { word_1: 'BOARD', word_2: 'TRUDE', both_words: false, related: false },
      { word_1: 'LIBE', word_2: 'HAIR', both_words: false, related: false },
      { word_1: 'MOCKET', word_2: 'MEET', both_words: false, related: false },

      { word_1: 'FLAFF', word_2: 'PLAW', both_words: false, related: false },
      { word_1: 'BALT', word_2: 'TRUDE', both_words: false, related: false },
      { word_1: 'LIBE', word_2: 'NUNE', both_words: false, related: false },
      { word_1: 'MOCKET', word_2: 'FULLOW', both_words: false, related: false }
    ];

    const jsPsych = initJsPsych();

    const timeline = [];

    var welcome = {
      type: jsPsychHtmlKeyboardResponse,
      stimulus:`
        <p>Welcome!</p>
        <p>Press spacebar to continue.</p>
      `,
      choices: [" "],
    };

    timeline.push(welcome);

    var instructions_1 = {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: `
        <p>You will see two sets of letters displayed in a box, like this:</p>
        <div class="fixation"><p class="top">HELLO</p><p class="bottom">WORLD</p></div>
        <p>Press Y if both sets are valid English words. Press N if one or both is not a word.</p>
        <p>Press Y to continue.</p>
      `,
      choices: ["y"],
    };

    timeline.push(instructions_1);

    var instructions_2 = {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: `
        <p>In this case, you would press N.</p>
        <div class="fixation"><p class="top">FOOB</p><p class="bottom">ARTIST</p></div>
        <p>Press N to begin the experiment.</p>
      `,
      choices: ["n"],
    };

    timeline.push(instructions_2);

    var lexical_decision_procedure = {
      timeline: [
        {
          type: jsPsychHtmlKeyboardResponse,
          stimulus: '<div class="fixation"></div>',
          choices: "NO_KEYS",
          trial_duration: 1000,
        },
        {
          type: jsPsychHtmlKeyboardResponse,
          stimulus: function () {
            let first_word = jsPsych.timelineVariable("word_1");
            let second_word = jsPsych.timelineVariable("word_2");
            first_word =
              '<div class="fixation"><p class="top">' + first_word + "</p>";
            second_word =
              '<div class="fixation"><p class="bottom">' + second_word + "</p>";
            return first_word + second_word;
          },
          choices: ["y", "n"],
          data: {
            both_words: jsPsych.timelineVariable("both_words"),
            related: jsPsych.timelineVariable("related"),
          },
          on_finish: function (data) {
            if (data.both_words) {
              data.correct = jsPsych.pluginAPI.compareKeys(data.response, "y");
            } else {
              data.correct = jsPsych.pluginAPI.compareKeys(data.response, "n");
            }
          },
        },
        {
          type: jsPsychHtmlKeyboardResponse,
          stimulus: function () {
            let last_correct = jsPsych.data
              .getLastTrialData()
              .values()[0].correct;
            if (last_correct) {
              return '<div class="fixation correct"></div>';
            } else {
              return '<div class="fixation incorrect"></div>';
            }
          },
          choices: "NO_KEYS",
          trial_duration: 2000,
        },
      ],
      timeline_variables: stimArray,
      randomize_order: true,
    };

    timeline.push(lexical_decision_procedure);

    var data_summary = {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: function () {
        // Calculate performance on task
        let correct_related = jsPsych.data
          .get()
          .filter({ related: true, correct: true })
          .count();
        let total_related = jsPsych.data
          .get()
          .filter({ related: true })
          .count();
        let mean_rt_related = jsPsych.data
          .get()
          .filter({ related: true, correct: true })
          .select("rt")
          .mean();

        let correct_unrelated = jsPsych.data
          .get()
          .filter({ related: false, both_words: true, correct: true })
          .count();
        let total_unrelated = jsPsych.data
          .get()
          .filter({ related: false, both_words: true })
          .count();
        let mean_rt_unrelated = jsPsych.data
          .get()
          .filter({ related: false, both_words: true, correct: true })
          .select("rt")
          .mean();

        // Show results
        let results = `<p>You were correct on ${correct_related} of ${total_related} related word pairings!
            Your average correct response time for these was ${Math.round(
              mean_rt_related
            )} milliseconds.</p>
            <p>For unrelated word pairings, you were correct on ${correct_unrelated} of ${total_unrelated}!
            Your average correct response time for these was ${Math.round(
              mean_rt_unrelated
            )} milliseconds.</p>`;
        return results;
      },
      choices: "NO_KEYS",
    };

    timeline.push(data_summary);

    jsPsych.run(timeline);
  </script>
</html>

Moving the timeline

Following the same procedure as in the Quickstart, create a new basic Pushkin site using pushkin install site or navigate to the root directory of an existing Pushkin site. Now run:

pushkin install experiment

Call your experiment "lexdec" and select the latest version of the basic experiment template. Choose 'no' when asked if you'd like to import a jsPsych experiment.

You should now have a folder in your site called /experiments/lexdec with the following content:

└── lexdec
    ├── api controllers
    ├── config.yaml
    ├── migrations
    ├── web page
    │    └── src
    │         ├── assets
    │         ├── experiment.js
    │         ├── experiment.test.js
    │         └── index.js
    └── worker

Open /lexdec/web page/src/experiment.js. It should look like this:

experiment.js
import jsPsychHtmlKeyboardResponse from '@jspsych/plugin-html-keyboard-response';

export function createTimeline(jsPsych) {
    const timeline = []

    var hello_trial = {
        type: jsPsychHtmlKeyboardResponse,
        stimulus: 'Hello, world!'
    }

    timeline.push(hello_trial);

    return timeline;
}

From the jsPsych code above, copy everything between const timeline = [] and jsPsych.run(timeline); (excluding those lines). Paste that content into experiment.js (replacing the existing content) between const timeline = [] and return timeline. Thus you should now have a function createTimeline() within which you build up and finally return the timeline for the experiment.

Importing plugins

In the jsPsych code above, plugins are loaded with <script> tags. In a Pushkin experiment, plugins are loaded with import statements. The basic template already includes the html-keyboard-response plugin as a dependency, so no additional modifications are needed. If you wanted to add additional jsPsych plugins to this experiment, you would use additional import statements in the same format described in the overview of experiment templates.

Moving the stimuli

In theory, there's nothing preventing you from declaring your stimuli inside experiment.js in the same way as shown above in lexical-decision.html; however, we can keep our experiment.js tidier by exporting the stimuli from a dedicated file /lexdec/web page/src/stim.js like this:

stim.js
// Example stimuli

const stimArray = [
    { word_1: 'SOCKS', word_2: 'SHOE', both_words: true, related: true },
    { word_1: 'SLOW', word_2: 'FAST', both_words: true, related: true },
    { word_1: 'QUEEN', word_2: 'KING', both_words: true, related: true },
    { word_1: 'LEAF', word_2: 'TREE', both_words: true, related: true },

    { word_1: 'SOCKS', word_2: 'TREE', both_words: true, related: false },
    { word_1: 'SLOW', word_2: 'SHOE', both_words: true, related: false },
    { word_1: 'QUEEN', word_2: 'FAST', both_words: true, related: false },
    { word_1: 'LEAF', word_2: 'KING', both_words: true, related: false },

    { word_1: 'AGAIN', word_2: 'PLAW', both_words: false, related: false },
    { word_1: 'BOARD', word_2: 'TRUDE', both_words: false, related: false },
    { word_1: 'LIBE', word_2: 'HAIR', both_words: false, related: false },
    { word_1: 'MOCKET', word_2: 'MEET', both_words: false, related: false },

    { word_1: 'FLAFF', word_2: 'PLAW', both_words: false, related: false },
    { word_1: 'BALT', word_2: 'TRUDE', both_words: false, related: false },
    { word_1: 'LIBE', word_2: 'NUNE', both_words: false, related: false },
    { word_1: 'MOCKET', word_2: 'FULLOW', both_words: false, related: false }
]

export default stimArray;

Then we need to import stimArray into experiment.js by adding the following line underneath the import statement for the plugin:

import stimArray from './stim';

Moving the CSS styling

The experiment above relies on CSS styling from <link> and <style> tags to display the experiment correctly. This styling needs be moved to /experiments/lexdec/web page/src/assets/experiment.css in order to style your Pushkin experiment. The new CSS file will look like this:

experiment.css
@import url('https://unpkg.com/jspsych@7.3.3/css/jspsych.css');

/* Fixation box styling */
.fixation {
    border: 2px solid black;
    height: 100px;
    width: 200px;
    font-size: 24px;
    position: relative;
    margin: auto;
}
.fixation p {
    width: 100%;
    position: absolute;
    margin: 0.25em;
}
.fixation p.top {
    top: 0px;
}
.fixation p.bottom {
    bottom: 0px;
}

/* Color for box border when correct */
.correct {
    border-color: green;
}

/* Color for box border when incorrect */
.incorrect {
    border-color: red;
}

Finishing up

At this point, you should be ready to run pushkin prep and pushkin start to see your new experiment! Refer back to the Quickstart for guidance on these commands.