Open AnafNeves opened 3 weeks ago
Take a look at the documentation. https://www.jspsych.org/latest/overview/timeline/#sampling-methods
Thanks @Shaobin-Jiang , but this solution unfortunately does not seem to be fit to our purpose.
Basically, the core of the issue is that the timeline variable can only be created after a couple of routines (after demographic questions, as it depends on their answers).
Setting a custom sample function that would fail as it seems to be called at initializion, before we create new_stimul_list
.
Tagging also @Max-Lovell for help
PS: what we tried with the sample feature was the following:
var fiction_phase1a = {
timeline_variables: stimuli_original,
timeline: [
fixationcross,
showimage,
ratings,
],
sample: {
type: 'custom',
fn: function (t) {
// returns a vector of integers corresponding of the position of each element of stimuli_new of stimuli_original
var idx = stimuli_new.map(subsetItem => {
return stimuli_original.findIndex(fullItem => {
return fullItem.stimulus === subsetItem.stimulus
})
})
return idx
}
}
}
But it didn't work, saying that stimuli_new
doesn't existed (it doesn't exist on experiment initialization, it gets created later)
I do not think so, as I added a trial object before the trial with a sample function and the sample function does not get called upon initialization.
let jsPsych = initJsPsych();
let test = {
type: jsPsychHtmlKeyboardResponse,
stimulus: 'hello world',
};
let fiction_phase1a = {
timeline_variables: [{stimulus: 123}],
timeline: [
{
type: jsPsychHtmlKeyboardResponse,
stimulus: jsPsych.timelineVariable('stimulus'),
},
],
sample: {
type: 'custom',
fn: function (t) {
// returns a vector of integers corresponding of the position of each element of stimuli_new of stimuli_original
var idx = stimuli_new.map((subsetItem) => {
return stimuli_original.findIndex((fullItem) => {
return fullItem.stimulus === subsetItem.stimulus;
});
});
return idx;
},
},
};
jsPsych.run([test, fiction_phase1a]);
If you're using survey.js, are you aware: https://www.jspsych.org/v8/overview/building-surveys/#:~:text=content%20and%20style-,survey%20plugin,Not%20well%2Dsuited%20for%20use%20with%20timeline%20variables,-Large%20set%20of
states:
- survey plugin
- Not well-suited for use with timeline variables
Which might be part of the problem? You could use another survey provider, or even write your own in HTML.
Either way, can you write a minimal reproduciable example - something I can copy paste and run the whole thing myself which highlights the exact issue?
@Shaobin-Jiang Here is a reproducible example:
<!DOCTYPE html>
<html>
<head>
<!-- Title shown in tab -->
<title>Experiment</title>
<!-- Load JsPsych -->
<script src="https://unpkg.com/jspsych@7.3.1"></script>
<link href="https://unpkg.com/jspsych@7.3.1/css/jspsych.css" rel="stylesheet" type="text/css" />
<script src="https://unpkg.com/@jspsych/plugin-html-keyboard-response@1.1.3"></script>
</head>
<body></body>
<script>
let jsPsych = initJsPsych({
on_finish: function () {
jsPsych.data.displayData("json") // Display data in browser
}
})
var timeline = []
var stimuli_all = [{ stimulus: "ONE" }, { stimulus: "TWO" }, { stimulus: "THREE" }]
var instructions = {
type: jsPsychHtmlKeyboardResponse,
stimulus: "BLABLA INSTRUCTIONS",
// CREATE NEW STIMULUS LIST AFTER THIS STAGE
on_finish: function () {
stimuli_new = [{ stimulus: "TWO" }, { stimulus: "ONE" }]
}
}
timeline.push(instructions)
// TRIALS
let trial = {
type: jsPsychHtmlKeyboardResponse,
stimulus: jsPsych.timelineVariable('stimulus'),
}
let timelime_trials = {
timeline_variables: stimuli_all,
timeline: [trial],
sample: {
type: 'custom',
fn: function (t) {
// returns a vector of integers corresponding of the position of each element of stimuli_new of stimuli_original
var idx = stimuli_new.map((subsetItem) => {
return stimuli_all.findIndex((fullItem) => {
return fullItem.stimulus === subsetItem.stimulus
})
})
return idx
},
},
}
timeline.push(timelime_trials)
jsPsych.run(timeline)
</script>
</html>
Essentially, stimuli_new
gets defined during the experiment (and does not exist at initialization), and later we use it in the sample
function to get the corresponding indices from the stimuli_all
list. However, the experiment does not load and throws that stimuli_new
is not defined, as if it called the sample
function on initialization
@Max-Lovell here the issue is not with the survey plugin but more general I think :)
Ah okay, well this is a little hacky but works. relies on the fact that:
https://www.jspsych.org/v7/overview/timeline/#conditional-timelines
The conditional function is evaluated whenever jsPsych is about to run the first trial on the timeline.
perhaps something you could do with looping timlines too but I couldn't figure it out. Can you work with this?
One thing to note is that the for loop MUST use let i=0
and not var i=0
. If you use var
, i will retain it's final value regardless of when stimuli_all[i] is evaluated, but with let it remains at the right value within the loop iteration.
<!DOCTYPE html>
<html>
<head>
<!-- Title shown in tab -->
<title>Experiment</title>
<!-- Load JsPsych -->
<script src="https://unpkg.com/jspsych@7.3.1"></script>
<link href="https://unpkg.com/jspsych@7.3.1/css/jspsych.css" rel="stylesheet" type="text/css" />
<script src="https://unpkg.com/@jspsych/plugin-html-keyboard-response@1.1.3"></script>
</head>
<body></body>
<script>
var jsPsych = initJsPsych({
on_finish: function () {
jsPsych.data.displayData("json") // Display data in browser
}
})
var timeline = []
var stimuli_all = ["ONE", "TWO", "THREE"]
var stimuli_new;
var instructions = {
type: jsPsychHtmlKeyboardResponse,
stimulus: "BLABLA INSTRUCTIONS",
// CREATE NEW STIMULUS LIST AFTER THIS STAGE
on_finish: function () {
stimuli_new = ["ONE", "TWO"]
}
}
timeline.push(instructions)
// TRIALS
for(let i=0; i<stimuli_all.length; i++){ // KEEP THIS AS LET AND NOT VAR
var trial = {
type: jsPsychHtmlKeyboardResponse,
stimulus: stimuli_all[i]
}
var newNode = {
timeline: [trial],
conditional_function: function(){ // Evaluated only at the start of trials
if(stimuli_new.includes(stimuli_all[i])){ // check stimuli new includes the current stimulus
return true; // continue
} else {
return false;
}
}
}
timeline.push(newNode)
}
jsPsych.run(timeline)
</script>
</html>
Cheers, we'll try that out! Also tagging @jodeleeuw in case there's a better solution
Having seen the original code, a minimum reproduciable example and a better solution is below - instead of creating a separate timeline for every trial, do that for every stimulus set, and use the conditional function on that timeline. Either that, or use https://www.jspsych.org/v7/reference/jspsych/#jspsychaddnodetoendoftimeline and to it in the on_finish function maybe.
(note NSFW, as this is stim from the original study):
<!DOCTYPE html>
<html>
<head>
<!-- Load JsPsych -->
<script src="https://unpkg.com/jspsych@7.3.1"></script>
<link href="https://unpkg.com/jspsych@7.3.1/css/jspsych.css" rel="stylesheet" type="text/css" />
<!-- Load plugins -->
<script src="https://unpkg.com/@jspsych/plugin-survey@1.0.1"></script>
<script src="https://unpkg.com/@jspsych/plugin-html-keyboard-response@1.1.3"></script>
<link rel="stylesheet" href="https://unpkg.com/@jspsych/plugin-survey@1.0.1/css/survey.css" />
</head>
<body></body>
<script>
// https://github.com/RealityBending/FictionEro/tree/main/study2/experiment
// Initialize experiment =================================================
var stimuli_list = [
{
"stimulus": "Opposite-sex_couple_001_v.jpg",
"Category": "Opposite-sex Couple",
"Orientation": "v"
},
{
"stimulus": "Opposite-sex_couple_002_h.jpg",
"Category": "Opposite-sex Couple",
"Orientation": "h"
},
{
"stimulus": "Male_couple_002_h.jpg",
"Category": "Male Couple",
"Orientation": "h"
},
{
"stimulus": "Male_couple_003_v.jpg",
"Category": "Male Couple",
"Orientation": "v"
},
{
"stimulus": "Female_couple_002_v.jpg",
"Category": "Female Couple",
"Orientation": "v"
},
{
"stimulus": "Female_couple_003_h.jpg",
"Category": "Female Couple",
"Orientation": "h"
},
{
"stimulus": "Male_001_v.jpg",
"Category": "Male",
"Orientation": "v"
},
{
"stimulus": "Male_002_v.jpg",
"Category": "Male",
"Orientation": "v"
},
{
"stimulus": "Female_001_h.jpg",
"Category": "Female",
"Orientation": "h"
},
{
"stimulus": "Female_002_h.jpg",
"Category": "Female",
"Orientation": "h"
}
]
const categorizedStimuli = stimuli_list.reduce((acc, item) => {
// If the category doesn't exist in our accumulator, create an array for it
if (!acc[item.Category]) {
acc[item.Category] = [];
}
// Push the current item to its category array
acc[item.Category].push(item);
return acc;
}, {});
var jsPsych = initJsPsych({
on_finish: function () {
jsPsych.data.displayData("json") // Display data in browser
}
})
var relevantPhotoCategories; // store selection here to filter which timeline to show
var demographics_questions = {
type: jsPsychSurvey,
survey_json: {
pages: [
{
elements: [
{
title: "What is your gender?",
name: "Gender",
type: "radiogroup",
choices: ["Male", "Female", "Other"],
},
],
},
{
elements: [
{
title: "What sexual orientation do you identify with?",
name: "SexualOrientation",
type: "radiogroup",
choices: ["Heterosexual", "Homosexual", "Bisexual"],
showOtherItem: true,
otherText: "Other",
otherPlaceholder: "Please specify",
isRequired: true,
},
],
},
],
},
data: {
screen: "demographic_questions",
},
on_finish: getRelevantPhotoCategory
}
function getRelevantPhotoCategory(data){
// Get the response data
const response = data.response;
// Extract gender and orientation
const gender = response.Gender;
const orientation = response.SexualOrientation;
// Create combined string
if(orientation === 'Bisexual'){
relevantPhotoCategories = ["Opposite-sex Couple", "Male Couple", "Female Couple", "Female", "Male"]
} else if(orientation === "Homosexual") {
relevantPhotoCategories = [gender, gender + " Couple"]
} else {
const oppositeSex = gender === "Male" ? "Female" : "Male"
relevantPhotoCategories = ["Opposite-sex Couple", oppositeSex, oppositeSex + " Couple" ]
}
data.relevantPhotoCategories = relevantPhotoCategories
// To filer stim: stimuli_list.filter(item => data.relevantPhotoCategories.includes(item.Category));
}
// init
var timeline = []
timeline.push(demographics_questions)
// First define the image trial
var fiction_showimage1 = {
type: jsPsychHtmlKeyboardResponse,
stimulus: function() {
// Create an img element with the stimulus filename
return `<img src="https://raw.githubusercontent.com/RealityBending/FictionEro/main/study2/experiment/stimuli/${jsPsych.timelineVariable('stimulus')}" style="max-width: 100%;">`;
},
choices: ["s"],
}
var categories = ["Opposite-sex Couple", "Male Couple", "Female Couple", "Female", "Male"];
// Loop through categories and create timeline segments
for(let i=0; i < categories.length; i++) {
let fiction_phase1a = {
timeline: [fiction_showimage1], // Put the trial object in an array
timeline_variables: categorizedStimuli[categories[i]],
conditional_function: function() {
if(relevantPhotoCategories.includes(categories[i])) {
console.log("Showing category:", categories[i]);
return true;
} else {
console.log("Skipping category:", categories[i]);
return false;
}
}
}
timeline.push(fiction_phase1a)
}
jsPsych.run(timeline)
</script>
</html>
I am working with @DominiqueMakowski to try to set up an experiment in which we need to filter an array of stimuli based on the demographic answers given by the participant at the beginning of the experiment.
However, setting timeline variable to be a function does not seem to work:
We tried as an alternative to put the whole timeline object into a function with the hope that that function would only be called after the demographic phase:
But that didn't do the trick either, as the function is called immediately on script initialization.
What is the best way to dynamically filter the timeline__variable? Thanks