From 45f6840b6b58335314730ca053f53ee4331f5ce5 Mon Sep 17 00:00:00 2001 From: Chris Jungerius Date: Mon, 11 May 2020 11:34:39 +0200 Subject: [PATCH 01/29] added original version of canvas keyboard response plugin --- plugins/jspsych-canvas-keyboard-response.js | 149 ++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 plugins/jspsych-canvas-keyboard-response.js diff --git a/plugins/jspsych-canvas-keyboard-response.js b/plugins/jspsych-canvas-keyboard-response.js new file mode 100644 index 00000000..01281be0 --- /dev/null +++ b/plugins/jspsych-canvas-keyboard-response.js @@ -0,0 +1,149 @@ +/** + * jspsych-canvas-keyboard-response + * Chris Jungerius (modified from Josh de Leeuw) + * + * plugin for displaying a canvas stimulus and getting a keyboard response + * + * documentation: TODO + * + **/ + + +jsPsych.plugins["canvas-keyboard-response"] = (function () { + + var plugin = {}; + + plugin.info = { + name: 'canvas-keyboard-response', + description: '', + parameters: { + drawing: { + type: jsPsych.plugins.parameterType.FUNCTION, + pretty_name: 'Drawing', + default: function (c) { let ctx = c.getContext("2d"); ctx.font = "30px Arial"; ctx.textAlign = "center"; ctx.fillText("Canvas Element", c.width / 2, c.height / 2) }, + description: 'the drawing function to apply to the canvas, should take the canvas object as argument' + }, + choices: { + type: jsPsych.plugins.parameterType.KEYCODE, + array: true, + pretty_name: 'Choices', + default: jsPsych.ALL_KEYS, + description: 'The keys the subject is allowed to press to respond to the stimulus.' + }, + prompt: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Prompt', + default: null, + description: 'Any content here will be displayed below the stimulus.' + }, + stimulus_duration: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Stimulus duration', + default: null, + description: 'How long to hide the stimulus.' + }, + trial_duration: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Trial duration', + default: null, + description: 'How long to show trial before it ends.' + }, + response_ends_trial: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Response ends trial', + default: true, + description: 'If true, trial will end when subject makes a response.' + }, + + } + } + + plugin.trial = function (display_element, trial) { + + var new_html = '
' + '' + '
'; + // add prompt + if (trial.prompt !== null) { + new_html += trial.prompt; + } + + // draw + display_element.innerHTML = new_html; + let c = document.getElementById("myCanvas") + trial.drawing(c) + // store response + var response = { + rt: null, + key: null + }; + + // function to end trial when it is time + var end_trial = function () { + + // kill any remaining setTimeout handlers + jsPsych.pluginAPI.clearAllTimeouts(); + + // kill keyboard listeners + if (typeof keyboardListener !== 'undefined') { + jsPsych.pluginAPI.cancelKeyboardResponse(keyboardListener); + } + + // gather the data to store for the trial + var trial_data = { + "rt": response.rt, + "stimulus": trial.drawing, + "key_press": response.key + }; + + // clear the display + display_element.innerHTML = ''; + + // move on to the next trial + jsPsych.finishTrial(trial_data); + }; + + // function to handle responses by the subject + var after_response = function (info) { + + // after a valid response, the stimulus will have the CSS class 'responded' + // which can be used to provide visual feedback that a response was recorded + display_element.querySelector('#jspsych-canvas-keyboard-response-stimulus').className += ' responded'; + + // only record the first response + if (response.key == null) { + response = info; + } + + if (trial.response_ends_trial) { + end_trial(); + } + }; + + // start the response listener + if (trial.choices != jsPsych.NO_KEYS) { + var keyboardListener = jsPsych.pluginAPI.getKeyboardResponse({ + callback_function: after_response, + valid_responses: trial.choices, + rt_method: 'performance', + persist: false, + allow_held_key: false + }); + } + + // hide stimulus if stimulus_duration is set + if (trial.stimulus_duration !== null) { + jsPsych.pluginAPI.setTimeout(function () { + display_element.querySelector('#jspsych-canvas-keyboard-response-stimulus').style.visibility = 'hidden'; + }, trial.stimulus_duration); + } + + // end trial if trial_duration is set + if (trial.trial_duration !== null) { + jsPsych.pluginAPI.setTimeout(function () { + end_trial(); + }, trial.trial_duration); + } + + }; + + return plugin; +})(); From 9f864f36021d8543ca9df3e3a84ada4b78a4ba48 Mon Sep 17 00:00:00 2001 From: Chris Jungerius Date: Mon, 11 May 2020 12:06:17 +0200 Subject: [PATCH 02/29] added skeleton documentation for canvas-keyboard-response --- .../jspsych-canvas-keyboard-response.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 docs/plugins/jspsych-canvas-keyboard-response.md diff --git a/docs/plugins/jspsych-canvas-keyboard-response.md b/docs/plugins/jspsych-canvas-keyboard-response.md new file mode 100644 index 00000000..e8885787 --- /dev/null +++ b/docs/plugins/jspsych-canvas-keyboard-response.md @@ -0,0 +1,19 @@ +# jspsych-canvas-keyboard-response + + +## Parameters + +Parameters with a default value of undefined must be specified. Other parameters can be left unspecified if the default value is acceptable. + +Parameter | Type | Default Value | Description +----------|------|---------------|------------ + +## Data Generated + +In addition to the [default data collected by all plugins](overview#datacollectedbyplugins), this plugin collects the following data for each trial. + +Name | Type | Value +-----|------|------ + + +## Examples \ No newline at end of file From d1437ea9dd6db8862726b2942c5feb53d0a3c416 Mon Sep 17 00:00:00 2001 From: Chris Jungerius Date: Mon, 11 May 2020 13:21:25 +0200 Subject: [PATCH 03/29] cleaned up canvas-keyboard-response and added option to resize canvas --- plugins/jspsych-canvas-keyboard-response.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/plugins/jspsych-canvas-keyboard-response.js b/plugins/jspsych-canvas-keyboard-response.js index 01281be0..e9dacbc1 100644 --- a/plugins/jspsych-canvas-keyboard-response.js +++ b/plugins/jspsych-canvas-keyboard-response.js @@ -19,7 +19,7 @@ jsPsych.plugins["canvas-keyboard-response"] = (function () { parameters: { drawing: { type: jsPsych.plugins.parameterType.FUNCTION, - pretty_name: 'Drawing', + pretty_name: 'Stimulus', default: function (c) { let ctx = c.getContext("2d"); ctx.font = "30px Arial"; ctx.textAlign = "center"; ctx.fillText("Canvas Element", c.width / 2, c.height / 2) }, description: 'the drawing function to apply to the canvas, should take the canvas object as argument' }, @@ -54,13 +54,19 @@ jsPsych.plugins["canvas-keyboard-response"] = (function () { default: true, description: 'If true, trial will end when subject makes a response.' }, + canvas_size: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Canvas size', + default: [500, 500], + description: 'The height and width of the canvas element' + } } } plugin.trial = function (display_element, trial) { - var new_html = '
' + '' + '
'; + var new_html = '
' + '' + '
'; // add prompt if (trial.prompt !== null) { new_html += trial.prompt; @@ -69,7 +75,7 @@ jsPsych.plugins["canvas-keyboard-response"] = (function () { // draw display_element.innerHTML = new_html; let c = document.getElementById("myCanvas") - trial.drawing(c) + trial.stimulus(c) // store response var response = { rt: null, From 04d71b715dc36e2e9f908e4b6c3206ecf8e66725 Mon Sep 17 00:00:00 2001 From: Chris Jungerius Date: Mon, 11 May 2020 13:45:51 +0200 Subject: [PATCH 04/29] added canvas-slider-response; changed canvas ID to a more fitting name --- plugins/jspsych-canvas-keyboard-response.js | 8 +- plugins/jspsych-canvas-slider-response.js | 204 ++++++++++++++++++++ 2 files changed, 208 insertions(+), 4 deletions(-) create mode 100644 plugins/jspsych-canvas-slider-response.js diff --git a/plugins/jspsych-canvas-keyboard-response.js b/plugins/jspsych-canvas-keyboard-response.js index e9dacbc1..388c7382 100644 --- a/plugins/jspsych-canvas-keyboard-response.js +++ b/plugins/jspsych-canvas-keyboard-response.js @@ -17,10 +17,10 @@ jsPsych.plugins["canvas-keyboard-response"] = (function () { name: 'canvas-keyboard-response', description: '', parameters: { - drawing: { + stimulus: { type: jsPsych.plugins.parameterType.FUNCTION, pretty_name: 'Stimulus', - default: function (c) { let ctx = c.getContext("2d"); ctx.font = "30px Arial"; ctx.textAlign = "center"; ctx.fillText("Canvas Element", c.width / 2, c.height / 2) }, + default: undefined, description: 'the drawing function to apply to the canvas, should take the canvas object as argument' }, choices: { @@ -66,7 +66,7 @@ jsPsych.plugins["canvas-keyboard-response"] = (function () { plugin.trial = function (display_element, trial) { - var new_html = '
' + '' + '
'; + var new_html = '
' + '' + '
'; // add prompt if (trial.prompt !== null) { new_html += trial.prompt; @@ -74,7 +74,7 @@ jsPsych.plugins["canvas-keyboard-response"] = (function () { // draw display_element.innerHTML = new_html; - let c = document.getElementById("myCanvas") + let c = document.getElementById("stimulus-canvas") trial.stimulus(c) // store response var response = { diff --git a/plugins/jspsych-canvas-slider-response.js b/plugins/jspsych-canvas-slider-response.js new file mode 100644 index 00000000..351ff9ae --- /dev/null +++ b/plugins/jspsych-canvas-slider-response.js @@ -0,0 +1,204 @@ +/** + * jspsych-canvas-slider-response + * a jspsych plugin for free response survey questions + * + * Chris jungerius (modified from Josh de Leeuw) + * + * documentation: docs.jspsych.org + * + */ + + +jsPsych.plugins['canvas-slider-response'] = (function () { + + var plugin = {}; + + plugin.info = { + name: 'canvas-slider-response', + description: '', + parameters: { + stimulus: { + type: jsPsych.plugins.parameterType.HTML_STRING, + pretty_name: 'Stimulus', + default: undefined, + description: 'the drawing function to apply to the canvas, should take the canvas object as argument' + }, + min: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Min slider', + default: 0, + description: 'Sets the minimum value of the slider.' + }, + max: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Max slider', + default: 100, + description: 'Sets the maximum value of the slider', + }, + start: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Slider starting value', + default: 50, + description: 'Sets the starting value of the slider', + }, + step: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Step', + default: 1, + description: 'Sets the step of the slider' + }, + labels: { + type: jsPsych.plugins.parameterType.HTML_STRING, + pretty_name: 'Labels', + default: [], + array: true, + description: 'Labels of the slider.', + }, + slider_width: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Slider width', + default: null, + description: 'Width of the slider in pixels.' + }, + button_label: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Button label', + default: 'Continue', + array: false, + description: 'Label of the button to advance.' + }, + require_movement: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Require movement', + default: false, + description: 'If true, the participant will have to move the slider before continuing.' + }, + prompt: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Prompt', + default: null, + description: 'Any content here will be displayed below the slider.' + }, + stimulus_duration: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Stimulus duration', + default: null, + description: 'How long to hide the stimulus.' + }, + trial_duration: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Trial duration', + default: null, + description: 'How long to show the trial.' + }, + response_ends_trial: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Response ends trial', + default: true, + description: 'If true, trial will end when user makes a response.' + }, + canvas_size: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Canvas size', + default: [500, 500], + description: 'The height and width of the canvas element' + } + + } + } + + plugin.trial = function (display_element, trial) { + + var html = '
'; + html += '
' + '' + '
'; + html += '
'; + html += ''; + html += '
' + for (var j = 0; j < trial.labels.length; j++) { + var width = 100 / (trial.labels.length - 1); + var left_offset = (j * (100 / (trial.labels.length - 1))) - (width / 2); + html += '
'; + html += '' + trial.labels[j] + ''; + html += '
' + } + html += '
'; + html += '
'; + html += '
'; + + if (trial.prompt !== null) { + html += trial.prompt; + } + + // add submit button + html += ''; + + display_element.innerHTML = html; + + // draw + let c = document.getElementById("stimulus-canvas") + trial.stimulus(c) + + var response = { + rt: null, + response: null + }; + + if (trial.require_movement) { + display_element.querySelector('#jspsych-canvas-slider-response-response').addEventListener('change', function () { + display_element.querySelector('#jspsych-canvas-slider-response-next').disabled = false; + }) + } + + display_element.querySelector('#jspsych-canvas-slider-response-next').addEventListener('click', function () { + // measure response time + var endTime = performance.now(); + response.rt = endTime - startTime; + response.response = display_element.querySelector('#jspsych-canvas-slider-response-response').value; + + if (trial.response_ends_trial) { + end_trial(); + } else { + display_element.querySelector('#jspsych-canvas-slider-response-next').disabled = true; + } + + }); + + function end_trial() { + + jsPsych.pluginAPI.clearAllTimeouts(); + + // save data + var trialdata = { + "rt": response.rt, + "response": response.response, + "stimulus": trial.stimulus + }; + + display_element.innerHTML = ''; + + // next trial + jsPsych.finishTrial(trialdata); + } + + if (trial.stimulus_duration !== null) { + jsPsych.pluginAPI.setTimeout(function () { + display_element.querySelector('#jspsych-canvas-slider-response-stimulus').style.visibility = 'hidden'; + }, trial.stimulus_duration); + } + + // end trial if trial_duration is set + if (trial.trial_duration !== null) { + jsPsych.pluginAPI.setTimeout(function () { + end_trial(); + }, trial.trial_duration); + } + + var startTime = performance.now(); + }; + + return plugin; +})(); From 3c284f35d5f84721be2e6cfd66fa580417b504d1 Mon Sep 17 00:00:00 2001 From: Chris Jungerius Date: Mon, 11 May 2020 13:56:20 +0200 Subject: [PATCH 05/29] added canvas-button-response --- plugins/jspsych-canvas-button-response.js | 199 ++++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 plugins/jspsych-canvas-button-response.js diff --git a/plugins/jspsych-canvas-button-response.js b/plugins/jspsych-canvas-button-response.js new file mode 100644 index 00000000..df6f612e --- /dev/null +++ b/plugins/jspsych-canvas-button-response.js @@ -0,0 +1,199 @@ +/** + * jspsych-canvas-button-response + * Chris Jungerius (modified from Josh de Leeuw) + * + * plugin for displaying a stimulus and getting a keyboard response + * + * documentation: docs.jspsych.org + * + **/ + +jsPsych.plugins["canvas-button-response"] = (function () { + + var plugin = {}; + + plugin.info = { + name: 'canvas-button-response', + description: '', + parameters: { + stimulus: { + type: jsPsych.plugins.parameterType.FUNCTION, + pretty_name: 'Stimulus', + default: undefined, + description: 'the drawing function to apply to the canvas, should take the canvas object as argument' + }, + choices: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Choices', + default: undefined, + array: true, + description: 'The labels for the buttons.' + }, + button_html: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Button HTML', + default: '', + array: true, + description: 'The html of the button. Can create own style.' + }, + prompt: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Prompt', + default: null, + description: 'Any content here will be displayed under the button.' + }, + stimulus_duration: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Stimulus duration', + default: null, + description: 'How long to hide the stimulus.' + }, + trial_duration: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Trial duration', + default: null, + description: 'How long to show the trial.' + }, + margin_vertical: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Margin vertical', + default: '0px', + description: 'The vertical margin of the button.' + }, + margin_horizontal: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Margin horizontal', + default: '8px', + description: 'The horizontal margin of the button.' + }, + response_ends_trial: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Response ends trial', + default: true, + description: 'If true, then trial will end when user responds.' + }, + canvas_size: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Canvas size', + default: [500, 500], + description: 'The height and width of the canvas element' + } + + } + } + + plugin.trial = function (display_element, trial) { + + // create canvas + var html = '
' + '' + '
'; + + //display buttons + var buttons = []; + if (Array.isArray(trial.button_html)) { + if (trial.button_html.length == trial.choices.length) { + buttons = trial.button_html; + } else { + console.error('Error in canvas-button-response plugin. The length of the button_html array does not equal the length of the choices array'); + } + } else { + for (var i = 0; i < trial.choices.length; i++) { + buttons.push(trial.button_html); + } + } + html += '
'; + for (var i = 0; i < trial.choices.length; i++) { + var str = buttons[i].replace(/%choice%/g, trial.choices[i]); + html += '
' + str + '
'; + } + html += '
'; + + //show prompt if there is one + if (trial.prompt !== null) { + html += trial.prompt; + } + display_element.innerHTML = html; + + //draw + let c = document.getElementById("stimulus-canvas") + trial.stimulus(c) + + // start time + var start_time = performance.now(); + + // add event listeners to buttons + for (var i = 0; i < trial.choices.length; i++) { + display_element.querySelector('#jspsych-canvas-button-response-button-' + i).addEventListener('click', function (e) { + var choice = e.currentTarget.getAttribute('data-choice'); // don't use dataset for jsdom compatibility + after_response(choice); + }); + } + + // store response + var response = { + rt: null, + button: null + }; + + // function to handle responses by the subject + function after_response(choice) { + + // measure rt + var end_time = performance.now(); + var rt = end_time - start_time; + response.button = choice; + response.rt = rt; + + // after a valid response, the stimulus will have the CSS class 'responded' + // which can be used to provide visual feedback that a response was recorded + display_element.querySelector('#jspsych-canvas-button-response-stimulus').className += ' responded'; + + // disable all the buttons after a response + var btns = document.querySelectorAll('.jspsych-canvas-button-response-button button'); + for (var i = 0; i < btns.length; i++) { + //btns[i].removeEventListener('click'); + btns[i].setAttribute('disabled', 'disabled'); + } + + if (trial.response_ends_trial) { + end_trial(); + } + }; + + // function to end trial when it is time + function end_trial() { + + // kill any remaining setTimeout handlers + jsPsych.pluginAPI.clearAllTimeouts(); + + // gather the data to store for the trial + var trial_data = { + "rt": response.rt, + "stimulus": trial.stimulus, + "button_pressed": response.button + }; + + // clear the display + display_element.innerHTML = ''; + + // move on to the next trial + jsPsych.finishTrial(trial_data); + }; + + // hide image if timing is set + if (trial.stimulus_duration !== null) { + jsPsych.pluginAPI.setTimeout(function () { + display_element.querySelector('#jspsych-canvas-button-response-stimulus').style.visibility = 'hidden'; + }, trial.stimulus_duration); + } + + // end trial if time limit is set + if (trial.trial_duration !== null) { + jsPsych.pluginAPI.setTimeout(function () { + end_trial(); + }, trial.trial_duration); + } + + }; + + return plugin; +})(); From a996795831d875c869a16780ff1e293a537a2a30 Mon Sep 17 00:00:00 2001 From: Chris Jungerius Date: Mon, 11 May 2020 14:41:19 +0200 Subject: [PATCH 06/29] created temp html file to test the three new plugin types, and fixed some bugs that emerged from first tests --- canvastest.html | 50 +++++++++++++++++++++++ plugins/jspsych-canvas-button-response.js | 2 +- plugins/jspsych-canvas-slider-response.js | 2 +- 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 canvastest.html diff --git a/canvastest.html b/canvastest.html new file mode 100644 index 00000000..bcf16862 --- /dev/null +++ b/canvastest.html @@ -0,0 +1,50 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/jspsych-canvas-button-response.js b/plugins/jspsych-canvas-button-response.js index df6f612e..56ecb0cc 100644 --- a/plugins/jspsych-canvas-button-response.js +++ b/plugins/jspsych-canvas-button-response.js @@ -103,7 +103,7 @@ jsPsych.plugins["canvas-button-response"] = (function () { html += '
'; for (var i = 0; i < trial.choices.length; i++) { var str = buttons[i].replace(/%choice%/g, trial.choices[i]); - html += '
' + str + '
'; + html += '
' + str + '
'; } html += '
'; diff --git a/plugins/jspsych-canvas-slider-response.js b/plugins/jspsych-canvas-slider-response.js index 351ff9ae..3f9b16b2 100644 --- a/plugins/jspsych-canvas-slider-response.js +++ b/plugins/jspsych-canvas-slider-response.js @@ -18,7 +18,7 @@ jsPsych.plugins['canvas-slider-response'] = (function () { description: '', parameters: { stimulus: { - type: jsPsych.plugins.parameterType.HTML_STRING, + type: jsPsych.plugins.parameterType.FUNCTION, pretty_name: 'Stimulus', default: undefined, description: 'the drawing function to apply to the canvas, should take the canvas object as argument' From 24d391fb5164ef0c612e3359a6c349791e34369f Mon Sep 17 00:00:00 2001 From: Chris Jungerius Date: Mon, 11 May 2020 14:46:52 +0200 Subject: [PATCH 07/29] confirmed trials are outputting all data correctly --- canvastest.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/canvastest.html b/canvastest.html index bcf16862..5869fbc4 100644 --- a/canvastest.html +++ b/canvastest.html @@ -43,7 +43,8 @@ timeline.push(sliderTrial) jsPsych.init({ - timeline: timeline + timeline: timeline, + on_trial_finish: function(data){console.log(data)} }) From c813314f98073f28bb8feb35865aa0a2738ef99e Mon Sep 17 00:00:00 2001 From: Chris Jungerius Date: Mon, 11 May 2020 14:50:42 +0200 Subject: [PATCH 08/29] removed testing html --- canvastest.html | 51 ------------------------------------------------- 1 file changed, 51 deletions(-) delete mode 100644 canvastest.html diff --git a/canvastest.html b/canvastest.html deleted file mode 100644 index 5869fbc4..00000000 --- a/canvastest.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - \ No newline at end of file From f17c0ddb6002a8bb9cf0456a966446073544e3b2 Mon Sep 17 00:00:00 2001 From: Chris Jungerius Date: Mon, 11 May 2020 14:55:58 +0200 Subject: [PATCH 09/29] started button and slider response docs --- .../plugins/jspsych-canvas-button-response.md | 19 +++++++++++++++++++ .../plugins/jspsych-canvas-slider-response.md | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 docs/plugins/jspsych-canvas-button-response.md create mode 100644 docs/plugins/jspsych-canvas-slider-response.md diff --git a/docs/plugins/jspsych-canvas-button-response.md b/docs/plugins/jspsych-canvas-button-response.md new file mode 100644 index 00000000..fda3abed --- /dev/null +++ b/docs/plugins/jspsych-canvas-button-response.md @@ -0,0 +1,19 @@ +# jspsych-canvas-button-response + + +## Parameters + +Parameters with a default value of undefined must be specified. Other parameters can be left unspecified if the default value is acceptable. + +Parameter | Type | Default Value | Description +----------|------|---------------|------------ + +## Data Generated + +In addition to the [default data collected by all plugins](overview#datacollectedbyplugins), this plugin collects the following data for each trial. + +Name | Type | Value +-----|------|------ + + +## Examples \ No newline at end of file diff --git a/docs/plugins/jspsych-canvas-slider-response.md b/docs/plugins/jspsych-canvas-slider-response.md new file mode 100644 index 00000000..a350d2d3 --- /dev/null +++ b/docs/plugins/jspsych-canvas-slider-response.md @@ -0,0 +1,19 @@ +# jspsych-canvas-slider-response + + +## Parameters + +Parameters with a default value of undefined must be specified. Other parameters can be left unspecified if the default value is acceptable. + +Parameter | Type | Default Value | Description +----------|------|---------------|------------ + +## Data Generated + +In addition to the [default data collected by all plugins](overview#datacollectedbyplugins), this plugin collects the following data for each trial. + +Name | Type | Value +-----|------|------ + + +## Examples \ No newline at end of file From c66f74fec4a1f8f9201b1575a8af3f8a26f406d5 Mon Sep 17 00:00:00 2001 From: Chris Jungerius Date: Mon, 11 May 2020 15:30:15 +0200 Subject: [PATCH 10/29] finished canvas-keyboard-response documentation --- .../jspsych-canvas-keyboard-response.md | 50 ++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/docs/plugins/jspsych-canvas-keyboard-response.md b/docs/plugins/jspsych-canvas-keyboard-response.md index e8885787..464dc4d2 100644 --- a/docs/plugins/jspsych-canvas-keyboard-response.md +++ b/docs/plugins/jspsych-canvas-keyboard-response.md @@ -1,5 +1,6 @@ # jspsych-canvas-keyboard-response +This plugin can be used to draw an image on a JavaScript canvas element, which can be useful for displaying parametrically defined shapes, and records responses generated with the keyboard. The stimulus can be displayed until a response is given, or for a pre-determined amount of time. The trial can be ended automatically if the subject has failed to respond within a fixed length of time. ## Parameters @@ -7,6 +8,13 @@ Parameters with a default value of undefined must be specified. Other parameters Parameter | Type | Default Value | Description ----------|------|---------------|------------ +stimulus | function | *undefined* | The function to draw on the canvas. This function must take a canvas element as its only argument, e.g. `foo(c)`. Note that the function will still generally need to set the correct context itself, using a line like let `ctx = c.getContext("2d")`. +canvas_size | array | [500, 500] | The size of the canvas element in pixels. +choices | array of keycodes | `jsPsych.ALL_KEYS` | This array contains the keys that the subject is allowed to press in order to respond to the stimulus. Keys can be specified as their [numeric key code](http://www.cambiaresearch.com/articles/15/javascript-char-codes-key-codes) or as characters (e.g., `'a'`, `'q'`). The default value of `jsPsych.ALL_KEYS` means that all keys will be accepted as valid responses. Specifying `jsPsych.NO_KEYS` will mean that no responses are allowed. +prompt | string | null | This string can contain HTML markup. Any content here will be displayed below the stimulus. The intention is that it can be used to provide a reminder about the action the subject is supposed to take (e.g., which key to press). +stimulus_duration | numeric | null | How long to display the stimulus in milliseconds. The visibility CSS property of the stimulus will be set to `hidden` after this time has elapsed. If this is null, then the stimulus will remain visible until the trial ends. +trial_duration | numeric | null | How long to wait for the subject to make a response before ending the trial in milliseconds. If the subject fails to make a response before this timer is reached, the subject's response will be recorded as null for the trial and the trial will end. If the value of this parameter is null, then the trial will wait for a response indefinitely. +response_ends_trial | boolean | true | If true, then the trial will end whenever the subject makes a response (assuming they make their response before the cutoff specified by the `timing_response` parameter). If false, then the trial will continue until the value for `trial_duration` is reached. You can use this parameter to force the subject to view a stimulus for a fixed amount of time, even if they respond before the time is complete. ## Data Generated @@ -14,6 +22,46 @@ In addition to the [default data collected by all plugins](overview#datacollecte Name | Type | Value -----|------|------ +key_press | numeric | Indicates which key the subject pressed. The value is the [numeric key code](http://www.cambiaresearch.com/articles/15/javascript-char-codes-key-codes) corresponding to the subject's response. +rt | numeric | The response time in milliseconds for the subject to make a response. The time is measured from when the stimulus first appears on the screen until the subject's response. +stimulus | function | The function that was drawn. +## Examples -## Examples \ No newline at end of file +### Displaying a drawing until subject gives a response + +```javascript + +function drawRect(c){ + var ctx = c.getContext('2d'); + ctx.beginPath(); + ctx.rect(30, 30, 200, 50); + ctx.stroke(); +} + +var trial = { + type: 'canvas-keyboard-response', + stimulus: drawRect, + choices: ['e','i'], + prompt: '

is this a circle or a rectangle? press "e" for circle and "i" for rectangle

', +} +``` + +### Displaying a circle for 1 second, no response allowed + +```javascript + +function drawCirc(c){ + var ctx = c.getContext('2d'); + ctx.beginPath(); + ctx.arc(100, 75, 50, 0, 2 * Math.PI); + ctx.stroke(); +} + +var trial = { + type: 'canvas-keyboard-response', + stimulus: drawCirc, + choices: jsPsych.NO_KEYS, + trial_duration: 1000, +} +``` \ No newline at end of file From b31f5f5cd4093cdab36c0ee33693a4226fb951f8 Mon Sep 17 00:00:00 2001 From: Chris Jungerius Date: Mon, 11 May 2020 16:34:43 +0200 Subject: [PATCH 11/29] finished canvas-button-response documentation --- .../plugins/jspsych-canvas-button-response.md | 36 +++++++++++++++++-- .../jspsych-canvas-keyboard-response.md | 2 +- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/docs/plugins/jspsych-canvas-button-response.md b/docs/plugins/jspsych-canvas-button-response.md index fda3abed..b91803bd 100644 --- a/docs/plugins/jspsych-canvas-button-response.md +++ b/docs/plugins/jspsych-canvas-button-response.md @@ -1,5 +1,6 @@ # jspsych-canvas-button-response +This plugin can be used to draw a stimulus on a JavaScript canvas element, which can be useful for displaying parametrically defined shapes, and records responses generated by button click. The stimulus can be displayed until a response is given, or for a pre-determined amount of time. The trial can be ended automatically if the subject has failed to respond within a fixed length of time. The button itself can be customized using HTML formatting. ## Parameters @@ -7,13 +8,44 @@ Parameters with a default value of undefined must be specified. Other parameters Parameter | Type | Default Value | Description ----------|------|---------------|------------ - +stimulus | function | *undefined* | The function to draw on the canvas. This function must take a canvas element as its only argument, e.g. `foo(c)`. Note that the function will still generally need to set the correct context itself, using a line like let `ctx = c.getContext("2d")`. +canvas_size | array | [500, 500] | The size of the canvas element in pixels. +choices | array of strings | [] | Labels for the buttons. Each different string in the array will generate a different button. +button_html | HTML string | `''` | A template of HTML for generating the button elements. You can override this to create customized buttons of various kinds. The string `%choice%` will be changed to the corresponding element of the `choices` array. You may also specify an array of strings, if you need different HTML to render for each button. If you do specify an array, the `choices` array and this array must have the same length. The HTML from position 0 in the `button_html` array will be used to create the button for element 0 in the `choices` array, and so on. +prompt | string | null | This string can contain HTML markup. Any content here will be displayed below the stimulus. The intention is that it can be used to provide a reminder about the action the subject is supposed to take (e.g., which key to press). +trial_duration | numeric | null | How long to wait for the subject to make a response before ending the trial in milliseconds. If the subject fails to make a response before this timer is reached, the subject's response will be recorded as null for the trial and the trial will end. If the value of this parameter is null, the trial will wait for a response indefinitely. +stimulus_duration | numeric | null | How long to display the stimulus in milliseconds. The visibility CSS property of the stimulus will be set to `hidden` after this time has elapsed. If this is null, then the stimulus will remain visible until the trial ends. +margin_vertical | string | '0px' | Vertical margin of the button(s). +margin_horizontal | string | '8px' | Horizontal margin of the button(s). +response_ends_trial | boolean | true | If true, then the trial will end whenever the subject makes a response (assuming they make their response before the cutoff specified by the `trial_duration` parameter). If false, then the trial will continue until the value for `timing_response` is reached. You can use this parameter to force the subject to view a stimulus for a fixed amount of time, even if they respond before the time is complete. ## Data Generated In addition to the [default data collected by all plugins](overview#datacollectedbyplugins), this plugin collects the following data for each trial. Name | Type | Value -----|------|------ +rt | numeric | The response time in milliseconds for the subject to make a response. The time is measured from when the stimulus first appears on the screen until the subject's response. +button_pressed | numeric | Indicates which button the subject pressed. The first button in the `choices` array is 0, the second is 1, and so on +stimulus | function | The function that was drawn -## Examples \ No newline at end of file +## Examples + +### Displaying question until subject gives a response + +```javascript +function filledCirc(canvas, color){ + ctx = canvas.getContext("2d"); + ctx.beginPath(); + ctx.arc(250, 250, 100, 0, 2 * Math.PI); + ctx.fillStyle = color; + ctx.fill() +} + +var trial = { + type: 'canvas-button-response', + stimulus: function(c){ filledCirc(c, 'blue') }, + choices: ['red','green','blue'], + prompt: '

What color is the circle?

', +} +``` \ No newline at end of file diff --git a/docs/plugins/jspsych-canvas-keyboard-response.md b/docs/plugins/jspsych-canvas-keyboard-response.md index 464dc4d2..a7abe7a0 100644 --- a/docs/plugins/jspsych-canvas-keyboard-response.md +++ b/docs/plugins/jspsych-canvas-keyboard-response.md @@ -1,6 +1,6 @@ # jspsych-canvas-keyboard-response -This plugin can be used to draw an image on a JavaScript canvas element, which can be useful for displaying parametrically defined shapes, and records responses generated with the keyboard. The stimulus can be displayed until a response is given, or for a pre-determined amount of time. The trial can be ended automatically if the subject has failed to respond within a fixed length of time. +This plugin can be used to draw a stimulus on a JavaScript canvas element, which can be useful for displaying parametrically defined shapes, and records responses generated with the keyboard. The stimulus can be displayed until a response is given, or for a pre-determined amount of time. The trial can be ended automatically if the subject has failed to respond within a fixed length of time. ## Parameters From 251a143adca6db6457a97b3b6951e5e70c307d06 Mon Sep 17 00:00:00 2001 From: Chris Jungerius Date: Mon, 11 May 2020 16:59:48 +0200 Subject: [PATCH 12/29] completed canvas-slider-response documentation --- .../plugins/jspsych-canvas-slider-response.md | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/docs/plugins/jspsych-canvas-slider-response.md b/docs/plugins/jspsych-canvas-slider-response.md index a350d2d3..5d6eb7ab 100644 --- a/docs/plugins/jspsych-canvas-slider-response.md +++ b/docs/plugins/jspsych-canvas-slider-response.md @@ -1,5 +1,6 @@ # jspsych-canvas-slider-response +This plugin can be used to draw a stimulus on a JavaScript canvas element, which can be useful for displaying parametrically defined shapes, and allows the subject to respond by dragging a slider. ## Parameters @@ -7,13 +8,48 @@ Parameters with a default value of undefined must be specified. Other parameters Parameter | Type | Default Value | Description ----------|------|---------------|------------ - +stimulus | function | *undefined* | The function to draw on the canvas. This function must take a canvas element as its only argument, e.g. `foo(c)`. Note that the function will still generally need to set the correct context itself, using a line like let `ctx = c.getContext("2d")`. +canvas_size | array | [500, 500] | The size of the canvas element in pixels. +labels | array of strings | [] | Labels displayed at equidistant locations on the slider. For example, two labels will be placed at the ends of the slider. Three labels would place two at the ends and one in the middle. Four will place two at the ends, and the other two will be at 33% and 67% of the slider width. +button_label | string | 'Continue' | Label of the button to end the trial. +min | integer | 0 | Sets the minimum value of the slider. +max | integer | 100 | Sets the maximum value of the slider. +start | integer | 50 | Sets the starting value of the slider +step | integer | 1 | Sets the step of the slider. This is the smallest amount by which the slider can change. +slider_width | integer | null | Set the width of the slider in pixels. If left null, then the width will be equal to the widest element in the display. +require_movement | boolean | false | If true, the subject must move the slider before clicking the continue button. +prompt | string | null | This string can contain HTML markup. Any content here will be displayed below the stimulus. The intention is that it can be used to provide a reminder about the action the subject is supposed to take (e.g., which key to press). +stimulus_duration | numeric | null | How long to display the stimulus in milliseconds. The visibility CSS property of the stimulus will be set to `hidden` after this time has elapsed. If this is null, then the stimulus will remain visible until the trial ends. +trial_duration | numeric | null | How long to wait for the subject to make a response before ending the trial in milliseconds. If the subject fails to make a response before this timer is reached, the subject's response will be recorded as null for the trial and the trial will end. If the value of this parameter is null, then the trial will wait for a response indefinitely. +response_ends_trial | boolean | true | If true, then the trial will end whenever the subject makes a response (assuming they make their response before the cutoff specified by the `timing_response` parameter). If false, then the trial will continue until the value for `trial_duration` is reached. You can use this parameter to force the subject to view a stimulus for a fixed amount of time, even if they respond before the time is complete. ## Data Generated In addition to the [default data collected by all plugins](overview#datacollectedbyplugins), this plugin collects the following data for each trial. Name | Type | Value -----|------|------ +response | numeric | The numeric value of the slider. +rt | numeric | The time in milliseconds for the subject to make a response. The time is measured from when the stimulus first appears on the screen until the subject's response +stimulus | function | The function that was drawn. +## Examples -## Examples \ No newline at end of file +### a color evaluation scale + +```javascript +function twoSquares(c) { + let colors = ['FF3333', 'FF6A33'] + ctx = c.getcontext('2d'); + ctx.fillStyle = colors[0]; + ctx.fillRect(200, 230, 40, 40); + ctx.fillStyle = colors[1]; + ctx.fillRect(260, 230, 40, 40); +} + +var trial = { + type: canvas-slider-response, + stimulus: twoSquares, + labels: ['0','10'], + prompt: '

How different would you say the colors of these two squares are on a scale from 0 (the same) to 10 (completely different)

', +} +``` \ No newline at end of file From 7aa6736c966cd9ea3d852fcc523cffc706b1f338 Mon Sep 17 00:00:00 2001 From: Chris Jungerius <37599089+cjungerius@users.noreply.github.com> Date: Mon, 11 May 2020 18:57:51 +0200 Subject: [PATCH 13/29] final cleanup of canvas-keyboard-response --- plugins/jspsych-canvas-keyboard-response.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/jspsych-canvas-keyboard-response.js b/plugins/jspsych-canvas-keyboard-response.js index 388c7382..5920dfff 100644 --- a/plugins/jspsych-canvas-keyboard-response.js +++ b/plugins/jspsych-canvas-keyboard-response.js @@ -4,7 +4,7 @@ * * plugin for displaying a canvas stimulus and getting a keyboard response * - * documentation: TODO + * documentation: docs.jspsych.org * **/ From 1467f3b1e4c01f90f1b8f6c3ca31406973db8406 Mon Sep 17 00:00:00 2001 From: awhug Date: Wed, 21 Oct 2020 16:08:11 +0800 Subject: [PATCH 14/29] Initial pass of maxdiff plugin --- docs/plugins/jspsych-maxdiff.md | 42 ++++++++ examples/jspsych-maxdiff.html | 33 +++++++ plugins/jspsych-maxdiff.js | 166 ++++++++++++++++++++++++++++++++ tests/plugins/plugin-maxdiff.js | 17 ++++ 4 files changed, 258 insertions(+) create mode 100644 docs/plugins/jspsych-maxdiff.md create mode 100644 examples/jspsych-maxdiff.html create mode 100644 plugins/jspsych-maxdiff.js create mode 100644 tests/plugins/plugin-maxdiff.js diff --git a/docs/plugins/jspsych-maxdiff.md b/docs/plugins/jspsych-maxdiff.md new file mode 100644 index 00000000..2d5e872b --- /dev/null +++ b/docs/plugins/jspsych-maxdiff.md @@ -0,0 +1,42 @@ +# jspsych-maxdfff plugin + +The maxdiff plugin displays a table with rows of alternatives to be endorsed as 'most' or 'least' on a particular criteria (e.g. importance, preference, similarity). The subject responds by selecting one radio button corresponding to an alternative in both the 'most' and 'least' column. The same alternative cannot be endorsed as both 'most' and 'least' simultaneously. + +## Parameters + +Parameters with a default value of *undefined* must be specified. Other parameters can be left unspecified if the default value is acceptable. + +Parameter | Type | Default Value | Description +----------|------|---------------|------------ +alternatives | array | *undefined* | An array of alternatives of string type to fill the rows of the maxdiff table. +labels | array | *undefined* | An array with exactly two labels of string type to display as column headings for the criteria of interest. Must be in the order of 'most' (first), then 'least' (second). +randomize_alternative_order | boolean | `false` | If true, the display order of `alternatives` is randomly determined at the start of the trial. +preamble | string | empty string | HTML formatted string to display at the top of the page above the maxdiff table. +required | boolean | `false` | If true, prevents the user from submitting the response and proceeding until a radio button in both the 'most' and 'least' columns has been selected. +button_label | string | 'Continue' | Label of the button. + + +## Data Generated + +In addition to the [default data collected by all plugins](overview#data-collected-by-plugins), this plugin collects the following data for each trial. + +Name | Type | Value +-----|------|------ +rt | numeric | The response time in milliseconds for the subject to make a response. The time is measured from when the maxdiff table first appears on the screen until the subject's response. +most | string | The alternative endorsed as 'most' on the criteria of interest. +least | string | The alternative endorsed as 'least' on the criteria of interest. + + +## Examples + +#### Basic example + +```javascript +var maxdiff_page = { + type: 'maxdiff', + alternatives: ['apple', 'orange', 'pear', 'banana'], + labels: ['Most Preferred', 'Least Preferred'], + preamble: '

Please select your most preferred and least preferred fruits.

' +}; +``` + diff --git a/examples/jspsych-maxdiff.html b/examples/jspsych-maxdiff.html new file mode 100644 index 00000000..7a87c0e6 --- /dev/null +++ b/examples/jspsych-maxdiff.html @@ -0,0 +1,33 @@ + + + + + + + + + + \ No newline at end of file diff --git a/plugins/jspsych-maxdiff.js b/plugins/jspsych-maxdiff.js new file mode 100644 index 00000000..9e2e35e3 --- /dev/null +++ b/plugins/jspsych-maxdiff.js @@ -0,0 +1,166 @@ +/** + * jspsych-maxdiff + * a jspsych plugin for maxdiff/conjoint analysis designs + * + */ + +jsPsych.plugins['maxdiff'] = (function () { + + var plugin = {}; + + plugin.info = { + name: 'maxdiff', + description: '', + parameters: { + alternatives: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Alternatives', + array: true, + default: undefined, + description: 'Alternatives presented in the Maxdiff table.' + }, + labels: { + type: jsPsych.plugins.parameterType.STRING, + array: true, + pretty_name: 'Labels', + default: undefined, + description: 'Labels to display for most or least preferred.' + }, + randomize_alternative_order: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Randomize alternative Order', + default: false, + description: 'If true, the order of the alternatives will be randomized' + }, + preamble: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Preamble', + default: '', + description: 'String to display at top of the page.' + }, + button_label: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Button label', + default: 'Continue', + description: 'Label of the button.' + }, + required: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Required', + default: false, + description: 'Makes answering the alternative required.' + } + } + } + + plugin.trial = function (display_element, trial) { + + const most_least = ["most", "least"] + var enable_submit = trial.required == true ? 'disabled = "disabled"' : ''; + + var html = ""; + // inject CSS for trial + html += ''; + + // show preamble text + if (trial.preamble !== null) { + html += '
' + trial.preamble + '
'; + } + html += '
'; + + // add maxdiff options /// + // generate alternative order. this is randomized here as opposed to randomizing the order of alternatives + // so that the data are always associated with the same alternative regardless of order + var alternative_order = []; + for (var i = 0; i < trial.alternatives.length; i++) { + alternative_order.push(i); + } + if (trial.randomize_alternative_order) { + alternative_order = jsPsych.randomization.shuffle(alternative_order); + } + + // Start with column headings + var maxdiff_table = ''; + + // construct each row of the maxdiff table + for (var i = 0; i < trial.alternatives.length; i++) { + var alternative = trial.alternatives[alternative_order[i]]; + // add alternative + maxdiff_table += ''; + maxdiff_table += ''; + maxdiff_table += ''; + } + maxdiff_table += '
' + trial.labels[0] + '' + trial.labels[1] + '

' + alternative + '


'; + html += maxdiff_table; + + // add submit button + html += ''; + html += '
'; + + display_element.innerHTML = html; + + // function to control responses + // first checks that the same alternative cannot be endorsed as 'most' and 'least' simultaneously + // then enables the submit button if the trial is required. + most_least.forEach(function(p) { + // Get all elements either 'most' or 'least' + document.getElementsByName(p).forEach(function(obj) { + obj.addEventListener('click', function() { + // Find the opposite (if most, then least & vice versa) + var op = obj.name == 'most' ? 'least' : 'most'; + // Get the opposite button identified by the class (one, two, etc) + var n = document.getElementsByClassName(obj.className).namedItem(op); + // If it's checked, uncheck it. + if (n.checked) { + n.checked = false; + } + + // check response + if (trial.required){ + // Now check if both most and least have been enabled + var most_checked = [...document.getElementsByName('most')].some(c => c.checked); + var least_checked = [...document.getElementsByName('least')].some(c => c.checked); + + // If at least one of both have been clicked, allow submission + if (most_checked && least_checked) { + document.getElementById("jspsych-maxdiff-next").disabled = false; + } else { + document.getElementById("jspsych-maxdiff-next").disabled = true; + } + } + }); + }); + }); + + // Get the data once the submit button is clicked + display_element.querySelector('#jspsych-maxdiff-form').addEventListener('submit', function(e){ + e.preventDefault(); + + // measure response time + var endTime = performance.now(); + var response_time = endTime - startTime; + + var most = parseInt(display_element.querySelectorAll('[name="most"]:checked')[0].getAttribute('data-name')); + var least = parseInt(display_element.querySelectorAll('[name="least"]:checked')[0].getAttribute('data-name')); + + // data saving + var trial_data = { + "rt": response_time, + "most": trial.alternatives[most], + "least": trial.alternatives[least] + }; + + // next trial + jsPsych.finishTrial(trial_data); + }); + + var startTime = performance.now(); + }; + + return plugin; +})(); \ No newline at end of file diff --git a/tests/plugins/plugin-maxdiff.js b/tests/plugins/plugin-maxdiff.js new file mode 100644 index 00000000..21215524 --- /dev/null +++ b/tests/plugins/plugin-maxdiff.js @@ -0,0 +1,17 @@ +const root = '../../'; +const utils = require('../testing-utils.js'); + +jest.useFakeTimers(); + +describe('maxdiff plugin', function(){ + + beforeEach(function(){ + require(root + 'jspsych.js'); + require(root + 'plugins/jspsych-maxdiff.js'); + }); + + test('loads correctly', function(){ + expect(typeof window.jsPsych.plugins['maxdiff']).not.toBe('undefined'); + }); + +}); From b0646f564924ff64105a9daa04840663f9999646 Mon Sep 17 00:00:00 2001 From: Angus Hughes <37681252+awhug@users.noreply.github.com> Date: Thu, 22 Oct 2020 11:23:07 +0800 Subject: [PATCH 15/29] simple randomisation test added, better comments --- plugins/jspsych-maxdiff.js | 31 +++++++++++----------- tests/plugins/plugin-maxdiff.js | 17 ------------ tests/plugins/plugin-maxdiff.test.js | 39 ++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 32 deletions(-) delete mode 100644 tests/plugins/plugin-maxdiff.js create mode 100644 tests/plugins/plugin-maxdiff.test.js diff --git a/plugins/jspsych-maxdiff.js b/plugins/jspsych-maxdiff.js index 9e2e35e3..57cf8606 100644 --- a/plugins/jspsych-maxdiff.js +++ b/plugins/jspsych-maxdiff.js @@ -1,5 +1,7 @@ /** * jspsych-maxdiff + * Angus Hughes + * * a jspsych plugin for maxdiff/conjoint analysis designs * */ @@ -55,6 +57,7 @@ jsPsych.plugins['maxdiff'] = (function () { plugin.trial = function (display_element, trial) { + // Set some trial parameters const most_least = ["most", "least"] var enable_submit = trial.required == true ? 'disabled = "disabled"' : ''; @@ -74,8 +77,8 @@ jsPsych.plugins['maxdiff'] = (function () { html += '
'; // add maxdiff options /// - // generate alternative order. this is randomized here as opposed to randomizing the order of alternatives - // so that the data are always associated with the same alternative regardless of order + // first generate alternative order, randomized here as opposed to randomizing the order of alternatives + // so that the data are always associated with the same alternative regardless of order. var alternative_order = []; for (var i = 0; i < trial.alternatives.length; i++) { alternative_order.push(i); @@ -91,9 +94,9 @@ jsPsych.plugins['maxdiff'] = (function () { for (var i = 0; i < trial.alternatives.length; i++) { var alternative = trial.alternatives[alternative_order[i]]; // add alternative - maxdiff_table += '
'; + maxdiff_table += '
'; maxdiff_table += '' + alternative + ''; - maxdiff_table += '
'; + maxdiff_table += '
'; } maxdiff_table += '

'; html += maxdiff_table; @@ -105,16 +108,15 @@ jsPsych.plugins['maxdiff'] = (function () { display_element.innerHTML = html; // function to control responses - // first checks that the same alternative cannot be endorsed as 'most' and 'least' simultaneously + // first checks that the same alternative cannot be endorsed as 'most' and 'least' simultaneously. // then enables the submit button if the trial is required. most_least.forEach(function(p) { // Get all elements either 'most' or 'least' - document.getElementsByName(p).forEach(function(obj) { - obj.addEventListener('click', function() { - // Find the opposite (if most, then least & vice versa) - var op = obj.name == 'most' ? 'least' : 'most'; - // Get the opposite button identified by the class (one, two, etc) - var n = document.getElementsByClassName(obj.className).namedItem(op); + document.getElementsByName(p).forEach(function(alt) { + alt.addEventListener('click', function() { + // Find the opposite (if most, then least & vice versa) identified by the class (jspsych-maxdiff-alt-1, 2, etc) + var op = alt.name == 'most' ? 'least' : 'most'; + var n = document.getElementsByClassName(alt.className).namedItem(op); // If it's checked, uncheck it. if (n.checked) { n.checked = false; @@ -122,11 +124,9 @@ jsPsych.plugins['maxdiff'] = (function () { // check response if (trial.required){ - // Now check if both most and least have been enabled + // Now check if one of both most and least have been enabled to allow submission var most_checked = [...document.getElementsByName('most')].some(c => c.checked); var least_checked = [...document.getElementsByName('least')].some(c => c.checked); - - // If at least one of both have been clicked, allow submission if (most_checked && least_checked) { document.getElementById("jspsych-maxdiff-next").disabled = false; } else { @@ -145,10 +145,11 @@ jsPsych.plugins['maxdiff'] = (function () { var endTime = performance.now(); var response_time = endTime - startTime; + // get the alternative number by the data-name attribute var most = parseInt(display_element.querySelectorAll('[name="most"]:checked')[0].getAttribute('data-name')); var least = parseInt(display_element.querySelectorAll('[name="least"]:checked')[0].getAttribute('data-name')); - // data saving + // data saving var trial_data = { "rt": response_time, "most": trial.alternatives[most], diff --git a/tests/plugins/plugin-maxdiff.js b/tests/plugins/plugin-maxdiff.js deleted file mode 100644 index 21215524..00000000 --- a/tests/plugins/plugin-maxdiff.js +++ /dev/null @@ -1,17 +0,0 @@ -const root = '../../'; -const utils = require('../testing-utils.js'); - -jest.useFakeTimers(); - -describe('maxdiff plugin', function(){ - - beforeEach(function(){ - require(root + 'jspsych.js'); - require(root + 'plugins/jspsych-maxdiff.js'); - }); - - test('loads correctly', function(){ - expect(typeof window.jsPsych.plugins['maxdiff']).not.toBe('undefined'); - }); - -}); diff --git a/tests/plugins/plugin-maxdiff.test.js b/tests/plugins/plugin-maxdiff.test.js new file mode 100644 index 00000000..6d22f534 --- /dev/null +++ b/tests/plugins/plugin-maxdiff.test.js @@ -0,0 +1,39 @@ +const root = '../../'; +const utils = require('../testing-utils.js'); + +jest.useFakeTimers(); + +describe('maxdiff plugin', function(){ + + beforeEach(function(){ + require(root + 'jspsych.js'); + require(root + 'plugins/jspsych-maxdiff.js'); + }); + + test('loads correctly', function(){ + expect(typeof window.jsPsych.plugins['maxdiff']).not.toBe('undefined'); + }); + + test('returns appropriate response with randomization', function(){ + var trial = { + type: 'maxdiff', + alternatives: ['a', 'b', 'c', 'd'], + labels: ['Most', 'Least'], + randomize_alternative_order: true + } + + jsPsych.init({ + timeline: [trial] + }); + + document.querySelector('input[data-name="0"][name="most"]').checked = true; + document.querySelector('input[data-name="1"][name="least"]').checked = true; + + utils.clickTarget(document.querySelector('#jspsych-maxdiff-next')); + + var maxdiff_data = jsPsych.data.get().values()[0]; + expect(maxdiff_data.most).toBe("a"); + expect(maxdiff_data.least).toBe("b"); + }); + +}); From 857db431664d1f69dc44d2a03de3c3fcebd03254 Mon Sep 17 00:00:00 2001 From: Angus Hughes <37681252+awhug@users.noreply.github.com> Date: Fri, 23 Oct 2020 11:05:13 +0800 Subject: [PATCH 16/29] maxdiff response column labels now more generic --- docs/plugins/jspsych-maxdiff.md | 16 +++++----- plugins/jspsych-maxdiff.js | 45 ++++++++++++++-------------- tests/plugins/plugin-maxdiff.test.js | 8 ++--- 3 files changed, 34 insertions(+), 35 deletions(-) diff --git a/docs/plugins/jspsych-maxdiff.md b/docs/plugins/jspsych-maxdiff.md index 2d5e872b..a9bb2d8a 100644 --- a/docs/plugins/jspsych-maxdiff.md +++ b/docs/plugins/jspsych-maxdiff.md @@ -1,6 +1,6 @@ -# jspsych-maxdfff plugin +# jspsych-maxdiff plugin -The maxdiff plugin displays a table with rows of alternatives to be endorsed as 'most' or 'least' on a particular criteria (e.g. importance, preference, similarity). The subject responds by selecting one radio button corresponding to an alternative in both the 'most' and 'least' column. The same alternative cannot be endorsed as both 'most' and 'least' simultaneously. +The maxdiff plugin displays a table with rows of alternatives to be endorsed, typically as 'most' or 'least' on a particular criteria (e.g. importance, preference, similarity). The subject responds by selecting one radio button corresponding to an alternative in both the left and right response columns. The same alternative cannot be endorsed on both the left and right response columns (e.g. 'most' and 'least') simultaneously. ## Parameters @@ -9,10 +9,10 @@ Parameters with a default value of *undefined* must be specified. Other paramete Parameter | Type | Default Value | Description ----------|------|---------------|------------ alternatives | array | *undefined* | An array of alternatives of string type to fill the rows of the maxdiff table. -labels | array | *undefined* | An array with exactly two labels of string type to display as column headings for the criteria of interest. Must be in the order of 'most' (first), then 'least' (second). +labels | array | *undefined* | An array with exactly two labels of string type to display as column headings (to the left and right of the alternatives) for responses on the criteria of interest. randomize_alternative_order | boolean | `false` | If true, the display order of `alternatives` is randomly determined at the start of the trial. preamble | string | empty string | HTML formatted string to display at the top of the page above the maxdiff table. -required | boolean | `false` | If true, prevents the user from submitting the response and proceeding until a radio button in both the 'most' and 'least' columns has been selected. +required | boolean | `false` | If true, prevents the user from submitting the response and proceeding until a radio button in both the left and right response columns has been selected. button_label | string | 'Continue' | Label of the button. @@ -23,8 +23,9 @@ In addition to the [default data collected by all plugins](overview#data-collect Name | Type | Value -----|------|------ rt | numeric | The response time in milliseconds for the subject to make a response. The time is measured from when the maxdiff table first appears on the screen until the subject's response. -most | string | The alternative endorsed as 'most' on the criteria of interest. -least | string | The alternative endorsed as 'least' on the criteria of interest. +labels | JSON string | A string in JSON format containing the labels corresponding to the left and right response columns. +left | string | The alternative endorsed on the left column. +right | string | The alternative endorsed on the right column. ## Examples @@ -38,5 +39,4 @@ var maxdiff_page = { labels: ['Most Preferred', 'Least Preferred'], preamble: '

Please select your most preferred and least preferred fruits.

' }; -``` - +``` \ No newline at end of file diff --git a/plugins/jspsych-maxdiff.js b/plugins/jspsych-maxdiff.js index 57cf8606..548f021a 100644 --- a/plugins/jspsych-maxdiff.js +++ b/plugins/jspsych-maxdiff.js @@ -19,18 +19,18 @@ jsPsych.plugins['maxdiff'] = (function () { pretty_name: 'Alternatives', array: true, default: undefined, - description: 'Alternatives presented in the Maxdiff table.' + description: 'Alternatives presented in the maxdiff table.' }, labels: { type: jsPsych.plugins.parameterType.STRING, array: true, pretty_name: 'Labels', default: undefined, - description: 'Labels to display for most or least preferred.' + description: 'Labels to display for left and right response columns.' }, randomize_alternative_order: { type: jsPsych.plugins.parameterType.BOOL, - pretty_name: 'Randomize alternative Order', + pretty_name: 'Randomize Alternative Order', default: false, description: 'If true, the order of the alternatives will be randomized' }, @@ -42,7 +42,7 @@ jsPsych.plugins['maxdiff'] = (function () { }, button_label: { type: jsPsych.plugins.parameterType.STRING, - pretty_name: 'Button label', + pretty_name: 'Button Label', default: 'Continue', description: 'Label of the button.' }, @@ -57,10 +57,6 @@ jsPsych.plugins['maxdiff'] = (function () { plugin.trial = function (display_element, trial) { - // Set some trial parameters - const most_least = ["most", "least"] - var enable_submit = trial.required == true ? 'disabled = "disabled"' : ''; - var html = ""; // inject CSS for trial html += '