scripts/experiments/stevens/stevens.js
import {generateDistribution} from "/scripts/experiment-properties/distribution/gaussian_distribution_generator.js";
import {initialize_random_order} from "/scripts/experiment-properties/balancing/generators/random_generator.js";
import {initialize_latin_square} from "/scripts/experiment-properties/balancing/generators/latin_square_generator.js";
import {balance_subconditions} from "/scripts/experiment-properties/balancing/balancing_controller.js";
import {get_data,
get_data_subset} from "/scripts/experiment-properties/data/data_controller.js";
import {randomize_position,
randomize_radius_position,
force_greater_right_position} from "/scripts/helpers/experiment_helpers.js";
export default class Stevens {
/**
* Initializes a Stevens experiment object.
*
* @param params {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["stevens"]["trial_structure"].includes(trial_structure)) {
throw Error(trial_structure + " is not supported.");}
else {
this.trial_structure = trial_structure;
}
if (!EXPERIMENTS["stevens"]["graph_type"].includes(graph_type)){
throw Error(graph_type + " is not supported.")}
else {
this.graph_type = graph_type;
};
if (!EXPERIMENTS["stevens"]["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.MAX_STEP_INTERVAL = 10;
// ========================================
// EXPERIMENT VARIABLES
this.input_count_array; // Array of length trials_per_round, each index representing num inputs per round
// for a given sub condition
this.sub_conditions_constants;
this.current_sub_condition_index;
this.round_end = false;
// ========================================
// PRACTICE EXPERIMENT VARIABLES
this.practice_conditions_constants;
this.adjusted_midpoint_matrix = {};
this.practice_trial_data = {};
this.practice_end = false;
// ========================================
// TEST EXPERIMENT VARIABLES
this.experiment_conditions_constants;
this.sub_condition_order = [];
// ========================================
// CURRENT TRIAL DATA
// Plotting-related vars
this.left_coordinates = "";
this.right_coordinates = "";
this.middle_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();
}
/**
* Orders the input data according to balancing type and
* initializes the Stevens 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;
var ordered_dataset = [];
switch (this.trial_structure) {
case "foundational":
this.set_foundational_dataset_order(dataset);
break;
case "design":
this.sub_condition_order = balance_subconditions(this.balancing_type, this.constructor.name.toLowerCase(), dataset.length);
break;
case "custom":
ordered_dataset = dataset;
break;
}
// Order the data set according to the latin square
for (let i=0; i < this.sub_condition_order.length; i++){
ordered_dataset[i] = dataset[this.sub_condition_order[i]];
// Alternate the start ref to be low or high for each subcondition
if (i%2 === 0) {
ordered_dataset[i]["start_ref"] = ordered_dataset[i]["low_ref"];
} else {
ordered_dataset[i]["start_ref"] = ordered_dataset[i]["high_ref"];
}
}
// Set experiment trials
this.experiment_conditions_constants = ordered_dataset;
}
/**
* Creates the practice dataset by taking the first FOUR subconditions.
*
* @param dataset {[{assoc array}, {assoc array}, ... ]} The data to be ordered.
*/
prepare_practice() {
let dataset = this.raw_constants;
let practice_dataset = [];
for (let i=0; i < 4; i++){
practice_dataset[i] = dataset[i];
this.practice_trial_data[i] = [];
// Alternate the start ref to be low or high for each subcondition
if (i%2 === 0) {
practice_dataset[i]["start_ref"] = dataset[i]["low_ref"];
} else {
practice_dataset[i]["start_ref"] = dataset[i]["high_ref"];
}
}
this.sub_conditions_constants = practice_dataset;
this.current_sub_condition_index = 0;
this.input_count_array = new Array(this.sub_conditions_constants[0].trials_per_round).fill(0);
}
/**
* Sets the subcondition order for foundational range.
* Needs to balance INDIVIDUALLY the round and test type conditions,
* then maintain that order (e.g. all test goes first, then consistency)
*
* @param dataset {[{assoc array}, {assoc array}, ... ]} The data used to be ordered.
*/
set_foundational_dataset_order(dataset) {
// To hold individual data sets according to round type
var test_dataset = [];
var consistency_dataset = [];
// To hold balanced indexes
var test_order = [];
var consistency_order = [];
// Extract dataset according to test or consistency round type
for (let subcondition of dataset) {
if (subcondition["round_type"] === "test") {
test_dataset.push(subcondition);
} else {
consistency_dataset.push(subcondition);
}
}
// Get balancing order for EACH round type dataset individually
switch(this.balancing_type) {
case 'latin_square':
test_order = initialize_latin_square(test_dataset.length);
consistency_order = initialize_latin_square(consistency_dataset.length);
break;
case 'random':
test_order = initialize_random_order(test_dataset.length);
consistency_order = initialize_random_order(consistency_dataset.length);
break;
default:
throw Error(this.balancing_type + " balancing type is not supported.");
}
// Since test dataset will run first, add index length of it to consistency order
for (let i = 0; i < consistency_order.length; i++) {
consistency_order[i] += test_dataset.length;
}
// Merge the two orders
this.sub_condition_order = test_order.concat(consistency_order);
}
/**
* Resets all relevant variables to now use the test version.
* (input_count_array, sub_conditions_constants, and current_sub_condition_index
* are shared variables between the practice and test trials).
*
* This function is called once all the practice trials have run.
*/
end_practice_experiment() {
this.sub_conditions_constants = this.experiment_conditions_constants;
this.input_count_array = new Array(this.sub_conditions_constants[0].trials_per_round).fill(0);
this.current_sub_condition_index = 0;
}
/**
* Calculates exclusion criteria using standard deviation and variance.
* Subcondition is flagged if:
* - Standard deviation > 0.2
* - Anchoring > 0.6
*
* @ return HTML of subcondition data to print onto screen
*/
calculate_exclusion_criteria() {
let string = "";
for (let i = 0; i < Object.keys(this.practice_trial_data).length; i++) {
let subcondition_data = this.practice_trial_data[i];
let mids = this.get_estimated_mids(subcondition_data);
let std_dev = this.get_standard_deviation(mids);
let anchoring_value = this.get_anchoring_value(mids);
let rounded_mids = [];
for (let mid of mids) {
rounded_mids.push(mid.toFixed(3));
}
let anchoring_color = "BLACK";
if (anchoring_value > 0.5) {
anchoring_color = "RED";
}
let std_dev_color = "BLACK";
if (std_dev > 0.2) {
std_dev_color = "RED";
}
string += `
<div align = "center" style = "text-align: left; float:left; width: 20vw">
<font size = 2><b> Subcondition: ${i+1} </b>
<br>
Midpoint values: ${rounded_mids}
<br>
<font color = ${std_dev_color}> Standard Deviation: ${std_dev} </font>
<br>
<font color = ${anchoring_color}> Anchoring Value: ${anchoring_value} </font>
<br>
<br>
</font>
</div>
`
}
return string;
}
/**
* Calculates the standard deviation for the specified subcondition.
* @ param {array} array of estimated mids for that trial
*
* @ return {double} standard deviation
*/
get_standard_deviation(estimated_mids) {
let values = [];
// Calculate mean:
let mean = 0;
for (let mid of estimated_mids) {
mean += mid;
}
mean = mean / estimated_mids.length;
// Calculate variance:
let variance = 0;
for (let mid of estimated_mids) {
variance += Math.pow(mid - mean, 2);
}
variance = variance / (estimated_mids.length - 1);
return Math.sqrt(variance).toFixed(3);
}
/**
* Calculates the anchoring value for the specified subcondition.
* @ param {array} array of estimated mids for that trial
*
* @ return {double} anchoring value
*/
get_anchoring_value(estimated_mids) {
let high_ref_trial_sum = 0;
let low_ref_trial_sum = 0;
// Iterate through each estimated mid (trial) of a given subcondition
for (let i = 0; i < estimated_mids.length; i++) {
// Evens have the low ref as their starter
if (i % 2 === 0) {
low_ref_trial_sum += estimated_mids[i];
} else {
high_ref_trial_sum += estimated_mids[i];
}
}
return Math.abs(high_ref_trial_sum - low_ref_trial_sum).toFixed(3);
}
/**
* Retrieves the estimated midpoints of each trial for the subcondition.
*
* @ return {array} of estimated mids
*/
get_estimated_mids(subcondition_data) {
let estimated_mids = [];
for (let trial of subcondition_data) {
estimated_mids.push(trial.estimated_mid);
}
return estimated_mids;
}
/**
* Generates a Stevens 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 stevens_exp = this;
var address = location.protocol + "//" + location.hostname + ":" + location.port + "/stevens_trial";
var trial = {
type:'external-html-keyboard-response',
url: address,
choices: [77, 90, 32, 81], // m = 77 (up), z = 90 (down), 32 = spacebar, 81 = q (exit button for debugging)
execute_script: true,
response_ends_trial: true,
data: {},
on_start: function(trial){ // NOTE: on_start takes in trial var
// Set the constants to be used:
var index = stevens_exp.current_sub_condition_index;
var constants = stevens_exp.sub_conditions_constants[index];
// Retrieve data from last trial:
var last_stevens_trial = stevens_exp.get_last_trial(trial, block_type, index);
// Handling saving the data:
stevens_exp.handle_data_saving(trial, block_type, constants, estimated_correlation, last_stevens_trial, index);
// Set the estimated correlation
var estimated_correlation = stevens_exp.update_estimated_correlation(trial, constants, last_stevens_trial);
console.log("round refreshes: " + trial.data.round_refreshes);
console.log("trial/round num: " + trial.data.trial_num);
console.log("num adjustments: " + trial.data.num_adjustments);
console.log("input count per trial: " + stevens_exp.input_count_array);
// Generate distributions
var high_coordinates = generateDistribution(constants.high_ref,
constants.error,
constants.num_points,
constants.num_SD,
constants.mean,
constants.SD);
var low_coordinates = generateDistribution(constants.low_ref,
constants.error,
constants.num_points,
constants.num_SD,
constants.mean,
constants.SD);
var estimated_coordinates = generateDistribution(estimated_correlation,
constants.error,
constants.num_points,
constants.num_SD,
constants.mean,
constants.SD);
// If there is a distractor population, generate it:
if (stevens_exp.condition_group === "distractor") {
stevens_exp.generate_distractor_coordinates(constants);
}
// Randomize position of the high and low correlated graphs for a given round
if (trial.data.round_refreshes == 1){
var result = randomize_position(trial,
high_coordinates,
low_coordinates,
constants.high_ref,
constants.low_ref);
trial.data.high_ref_is_right = result.base_is_right;
}
if (trial.data.high_ref_is_right){
stevens_exp.right_coordinates = high_coordinates;
stevens_exp.left_coordinates = low_coordinates;
stevens_exp.coordinates = [low_coordinates, estimated_coordinates, high_coordinates];
}
else{
stevens_exp.right_coordinates = low_coordinates;
stevens_exp.left_coordinates = high_coordinates;
stevens_exp.coordinates = [high_coordinates, estimated_coordinates, low_coordinates];
}
stevens_exp.trial_data = trial.data;
console.log("[RIGHT] Correlation: " + trial.data.right_correlation);
console.log("[MIDPOINT] Correlation: " + trial.data.estimated_mid);
console.log("[LEFT] Correlation: " + trial.data.left_correlation);
}
};
return trial;
}
/**
* Will generate the distractor coordinates and save them to the instance.
*
* @param {object} constants (for the given trial)
*/
generate_distractor_coordinates(constants) {
let left_dist_coordinates = generateDistribution(constants.dist_base,
constants.dist_error,
constants.dist_num_points,
constants.num_SD,
constants.mean,
constants.SD);
let middle_dist_coordinates = generateDistribution(constants.dist_base,
constants.dist_error,
constants.dist_num_points,
constants.num_SD,
constants.mean,
constants.SD);
let right_dist_coordinates = generateDistribution(constants.dist_base,
constants.dist_error,
constants.dist_num_points,
constants.num_SD,
constants.mean,
constants.SD);
this.distractor_coordinates = [left_dist_coordinates, middle_dist_coordinates, right_dist_coordinates];
}
/**
* Retrieves the last stevens trial depending on block_type for a
* given sub condition index.
* If this is the first trial of a given block_type, returns null.
*
* @param trial {object}
* block_type {string} "test" or "practice"
* index {integer}
* @return last_stevens_trial {object}
*/
get_last_trial(trial, block_type, index) {
var last_stevens_trial;
trial.data.type = "stevens";
// Set trial run_type depending on block type
// (we need to set trial's run_type so we can do the filter in the
// next if block)
if (block_type == "test"){
trial.data.run_type = "test";
}
else{
trial.data.run_type = "practice";
}
// Retrieve previous stevens trial if it exists
if (block_type == "practice" && jsPsych.data.get().filter({type: "stevens", run_type: "practice", sub_condition: index}).last(1).values()[0]){
last_stevens_trial = jsPsych.data.get().filter({type: "stevens", run_type: "practice", sub_condition: index}).last(1).values()[0];
}
else if (block_type == "test" && jsPsych.data.get().filter({type: "stevens", run_type: "test", sub_condition: index}).last(1).values()[0]){
last_stevens_trial = jsPsych.data.get().filter({type: "stevens", run_type: "test", sub_condition: index}).last(1).values()[0];
}
else{
last_stevens_trial = null;
}
return last_stevens_trial;
}
/**
* 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)
* this.trial_variables =
* {type: 'stevens',
* run_type: '',
* left_correlation: '',
* right_correlation: '',
* round_refreshes: 0, // Number of times there is a refresh for a given round
* high_ref_is_right: false
* start_ref: ''
* };
*
* These are variables created WITHIN the trial logic that were not present in excel (but need to be
* outputted to results).
* this.export_variables =
* {trial_num: 0, // Round index trial is currently on (aka trial_num from excel)
* sub_condition: '', // Chronological ordering of sub_condition [1, 2, 3 ... ]
* balanced_sub_condition: '', // Index of sub_condition according to balancing order
* estimated_mid: '',
* num_adjustments: 0, // Number of inputs for a given round (aka num_adjustments from excel)
* trials_per_round: '',
* };
*
* @param trial {object}
* block_type {string} "test" or "practice"
* constants {assoc array}
* estimated_correlation {double}
* last_stevens_trial {object}
* index {integer}
*/
handle_data_saving(trial, block_type, constants, estimated_correlation, last_stevens_trial, index) {
trial.data = Object.assign({}, trial.data, constants);
trial.data.sub_condition = index;
trial.data.balanced_sub_condition = this.sub_condition_order[index];
trial.trial_duration = trial.data.regen_rate;
// If trial is still part of same sub-condition, carry over constants from
// the previous trial
if (last_stevens_trial){
trial.data.step_size = last_stevens_trial.step_size;
trial.data.right_correlation = last_stevens_trial.right_correlation;
trial.data.left_correlation = last_stevens_trial.left_correlation;
trial.data.high_ref_is_right = last_stevens_trial.high_ref_is_right;
// If a round has just ended:
// - increment the trial_num
// - set refresh number back to 1
// - reset the number of adjustments to 0
// - swap the start ref to be high/low depending on the previous round's start ref
if (this.round_end == true){
trial.data.trial_num = last_stevens_trial.trial_num + 1;
trial.data.num_adjustments = 0;
trial.data.round_refreshes = 1;
if (last_stevens_trial.start_ref === constants.high_ref) {
trial.data.start_ref = constants.low_ref;
} else {
trial.data.start_ref = constants.high_ref;
}
this.round_end = false; //Reset flag
}
// Else trial_num, num_adjustments and start_ref is the same, but round_refresh ++
else{
trial.data.trial_num = last_stevens_trial.trial_num;
trial.data.num_adjustments = last_stevens_trial.num_adjustments;
trial.data.start_ref = last_stevens_trial.start_ref;
trial.data.round_refreshes = last_stevens_trial.round_refreshes + 1;
}
}
// Else this is the first refresh of a given trial
else{
trial.data.trial_num = 0;
trial.data.num_adjustments = 0;
trial.data.round_refreshes = 1;
}
}
/**
* Updates the estimated correlation.
* If :
* Is the first trial, will initialize the correlation and step size.
* Else:
* If there was a key press in previous trial, will calculate the
* the estimated correlation (depending on whether it was an inc or dec).
* Else if no key press in previous trial, will set estimated correlation
* to the previous trial's.
*
* @param trial {object}
* constants {object}
* last_trial {object}
* @return estimated_correlation {double}
*/
update_estimated_correlation(trial, constants, last_trial) {
var estimated_correlation;
var index = this.current_sub_condition_index;
// If first trial (estimated_correlation is null), so initialize
// estimated midpoint and set step size:
if (trial.data.round_refreshes == 1){
//Initialize the estimated midpoint correlation:
//estimated_correlation = Math.random() < 0.5 ? constants.low_ref : constants.high_ref;
estimated_correlation = trial.data.start_ref;
trial.data.estimated_mid = estimated_correlation;
trial.data.step_size = (constants.high_ref - constants.low_ref) / this.MAX_STEP_INTERVAL;
}
// If there is input on PREVIOUS trial, change the midpoint + increment trial number
// (Since we are plotting the new middle graph based on PREVIOUS input, we look
// at the last_trials's estimated_correlation and step size.)
else if (last_trial.key_press && (last_trial.key_press == trial.choices[0] || last_trial.key_press == trial.choices[1])){
// Need to check that if hits either high or low ref, it DOESN'T count as a num_adjustment
let is_unchanged = false;
switch (last_trial.key_press){
case trial.choices[0]: // up
estimated_correlation = Math.min(constants.high_ref, last_trial.estimated_mid + (Math.random() * last_trial.step_size));
// If they've hit the max (high_ref)
if (estimated_correlation === constants.high_ref) {
is_unchanged = true;
}
break;
case trial.choices[1]: // down
estimated_correlation = Math.max(constants.low_ref, last_trial.estimated_mid - (Math.random() * last_trial.step_size));
// If they've hit the min (low_ref)
if (estimated_correlation === constants.low_ref) {
is_unchanged = true;
}
break;
}
// For valid changes (i.e not going beyond max or below min), can then
// increment num_adjustments
if (!is_unchanged){
trial.data.num_adjustments = last_trial.num_adjustments + 1;
this.input_count_array[trial.data.trial_num]++;
}
}
// Else use the previous trial's midpoint
else{
// QUESTION: If there is user input, on the next viz, the graph will display
// that estimated correlation. However, AFTER that, we are changing the
// estimated correlation??
// Based on StevensTrial.java (line 75), the estimated midpoint gets updated this way:
// var prev_constants = this.sub_conditions_constants[current_sub_condition_index-1];
// if (last_trial.estimated_correlation == prev_constants.high_ref){
// estimated_midpoint = constants.low_ref;
// }
// else{
// estimated_midpoint = constants.high_ref;
// }
estimated_correlation = last_trial.estimated_mid;
}
// Update the trial's estimated_mid
trial.data.estimated_mid = estimated_correlation;
return estimated_correlation;
}
/**
* Determines whether the round can end or not. A round can end ONLY if
* there has been at least 1 input from the user on the given round
*
* @return {boolean} True if sub condition should end.
*/
end_round() {
let last_trial = jsPsych.data.get().last(1).values()[0];
return !(last_trial.num_adjustments === 0);
}
/**
* Determines whether the current sub condition can end or not.
*
* @return {boolean} True if sub condition should end.
*/
end_sub_condition() {
var trials_per_round = this.sub_conditions_constants[0].trials_per_round;
if (this.input_count_array[trials_per_round - 1] == 0){
return false;
}
else{
// Reset array
this.input_count_array = new Array(this.sub_conditions_constants[0].trials_per_round).fill(0);
return true;
}
}
/**
* When called, will save individual trial data into a CSV.
*/
export_trial_data() {
var csv = 'condition,trial_num,sub_condition,balanced_sub_condition,high_ref,estimated_mid,low_ref,num_adjustments,trials_per_round,error,sum_rt,num_points,mean,SD,num_SD,round_type,step_size,point_color,background_color,text_color,axis_color,point_size,regen_rate\n';
// Get most recent subcondition - will have the max subcondition value
var max_sub_condition = jsPsych.data.get().filter({type: 'stevens', run_type: 'test'}).last(1).values()[0].sub_condition;
var data = [];
// Iterate through each sub condition
for (let i = 0; i<=max_sub_condition; i++){
var condition_data = jsPsych.data.get().filter({type: 'stevens', run_type: 'test', sub_condition: i})
.filterCustom(function(x){ //Don't include trials with no user input
return x.rt != null;
});
var condition_values = condition_data.values()[0];
var max_trial_num = condition_data.last(1).values()[0].trial_num; //The last trial of this sub-condition
//has the last trial num
// Iterate through each trial of a given sub condition
for (let j = 0; j<=max_trial_num; j++){
//Data for a given trial of a sub condition
var trial_data = condition_data.filter({trial_num: j});
//Take the last trial's estimated mid since we want the most recent value
var last_estimated_mid = trial_data.last(1).values()[0].estimated_mid;
var last_num_adjustments = trial_data.last(1).values()[0].num_adjustments;
var sum_rt = trial_data.filterCustom(function(x){ return x.key_press != 81 }) //Don't use the exit trial rt
.select('rt')
.sum();
var row = [this.condition_name];
row.push(j+1);
row.push(condition_values.sub_condition);
row.push(condition_values.balanced_sub_condition);
row.push(condition_values.high_ref);
row.push(last_estimated_mid);
row.push(condition_values.low_ref);
row.push(last_num_adjustments);
row.push(condition_values.trials_per_round);
row.push(condition_values.error);
row.push(sum_rt);
row.push(condition_values.num_points);
row.push(condition_values.mean);
row.push(condition_values.SD);
row.push(condition_values.num_SD);
row.push(condition_values.round_type);
row.push(condition_values.step_size);
row.push(condition_values.point_color);
row.push(condition_values.background_color);
row.push(condition_values.text_color);
row.push(condition_values.axis_color);
row.push(condition_values.point_size);
row.push(condition_values.regen_rate);
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 + "_stevens_trial_results.csv";
hiddenElement.click();
}
/**
* When called, will save aggregated trial data into a CSV.
*/
export_summary_data() {
var csv = 'SUBJECT_ID,SUBJECT_INITIALS,ROUND_TYPE,NUM_TRIALS,HIGH_REF,ESTIMATED_MIDPOINT,LOW_REF\n';
var data = [];
// Organize each row of the csv
for (let i = 0; i<this.sub_conditions_constants.length; i++){
var row = [];
var constants = this.sub_conditions_constants[i];
var condition_data = jsPsych.data.get().filter({type: 'stevens', run_type: 'test', balanced_sub_condition: this.sub_condition_order[i]})
.filterCustom(function(x){ //Don't include the exit trials
return x.correct != -1;
})
.filterCustom(function(x){ //Don't include trials with no user input
return x.rt != null;
});
row.push(this.subject_id);
row.push(this.subject_initials);
row.push(constants.round_type);
row.push(constants.trials_per_round);
row.push(constants.high_ref);
row.push(condition_data.select('estimated_mid').mean());
row.push(constants.low_ref);
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 + "_stevens_summary_results.csv";
hiddenElement.click();
}
}