remove palmer plugin - fixes #401

Snap library is not handling event listeners correctly, or some other
change to jsPsych broke compatibility. Either way, this plugin isn't
used enough to warrant the effort to make it compatible right now.
This commit is contained in:
Josh de Leeuw 2017-06-30 23:24:10 -04:00
parent 991674be45
commit d9b3deb582
5 changed files with 0 additions and 603 deletions

View File

@ -1,96 +0,0 @@
# jspsych-palmer
The Palmer plugin displays a programmatically generated stimulus designed to mimic stimuli used by [Palmer (1977)](jspsych-palmer.md#references) and [Goldstone et al. (2005)](jspsych-palmer.md#references).
The stimuli look like this (there are several options to configure the appearance of the stimuli below):
![Sample stimulus](/img/palmer_stim.png)
Subjects can edit the stimuli by clicking on two adjacent circles to add or remove a line segment between them. The plugin also contains a function for generating non-editable versions of the stimuli that can be used in other plugins that accept HTML-formatted stimuli, like the single-stim and categorize plugins.
## Dependency
This plugin requires the Snap.svg library, available at [http://www.snapsvg.io](http://www.snapsvg.io). You must include the library in the `<head>` section of your experiment page.
## Parameters
This table lists the parameters associated with this plugin. 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
----------|------|---------------|------------
configuration | array | *undefined* | A configuration is an array of 1s and 0s. A 1 represents the presence of a line segment, and a 0 represents the absence of a line segment. The number of elements in a configuration array should be the number of possible line segments in the stimulus. For the default size of 3x3, this is 20 (6 horizontal + 6 vertical + 8 diagonal). The order of the lines in the array are horizontal, vertical, and then diagonal. The easiest way to see this is just to try different configurations. This represents the target configuration, as the stimulus will be blank initially.
show_feedback | boolean | false | Show corrective feedback after the subject submits a response.
grid_spacing | numeric | 75 | Distance in pixels between the rows and columns.
circle_radius | numeric | 20 | Radius in pixels of the circles.
square_size | numeric | 3 | Number of rows and columns in the stimulus.
timing_feedback | numeric | 1000 | How long to show the feedback for in milliseconds.
prompt | string | "" | This string can contain HTML markup. Any content here will be displayed below the stimulus.
button_label | string | 'Done' | The text that appears on the button to finish the trial.
## 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.
correct | boolean | `true` if the subject produced exactly the right configuration.
configuration | JSON string | Configuration that the subject produced, in the same format as the configuration array parameter for the trial.
target_configuration | JSON string | The correct configuration (from configuration parameter).
num_wrong | numeric | The number of line segments that didn't match between what the subject produced and the target configuration.
## Stimulus Creation
The palmer plugin contains a stimulus generation method (`jsPsych.palmer.generate_stimulus`) that can be used independently of actually running trials with the plugin. You can use this method to generate stimuli (created as HTML strings) to embed in other plugins that can use HTML as input, such as jspsych-categorize. An example is provided here:
```javascript
var square_size = 3; // number of rows and columns
var grid_spacing = 75; // pixels
var circle_size = 20; // radius in pixels
var configuration = [0,1,0,0,1,1,0,0,1,1,0,0,1,0,0,0,1,0,0,1]; // see definition above
var stimulus = jsPsych.palmer.generate_stimulus(square_size, grid_spacing, circle_size, configuration);
// stimulus now contains an HTML string that will generate the corresponding stimulus.
```
## Examples
These examples show how to define a block using the single-stim plugin to achieve various goals.
#### Matching a previously seen stimulus
```javascript
// create a stimulus using the generate_stimulus method
var square_size = 3; // number of rows and columns
var grid_spacing = 75; // pixels
var circle_size = 20; // radius in pixels
var configuration = [0,1,0,0,1,1,0,0,1,1,0,0,1,0,0,0,1,0,0,1]; // see definition above
var stimulus = jsPsych.palmer.generate_stimulus(square_size, grid_spacing, circle_size, configuration);
// show target stimulus using single-stim plugin
var stim_block = {
type: 'single-stim',
stimulus: stimulus,
is_html: true,
timing_response: 2000,
response_ends_trial: false
}
var test_block = {
type: 'palmer',
configuration: configuration,
editable: true,
show_feedback: true,
timing_feedback: 2000,
prompt: '<p>Create the image you just saw. Click two circles to add or remove a line between them. Click submit when you are done.</p>'
};
```
## References
Goldstone, R. L., Rogosky, B. J., Pevtzow, R., & Blair, M. (2005). Perceptual and semantic reorganization during category learning. _In H. Cohen & C. Lefebvre (Eds.) Handbook of Categorization in Cognitive Science_. (pp. 651-678). Amsterdam: Elsevier.
Palmer, S. (1977). Hierarchical Structure in Perceptual Representation. _Cognitive Psychology, 9_, 441.

View File

@ -74,7 +74,6 @@ This table is a description of all plugins that are currently bundled with jsPsy
[jspsych&#8209;html](jspsych-html) | Displays an external HTML page (such as a consent form) and lets the subject respond by clicking a button or pressing a key. Plugin can validate their response, which is useful for making sure that a subject has granted consent before starting the experiment.
[jspsych&#8209;instructions](jspsych-instructions) | For displaying instructions to the subject.
[jspsych&#8209;multi&#8209;stim&#8209;multi&#8209;response](jspsych-multi-stim-multi-response) | A more generalized version of the single-stim plugin. Can display multiple stimuli in a single trial, and collect multiple responses in a single trial.
[jspsych&#8209;palmer](jspsych-palmer) | Shows grid-like stimuli inspired by Stephen Palmer's work. The stimuli are editable: subjects can add and subtract parts interactively. Also contains a method for generating the HTML code to render the stimuli, allowing them to be used in other plugins.
[jspsych&#8209;reconstruction](jspsych-reconstruction) | The subject interacts with a stimulus by modifying a parameter of the stimulus and observing the change in the stimulus in real-time.
[jspsych&#8209;resize](jspsych-resize) | Calibrate the display so that materials display with a known physical size.
[jspsych&#8209;same&#8209;different](jspsych-same-different) | A same-different judgment task. A stimulus is shown, followed by a brief gap, and then another stimulus is shown. The subject indicates whether the stimuli are the same or different.

View File

@ -1,46 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<script src="js/snap.svg-min.js"></script>
<script src="../jspsych.js"></script>
<script src="../plugins/jspsych-palmer.js"></script>
<script src="../plugins/jspsych-single-stim.js"></script>
<link rel="stylesheet" href="../css/jspsych.css"></link>
</head>
<body></body>
<script>
// create a stimulus using the generate_stimulus method
var square_size = 3; // number of rows and columns
var grid_spacing = 75; // pixels
var circle_size = 20; // radius in pixels
var configuration = [0,1,0,0,1,1,0,0,1,1,0,0,1,0,0,0,1,0,0,1]; // see definition above
var stimulus = jsPsych.plugins.palmer.generate_stimulus(square_size, grid_spacing, circle_size, configuration);
// show stimulus using single-stim plugin
var stim_block = {
type: 'single-stim',
stimulus: stimulus,
is_html: true,
timing_response: 2000,
response_ends_trial: false
};
var test_block = {
type: 'palmer',
configuration: [0,1,0,0,1,1,0,0,1,1,0,0,1,0,0,0,1,0,0,1],
editable: true,
show_feedback: true,
timing_feedback: 2000,
prompt: '<p>Create the image you just saw. Click two circles to add or remove a line between them. Click submit when you are done.</p>'
};
jsPsych.init({
timeline: [stim_block, test_block],
on_finish: function() { jsPsych.data.displayData(); }
});
</script>
</html>

View File

@ -1,444 +0,0 @@
/**
* jspsych-palmer
* Josh de Leeuw (October 2013)
*
* a jspsych plugin for presenting and querying about stimuli modeled after
*
* Palmer, S. (1977). Hierarchical Structure in Perceptual Representation. Cognitive Psychology, 9, 441.
*
* and
*
* Goldstone, R. L., Rogosky, B. J., Pevtzow, R., & Blair, M. (2005). Perceptual and semantic reorganization during category learning.
* In H. Cohen & C. Lefebvre (Eds.) Handbook of Categorization in Cognitive Science. (pp. 651-678). Amsterdam: Elsevier.
*
* documentation: docs.jspsych.org
*
*/
jsPsych.plugins.palmer = (function() {
var plugin = {};
plugin.info = {
name: 'palmer',
description: '',
parameters: {
configuration: {
type: [jsPsych.plugins.parameterType.INT],
default: undefined,
array: true,
no_function: false,
description: ''
},
show_feedback: {
type: [jsPsych.plugins.parameterType.BOOL],
default: false,
no_function: false,
description: ''
},
grid_spacing: {
type: [jsPsych.plugins.parameterType.INT],
default: 75,
no_function: false,
description: ''
},
circle_radius: {
type: [jsPsych.plugins.parameterType.INT],
default: 20,
no_function: false,
description: ''
},
square_size: {
type: [jsPsych.plugins.parameterType.INT],
default: 3,
no_function: false,
description: ''
},
timing_feedback: {
type: [jsPsych.plugins.parameterType.INT],
default: 1000,
no_function: false,
description: ''
},
prompt: {
type: [jsPsych.plugins.parameterType.STRING],
default: '',
no_function: false,
description: ''
},
button_label: {
type: [jsPsych.plugins.parameterType.STRING],
default: 'Done',
no_function: false,
description: ''
}
}
}
plugin.trial = function(display_element, trial) {
// default parameter settings
trial.show_feedback = (typeof trial.show_feedback === 'undefined') ? false : trial.show_feedback;
trial.grid_spacing = trial.grid_spacing || 75;
trial.square_size = trial.square_size || 3;
trial.circle_radius = trial.circle_radius || 20;
trial.timing_item = trial.timing_item || 1000;
trial.timing_feedback = trial.timing_feedback || 1000;
trial.prompt = (typeof trial.prompt === 'undefined') ? "" : trial.prompt;
trial.button_label = typeof trial.button_label === 'undefined' ? 'Next' : trial.button_label;
// if any trial variables are functions
// this evaluates the function and replaces
// it with the output of the function
trial = jsPsych.pluginAPI.evaluateFunctionParameters(trial);
// variables to keep track of user interaction
var start_circle = -1;
var end_circle = -1;
var line_started = false;
var size = trial.grid_spacing * (trial.square_size + 1);
display_element.innerHTML = "<svg id='jspsych-palmer-snapCanvas' width='" + size + "' height='" + size + "'></svg>";
var paper = Snap("#jspsych-palmer-snapCanvas");
// create the circles at the vertices.
var circles = [];
var node_idx = 0;
for (var i = 1; i <= trial.square_size; i++) {
for (var j = 1; j <= trial.square_size; j++) {
var circle = paper.circle(trial.grid_spacing * j, trial.grid_spacing * i, trial.circle_radius);
circle.attr("fill", "#000").attr("stroke-width", "0").attr("stroke", "#000").data("node", node_idx);
circle.hover(
function() {
this.attr("stroke-width", "2");
//this.attr("stroke", "#000");
},
function() {
this.attr("stroke-width", "0");
//this.attr("stroke", "#fff")
}).click(
function() {
if (!line_started) {
line_started = true;
start_circle = this.data("node");
this.attr("fill", "#777").attr("stroke", "#777");
} else {
end_circle = this.data("node");
draw_connection(start_circle, end_circle);
}
});
node_idx++;
circles.push(circle);
}
}
function draw_connection(start_circle, end_circle) {
var the_line = getLineIndex(start_circle, end_circle);
if (the_line > -1) {
toggle_line(the_line);
}
// reset highlighting on circles
for (var i = 0; i < circles.length; i++) {
circles[i].attr("fill", "#000").attr("stroke", "#000");
}
// cleanup the variables
line_started = false;
start_circle = -1;
end_circle = -1;
}
// create all possible lines that connect circles
var horizontal_lines = [];
var vertical_lines = [];
var backslash_lines = [];
var forwardslash_lines = [];
for (var i = 0; i < trial.square_size; i++) {
for (var j = 0; j < trial.square_size; j++) {
var current_item = (i * trial.square_size) + j;
// add horizontal connections
if (j < (trial.square_size - 1)) {
horizontal_lines.push([current_item, current_item + 1]);
}
// add vertical connections
if (i < (trial.square_size - 1)) {
vertical_lines.push([current_item, current_item + trial.square_size]);
}
// add diagonal backslash connections
if (i < (trial.square_size - 1) && j < (trial.square_size - 1)) {
backslash_lines.push([current_item, current_item + trial.square_size + 1]);
}
// add diagonal forwardslash connections
if (i < (trial.square_size - 1) && j > 0) {
forwardslash_lines.push([current_item, current_item + trial.square_size - 1]);
}
}
}
var lines = horizontal_lines.concat(vertical_lines).concat(backslash_lines).concat(forwardslash_lines);
// actually draw the lines
var lineIsVisible = [];
var lineElements = [];
for (var i = 0; i < lines.length; i++) {
var line = paper.path("M" + circles[lines[i][0]].attr("cx") + " " + circles[lines[i][0]].attr("cy") + "L" + circles[lines[i][1]].attr("cx") + " " + circles[lines[i][1]].attr("cy")).attr("stroke-width", "8").attr("stroke", "#000");
line.attr({
visibility: 'hidden'
});
lineElements.push(line);
lineIsVisible.push(0);
}
// define some helper functions to toggle lines on and off
// this function gets the index of a line based on the two circles it connects
function getLineIndex(start_circle, end_circle) {
var the_line = -1;
for (var i = 0; i < lines.length; i++) {
if ((start_circle == lines[i][0] && end_circle == lines[i][1]) || (start_circle == lines[i][1] && end_circle == lines[i][0])) {
the_line = i;
break;
}
}
return the_line;
}
// this function turns a line on/off based on the index (the_line)
function toggle_line(the_line) {
if (the_line > -1) {
if (lineIsVisible[the_line] === 0) {
lineElements[the_line].attr({
visibility: 'visible'
});
lineElements[the_line].prependTo(paper);
lineIsVisible[the_line] = 1;
} else {
lineElements[the_line].attr({
visibility: 'hidden'
});
lineElements[the_line].prependTo(paper);
lineIsVisible[the_line] = 0;
}
}
}
// this function takes an array of length = num lines, and displays the line whereever there
// is a 1 in the array.
function showConfiguration(configuration) {
for (var i = 0; i < configuration.length; i++) {
if (configuration[i] != lineIsVisible[i]) {
toggle_line(i);
}
}
}
// highlight a line
function highlightLine(line) {
lineElements[line].attr("stroke", "#f00");
}
// start recording the time
var startTime = (new Date()).getTime();
display_element.innerHTML += '<p><button id="jspsych-palmer-submitButton" class="jspsych-btn" type="button">'+trial.button_label+'</button></p>';
display_element.querySelector('#jspsych-palmer-submitButton').addEventListener('click', function() {
save_data();
});
if (trial.prompt !== "") {
display_element.innerHTML += '<div id="jspsych-palmer-prompt">'+trial.prompt+'</div>';
}
function arrayDifferences(arr1, arr2) {
var n_diff = 0;
for (var i = 0; i < arr1.length; i++) {
if (arr1[i] != arr2[i]) {
n_diff++;
}
}
return n_diff;
}
// save data
var trial_data = {};
function save_data() {
// measure RT
var endTime = (new Date()).getTime();
var response_time = endTime - startTime;
// check if configuration is correct
// this is meaningless for trials where the user can't edit
var n_diff = arrayDifferences(trial.configuration, lineIsVisible);
var correct = (n_diff === 0);
trial_data = {
"configuration": JSON.stringify(lineIsVisible),
"target_configuration": JSON.stringify(trial.configuration),
"rt": response_time,
"correct": correct,
"num_wrong": n_diff,
};
if (trial.show_feedback) {
// hide the button
display_element.querySelector('#jspsych-palmer-submitButton').style.display = 'none';
display_element.querySelector('#jspsych-palmer-prompt').style.display = 'none';
showConfiguration(trial.configuration);
var feedback = "";
if (correct) {
feedback = "Correct!";
} else {
if (n_diff > 1) {
feedback = "You missed " + n_diff + " lines. The correct symbol is shown above.";
} else {
feedback = "You missed 1 line. The correct symbol is shown above.";
}
}
display_element.innerHTML += "<p id='jspsych-palmer-feedback'>" + feedback + "</p>";
jsPsych.pluginAPI.setTimeout(function() {
next_trial();
}, trial.timing_feedback);
} else {
next_trial();
}
}
function next_trial() {
display_element.innerHTML = '';
// next trial
jsPsych.finishTrial(trial_data);
}
};
// method for drawing palmer stimuli.
// returns the string description of svg element containing the stimulus
plugin.generate_stimulus = function(square_size, grid_spacing, circle_radius, configuration) {
var size = grid_spacing * (square_size + 1);
// create a div to hold the generated svg object
var stim_div = document.querySelector('html').innerHTML += '<div id="jspsych-palmer-container" style="display:none;"><svg id="jspsych-palmer-temp-stim" width="' + size + '" height="' + size + '"></svg></div>';
// create the snap object
var paper = Snap("#jspsych-palmer-temp-stim");
// create the circles at the vertices.
var circles = [];
var node_idx = 0;
for (var i = 1; i <= square_size; i++) {
for (var j = 1; j <= square_size; j++) {
var circle = paper.circle(grid_spacing * j, grid_spacing * i, circle_radius);
circle.attr("fill", "#000").attr("stroke-width", "0").attr("stroke", "#000").data("node", node_idx);
node_idx++;
circles.push(circle);
}
}
// create all possible lines that connect circles
var horizontal_lines = [];
var vertical_lines = [];
var backslash_lines = [];
var forwardslash_lines = [];
for (var i = 0; i < square_size; i++) {
for (var j = 0; j < square_size; j++) {
var current_item = (i * square_size) + j;
// add horizontal connections
if (j < (square_size - 1)) {
horizontal_lines.push([current_item, current_item + 1]);
}
// add vertical connections
if (i < (square_size - 1)) {
vertical_lines.push([current_item, current_item + square_size]);
}
// add diagonal backslash connections
if (i < (square_size - 1) && j < (square_size - 1)) {
backslash_lines.push([current_item, current_item + square_size + 1]);
}
// add diagonal forwardslash connections
if (i < (square_size - 1) && j > 0) {
forwardslash_lines.push([current_item, current_item + square_size - 1]);
}
}
}
var lines = horizontal_lines.concat(vertical_lines).concat(backslash_lines).concat(forwardslash_lines);
// actually draw the lines
var lineIsVisible = [];
var lineElements = [];
for (var i = 0; i < lines.length; i++) {
var line = paper.path("M" + circles[lines[i][0]].attr("cx") + " " + circles[lines[i][0]].attr("cy") + "L" + circles[lines[i][1]].attr("cx") + " " + circles[lines[i][1]].attr("cy")).attr("stroke-width", "8").attr("stroke", "#000");
line.attr({
visibility: 'hidden'
});
lineElements.push(line);
lineIsVisible.push(0);
}
// define some helper functions to toggle lines on and off
// this function turns a line on/off based on the index (the_line)
function toggle_line(the_line) {
if (the_line > -1) {
if (lineIsVisible[the_line] === 0) {
lineElements[the_line].attr({
visibility: 'visible'
});
lineElements[the_line].prependTo(paper);
lineIsVisible[the_line] = 1;
} else {
lineElements[the_line].attr({
visibility: 'hidden'
});
lineElements[the_line].prependTo(paper);
lineIsVisible[the_line] = 0;
}
}
}
// displays the line wherever there
// is a 1 in the array.
// showConfiguration(configuration)
for (var i = 0; i < configuration.length; i++) {
if (configuration[i] == 1) {
toggle_line(i);
}
}
var svg = document.getElementById("jspsych-palmer-container").innerHTML;
document.getElementById('jspsych-palmer-container').outerHTML = '';
return svg;
};
return plugin;
})();

View File

@ -1,16 +0,0 @@
const root = '../../';
jest.useFakeTimers();
describe('palmer plugin', function(){
beforeEach(function(){
require(root + 'jspsych.js');
require(root + 'plugins/jspsych-palmer.js');
});
test('loads correctly', function(){
expect(typeof window.jsPsych.plugins['palmer']).not.toBe('undefined');
});
});