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:
<!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:
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:
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:
// 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:
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:
@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.