Home Reference Source

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();
}