scripts/experiments/visual_search/visual_search_timeline.js
import {generateRandomDistribution} from "/scripts/experiment-properties/distribution/random_distribution_generator.js";
import {balance_subconditions} from "/scripts/experiment-properties/balancing/balancing_controller.js";
import {get_data} from "/scripts/experiment-properties/data/data_controller.js";
import {get_color_paths, hex_to_color} from "/scripts/experiments/visual_search/visual_search.js";
//=============================================
// EXPERIMENT CONSTRUCTOR
class Visual_Search {
/**
* Initializes a Visual_Search experiment object.
*
* @param params {object} Parameters passed in from routing
*/
constructor(params) {
let trial_structure = params["trial_structure"];
let condition_name = params["condition"];
let graph_type = params["graph_type"];
let balancing_type = params["balancing"];
this.condition_name = condition_name;
this.condition_group = this.condition_name.split('_')[0];
// **NOTE: EXPERIMENTS variable comes from /public/config/experiments-config.js
if (!EXPERIMENTS["visual_search"]["trial_structure"].includes(trial_structure)) {
throw Error(trial_structure + " is not supported.");}
else {
this.trial_structure = trial_structure;
}
if (!EXPERIMENTS["visual_search"]["graph_type"].includes(graph_type)){
throw Error(graph_type + " is not supported.")}
else {
this.graph_type = graph_type;
};
if (!EXPERIMENTS["visual_search"]["balancing_type"].includes(balancing_type)) {
throw Error(balancing_type + " is not supported.") }
else {
this.balancing_type = balancing_type;
}
this.condition_name = condition_name;
this.subject_id = params["subject_id"];
this.subject_initials = params["subject_initials"];
// ========================================
// EXPERIMENT VARIABLES
this.raw_sub_conds; // subconditions in visual_search_data.js
this.target_color = "#dbc667";
// ========================================
// TEST EXPERIMENT VARIABLES
this.sub_condition_order;
this.experiment_conditions_constants = [];
this.current_sub_condition_index;
/// ========================================
// CURRENT TRIAL DATA
// Plotting-related vars
this.target_coordinates = "";
this.distractor_coordinates = "";
// JsPsych trial_data for the current trial
this.trial_data = "";
// ========================================
// PREPARE EXPERIMENT
// Extract raw constants
this.raw_sub_conds = get_data(this);
// Prepare experiment
let dataset = this.raw_sub_conds;
this.sub_condition_order = balance_subconditions(this.balancing_type, this.constructor.name.toLowerCase(), dataset.length);
let ordered_dataset = [];
// Order the data set according to the randomly ordered array
for (let i = 0; i < this.sub_condition_order.length; i++) {
ordered_dataset[i] = dataset[this.sub_condition_order[i]];
}
// Set experiment trials
this.experiment_conditions_constants = ordered_dataset;
this.current_sub_condition_index = 0;
}
}
//================================================
// TIMELINE
import {get_instructions} from "/scripts/experiment-properties/instructions/instructions_controller.js";
export var visual_search_exp = new Visual_Search(params);
var timeline = [];
var address = location.protocol + "//" + location.hostname + ":" + location.port;
// Firefox check for formatting
if (typeof InstallTrigger !== 'undefined') {
var isFirefox = true;
} else {
var isFirefox = false;
}
// =========================================================
// WELCOME TRIAL BLOCK
let welcome = {
type: 'html-keyboard-response',
stimulus: '<div align = "center">' + `<img src="${address}/img/VCL_lab_logo.png"></img> <br>` +
'Welcome to the <b>Visual Search Task</b> Experiment.' +
'<br><br><p><font size = 15>Press any key to begin.<p></font>' +
'</div>',
data: {type: 'instruction'}
};
timeline.push(welcome);
// =========================================================
// INSTRUCTION TRIAL BLOCKS
var instructions = {
type: "html-keyboard-response",
stimulus: function(){
return get_instructions(visual_search_exp);
}
};
timeline.push(instructions);
let ready = {
type: 'html-keyboard-response',
stimulus: "<div align = 'center'> <font size = 20><p>Ready? We will now begin the experiment. <p>" + "<br><br><p><b>Press any key to begin.</b></p></font></div>",
data: {type: 'instruction'}
}
timeline.push(ready);
// =====================================================================
// FIXATION
let fixation = {
type: 'html-keyboard-response',
stimulus: '<div style="font-size:60px;">+</div>',
choices: jsPsych.NO_KEYS,
trial_duration: 1000,
data: {type: 'fixation'}
};
// =====================================================================
// STIMULUS
var trial = {
type:'external-html-keyboard-response',
url: address + "/visual_search_trial",
choices: ['z', 'm', 'q'],
execute_script: true,
on_start: function(trial){ // NOTE: on_start takes in trial var
var index = visual_search_exp.current_sub_condition_index;
var constants = visual_search_exp.experiment_conditions_constants[index];
trial.data = constants;
handle_data_saving(trial, "test", constants, index);
var base_coordinates = generateRandomDistribution(constants.row, constants.col, constants.num_points, null);
visual_search_exp.coordinates = [base_coordinates];
visual_search_exp.trial_data = trial.data;
},
on_finish: function(data){
var index = visual_search_exp.current_sub_condition_index;
var constants = visual_search_exp.experiment_conditions_constants[index];
if (data.key_press == jsPsych.pluginAPI.convertKeyCharacterToKeyCode('z') && constants.target_present) {
data.correct = true;
} else if (data.key_press == jsPsych.pluginAPI.convertKeyCharacterToKeyCode('m') && !constants.target_present) {
data.correct = true;
} else {
data.correct = false;
}
}
};
// ========================================================
// FEEDBACK
var feedback = {
type: 'html-keyboard-response',
trial_duration: null,
response_ends_trial: true,
data: {type: 'feedback'},
stimulus: function(){
var last_trial = JSON.parse(jsPsych.data.getLastTrialData().json());
var last_trial_correct = last_trial[0]["correct"];
// For debugging purposes:
if (last_trial_correct == -1){
return '<p>' +
'<font style="font-size:50px; color:blue">Exiting from experiment.<p></font>'
}
else if (last_trial_correct){
return '<p><i class="fa fa-check-circle" style="font-size:50px; color:green; margin-right: 10px;"></i>' +
'<font style="font-size:50px; color:green">Correct!<p></font>'
}
else{
return '<p><i class="fa fa-close" style="font-size:50px; color:red; margin-right: 10px;"></i>' +
'<font style="font-size:50px; color:red;"">Incorrect!<p></font>'
}
}
};
// =========================================================
// EXPERIMENT TRIAL BLOCKS
var experiment = {
timeline: [fixation, trial, feedback],
loop_function: function(data){ // Return true if timeline should continue
// Return false if timeline should end
// For debugging, if you want to exit out of experiment, press q:
if (jsPsych.pluginAPI.convertKeyCharacterToKeyCode('q') == data.values()[0].key_press){
return false;
}
if (visual_search_exp.current_sub_condition_index < (visual_search_exp.experiment_conditions_constants.length-1)){
visual_search_exp.current_sub_condition_index++;
console.log("!!!!!!!!!! Moved to new sub condition at index "
+ visual_search_exp.current_sub_condition_index);
return true;
}
// Else end experiment
else{
return false;
}
},
on_finish: function(data){
visual_search_exp.trial_data = data;
}
};
timeline.push(experiment);
console.log("======================");
// ---------------------------------------------------------------------
// TARGET ID INSTRUCTIONS
let instructions2 = {
type: "html-keyboard-response",
stimulus: "<div align = 'center'>" +
"<p>Now, you will be shown all of the colours you have interacted with thus far. " +
"From the colours shown,<br>" +
"<strong>choose the colour that you have been searching for in this task.</strong></p>" +
"<p>Please press the <b>letter key</b> it is associated with to indicate your choice.</p>" +
"</div>"
};
timeline.push(instructions2);
// ---------------------------------------------------------------------
// RATING WHEEL
var target_point_color = visual_search_exp.experiment_conditions_constants[0].target_color;
const LETTERS = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M"];
const PATHS = get_color_paths(target_point_color).sort( () => Math.random() - 0.5);
let mapping = {};
// Map the letters to the paths
for (let letter of LETTERS){
mapping[letter] = PATHS.pop();
}
let rating_wheel = {
type: 'multiple-ensembles-vizsearch-rating',
letter_mapping: mapping,
choices: ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M"],
trial_duration: null,
response_ends_trial: true,
data: {run_type: 'test',
type: 'visual_search'},
on_finish: function(data) {
let letter = jsPsych.pluginAPI.convertKeyCodeToKeyCharacter(data.key_press);
letter = letter.toUpperCase();
let path = mapping[letter];
let color = path.split("/")[8]; // getting color from url
color = color.split(".")[0];
data.rating_color = color;
}
};
timeline.push(rating_wheel);
// ---------------------------------------------------------------------
// CONFIDENCE RATING
let rating = {
type: 'html-keyboard-response',
stimulus: "<div align = 'center'> <p>Rate how confident you are in your choice</p><p>on a scale of 1 to 7, with 7 being the most confident. </p>" + "<p><b>Please press the number key that corresponds to your choice:</b></p>"+
"<br><p><font size = 15><i>Least confident</i> <---1--2--3--4--5--6--7---> <i>Most confident</i></font></p></div>",
choices: ['1', '2', '3', '4', '5','6','7'],
data: {run_type: 'test',
type: 'visual_search'},
on_finish: function(data) {
data.confidence_rating = jsPsych.pluginAPI.convertKeyCodeToKeyCharacter(data.key_press);
}
};
timeline.push(rating);
// =========================================================
// DATA DOWNLOADING
var experiment_end = {
type: 'html-keyboard-response',
stimulus: '<div align = "center">' +
'<p><font size = 10>You have completed the experiment!<p></font>' +
'<br>' +
'Trial and summary data files will now automatically download locally.' +
'</div>' ,
on_start: function(){
export_trial_data();
export_summary_data();
// Reset background color to feedback
document.body.style.backgroundColor = visual_search_exp.trial_data.feedback_background_color;
}
};
timeline.push(experiment_end);
// =========================================================
// START JSPSYCH
jsPsych.init({
timeline: timeline,
on_finish: function(){
jsPsych.data.displayData();
}
});
// =========================================================
// HELPERS
/**
* Handles saving the relevant data on a given trial.
*
*/
function handle_data_saving(trial, block_type, constants, index) {
// Add all constants from excel
trial.data = constants;
// Adding constants that required computation (not from excel)
trial.data.type = "visual_search";
trial.data.sub_condition = index;
// Block specific saves
if (block_type == "test"){
trial.data.run_type = "test";
}
else{
trial.data.run_type = "practice";
}
}
/*
* Saves experiment data as csv
* */
function export_trial_data() {
var trial_data = jsPsych.data.get().filter({type: 'visual_search', run_type: 'test'})
.ignore('type')
.ignore('run_type')
.ignore('left_correlation')
.ignore('right_correlation')
// These are variables forced on by jsPsych
.ignore('stimulus')
.ignore('key_press')
.ignore('choices')
.ignore('trial_type')
.ignore('trial_index')
.ignore('time_elapsed')
.ignore('internal_node_id');
var string = "S" + visual_search_exp.subject_id + "_" + visual_search_exp.condition_name + "_visual_search_trial_results.csv";
trial_data.localSave('csv', string);
}
/**
* When called, will save aggregated trial data into a CSV.
*/
function export_summary_data() {
var csv = 'SUBJECT_ID,SUBJECT_INITIALS,CONDITION_NAME,NUM_POINTS,ROW,COL,TARGET_PRESENT,TRIALS\n';
var data = [];
// Organize each row of the csv
for (let i = 0; i<visual_search_exp.experiment_conditions_constants.length; i++){
var row = [visual_search_exp.subject_id, visual_search_exp.subject_initials, visual_search_exp.condition_name];
var constants = visual_search_exp.experiment_conditions_constants[i];
var condition_data = jsPsych.data.get();
row.push(constants.num_points);
row.push(constants.row);
row.push(constants.col);
row.push(constants.target_present);
row.push(condition_data.count());
data.push(row);
}
// Append each row
data.forEach(function(row){
csv += row.join(',');
csv += "\n";
});
var hiddenElement = document.createElement('a');
hiddenElement.href = 'data:text/csv;charset=utf-8,' + encodeURI(csv);
hiddenElement.target = '_blank';
hiddenElement.download = "S" + visual_search_exp.subject_id + "_" + visual_search_exp.condition_name + "_visual_search_summary_results.csv";
hiddenElement.click();
}