Home Reference Source

scripts/experiments/jnd/jnd.js

import { balance_subconditions } from '/scripts/experiment-properties/balancing/balancing_controller.js'
import { initialize_random_order } from '/scripts/experiment-properties/balancing/generators/random_generator.js'
import { get_data } from '/scripts/experiment-properties/data/data_controller.js'
import { generateDistribution } from '/scripts/experiment-properties/distribution/gaussian_distribution_generator.js'
import { randomize_position } from '/scripts/helpers/experiment_helpers.js'

export default class JND {

  /**
   * Initializes a JND experiment object. 
   *
   * @param  {assoc array}  Parameters passed 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"];
    let conversion_factor = params["conversion_factor"];

    this.condition_name = condition_name; 
    this.condition_group = this.condition_name.split('_')[0]; // Mostly to handle "distractor" conditions.
                                                              // TODO: Should have a better flag for it.
    this.subject_id = params["subject_id"];
    this.subject_initials = params["subject_initials"];

    // ========================================
    // PARAMETER CHECKING

    // **NOTE: EXPERIMENTS variable comes from /public/config/experiments-config.js
    if (!EXPERIMENTS["jnd"]["trial_structure"].includes(trial_structure)) {
      throw Error(trial_structure + " is not supported.");}
    else {
      this.trial_structure = trial_structure;
    }

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

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

    // ========================================
    // EXPERIMENT CONSTANTS

    this.PIXELS_PER_CM = conversion_factor;
    this.MIN_CORRELATION = 0.0;
    this.MAX_CORRELATION = 1.0;
    this.MIN_TRIALS = 24;
    this.MAX_TRIALS = 52;
    this.WINDOW_SIZE = 24;
    this.WINDOW_INTERVAL = 3;
    this.CONVERGENCE_THRESHOLD = 0.75; 
    this.INCORRECT_MULTIPLIER = 3;

    // ========================================
    // PRACTICE EXPERIMENT VARIABLES

    this.practice_conditions_constants;
    this.current_practice_condition_index; 

    // ========================================
    // TEST EXPERIMENT VARIABLES

    this.first_trial_of_sub_condition = true;
    this.sub_condition_order;
    this.sub_conditions_constants;
    this.current_sub_condition_index;
    this.adjusted_quantity_matrix = {};   // The matrix is in this format:
                                          // { sub_condition_index : [adjusted_quantity1, adjusted_quantity2 ... ] }

    // ========================================
    // CURRENT TRIAL DATA

    // Plotting-related vars
    this.left_coordinates = "";
    this.right_coordinates = "";
    this.distractor_coordinates = "";
    
    // JsPsych trial_data for the current trial
    this.trial_data = "";

    // ========================================
    // PREPARE EXPERIMENT

    // Extract raw constants
    this.raw_constants = get_data(this);
    
    // Prepare experiment + practice data
    this.prepare_experiment();
    this.prepare_practice();   

    // ========================================
    // Adjusted Statistic Values
    // ** Previously were set inside the get_next_adjusted_statistic method.
    // ** Replacing constants into variables to be set inside the method. 
    let correct_statistic_increment;
    let incorrect_statistic_increment;
  }

  /**
   * Orders the input data according to balancing type and
   * initializes the JND object's variables.  
   *
   * @param  balancing_type {string}                             Type of balancing. Currently only latin_square
   *                                                             is supported.
   *         dataset {[{assoc array}, {assoc array}, ... ]}      The data to be ordered. 
   */ 
  prepare_experiment() {

    let dataset = this.raw_constants;

    this.sub_condition_order = balance_subconditions(this.balancing_type, this.constructor.name.toLowerCase(), dataset.length);

    var ordered_dataset = [];

    // Order the data set according to the latin square
    // Initialize adjusted_quantity_matrix size 
    for (let i=0; i < this.sub_condition_order.length; i++){
      ordered_dataset[i] = dataset[this.sub_condition_order[i]];
      this.adjusted_quantity_matrix[i] = [];
    }

    // Set experiment trials 
    this.sub_conditions_constants = ordered_dataset;
    this.current_sub_condition_index = 0;  
  }

  /**
   * Orders the input dataset by randomizing it, and initializes the practice variables.
   *
   * @param  dataset {[{assoc array}, {assoc array}, ... ]}   The data to be ordered. 
   */
  prepare_practice() {

    let dataset = this.raw_constants;

    this.sub_condition_order = initialize_random_order(dataset.length);
    let practice_dataset = [];

    // Order the data set according to the latin square
    // Initialize adjusted_quantity_matrix size 
    for (let i=0; i < this.sub_condition_order.length; i++){
      practice_dataset[i] = dataset[this.sub_condition_order[i]];
    }

    // Set practice trials
    this.practice_conditions_constants = practice_dataset;
    this.current_practice_condition_index = 0;
  }

  /**
   * Generates a JND trial object for use in the JsPsych timeline.
   *
   * @param  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 jnd_exp = this; 
    var address = location.protocol + "//" + location.hostname + ":" + location.port + "/jnd_trial"; 

    var trial = {
      type:'external-html-keyboard-response',
      url: address,
      choices:['z', 'm', 'q'], //q is exit button (for debugging)
      execute_script: true,
      response_ends_trial: true,
      post_trial_gap: 300,
      on_start: function(trial){ // NOTE: on_start takes in trial var 

        // Set the constants to be used:
        if (block_type == "test"){ 
          var index = jnd_exp.current_sub_condition_index; 
          var constants = jnd_exp.sub_conditions_constants[index];
        }
        else { 
          var index = jnd_exp.current_practice_condition_index; 
          var constants = jnd_exp.practice_conditions_constants[index];
        }

        // Calculate adjusted value
        var adjusted_value = jnd_exp.calculate_adjusted_value(constants);

        // Handling saving this trial's data: 
        jnd_exp.handle_data_saving(trial, block_type, constants, index, adjusted_value);

        // Generate distributions
        var base_coordinates = generateDistribution(constants.base_correlation, 
                                                    constants.error, 
                                                    constants.num_points, 
                                                    constants.num_SD, 
                                                    constants.mean, 
                                                    constants.SD);

        var adjusted_coordinates = jnd_exp.generate_adjusted_distribution(constants, adjusted_value);

        // trial block for symmetric jnd 
        if (jnd_exp.condition_group === "symmetric"){
          var adjusted_value_high = constants.adjusted_correlation_high;
          var adjusted_value_low = constants.adjusted_correlation_low;

          jnd_exp.handle_data_saving(trial, block_type, constants, index, adjusted_value_high);
          jnd_exp.handle_data_saving(trial, block_type, constants, index, adjusted_value_low);

          var low_coordinates = generateDistribution(constants.base_correlation, 
                                                      constants.error, 
                                                      constants.num_points, 
                                                      constants.num_SD, 
                                                      constants.mean, 
                                                      constants.SD,
                                                      constants.adjusted_correlation_low);

          var high_coordinates = generateDistribution(constants.base_correlation, 
                                                      constants.error, 
                                                      constants.num_points, 
                                                      constants.num_SD, 
                                                      constants.mean, 
                                                      constants.SD,
                                                      constants.adjusted_correlation_high);                    

        }

        if (jnd_exp.condition_group === "distractor"){
          var left_dist_coordinates = generateDistribution(constants.dist_base,
                                                           constants.dist_error,
                                                           constants.dist_num_points,
                                                           constants.num_SD,
                                                           constants.mean,
                                                           constants.SD);

          var right_dist_coordinates = generateDistribution(constants.dist_base,
                                                           constants.dist_error,
                                                           constants.dist_num_points,
                                                           constants.num_SD,
                                                           constants.mean,
                                                           constants.SD);

          jnd_exp.distractor_coordinates = [left_dist_coordinates, right_dist_coordinates];
        }

        // Randomize position of the base and adjusted graphs
        var result = randomize_position(trial, 
                                       base_coordinates,
                                       adjusted_coordinates, 
                                       constants, 
                                       adjusted_value);
        // // For testing purposes, can force R graph to have greater correlation
        // var result = force_greater_right_position(trial,
        //                                           base_coordinates,
        //                                           adjusted_coordinates,
        //                                           constants.base_correlation,
        //                                           adjusted_correlation);

        // Set up D3 variables for plotting
        jnd_exp.coordinates = [result.left, result.right];
         
        jnd_exp.trial_data = trial.data; 

        if (constants.task) {
          console.log("[TASK TYPE]: " + constants.task);
        }
        console.log("[RIGHT] Correlation: " + trial.data.right_correlation);
        console.log("[RIGHT] Num points: " + trial.data.right_num_points);

        console.log("[LEFT] Correlation: " + trial.data.left_correlation);
        console.log("[LEFT] Num points: " + trial.data.left_num_points);

      },
      on_finish: function(data){ // NOTE: on_finish takes in data var 
        // Set the constants to be used:
        let index;
        let constants;
        if (block_type == "test"){ 
          index = jnd_exp.current_sub_condition_index; 
          constants = jnd_exp.sub_conditions_constants[index];
        }
        else { 
          index = jnd_exp.current_practice_condition_index; 
          constants = jnd_exp.practice_conditions_constants[index];
        }

        jnd_exp.check_response(data, constants);
        console.log("RESPONSE: " + data.correct);
      } 
    };

    return trial; 
  }

  /**
   * Generates the adjusted distribution depending on the type of task.
   * If no task is specified, defaults to using correlation.
   * 
   * @return {object}            Adjusted coordinates
   */
  generate_adjusted_distribution(constants, adjusted_value) {

    let adjusted_coordinates;

    if (constants.task) {

      switch (constants.task) {
        case "numerosity":
          // Use base correlation but w/ adjusted num of points
          adjusted_coordinates = generateDistribution(constants.base_correlation, 
                                                      constants.error, 
                                                      adjusted_value, 
                                                      constants.num_SD, 
                                                      constants.mean,
                                                      constants.SD);
          break;
        case "correlation":
          adjusted_coordinates = generateDistribution(adjusted_value, 
                                                  constants.error, 
                                                  constants.num_points, 
                                                  constants.num_SD, 
                                                  constants.mean,
                                                  constants.SD);
          break;
        default:
          throw Error("Generating adjusted distribution has not been handled for task: " + constants.task);
      }

      
    } else {

      adjusted_coordinates = generateDistribution(adjusted_value, 
                                                  constants.error, 
                                                  constants.num_points, 
                                                  constants.num_SD, 
                                                  constants.mean,
                                                  constants.SD);
    }

    return adjusted_coordinates;
  }

  /**
   * Handles saving the relevant data on a given trial.
   *
   * For reference, these are the helper variables created to assist in trial logic (i.e not present in excel)
   * trial_variables =         
   *       {type: 'jnd',
   *       run_type: '',
   *       left_correlation: '',
   *       right_correlation: '',
   *       };
   *
   * These are variables created WITHIN the trial logic that were not present in excel (but need to be
   * outputted to results).     
   * export_variables = 
   *       {sub_condition: '',           // Chronological ordering of sub_condition [1, 2, 3 ... ]
   *        balanced_sub_condition: '',  // Index of sub_condition according to balancing order
   *        jnd: '',
   *        base_correlation: '',
   *        adjusted_value: '',
   *        correct: '',
   *       };
   *
   * @param trial {object}
   *        block_type {string}           "test" or "practice"
   *        constants {assoc array}
   *        index {integer}
   *        adjusted_value {double}
   */
  handle_data_saving(trial, block_type, constants, index, adjusted_value) {

    // Add all constants from excel
    trial.data = constants;

    // Adding constants that required computation (not from excel)
    trial.data.type = "jnd";
    trial.data.adjusted_value = adjusted_value;

    if (constants.task) {

      switch (constants.task) {
        case "numerosity":
          trial.data.adjusted_value_type = "number of points";
          trial.data.jnd = Math.abs(adjusted_value - constants.num_points);
          break;
        case "correlation":
          trial.data.adjusted_value_type = "correlation";
          trial.data.jnd = Math.abs(adjusted_value - constants.base_correlation);
          break;
        default:
          throw Error("Calculations for jnd has not been handled for task: " + constants.task);
      }
      
    } else {
      trial.data.jnd = Math.abs(adjusted_value - constants.base_correlation);
    }

    trial.data.sub_condition = index; 
    trial.data.balanced_sub_condition = this.sub_condition_order[index];

    // Block specific saves 
    if (block_type == "test"){
      this.adjusted_quantity_matrix[index].push(adjusted_value);
      trial.data.run_type = "test";
    }
    else{
      trial.data.run_type = "practice";
    }
  }

  /**
   * Determines whether the current sub condition can end or not.
   * 
   * @return {boolean}            True if sub condition should end.
   */
  end_sub_condition() {

    if (this.adjusted_quantity_matrix[this.current_sub_condition_index].length == this.MAX_TRIALS ||
          this.is_converged_in_window()){
      return true;
    }
    else {
      return false;
    }
  }

  /**
   * Checks whether we have just started a subcondition.
   * 
   * @return {boolean}            True if sub condition has just started.
   */
  is_start_of_subcondition() {

    if (this.adjusted_quantity_matrix[this.current_sub_condition_index].length === 0) {
      return true;
    }
    return false;
  }

  /**
   * Determines whether current subcondition has converged or not.
   *
   * @return {boolean}            True if converged.
   */
  is_converged_in_window() {
    
    var converged = false;
    var num_completed_trials = this.adjusted_quantity_matrix[this.current_sub_condition_index].length;

    // Check if we have completed the minimum number of trials
    // and if the number of completed trials is greater than the window size
    if (num_completed_trials >= this.MIN_TRIALS && num_completed_trials >= this.WINDOW_SIZE) {

      // 2D Matrix of windows of adjusted quantities
      var adjusted_quantity_windows = [];

      // The index of the last trial
      var last_trial = num_completed_trials - 1;

      // Compute the interval size and remainder
      // The remainder is computed in case the window size isn't divisible by the # intervals
      var interval_size = this.WINDOW_SIZE / this.WINDOW_INTERVAL;
      var interval_remainder = this.WINDOW_SIZE % this.WINDOW_INTERVAL;

      // This is the first trial in the window
      // For example:
      // numCompletedTrials = 5
      // windowSize = 3
      // [ 0 1 2 3 4 5 6 7 8 9 ]
      // windowStart would be at index: 5 - 3 = 2
      var window_start = num_completed_trials - this.WINDOW_SIZE;
      console.log("num completed: " + num_completed_trials);
      console.log("window start: " + window_start);

      // Iterate over all of the trials from the start of the window to the last trial
      // and organize them into the 2D adjustedQuantityWindows matrix
      while (window_start < last_trial) {

        // While we have extra elements that don't fit into an interval
        // add one extra to each window interval
        var current_interval_size = interval_remainder > 0 ? interval_size + 1 : interval_size;
        if (interval_remainder > 0) {
          interval_remainder--;
        }

        // Collect the adjusted quantity values from the trials into the double[]
        var adjusted_quantities = [];
        for (let i = 0; i < current_interval_size; ++i) {
          var adjusted_quantity = this.adjusted_quantity_matrix[this.current_sub_condition_index][i + window_start];
          adjusted_quantities.push(adjusted_quantity);
        }

        // Set the window start to the next interval
        window_start += current_interval_size;
        adjusted_quantity_windows.push(adjusted_quantities);
      }

      console.log(adjusted_quantity_windows);

      var variance = [];
      var mean = [];
      for (let i = 0; i < adjusted_quantity_windows.length; i++){
        variance.push(math.var(adjusted_quantity_windows[i]));
        mean.push(math.mean(adjusted_quantity_windows[i]));
      }

      var mean_of_variances = math.mean(variance);
      var variance_of_means = math.var(mean);
      var F = variance_of_means/mean_of_variances;
      console.log("F: " + F);
      // Convergence if the F value is < 1 - convergenceThreshold
      // if the F is greater than 0.25, then converge 
      converged = F < (1 - this.CONVERGENCE_THRESHOLD);
    }

    if (converged) {console.log("CONVERGED!!!!")};

    return converged;

  }

  /**
   * Calculates the adjusted value depending on whether this is the
   * first trial of the sub condition or not.
   *
   * @param  constants {assoc array}
   * @return adjusted_value {double}          
   */
  calculate_adjusted_value(constants) {

    // For the first trial, we need to initialize the adjusted correlation:
    if (this.first_trial_of_sub_condition){
      var adjusted_value = this.initialize_adjusted_statistic(constants);
      // Set flag to false
      this.first_trial_of_sub_condition = false;
    }
    else{
      var last_JND_trial = jsPsych.data.get().filter({type: "jnd"}).last(1).values()[0];

      var adjusted_value = this.get_next_adjusted_statistic(last_JND_trial, constants);
    }
    return adjusted_value; 
  }

  /**
   * Initializes the adjusted value for the first time.
   *
   * @param  {object}             constants
   * @return {double}             adjusted_value  
   */
  initialize_adjusted_statistic(constants) {
    let adjusted_value;

    if (constants.reference_start) {
      adjusted_value = constants.reference_start;
    } 
    else {
      if (constants.converge_from_above){
        adjusted_value = Math.min(this.MAX_CORRELATION, constants.base_correlation + constants.initial_difference); 
      }
      else {
        adjusted_value = Math.max(this.MIN_CORRELATION, constants.base_correlation - constants.initial_difference);
      };
    }
    return adjusted_value;
  }
  
  /**
   * Calculates the next adjusted value.
   *
   * @param  {object} last_JND_trial
   *.        {object} constants
   * @return {double} next_adjusted_statistic         
   */
  get_next_adjusted_statistic(last_JND_trial, constants){
    let next_adjusted_statistic;

    if (constants.task){

      switch (constants.task) {

        case "numerosity":
          if (last_JND_trial.correct) {
            correct_numerosity_statistic_increment = 1;
            next_adjusted_statistic = last_JND_trial.adjusted_value + correct_numerosity_statistic_increment;
          } else {
            let incorrect_numerosity_statistic_increment = 3;
            next_adjusted_statistic = last_JND_trial.adjusted_value - incorrect_numerosity_statistic_increment;
          }
          break;

        case "correlation":

          // For num_corr conditions, guard to force adjusted stat to be use this calculation instead of the 
          // traditional min/max AKA what is in get_next_adjusted_correlation
          if (this.condition_name.split("_").includes("num") && this.condition_name.split("_").includes("corr")) {
            if (last_JND_trial.correct) {
              correct_statistic_increment = 0.01;
              next_adjusted_statistic = last_JND_trial.adjusted_value + correct_statistic_increment;
            } else {
              incorrect_statistic_increment = 0.03;
              next_adjusted_statistic = last_JND_trial.adjusted_value - incorrect_statistic_increment;
            }
          }
          // Checks if the condition under correlation is symmetric
          else if (this.condition.name.includes("symmetric")){
            if (last_JND_trial.correct){
              correct_statistic_increment = 0.1; 
              next_adjusted_statistic = last_JND_trial.adjusted_value + correct_statistic_increment;
            } else {
              incorrect_statistic_increment = 0.05;
              next_adjusted_statistic = last_JND_trial.adjusted_value - incorrect_statistic_increment;
            }
          }
          else {
            next_adjusted_statistic = this.get_next_adjusted_correlation(last_JND_trial, constants);
          }
          break;

        default:
          throw Error("Calculations for getting next adjusted statistic has not been handled for task: " + constants.task);
          break;
      }
    } 
    else {
      let initial_difference = constants.base_correlation;
      next_adjusted_statistic = this.get_next_adjusted_correlation(last_JND_trial, constants);
    }

    return next_adjusted_statistic;
  }


  /**
   * Calculates the next adjusted correlation (the traditional way).
   *
   * @param  {object} last_JND_trial
   *.        {object} constants
   * @return {double} next_adjusted_statistic      
   */
  get_next_adjusted_correlation(last_JND_trial, constants){
    let next_adjusted_statistic;
    let initial_difference = constants.base_correlation;

    if (constants.converge_from_above) {
      if (last_JND_trial.correct) {
        next_adjusted_statistic = Math.max(initial_difference, last_JND_trial.adjusted_value - constants.max_step_size);
      } else {
        next_adjusted_statistic = Math.min(this.MAX_CORRELATION, last_JND_trial.adjusted_value + constants.max_step_size
                                  * this.INCORRECT_MULTIPLIER);
      }
    } else {
      if (last_JND_trial.correct) {
        next_adjusted_statistic = Math.min(initial_difference, last_JND_trial.adjusted_value + constants.max_step_size);
      } else {
        next_adjusted_statistic = Math.max(this.MIN_CORRELATION, last_JND_trial.adjusted_value - constants.max_step_size
                                  * this.INCORRECT_MULTIPLIER);
      }
    }
    return next_adjusted_statistic;
  }

  /**
   * Given a JND trial data, determines whether response is 
   * correct or not.
   *
   * @param  {JsPsych.data}  data
   * @param. {object}        constants
   * @return {boolean}          
   */ 
  check_response(data, constants) {

    // For debugging purposes:
    if (data.key_press == jsPsych.pluginAPI.convertKeyCharacterToKeyCode('q')){
      data.correct = -1;
      return -1; 
    }

    let right_greater_clause;
    let left_greater_clause;

    if (!constants.task || constants.task === "correlation") {

      right_greater_clause = data.right_correlation > data.left_correlation;
      left_greater_clause = data.left_correlation > data.right_correlation;

    } else if (constants.task === "numerosity") {

      right_greater_clause = data.right_num_points > data.left_num_points;
      left_greater_clause = data.left_num_points > data.right_num_points;

    } else {
      throw Error("Check response function has not been handled for task: " + constants.task);
    }

    if (right_greater_clause && (data.key_press == jsPsych.pluginAPI.convertKeyCharacterToKeyCode('m')) ||
        left_greater_clause && (data.key_press == jsPsych.pluginAPI.convertKeyCharacterToKeyCode('z'))){

      data.correct = true;
      return true;
    }
    // Assuming that if base_correlation = adjusted_correlation, at this point 
    // any user choice is wrong.
    else {
      data.correct = false;
      return false;
    }
  }

  /**
   * When called, will save individual trial data into a CSV.     
   */
  export_trial_data() {

    var trial_data = jsPsych.data.get().filter({type: 'jnd', run_type: 'test'})
                                       .filterCustom(function(x){ //Don't include the exit trials
                                         return x.correct != -1; 
                                       })
                                       // JND's trial variables
                                       .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');

    // TODO: js converting key_string to use double quotes, needs to be single to pass into ignore() fxn
    //
    // for (var key in jnd_exp.trial_variables){
    //  var key_string = '${key}';
    //  trial_data.ignore(key);
    // }

    var string = "S" + this.subject_id + "_" + this.condition_name + "_jnd_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,PLOT,BASE,ABOVE,NUM_POINTS,JND,TRIALS\n';

    var data = [];
    
    // Organize each row of the csv
    for (let i = 0; i<this.sub_conditions_constants.length; i++){
      var row = [this.subject_id, this.subject_initials, this.condition_name];
      var constants = this.sub_conditions_constants[i];
      var condition_data = jsPsych.data.get().filter({type: 'jnd', run_type: 'test', balanced_sub_condition: this.sub_condition_order[i]})
                                             .filterCustom(function(x){ //Don't include the exit trials
                                                return x.correct != -1; 
                                             })

      row.push(constants.base_correlation);
      row.push(constants.converge_from_above);
      row.push(constants.num_points);
      row.push(condition_data.select('jnd').mean());
      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 + "_jnd_summary_results.csv";
    hiddenElement.click();
  }
}