Home Reference Source

scripts/experiments/numerosity/numerosity.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";

export default class Numerosity {
  /**
   * Initializes a Numerosity experiment object.
   *
   * @param  params          {object}    Parameters passed in from routing
   */
  constructor(params) {

    var address = location.protocol + "//" + location.hostname + ":" + location.port; 

    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["numerosity"]["trial_structure"].includes(trial_structure)) {
      throw Error(trial_structure + " is not supported.");}
    else {
      this.trial_structure = trial_structure;
    }

    if (!EXPERIMENTS["numerosity"]["graph_type"].includes(graph_type)){
      throw Error(graph_type + " is not supported.")} 
    else { 
      this.graph_type = graph_type;
    };  

    if (!EXPERIMENTS["numerosity"]["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 estimation_data.js
    this.target_color = "#dbc667";

    // ========================================
    // TEST EXPERIMENT VARIABLES
    this.sub_condition_order;
    this.experiment_conditions_constants = [];
    this.current_sub_condition_index;
    this.trial_responses = [];

    /// ========================================
    // 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
    this.prepare_experiment();
  }

  /**
   * Orders the input data according to balancing type and
   * initializes the Estimation object's variables.
   *
   */
  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;  
  }

  /**
   * Generates a Numerosity object for use in the JsPsych timeline.
   * Numerosity currently does not support practice trials
   * 
   * Each trial begins with a fixation trial block,
   * followed by the stimulus,
   * ended by a feedback trial block where the subject must estimate the
   * number of target stimuli via the slider
   *
   * @param  block_type {string}     "test" or "practice"
   * @return trial {object}
   */
  generate_trial(block_type) {

    if ((block_type !== "test") && (block_type !== "practice")) {
        throw Error(block_type + " is not supported.")
    }
    // Initialize a variable for this so it is usable inside on_start
    var numerosity_exp = this;
    var address = location.protocol + "//" + location.hostname + ":" + location.port + "/numerosity_trial";


    let group = {};
    var fixation = {
      type: 'html-keyboard-response',
      stimulus: '<div style="font-size:60px;">+</div>',
      choices: jsPsych.NO_KEYS,
      trial_duration: 1000,
      data: {type: 'fixation'}
    };

    var trial = {
      type:'external-html-keyboard-response',
      url: address,
      choices: jsPsych.NO_KEYS,
      trial_duration: 2000,
      execute_script: true,
      on_start: function(trial){ // NOTE: on_start takes in trial var
        var index = numerosity_exp.current_sub_condition_index; 
        var constants = numerosity_exp.experiment_conditions_constants[index];

        trial.data = constants;
        numerosity_exp.set_target_color(constants);

        var base_coordinates = generateRandomDistribution(constants.row, constants.col, constants.target_num_points, null);
        numerosity_exp.coordinates = [base_coordinates];

        if (numerosity_exp.condition_group === "distractor") {
          var distractor_coordinates = generateRandomDistribution(constants.row, constants.col, constants.dist_num_points, base_coordinates);
          numerosity_exp.distractor_coordinates = [distractor_coordinates];
        }

        numerosity_exp.trial_data = trial.data;
      }
    };
    var slider_response = {
      type: 'html-slider-response',
      labels: [8,62],
      min: 8,
      max: 62,
      start: 35,
      stimulus:   
          "<p>How many of this square did you see?",
      prompt: '<p>Select the number by sliding the bar</p>',
      on_start: function(slider_response) {
        slider_response.stimulus =  "<p>How many of this square did you see?" +
        "<div align = 'center' style='height: 200px; display: block;'>"+
        `<img src='http://localhost:8080/img/instructions/numerosity/${numerosity_exp.target_color}.png'></img>`+
        "</div>" + 
        "<div align = 'center' style='height: 25px; display: block;'>"+
        "</div><p>  </p>";

        var index = numerosity_exp.current_sub_condition_index; 
        var constants = numerosity_exp.experiment_conditions_constants[index];
        slider_response.data = constants;
        numerosity_exp.handle_data_saving(slider_response, block_type, constants, index);
      }
    };

    //trial.data.slider_response = slider_response.data.response;
    group.timeline = [fixation, trial, slider_response];
    return group;
  }

  /*
    Fetches and sets target color from subcondition constants
    for use in the response/feedback trial block.
  */
  set_target_color(target) {
    console.log(target);
    var target_hex;
    if (target.point_color) {
      console.log("targetpointcolor");
      target_hex = target.point_color.substring(1); //removing the # symbol from the target.point_color
    }
    if (target.target_color) {
      console.log("targettargetcolor");
      target_hex = target.target_color.substring(1);
    }
    if (target.mix_by_attribute) {
      if (target.mix_by_attribute.point_color) {
        target_hex = target.mix_by_attribute.point_color[0].substring(1);
      }
    }
 
    this.target_color = "num_" + target_hex;
    console.log(this.target_color);
  }

  /**
 * Handles saving the relevant data on a given trial.
 *
 */
  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 = "numerosity";

    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
  * */
  export_trial_data() {
      var trial_data = jsPsych.data.get().filter({type: 'numerosity', 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" + this.subject_id + "_" + this.condition_name + "_numerosity_trial_results.csv";

      trial_data.localSave('csv', string);
  }

  /**
  * When called, will save aggregated trial data into a CSV.     
  */
  export_summary_data() {
    var csv = 'SUBJECT_ID,SUBJECT_INITIALS,CONDITION_NAME,NUM_TARGET_POINTS,ROW,COL,TRIALS\n';

    var data = [];
    
    // Organize each row of the csv
    for (let i = 0; i<this.experiment_conditions_constants.length; i++){
      var row = [this.subject_id, this.subject_initials, this.condition_name];
      var constants = this.experiment_conditions_constants[i];
      var condition_data = jsPsych.data.get();

      row.push(constants.target_num_points);
      row.push(constants.row);
      row.push(constants.col);
      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" + this.subject_id + "_" + this.condition_name + "_numerosity_summary_results.csv";
    hiddenElement.click();
  }
}