14 Saving and Labeling Data
- Understand how jsPsych stores data and the difference between browser memory and permanent storage
- Add meaningful labels using the data parameter, timeline data, and timeline variables
- Apply global properties to all trials using jsPsych.data.addProperties()
- Save data locally for testing using jsPsych.data.get().localSave()
14.1 How jsPsych Stores Data
Before we dive into labeling data, it’s important to understand how jsPsych actually saves your experimental data. jsPsych uses a simple but powerful principle: one event per row. Every time something happens in your experiment (a participant responds to a stimulus, views an instruction screen, or completes any trial) jsPsych creates a new row of data.
Let’s start with a simple example to see this in action:
// 1. Initialize jsPsych
const jsPsych = initJsPsych();
// 2. Define our trials
const welcome = {
type: jsPsychHtmlKeyboardResponse,
stimulus: '<p>Welcome to the experiment!</p><p>Press any key to continue.</p>'
};
const question1 = {
type: jsPsychHtmlKeyboardResponse,
stimulus: '<p>Do you like chocolate?</p><p>Press Y for yes, N for no.</p>',
choices: ['y', 'n']
};
const question2 = {
type: jsPsychHtmlKeyboardResponse,
stimulus: '<p>Do you like vanilla?</p><p>Press Y for yes, N for no.</p>',
choices: ['y', 'n']
};
const goodbye = {
type: jsPsychHtmlKeyboardResponse,
stimulus: '<p>Thank you for participating!</p><p>Press any key to finish.</p>'
};
// 3. Run jsPsych with our trials
jsPsych.run([welcome, question1, question2, goodbye]);When this simple experiment runs, jsPsych automatically collects data for each trial. Here’s what the resulting CSV file would look like:
| rt | stimulus | response | trial_type | trial_index | plugin_version | time_elapsed |
|---|---|---|---|---|---|---|
| 4128 | <p>Welcome to the experiment!</p><p>Press any key to continue.</p> |
k | html-keyboard-response | 0 | 2.1.0 | 4128 |
| 1556 | <p>Do you like chocolate?</p><p>Press Y for yes, N for no.</p> |
y | html-keyboard-response | 1 | 2.1.0 | 5689 |
| 411 | <p>Do you like vanilla?</p><p>Press Y for yes, N for no.</p> |
y | html-keyboard-response | 2 | 2.1.0 | 6101 |
Notice that we have three rows, one for each trial that ran. jsPsych automatically includes several pieces of information in each row. The trial_type column tells us which plugin was used, while trial_index shows the order in which trials occurred (starting from 0). The time_elapsed column records the total time since the experiment started in milliseconds, and rt shows the response time for this specific trial. The stimulus column contains what was presented to the participant, response records what key they pressed, and plugin_version tells us which version of jsPsych was used.
It’s important to understand that jsPsych saves a row for everything that happens in your experiment. Instruction screens, fixation crosses, feedback screens, and actual experimental trials all get their own rows. This means you’ll often need to filter your data during analysis to focus only on the trials that contain your actual experimental responses.
While this data captures the basic mechanics of what happened, notice how little meaningful information we actually have about our experiment. Looking at row 2, we can see that the participant pressed ‘y’ in response to the chocolate question, but we have no easy way to know what ‘y’ means, whether this was correct, or what type of question this was. The same problem exists for row 3 with the vanilla question. Without additional context, we can’t easily determine what their response pattern means or how to categorize these responses for analysis. .
To best understand why this is a problem, we can return to the memory experiment in Chapter 11. Let’s have a look at our code and the resulting data:
// 1. Initialize jsPsych
const jsPsych = initJsPsych();
// 2. Define our trials
const welcome = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `Welcome to the Experiment! Press any key to begin.`,
choices: "ALL_KEYS",
post_trial_gap: 500
}
const instructions = {
type: jsPsychInstructions,
pages: [
// Page 1: Welcome and Overview
`<div class='instructionStyle'>
<p>You will participate in a memory experiment with two phases:</p>
<p><strong>Phase 1:</strong> Study a list of words</p>
<p><strong>Phase 2:</strong> Decide if words are OLD or NEW</p>
<p>Click Next to continue.</p>
</div>`,
// Page 2: Study Phase Instructions
`<div class='instructionStyle'>
<p>During the study phase, you will see words appear one at a time. Read each word carefully and try to remember it.</p>
<p>Each word appears for 1 second.You will study about 15 words total.</p>
<p>You do not need to press any keys during this phase.</p>
</div>`,
// Page 3: Test Phase Instructions
`<div class='instructionStyle'>
<p>During the test phase, you will see words one at a time. Some words are OLD (from the study list). Some words are NEW (not from the study list).</p>
<p>Press the 'OLD' button if the word was old.</p>
<p>Press the 'NEW' button if the word was new</p>
</div>`,
// Page 4: Stay focused
`<div class='instructionStyle'>
<p>Trust your first instinct.</p>
<p>If unsure, make your best guess.</p>
</div>`,
// Page 5: Final Instructions
`<div class='instructionStyle'>
<p>Try to stay focused throughout the experiment. The experiment only takes a few minutes.</p>
<p>Click Next to start the study phase.</p>
</div>`
],
key_forward: 'ArrowRight',
key_backward: 'ArrowLeft',
allow_backward: true,
show_clickable_nav: true,
button_label_previous: 'Back',
button_label_next: 'Next'
};
let study = {
timeline: [
{
type: jsPsychHtmlKeyboardResponse,
stimulus: "+",
choices: "NO_KEYS",
post_trial_gap: 250,
trial_duration: 500,
css_classess: "wordStyle"
},
{
type: jsPsychHtmlKeyboardResponse,
stimulus: jsPsych.timelineVariable("word"),
choices: "NO_KEYS",
post_trial_gap: 500,
trial_duration: 1000,
css_classess: "wordStyle"
}
],
timeline_variables: [
{word: "BED"},
{word: "REST"},
{word: "AWAKE"},
{word: "TIRED"},
{word: "DREAM"},
{word: "WAKE"},
{word: "SNOOZE"},
{word: "BLANKET"},
{word: "DOZE"},
{word: "SLUMBER"},
{word: "SNORE"},
{word: "NAP"},
{word: "PEACE"},
{word: "YAWN"},
{word: "DROWSY"}
],
randomize_order: true
}
let test = {
timeline: [
{
type: jsPsychHtmlButtonResponse,
stimulus: jsPsych.timelineVariable("word"),
post_trial_gap: 500,
choices: ["OLD", "NEW"],
css_classess: "wordStyle"
}
],
timeline_variables: [
// OLD WORDS
{word: "BED"},
{word: "REST"},
{word: "AWAKE"},
{word: "TIRED"},
{word: "DREAM"},
{word: "WAKE"},
{word: "SNOOZE"},
{word: "BLANKET"},
{word: "DOZE"},
{word: "SLUMBER"},
{word: "SNORE"},
{word: "NAP"},
{word: "PEACE"},
{word: "YAWN"},
{word: "DROWSY"},
// NEW WORDS
{word: "DOCTOR"},
{word: "NURSE"},
{word: "SICK"},
{word: "LAWYER"},
{word: "MEDICINE"},
{word: "HEALTH"},
{word: "HOSPITAL"},
{word: "DENTIST"},
{word: "PHYSICIAN"},
{word: "ILL"},
{word: "PATIENT"},
{word: "OFFICE"},
{word: "STETHOSCOPE"},
{word: "SURGEON"},
{word: "CLINIC"},
{word: "CURE"},
// critical word
{word: "SLEEP"}
],
randomize_order: true
}
// 3. Run jsPsych with our trials
jsPsych.run([
welcome,
instructions,
study,
test
]);The resulting data from running this code looks like this:
| rt | stimulus | response | trial_type | trial_index | plugin_version | time_elapsed | view_history |
|---|---|---|---|---|---|---|---|
| 9689 | Welcome … | a | html-keyboard-response | 0 | 2.1.0 | 9690 | |
| 3973 | instructions | 1 | 2.1.0 | 14173 | [{" page_index":0, "viewing_time":1014} ... { "page_index":4, "viewing_time":1093 }] |
||
| null | + | null | html-keyboard-response | 2 | 2.1.0 | 14685 | |
| null | SNOOZE | null | html-keyboard-response | 3 | 2.1.0 | 15952 | |
| null | + | null | html-keyboard-response | 4 | 2.1.0 | 16954 | |
| null | AWAKE | null | html-keyboard-response | 5 | 2.1.0 | 18205 | |
| null | + | null | html-keyboard-response | 6 | 2.1.0 | 19206 | |
| null | BLANKET | null | html-keyboard-response | 7 | 2.1.0 | 20491 | |
| null | + | null | html-keyboard-response | 8 | 2.1.0 | 21491 | |
| null | PEACE | null | html-keyboard-response | 9 | 2.1.0 | 22743 | |
| null | + | null | html-keyboard-response | 10 | 2.1.0 | 23743 | |
| null | SNORE | null | html-keyboard-response | 11 | 2.1.0 | 24994 | |
| null | + | null | html-keyboard-response | 12 | 2.1.0 | 26012 | |
| null | YAWN | null | html-keyboard-response | 13 | 2.1.0 | 27262 | |
| null | + | null | html-keyboard-response | 14 | 2.1.0 | 28264 | |
| null | SLUMBER | null | html-keyboard-response | 15 | 2.1.0 | 29531 | |
| null | + | null | html-keyboard-response | 16 | 2.1.0 | 30532 | |
| null | REST | null | html-keyboard-response | 17 | 2.1.0 | 31783 | |
| null | + | null | html-keyboard-response | 18 | 2.1.0 | 32801 | |
| null | TIRED | null | html-keyboard-response | 19 | 2.1.0 | 34051 | |
| null | + | null | html-keyboard-response | 20 | 2.1.0 | 35053 | |
| null | DROWSY | null | html-keyboard-response | 21 | 2.1.0 | 36320 | |
| null | + | null | html-keyboard-response | 22 | 2.1.0 | 37321 | |
| null | DREAM | null | html-keyboard-response | 23 | 2.1.0 | 38571 | |
| null | + | null | html-keyboard-response | 24 | 2.1.0 | 39573 | |
| null | WAKE | null | html-keyboard-response | 25 | 2.1.0 | 40823 | |
| null | + | null | html-keyboard-response | 26 | 2.1.0 | 41824 | |
| null | BED | null | html-keyboard-response | 27 | 2.1.0 | 43075 | |
| null | + | null | html-keyboard-response | 28 | 2.1.0 | 44092 | |
| null | NAP | null | html-keyboard-response | 29 | 2.1.0 | 45344 | |
| null | + | null | html-keyboard-response | 30 | 2.1.0 | 46347 | |
| null | DOZE | null | html-keyboard-response | 31 | 2.1.0 | 47615 | |
| 2016 | STETHOSCOPE | 0 | html-button-response | 32 | 2.1.0 | 50150 | |
| 407 | BLANKET | 1 | html-button-response | 33 | 2.1.0 | 51072 | |
| 313 | DROWSY | 0 | html-button-response | 34 | 2.1.0 | 51894 | |
| 302 | REST | 0 | html-button-response | 35 | 2.1.0 | 52700 | |
| 328 | HEALTH | 0 | html-button-response | 36 | 2.1.0 | 53546 | |
| 362 | SLUMBER | 1 | html-button-response | 37 | 2.1.0 | 54413 | |
| 250 | PATIENT | 0 | html-button-response | 38 | 2.1.0 | 55170 | |
| 388 | YAWN | 0 | html-button-response | 39 | 2.1.0 | 56072 | |
| 412 | DENTIST | 0 | html-button-response | 40 | 2.1.0 | 56997 | |
| 568 | ILL | 0 | html-button-response | 41 | 2.1.0 | 58071 | |
| 962 | DREAM | 0 | html-button-response | 42 | 2.1.0 | 59535 | |
| 433 | SLEEP | 1 | html-button-response | 43 | 2.1.0 | 60474 | |
| 389 | MEDICINE | 1 | html-button-response | 44 | 2.1.0 | 61380 | |
| 426 | LAWYER | 1 | html-button-response | 45 | 2.1.0 | 62319 | |
| 449 | NAP | 0 | html-button-response | 46 | 2.1.0 | 63275 | |
| 444 | DOCTOR | 0 | html-button-response | 47 | 2.1.0 | 64221 | |
| 308 | CURE | 0 | html-button-response | 48 | 2.1.0 | 65034 | |
| 333 | PEACE | 0 | html-button-response | 49 | 2.1.0 | 65877 | |
| 1072 | OFFICE | 0 | html-button-response | 50 | 2.1.0 | 67451 | |
| 544 | TIRED | 1 | html-button-response | 51 | 2.1.0 | 68524 | |
| 359 | SICK | 1 | html-button-response | 52 | 2.1.0 | 69390 | |
| 417 | NURSE | 0 | html-button-response | 53 | 2.1.0 | 70313 | |
| 533 | AWAKE | 0 | html-button-response | 54 | 2.1.0 | 71347 | |
| 285 | SNOOZE | 1 | html-button-response | 55 | 2.1.0 | 72134 | |
| 241 | HOSPITAL | 0 | html-button-response | 56 | 2.1.0 | 72893 | |
| 476 | SURGEON | 1 | html-button-response | 57 | 2.1.0 | 73877 | |
| 252 | SNORE | 0 | html-button-response | 58 | 2.1.0 | 74636 | |
| 315 | BED | 0 | html-button-response | 59 | 2.1.0 | 75467 | |
| 409 | WAKE | 0 | html-button-response | 60 | 2.1.0 | 76379 | |
| 628 | PHYSICIAN | 0 | html-button-response | 61 | 2.1.0 | 77514 | |
| 614 | DOZE | 0 | html-button-response | 62 | 2.1.0 | 78635 | |
| 1271 | CLINIC | 1 | html-button-response | 63 | 2.1.0 | 80410 |
If you’ve just collected this data how would you analyze it? How would you know which words were the studied words and which were the new ones? Which rows are part of the study phase versus the test phase? Which word is the critical lure?
Of course, you could go through this file row-by-row and figure it out, but if we had properly labelled each row, then we’ll have a much easier time analyzing the data when we have 100 of these files. This is exactly why we need to add our own data labels to make the dataset meaningful and analysis-ready. We’ll return to our memory experiment later to illustrate proper data labeling.
14.2 Saving Data
14.2.1 Where Data Lives: Browser Memory vs. Permanent Storage
A quick aside before discussing data labeling: It’s important to understand that when jsPsych collects this data, it’s initially stored only in the browser’s memory. This means the data exists only as long as the browser tab is open. If the participant closes the browser window or refreshes the page, all the data disappears forever.
For a real experiment, you need to transfer this data from the browser’s temporary memory to somewhere permanent. There are several options for permanent storage:
- Save to a server database (most common for online studies)
- Save to a file on a server (requires server setup)
- Save locally to the participant’s computer (good for testing and development)
In this chapter, we’ll focus on saving data locally, which is perfect for testing your experiments during development. Later in the book, we’ll cover how to set up server-based data storage for real data collection.
14.2.2 Saving Data Locally
For development and testing purposes, jsPsych provides a convenient way to save data directly to your computer using the localSave() method. This downloads the data as a file that you can open in Excel or other data analysis programs.
The syntax to call this function looks like this:
- The
jsPsych.data.get()part retrieves all of the data. - The
.localSave('csv', 'experiment_data.csv')saves the data as a csv file, with the file name ‘experiment_data.csv’ (you can change that part to be whatever you want it to be).
In order to give participants the ability to download their own data, however, we need to incorporate this method into our experiment as a custom trial.
14.2.3 Creating a Data Saving Trial
To save data, we need to create a special trial that gives participants a way to download their data. This requires some techniques we haven’t covered yet, so let’s break down each part:
const saveData = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `
<div style='text-align: center;'>
<p>Experiment complete!</p>
<p>Click the button below to save your data locally:</p>
<button id='save-btn' style='padding: 10px 20px; font-size: 16px; cursor: pointer;'>
Click here to save the data locally
</button>
</div>
`,
choices: 'NO_KEYS',
trial_duration: null,
on_load: function() {
document.getElementById('save-btn').addEventListener('click', function() {
jsPsych.data.get().localSave('csv', 'experiment_data.csv');
});
}
};Let’s understand what each part does:
14.2.3.1 (1) The HTML Structure:
stimulus: `
<div style='text-align: center;'>
<p>Experiment complete!</p>
<p>Click the button below to save your data locally:</p>
<button id='save-btn' style='padding: 10px 20px; font-size: 16px; cursor: pointer;'>
Click here to save the data locally
</button>
</div>
`We create HTML that includes a clickable button. The button has an id='save-btn' so we can reference it later.
14.2.3.2 (2) Disabling Keyboard Input
This should be familiar:
- ‘NO_KEYS’ means participants can’t press keys to continue
- null duration means the trial stays active until we end it programmatically
- This forces participants to use the button instead of keyboard shortcuts
This means that participants cannot click or press anything to make this screen change.
14.2.4 Save Data Example
Let’s see a full example of the local data saving. Take note that we need to add our saveData to the end of our timeline when we run our experiment.
<!DOCTYPE html>
<html>
<head>
<title>Saving Data</title>
<!-- jsPsych -->
<script src="jspsych/jspsych.js"></script>
<link href="jspsych/jspsych.css" rel="stylesheet" type="text/css" />
<!-- jPsych plugins -->
<script src="jspsych/plugin-html-keyboard-response.js"></script>
<!-- custom CSS -->
<link href="jspsych/style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<!-- custom JS -->
<script src="exp.js"></script>
</body>
</html>// 1. Initialize jsPsych
const jsPsych = initJsPsych();
// 2. Define our trials
const welcome = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `<p>Welcome to the Experiment!</p>
<p>Press any key to begin.</p>`,
choices: "ALL_KEYS",
post_trial_gap: 1000
}
const questions = {
timeline: [
{
type: jsPsychHtmlKeyboardResponse,
stimulus: jsPsych.timelineVariable("question"),
choices: ['y', 'n'],
post_trial_gap: 250
}
],
timeline_variables: [
{question: `<p>Do you like chocolate?</p><p>Press Y for yes, N for no.</p>`},
{question: `<p>Do you like vanilla?</p><p>Press Y for yes, N for no.</p>`},
]
}
const saveData = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `
<div style='text-align: center;'>
<p>Experiment complete!</p>
<p>Click the button below to save your data locally:</p>
<button id='save-btn' style='padding: 10px 20px; font-size: 16px; cursor: pointer;'>
Click here to save the data locally
</button>
</div>
`,
choices: 'NO_KEYS', // Disable keyboard responses
trial_duration: null, // Keep trial active indefinitely
on_load: function() {
document.getElementById('save-btn').addEventListener('click', function() {
// Save the data as a CSV file
jsPsych.data.get().localSave('csv', 'experiment_data.csv');
});
}
};
jsPsych.run(
[
welcome,
questions,
saveData
]
);We’ll use this pattern throughout the course for testing and development. Note that localSave() only works for local testing and development. For actual data collection with participants, you’ll need a more robust solution for storing data permanently on a server.
14.3 Labeling Data
While jsPsych automatically saves basic information, notice what’s missing from our first data table above. Looking at the data, we can see that trial 1 showed a question about chocolate and the participant pressed ‘y’, while trial 2 asked about vanilla and they pressed ‘n’. However, we have no idea what these questions were actually asking (we have to read the full HTML stimulus), what each response means, or how to categorize these trials for analysis.
This is where data labeling becomes important. We need to add meaningful information to each trial so that our data tells the complete story of what happened during the experiment.
14.3.1 Adding Context with the data Parameter
The data parameter is always available and allows us to add custom information to any trial. Whenever we add a data parameter, jsPsych creates a new column with that name and adds that label when it’s present. The data parameter takes an object with key-value pairs. Each key-value pair indicates the column label (key) and the value that should be included in that column, on that row (value).
Let’s look at the first example from above and try to improve our data by adding some context:
const question1 = {
type: jsPsychHtmlKeyboardResponse,
stimulus: '<p>Do you like chocolate?</p><p>Press Y for yes, N for no.</p>',
choices: ['y', 'n'],
data: {
question_type: 'preference',
food_item: 'chocolate'
}
};
const question2 = {
type: jsPsychHtmlKeyboardResponse,
stimulus: '<p>Do you like vanilla?</p><p>Press Y for yes, N for no.</p>',
choices: ['y', 'n'],
data: {
question_type: 'preference',
food_item: 'vanilla'
}
};Now our CSV output becomes much more informative:
| trial_type | trial_index | time_elapsed | rt | stimulus | response | question_type | food_item |
|---|---|---|---|---|---|---|---|
| html-keyboard-response | 0 | 1247 | 1247 | <p>Welcome...</p> |
” ” | ||
| html-keyboard-response | 1 | 3891 | 2644 | <p>Do you like chocolate?</p>... |
y | preference | chocolate |
| html-keyboard-response | 2 | 6234 | 2343 | <p>Do you like vanilla?</p>... |
n | preference | vanilla |
Notice how our custom data (question_type and food_item) appears as new columns in the CSV. The welcome trial has empty cells for these columns because we didn’t add this data to that trial. Now, in your analysis software you could filter it by indicating that you only want to see rows where question_type is equal to "preference". That would filter out the instructions and leave you with just the analysis-relevant questions.
I could also use the food_item column to determine which question was asked on each row without having to figure it out from the stimulus HTML; Much better!
14.3.2 Adding Data to Timelines
One of the powerful features of jsPsych’s data system is that you can add data labels to entire timelines. When you add data to a timeline, that information is automatically included in every trial within that timeline. This creates a hierarchical data system where information can be applied at different levels.
Consider this example where we’re organizing our questions into blocks:
const preference_block = {
timeline: [
{
type: jsPsychHtmlKeyboardResponse,
stimulus: '<p>Do you like chocolate?</p><p>Press Y for yes, N for no.</p>',
choices: ['y', 'n']
},
{
type: jsPsychHtmlKeyboardResponse,
stimulus: '<p>Do you like vanilla?</p><p>Press Y for yes, N for no.</p>',
choices: ['y', 'n']
}
],
data: {
block_type: 'preferences',
category: 'food'
}
}In this example, both trials will automatically include block_type: 'preferences' and category: 'food' in their data. This saves you from having to add the same information to each individual trial.
You can combine timeline-level and trial-level data. The trial-level data will be added to the timeline-level data:
const preference_block = {
timeline: [
{
type: jsPsychHtmlKeyboardResponse,
stimulus: '<p>Do you like chocolate?</p><p>Press Y for yes, N for no.</p>',
choices: ['y', 'n'],
data: { food_item: 'chocolate' }
},
{
type: jsPsychHtmlKeyboardResponse,
stimulus: '<p>Do you like vanilla?</p><p>Press Y for yes, N for no.</p>',
choices: ['y', 'n'],
data: { food_item: 'vanilla' }
}
],
data: {
block_type: 'preferences',
category: 'food'
}
}Each trial will now have both the timeline data (block_type and category) and its specific trial data (food_item). This hierarchical approach lets you organize your data labels efficiently, applying broad categories at the timeline level and specific details at the trial level.
14.3.3 Dynamic Data with Timeline Variables
When you’re using timeline variables to create multiple similar trials, you can use jsPsych.timelineVariable() to dynamically pull data labels from your timeline variables. This is extremely powerful for creating well-labeled data without repetition.
Here’s an example using timeline variables for our food preference questions. Take note of how I include relevant data labels in the timeline_variables and reference them in my data object after.
// Create the timeline
const preference_block = {
timeline: [
{
type: jsPsychHtmlKeyboardResponse,
stimulus: jsPsych.timelineVariable('question'),
choices: ['y', 'n'],
data: {
food_item: jsPsych.timelineVariable('food_item'),
food_category: jsPsych.timelineVariable('food_category'),
expected_popularity: jsPsych.timelineVariable('expected_popularity'),
question_type: 'preference'
}
}],
timeline_variables: [
{
question: '<p>Do you like chocolate?</p><p>Press Y for yes, N for no.</p>',
food_item: 'chocolate',
food_category: 'sweet',
expected_popularity: 'high'
},
{
question: '<p>Do you like vanilla?</p><p>Press Y for yes, N for no.</p>',
food_item: 'vanilla',
food_category: 'sweet',
expected_popularity: 'medium'
},
{
question: '<p>Do you like broccoli?</p><p>Press Y for yes, N for no.</p>',
food_item: 'broccoli',
food_category: 'vegetable',
expected_popularity: 'low'
}
],
randomize_order: true,
data: {
block_type: 'food_preferences',
block_number: 1
}
};This approach automatically pulls the appropriate data labels for each trial from the timeline variables. Each trial will have:
- Timeline-level data:
block_type: 'food_preferences'andblock_number: 1 - Static trial data:
question_type: 'preference' - Dynamic trial data:
food_item,food_category, andexpected_popularitythat change based on the timeline variable for that specific trial
The resulting data will look like this:
| trial_type | trial_index | stimulus | response | block_type | block_number | question_type | food_item | food_category | expected_popularity |
|---|---|---|---|---|---|---|---|---|---|
| html-keyboard-response | 0 | <p>Do you like chocolate?</p>... |
y | food_preferences | 1 | preference | chocolate | sweet | high |
| html-keyboard-response | 1 | <p>Do you like broccoli?</p>... |
n | food_preferences | 1 | preference | broccoli | vegetable | low |
| html-keyboard-response | 2 | <p>Do you like vanilla?</p>... |
y | food_preferences | 1 | preference | vanilla | sweet | medium |
14.3.4 Saving Trial Parameters
14.3.5 Adding Global Properties to All Trials
One of the most common uses of the data module is adding information that applies to every trial in your experiment. The jsPsych.data.addProperties() function lets you add properties to all trials, both past and future ones.
This function takes an object as input, with each key-value pair indicating a new column and value to add to every row.
// Add these properties to every trial in the experiment
jsPsych.data.addProperties({
subject_id: "001",
condition: "control",
experiment_version: "2.1",
session_date: new Date().toISOString().split('T')[0] // YYYY-MM-DD format
});This is useful because it’s (1) retroactive: Even trials that already happened get these properties and (2) it’s persistent: All future trials automatically include these properties
Here are some common properties that you might want to add to all trials:
jsPsych.data.addProperties({
// Participant identification
subject_id: jsPsych.randomization.randomID(8),
// Experimental design
condition: "experimental", // or randomly assigned
experiment_version: "1.0",
researcher: "Dr. Smith",
// Session information
session_date: new Date().toISOString().split('T')[0], // YYYY-MM-DD format
start_time: new Date().toISOString().split('T')[1],
browser: navigator.userAgent,
screen_width: screen.width,
screen_height: screen.height
});14.3.6 Data Labels: Full Example
Now let’s return to our memory experiment and apply the concepts we’ve just learned to apply proper data labels.
For this example, I’ve added (1) global data properties that get added to every row, (2) trial- and timeline-level data labels, and (3) added the save data trial to the end of the experiment.
You can try running this in the example below and you can save the resulting data to your computer.
<!DOCTYPE html>
<html>
<head>
<title>Saving Data</title>
<!-- jsPsych -->
<script src="jspsych/jspsych.js"></script>
<link href="jspsych/jspsych.css" rel="stylesheet" type="text/css" />
<!-- jPsych plugins -->
<script src="jspsych/plugin-html-keyboard-response.js"></script>
<script src="jspsych/plugin-instructions.js"></script>
<!-- custom CSS -->
<link href="jspsych/style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<!-- custom JS -->
<script src="exp.js"></script>
</body>
</html>// 1. Initialize jsPsych
const jsPsych = initJsPsych();
jsPsych.data.addProperties({
// Participant identification
subject_id: jsPsych.randomization.randomID(8),
// Experimental design
condition: "experimental", // or randomly assigned
experiment_version: "1.0",
researcher: "Dr. Smith",
// Session information
session_date: new Date().toISOString().split('T')[0], // YYYY-MM-DD format
start_time: new Date().toISOString().split('T')[1],
browser: navigator.userAgent,
screen_width: screen.width,
screen_height: screen.height
});
// 2. Define our trials
const welcome = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `Welcome to the Experiment! Press any key to begin.`,
choices: "ALL_KEYS",
post_trial_gap: 500,
data: {
phase: "welcome",
trial_type: "instruction"
}
}
const instructions = {
type: jsPsychInstructions,
pages: [
// Page 1: Welcome and Overview
`<div class='instructionStyle'>
<p>You will participate in a memory experiment with two phases:</p>
<p><strong>Phase 1:</strong> Study a list of words</p>
<p><strong>Phase 2:</strong> Decide if words are OLD or NEW</p>
<p>Click Next to continue.</p>
</div>`,
// Page 2: Study Phase Instructions
`<div class='instructionStyle'>
<p>During the study phase, you will see words appear one at a time. Read each word carefully and try to remember it.</p>
<p>Each word appears for 1 second. You will study about 15 words total.</p>
<p>You do not need to press any keys during this phase.</p>
</div>`,
// Page 3: Test Phase Instructions
`<div class='instructionStyle'>
<p>During the test phase, you will see words one at a time. Some words are OLD (from the study list). Some words are NEW (not from the study list).</p>
<p>Press the 'OLD' button if the word was old.</p>
<p>Press the 'NEW' button if the word was new</p>
</div>`,
// Page 4: Stay focused
`<div class='instructionStyle'>
<p>Trust your first instinct.</p>
<p>If unsure, make your best guess.</p>
</div>`,
// Page 5: Final Instructions
`<div class='instructionStyle'>
<p>Try to stay focused throughout the experiment. The experiment only takes a few minutes.</p>
<p>Click Next to start the study phase.</p>
</div>`
],
key_forward: 'ArrowRight',
key_backward: 'ArrowLeft',
allow_backward: true,
show_clickable_nav: true,
button_label_previous: 'Back',
button_label_next: 'Next',
data: {
phase: "instructions",
trial_type: "instruction"
}
};
let study = {
timeline: [
{
type: jsPsychHtmlKeyboardResponse,
stimulus: "+",
choices: "NO_KEYS",
post_trial_gap: 250,
trial_duration: 500,
css_classes: "wordStyle",
data: {
phase: "study",
trial_type: "fixation"
}
},
{
type: jsPsychHtmlKeyboardResponse,
stimulus: jsPsych.timelineVariable("word"),
choices: "NO_KEYS",
post_trial_gap: 500,
trial_duration: 1000,
css_classes: "wordStyle",
data: {
phase: "study",
trial_type: "word_presentation",
word: jsPsych.timelineVariable("word"),
word_type: "study_item"
}
}
],
timeline_variables: [
{word: "BED"},
{word: "REST"},
{word: "AWAKE"},
{word: "TIRED"},
{word: "DREAM"},
{word: "WAKE"},
{word: "SNOOZE"},
{word: "BLANKET"},
{word: "DOZE"},
{word: "SLUMBER"},
{word: "SNORE"},
{word: "NAP"},
{word: "PEACE"},
{word: "YAWN"},
{word: "DROWSY"}
],
randomize_order: true
}
let test = {
timeline: [
{
type: jsPsychHtmlButtonResponse,
stimulus: jsPsych.timelineVariable("word"),
post_trial_gap: 500,
choices: ["OLD", "NEW"],
css_classes: "wordStyle",
data: {
phase: "test",
trial_type: "recognition_test",
word: jsPsych.timelineVariable("word"),
word_type: jsPsych.timelineVariable("word_type"),
correct_response: jsPsych.timelineVariable("correct_response")
}
}
],
timeline_variables: [
// OLD WORDS (should respond "OLD")
{word: "BED", word_type: "old", correct_response: "OLD"},
{word: "REST", word_type: "old", correct_response: "OLD"},
{word: "AWAKE", word_type: "old", correct_response: "OLD"},
{word: "TIRED", word_type: "old", correct_response: "OLD"},
{word: "DREAM", word_type: "old", correct_response: "OLD"},
{word: "WAKE", word_type: "old", correct_response: "OLD"},
{word: "SNOOZE", word_type: "old", correct_response: "OLD"},
{word: "BLANKET", word_type: "old", correct_response: "OLD"},
{word: "DOZE", word_type: "old", correct_response: "OLD"},
{word: "SLUMBER", word_type: "old", correct_response: "OLD"},
{word: "SNORE", word_type: "old", correct_response: "OLD"},
{word: "NAP", word_type: "old", correct_response: "OLD"},
{word: "PEACE", word_type: "old", correct_response: "OLD"},
{word: "YAWN", word_type: "old", correct_response: "OLD"},
{word: "DROWSY", word_type: "old", correct_response: "OLD"},
// NEW WORDS (should respond "NEW")
{word: "DOCTOR", word_type: "new", correct_response: "NEW"},
{word: "NURSE", word_type: "new", correct_response: "NEW"},
{word: "SICK", word_type: "new", correct_response: "NEW"},
{word: "LAWYER", word_type: "new", correct_response: "NEW"},
{word: "MEDICINE", word_type: "new", correct_response: "NEW"},
{word: "HEALTH", word_type: "new", correct_response: "NEW"},
{word: "HOSPITAL", word_type: "new", correct_response: "NEW"},
{word: "DENTIST", word_type: "new", correct_response: "NEW"},
{word: "PHYSICIAN", word_type: "new", correct_response: "NEW"},
{word: "ILL", word_type: "new", correct_response: "NEW"},
{word: "PATIENT", word_type: "new", correct_response: "NEW"},
{word: "OFFICE", word_type: "new", correct_response: "NEW"},
{word: "STETHOSCOPE", word_type: "new", correct_response: "NEW"},
{word: "SURGEON", word_type: "new", correct_response: "NEW"},
{word: "CLINIC", word_type: "new", correct_response: "NEW"},
{word: "CURE", word_type: "new", correct_response: "NEW"},
// CRITICAL LURE (never studied, but related to study words)
{word: "SLEEP", word_type: "critical_lure", correct_response: "NEW"}
],
randomize_order: true
}
const saveData = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `
<div style='text-align: center;'>
<p>Experiment complete!</p>
<p>Click the button below to save your data locally:</p>
<button id='save-btn' style='padding: 10px 20px; font-size: 16px; cursor: pointer;'>
Click here to save the data locally
</button>
</div>
`,
choices: 'NO_KEYS', // Disable keyboard responses
trial_duration: null, // Keep trial active indefinitely
on_load: function() {
document.getElementById('save-btn').addEventListener('click', function() {
// Save the data as a CSV file
jsPsych.data.get().localSave('csv', 'recognition_data.csv');
});
}
};
// 3. Run jsPsych with our trials
jsPsych.run([
welcome,
instructions,
study,
test,
saveData
]);Now, let’s look at the data that would result from this new labeling:
| phase | trial_type | rt | stimulus | response | trial_index | plugin_version | time_elapsed | subject_id | condition | experiment_version | researcher | session_date | start_time | browser | screen_width | screen_height | view_history | word | word_type | correct_response |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| welcome | html-keyboard-response | 2257 | Welcome to the Experiment! Press any key to begin. | f | 0 | 2.1.0 | 2258 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | ||||
| instructions | instructions | 3088 | 1 | 2.1.0 | 5851 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | [{“page_index”: 0,“viewing_time”: 1297} … {“page_index”: 4,“viewing_time”:384 }] | |||||
| study | html-keyboard-response | null | + | null | 2 | 2.1.0 | 6356 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | ||||
| study | html-keyboard-response | null | WAKE | null | 3 | 2.1.0 | 7621 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | WAKE | study_item | ||
| study | html-keyboard-response | null | + | null | 4 | 2.1.0 | 8692 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | ||||
| study | html-keyboard-response | null | DROWSY | null | 5 | 2.1.0 | 9963 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | DROWSY | study_item | ||
| study | html-keyboard-response | null | + | null | 6 | 2.1.0 | 11012 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | ||||
| study | html-keyboard-response | null | SLUMBER | null | 7 | 2.1.0 | 12269 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | SLUMBER | study_item | ||
| study | html-keyboard-response | null | + | null | 8 | 2.1.0 | 13286 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | ||||
| study | html-keyboard-response | null | DREAM | null | 9 | 2.1.0 | 14651 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | DREAM | study_item | ||
| study | html-keyboard-response | null | + | null | 10 | 2.1.0 | 15691 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | ||||
| study | html-keyboard-response | null | BLANKET | null | 11 | 2.1.0 | 16977 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | BLANKET | study_item | ||
| study | html-keyboard-response | null | + | null | 12 | 2.1.0 | 18010 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | ||||
| study | html-keyboard-response | null | TIRED | null | 13 | 2.1.0 | 19285 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | TIRED | study_item | ||
| study | html-keyboard-response | null | + | null | 14 | 2.1.0 | 110847 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | ||||
| study | html-keyboard-response | null | YAWN | null | 15 | 2.1.0 | 112112 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | YAWN | study_item | ||
| study | html-keyboard-response | null | + | null | 16 | 2.1.0 | 113136 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | ||||
| study | html-keyboard-response | null | BED | null | 17 | 2.1.0 | 114391 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | BED | study_item | ||
| study | html-keyboard-response | null | + | null | 18 | 2.1.0 | 115456 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | ||||
| study | html-keyboard-response | null | DOZE | null | 19 | 2.1.0 | 116821 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | DOZE | study_item | ||
| study | html-keyboard-response | null | + | null | 20 | 2.1.0 | 117823 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | ||||
| study | html-keyboard-response | null | AWAKE | null | 21 | 2.1.0 | 119200 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | AWAKE | study_item | ||
| study | html-keyboard-response | null | + | null | 22 | 2.1.0 | 120220 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | ||||
| study | html-keyboard-response | null | NAP | null | 23 | 2.1.0 | 121481 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | NAP | study_item | ||
| study | html-keyboard-response | null | + | null | 24 | 2.1.0 | 122498 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | ||||
| study | html-keyboard-response | null | SNORE | null | 25 | 2.1.0 | 123824 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | SNORE | study_item | ||
| study | html-keyboard-response | null | + | null | 26 | 2.1.0 | 124842 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | ||||
| study | html-keyboard-response | null | SNOOZE | null | 27 | 2.1.0 | 126112 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | SNOOZE | study_item | ||
| study | html-keyboard-response | null | + | null | 28 | 2.1.0 | 127132 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | ||||
| study | html-keyboard-response | null | PEACE | null | 29 | 2.1.0 | 128385 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | PEACE | study_item | ||
| study | html-keyboard-response | null | + | null | 30 | 2.1.0 | 129418 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | ||||
| study | html-keyboard-response | null | REST | null | 31 | 2.1.0 | 130701 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | REST | study_item | ||
| test | html-button-response | 3542 | HEALTH | 0 | 32 | 2.1.0 | 134745 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | HEALTH | new | NEW | |
| test | html-button-response | 611 | BED | 1 | 33 | 2.1.0 | 135857 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | BED | old | OLD | |
| test | html-button-response | 289 | TIRED | 0 | 34 | 2.1.0 | 136658 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | TIRED | old | OLD | |
| test | html-button-response | 279 | REST | 0 | 35 | 2.1.0 | 137459 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | REST | old | OLD | |
| test | html-button-response | 763 | HOSPITAL | 1 | 36 | 2.1.0 | 138731 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | HOSPITAL | new | NEW | |
| test | html-button-response | 366 | DOZE | 0 | 37 | 2.1.0 | 139620 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | DOZE | old | OLD | |
| test | html-button-response | 455 | SURGEON | 0 | 38 | 2.1.0 | 140587 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | SURGEON | new | NEW | |
| test | html-button-response | 326 | SNORE | 0 | 39 | 2.1.0 | 141428 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | SNORE | old | OLD | |
| test | html-button-response | 386 | CURE | 1 | 40 | 2.1.0 | 142322 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | CURE | new | NEW | |
| test | html-button-response | 361 | AWAKE | 0 | 41 | 2.1.0 | 143198 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | AWAKE | old | OLD | |
| test | html-button-response | 431 | DROWSY | 0 | 42 | 2.1.0 | 144140 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | DROWSY | old | OLD | |
| test | html-button-response | 646 | SICK | 0 | 43 | 2.1.0 | 145288 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | SICK | new | NEW | |
| test | html-button-response | 531 | OFFICE | 0 | 44 | 2.1.0 | 146344 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | OFFICE | new | NEW | |
| test | html-button-response | 385 | PATIENT | 0 | 45 | 2.1.0 | 147269 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | PATIENT | new | NEW | |
| test | html-button-response | 363 | DOCTOR | 0 | 46 | 2.1.0 | 148150 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | DOCTOR | new | NEW | |
| test | html-button-response | 363 | LAWYER | 0 | 47 | 2.1.0 | 149049 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | LAWYER | new | NEW | |
| test | html-button-response | 505 | ILL | 1 | 48 | 2.1.0 | 150077 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | ILL | new | NEW | |
| test | html-button-response | 369 | BLANKET | 1 | 49 | 2.1.0 | 150959 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | BLANKET | old | OLD | |
| test | html-button-response | 316 | YAWN | 1 | 50 | 2.1.0 | 151792 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | YAWN | old | OLD | |
| test | html-button-response | 563 | PEACE | 0 | 51 | 2.1.0 | 152871 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | PEACE | old | OLD | |
| test | html-button-response | 434 | CLINIC | 0 | 52 | 2.1.0 | 153813 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | CLINIC | new | NEW | |
| test | html-button-response | 387 | SLEEP | 0 | 53 | 2.1.0 | 154751 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | SLEEP | critical_lure | NEW | |
| test | html-button-response | 469 | SLUMBER | 0 | 54 | 2.1.0 | 155731 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | SLUMBER | old | OLD | |
| test | html-button-response | 470 | NURSE | 0 | 55 | 2.1.0 | 156717 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | NURSE | new | NEW | |
| test | html-button-response | 368 | SNOOZE | 0 | 56 | 2.1.0 | 157603 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | SNOOZE | old | OLD | |
| test | html-button-response | 351 | DREAM | 1 | 57 | 2.1.0 | 158471 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | DREAM | old | OLD | |
| test | html-button-response | 400 | NAP | 1 | 58 | 2.1.0 | 159391 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | NAP | old | OLD | |
| test | html-button-response | 767 | PHYSICIAN | 0 | 59 | 2.1.0 | 160663 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | PHYSICIAN | new | NEW | |
| test | html-button-response | 526 | WAKE | 0 | 60 | 2.1.0 | 161700 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | WAKE | old | OLD | |
| test | html-button-response | 615 | MEDICINE | 0 | 61 | 2.1.0 | 162844 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | MEDICINE | new | NEW | |
| test | html-button-response | 697 | STETHOSCOPE | 0 | 62 | 2.1.0 | 164044 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | STETHOSCOPE | new | NEW | |
| test | html-button-response | 1572 | DENTIST | 0 | 63 | 2.1.0 | 166119 | m8kx3pqw | experimental | 1.0 | Dr. Smith | 2025-09-14 | 13:56:07.382Z | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 | 1760 | 1100 | DENTIST | new | NEW |
What we’ve improved:
Consistent phase labeling: Every trial now has a phase field (welcome, instructions, study, test)
Trial type classification: Each trial has a trial_type field describing what kind of trial it is (fixation, word_presentation, recognition_test, etc.)
Stimulus tracking: Test trials include the actual word being presented and its word_type (old, new, critical_lure)
Response accuracy: Each test trial includes the correct_response, making it easy to calculate accuracy later
Critical lure identification: The word "SLEEP" is specifically labeled as a critical_lure. This is the key manipulation in DRM experiments, as it’s semantically related to the study words but was never actually presented.
With these labels, analyzing your data becomes straightforward. You can easily filter trials by phase, calculate accuracy rates for different word types, and examine false alarm rates for critical lures.
14.4 Basic Data Updates with on_finish
Often you’ll want to add computed information to your data after a trial completes (e.g., determine whether the response was accurate). The on_finish callback function is perfect for this. It receives the trial’s data as a parameter and allows you to modify or add to it.
When the on_finish function runs, it receives a data parameter that contains all the information collected during that specific trial. This includes:
Automatic jsPsych properties:
trial_type: The plugin that was used (e.g., “html-keyboard-response”)trial_index: The position of this trial in the experiment (starts at 0)time_elapsed: Total time since the experiment started (in milliseconds)rt: Response time for this trial (in milliseconds)response: The participant’s response (key pressed, button clicked, etc.)
Properties from your trial definition:
- Any properties you added in the data object of your trial
- Timeline variables that were active for this trial
Properties from addProperties():
- Any global properties you added (like
subject_id,condition, etc.)
First, here’s an example to show the proper syntax of adding the on_finish parameter:
const recognition_trial = {
timeline: [
{
type: jsPsychHtmlKeyboardResponse,
stimulus: jsPsych.timelineVariable("word"),
choices: ['f', 'j'],
data: {
task: "recognition_test",
word: jsPsych.timelineVariable("word"),
word_type: jsPsych.timelineVariable("word_type"),
correct_response: jsPsych.timelineVariable("correct_response")
},
on_finish: function(data) {
// on_finish code goes inside here
}
}
],
timeline_variables: [
{ word: "DOCTOR", word_type: "old", correct_response: "f" },
{ word: "PLANET", word_type: "new", correct_response: "j" },
{ word: "GARDEN", word_type: "old", correct_response: "f" },
{ word: "WINDOW", word_type: "new", correct_response: "j" },
{ word: "BRIDGE", word_type: "old", correct_response: "f" },
{ word: "FOREST", word_type: "new", correct_response: "j" }
]
};Notice that we add it just like the other parameters on_finish:. However, it’s value is a function: function(data){ }. We add the data in the parentheses because this is how we add inputs to our functions and we want the data from that trial to be available inside the function.
Using that function, we can access any of trial properties by using standard object notation like data.rt or data["rt"]. Here’s an example showing what’s available by accessing them inside the function and printing them in the JavaScript console:
const recognition_trial = {
timeline: [
{
type: jsPsychHtmlKeyboardResponse,
stimulus: jsPsych.timelineVariable("word"),
choices: ['f', 'j'],
data: {
task: "recognition_test",
word: jsPsych.timelineVariable("word"),
word_type: jsPsych.timelineVariable("word_type"),
correct_response: jsPsych.timelineVariable("correct_response")
},
on_finish: function(data) {
// At this point, 'data' contains:
console.log("Trial type:", data.trial_type); // "html-keyboard-response"
console.log("Trial index:", data.trial_index); // e.g., 15
console.log("Time elapsed:", data.time_elapsed); // e.g., 45230
console.log("Response time:", data.rt); // e.g., 1205
console.log("Key pressed:", data.response); // "f" or "j"
console.log("Task:", data.task); // "recognition_test"
console.log("Word shown:", data.word); // e.g., "DOCTOR"
console.log("Word type:", data.word_type); // e.g., "new"
console.log("Correct response:", data.correct_response); // e.g., "j"
console.log("Subject ID:", data.subject_id); // from addProperties()
// At this point, we're just examining the data, not changing it yet
// This code would just print the data values in the console
}
}
],
timeline_variables: [
{ word: "DOCTOR", word_type: "old", correct_response: "f" },
{ word: "PLANET", word_type: "new", correct_response: "j" },
{ word: "GARDEN", word_type: "old", correct_response: "f" },
{ word: "WINDOW", word_type: "new", correct_response: "j" },
{ word: "BRIDGE", word_type: "old", correct_response: "f" },
{ word: "FOREST", word_type: "new", correct_response: "j" }
]
};However, we typically don’t just want to access the data, we want to do something with in. The on_finish function is your opportunity to add new properties or modify existing ones based on what happened during the trial. This is commonly used to assess accuracy and categorize performance.
14.4.1 Checking Accuracy
Let’s look at an example of how we can determine whether the participant provided the correct response at the end of the trial. This is probably the most common use-case for modifying the data at the end of the trial:
const recognition_trial = {
timeline: [
{
type: jsPsychHtmlKeyboardResponse,
stimulus: jsPsych.timelineVariable("word"),
choices: ['f', 'j'],
data: {
task: "recognition_test",
word: jsPsych.timelineVariable("word"),
word_type: jsPsych.timelineVariable("word_type"),
correct_response: jsPsych.timelineVariable("correct_response")
},
on_finish: function(data) {
// Use compareKeys() to properly compare key responses
if (jsPsych.pluginAPI.compareKeys(data.response, data.correct_response)) {
data.correct = true;
} else {
data.correct = false;
}
// Or more concisely:
// data.correct = jsPsych.pluginAPI.compareKeys(data.response, data.correct_response);
}
}
],
timeline_variables: [
{ word: "DOCTOR", word_type: "old", correct_response: "f" },
{ word: "PLANET", word_type: "new", correct_response: "j" },
{ word: "GARDEN", word_type: "old", correct_response: "f" },
{ word: "WINDOW", word_type: "new", correct_response: "j" },
{ word: "BRIDGE", word_type: "old", correct_response: "f" },
{ word: "FOREST", word_type: "new", correct_response: "j" }
]
};In this code, I check whether data.response is equal to data.correct_response. If it is true, then I update data.correct to be true. If data.response is NOT equal to data.correct_response, then I set it to false. This will update my data file that is saved at the end with a new column called correct with true/false values.
Here is an example of the data output. Take note of the new columns that were added, including task, word, etc.
| rt | stimulus | response | trial_type | trial_index | plugin_version | time_elapsed | subject_id | experiment_version | task | word | word_type | correct_response | correct |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2121 | ” | f | html-keyboard-response | 0 | 2.1.0 | 2121 | q4jr9zbl | 1 | |||||
| 879 | DOCTOR | f | html-keyboard-response | 1 | 2.1.0 | 3002 | q4jr9zbl | 1 | recognition_test | DOCTOR | old | f | TRUE |
| 3264 | PLANET | j | html-keyboard-response | 2 | 2.1.0 | 6266 | q4jr9zbl | 1 | recognition_test | PLANET | new | j | TRUE |
| 767 | GARDEN | f | html-keyboard-response | 3 | 2.1.0 | 7033 | q4jr9zbl | 1 | recognition_test | GARDEN | old | f | TRUE |
| 808 | WINDOW | j | html-keyboard-response | 4 | 2.1.0 | 7842 | q4jr9zbl | 1 | recognition_test | WINDOW | new | j | TRUE |
Different plugins represent responses in different ways, so you’ll need to adjust your accuracy checking accordingly. When using the keyboard response plugin, we need to compare the key the participant pressed against the correct key. It’s important to use jsPsych’s built-in compareKeys() function rather than simple equality checks. This function handles case sensitivity and different key representations properly. For example, if the participant had caps lock on and the correct response was set to "e", then "e" === "E" would be false. We want that comparison to be true regardless of case, so we use the built-in function to handle the comparison correctly.
Here’s an example using the button response plugin, which represents button responses numerically (0 for the first button, 1 for the second, etc.):
const recognition_trial = {
timeline: [
{
type: jsPsychHtmlButtonResponse,
stimulus: jsPsych.timelineVariable("word"),
choices: ['OLD', 'NEW'],
data: {
task: "recognition_test",
word: jsPsych.timelineVariable("word"),
word_type: jsPsych.timelineVariable("word_type"),
correct_response: jsPsych.timelineVariable("correct_response") // This should be 0 or 1 (0 == 'OLD' and 1 == 'NEW')
},
on_finish: function(data) {
// For button responses, we compare numbers directly
if (data.response === data.correct_response) {
data.correct = true;
} else {
data.correct = false;
}
// Or more concisely:
// data.correct = data.response === data.correct_response;
}
}
],
timeline_variables: [
{ word: "DOCTOR", word_type: "old", correct_response: "f" },
{ word: "PLANET", word_type: "new", correct_response: "j" },
{ word: "GARDEN", word_type: "old", correct_response: "f" },
{ word: "WINDOW", word_type: "new", correct_response: "j" },
{ word: "BRIDGE", word_type: "old", correct_response: "f" },
{ word: "FOREST", word_type: "new", correct_response: "j" }
]
};Important Note: Always check the “Data Generated” section of each plugin’s documentation to understand how responses are represented. For keyboard plugins, responses are typically strings (the key pressed), while button plugins use numeric indices (0, 1, 2, etc.).
14.4.2 Categorizing Response Times:
To provide another example of modifying the data at the end of the trial, let’s look at how you can add categories and computed measures based on performance. This could be useful for adding feedback after the trial, something we’ll return to in the following chapters. It’s important to understand that on_finish is an arbitrary function, which means we can add whatever code we want here and it will be executed after the trials.
In this example, I will (1) check accuracy, (2) check the response time and categorize it based on speed, and (3) use BOTH response time and accuracy to categorize their performance.
const stroop_trial = {
timeline: [
{
type: jsPsychHtmlKeyboardResponse,
stimulus: jsPsych.timelineVariable("stimulus"),
choices: ['r', 'g', 'b'],
data: {
task: "stroop",
condition: jsPsych.timelineVariable("condition"),
correct_key: jsPsych.timelineVariable("correct_key")
},
on_finish: function(data) {
// Determine accuracy using compareKeys
data.correct = jsPsych.pluginAPI.compareKeys(data.response, data.correct_key);
// Categorize response time
if (data.rt < 300) {
data.rt_category = "fast";
} else if (data.rt < 800) {
data.rt_category = "normal";
} else {
data.rt_category = "slow";
}
// Add performance score combining accuracy and speed
if (data.correct && data.rt < 600) {
data.performance = "excellent";
} else if (data.correct) {
data.performance = "good";
} else {
data.performance = "needs_improvement";
}
}
}
],
timeline_variables: [
{stimulus: `<span style='color: red;'>RED</span>`, condition: "congruent", correct_key: "r"},
{stimulus: `<span style='color: blue;'>RED</span>`, condition: "incongruent", correct_key: "b"},
{stimulus: `<span style='color: green;'>GREEN</span>>`, condition: "incongruent", correct_key: "g"},
{stimulus: `<span style='color: red;'>BLUE</span>`, condition: "incongruent", correct_key: "r"},
{stimulus: `<span style='color: blue;'>BLUE</span>`, condition: "congruent", correct_key: "b"},
{stimulus: `<span style='color: green;'>RED</span>`, condition: "incongruent", correct_key: "g"},
]
};Again, we are updating the data that is being saved. So, our data output will now include new columns for our categories and values if one is provided:
| rt | stimulus | response | trial_type | trial_index | plugin_version | time_elapsed | subject_id | experiment_version | task | condition | correct_key | correct | rt_category | performance |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 542 | RED | r | html-keyboard-response | 0 | 2.1.0 | 542 | m8kx3pqw | 1 | stroop | congruent | r | TRUE | normal | excellent |
| 1205 | RED | b | html-keyboard-response | 1 | 2.1.0 | 1747 | m8kx3pqw | 1 | stroop | incongruent | r | FALSE | slow | needs_improvement |
| 287 | GREEN | g | html-keyboard-response | 2 | 2.1.0 | 2034 | m8kx3pqw | 1 | stroop | congruent | g | TRUE | fast | excellent |
| 934 | BLUE | r | html-keyboard-response | 3 | 2.1.0 | 2968 | m8kx3pqw | 1 | stroop | incongruent | b | FALSE | slow | needs_improvement |
| 678 | BLUE | b | html-keyboard-response | 4 | 2.1.0 | 3646 | m8kx3pqw | 1 | stroop | congruent | b | TRUE | normal | good |
| 445 | RED | g | html-keyboard-response | 5 | 2.1.0 | 4091 | m8kx3pqw | 1 | stroop | incongruent | r | FALSE | normal | needs_improvement |
Some final points about on_finish:
- Timing:
on_finishruns immediately after the participant responds, before the next trial begins - Persistence: Any properties you add or modify in
on_finishbecome part of the permanent data record for that trial - Flexibility: You can add as many new properties as you want, and they’ll all appear in your final dataset
The on_finish function is your opportunity to enrich your data with computed information that will be valuable during analysis. By adding accuracy scores, performance categories, and other derived measures, you make your data much more useful for statistical analysis.
14.5 Final Thoughts on Data Labeling
Data labeling is one of the most important skills in experimental programming because it determines whether your results will be analyzable and meaningful. Throughout this chapter, you’ve learned multiple techniques for adding context to your data including simple trial labels and global properties that track participants and experimental conditions. The key insight is to think about your data analysis needs before you start collecting data. What variables will you need to group trials? What information will help you identify different experimental conditions? By planning your data labels carefully and testing them with local saves, you’ll avoid the frustration of collecting unusable data and ensure your experiments produce clean, analysis-ready datasets from the start.