Skip to main content

chiasm-jspsych-integration.js

This helper library bridges Chiasm eye-tracking and jsPsych. Save it as chiasm-jspsych-integration.js next to your experiment's index.html and load it with a <script> tag. See the jsPsych tutorial for a full walkthrough.

/* Chiasm.jsPsych Integration

- Prediction buffer
- Assigning predictions to trials
- Attaching start/stop to trials
- Final save

*/
let predictionBuffer = [];

const chiasmJsPsych = {
tracker: null,

setup: async function(expId, ppId, authToken) {
const chiasm = await initChiasmTracker({
authToken: authToken,
...(typeof window !== 'undefined' ? window.CHIASM_TEST_OVERRIDES || {} : {}),
});

if (!chiasm) {
throw new Error('Tracker failed to initialize');
}

chiasm.setUserPredictionCallback(pred => {
predictionBuffer.push({ ...pred });
});

chiasm.setExpInfo(expId, ppId, true);
await chiasm.startSession();
await chiasm.showScreenCalibration();
await chiasm.setupTrackerWithRetries();

this.tracker = chiasm;
},

createChiasmSetup: function(expId, ppId, authToken) {
return {
type: jsPsychCallFunction,
async: true,
func: async function(done) {
try {
await chiasmJsPsych.setup(expId, ppId, authToken);
} catch (error) {
console.error('Chiasm setup failed:', error);
alert('Eye-tracking setup failed. The experiment will continue without gaze recording.');
}
done();
}
};
},

attachToTrial: function(trial) {

// Preserve original on_load if it exists
const originalOnLoad = trial.on_load;

trial.on_load = async() => {
if (originalOnLoad && typeof originalOnLoad === 'function') {
originalOnLoad.call(trial);
}

if (!this.tracker) return;

let eventId = null;
if (trial.chiasm_event_id !== undefined) {
eventId = typeof trial.chiasm_event_id === 'function'
? trial.chiasm_event_id.call(trial)
: trial.chiasm_event_id;
}
else if (typeof jsPsych !== 'undefined' && typeof jsPsych.getProgress === 'function') {
// Fallback: use the jsPsych global trial index as the event ID so each
// trial gets a unique label (matches data.trial_index seen in on_finish).
eventId = `trial_${jsPsych.getProgress().current_trial_global}`;
}

window.currentTrialTimestamps = eventId != null
? await this.tracker.startRecording(eventId)
: await this.tracker.startRecording();
};

const originalOnFinish = trial.on_finish;

trial.on_finish = async (data) => {
if (this.tracker) {
await this.tracker.stopRecording();
data.chiasm_timestamps = (window.currentTrialTimestamps || []).map(f => ({
trial_index: data.trial_index,
frame_number: f.frameNumber,
frame_id: f.frameID,
timestamp: f.timestamp,
}));
}

if (originalOnFinish && typeof originalOnFinish === 'function') {
await originalOnFinish.call(trial, data);
}
};

return trial;
},

finalize: async function(jsPsychInstance) {
if (this.tracker) {
await this.tracker.ensureAllPredictionsReturned();
}

const trials = jsPsychInstance.data.get().values();

const trialByFrameId = new Map();
for (const trial of trials) {
for (const ts of trial.chiasm_timestamps || []) {
if (ts.frame_id) trialByFrameId.set(ts.frame_id, trial);
}
}

for (const pred of predictionBuffer) {
const trial = trialByFrameId.get(pred.frameId);
if (trial) {
if (!trial.chiasm_predictions) trial.chiasm_predictions = [];
trial.chiasm_predictions.push(pred);
}
}

const blob = new Blob([JSON.stringify(trials, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "chiasm_experiment_data.json";
a.click();
URL.revokeObjectURL(url);
},

cleanupTracker: async function() {
if (this.tracker) {
await this.tracker.cleanupTracker();
}
}
};