サイトアイコン kscscr

jsPsych(ver6/ver8対応版)で作成した実験をQualtricsに埋め込んでWebで実行する方法:新しいアンケート回答エクスペリエンスにも対応

jsPsych

jsPsych ver8を、Qualtricsに埋め込んで、実験を実施する方法です。Qualtricsの新しいアンケート回答エクスペリエンスにも対応しています。Codexと議論をしながら、新しく作成してみました。

以前、執筆した「jsPsychで作成した実験をQualtricsに埋め込んでWebで実行する方法・Rによるデータ読み込み」は、jsPsych ver6でしか動作しませんでしたが、今回はjsPsych ver8でも動作するようにしています。

基本的な実装方法は、jsPsych ver6時代と同じです。変更点はQualtricsに実装するJavaScriptコード部分になります。

【要注意】jsPsychをQualtricsの「新しいアンケート回答エクスペリエンス」で使う際の注意点:埋め込みデータの扱いやサイズに注意

上の記事で紹介している注意点にも気をつけてください。

(同じ)必要なモノ・全体の流れ

  1. jsPsychのHTMLファイルを編集・分割
  2. サーバーにアップロード
  3. Qualtrics上の設定

大きく分けてやらなければならないことは上記の3つ。まずjsPsychのファイルを抽出し分割します。ここで必ず作成しなければならないのが「main.js」ファイルと呼ばれる実験のコアとなる部分。これと画像などをサーバーにアップロードし、jsPsychのプラグインやCSSをQualtricsで呼び出すための設定を行えば完成。

(同じ)実験プログラムをQualtrics様に編集する

main.jsの作成

RMarkdownで作成しknitした、「experiment.html」というファイルが作成されている状態からスタートします。

//この上にはheadタグなど
.
.
<script src="jspsych/dist/jspsych.js"></script>
<script src="jspsych/dist/plugin-instructions.js"></script>
<script src="jspsych/dist/plugin-html-keyboard-response.js"></script>
<script src="jspsych/dist/plugin-iat-html.js"></script>
<script src="jspsych/dist/plugin-survey-text.js"></script>
<script src="jspsych/dist/plugin-fullscreen.js"></script>
<link href="jspsych/dist/jspsych.css" rel="stylesheet">
 
//!ここから!
<script>
//実験部分
var trial = {
    type: jsPsychInstructions,
    pages: [
    'Welcome to the experiment. Click next to begin.',
    'This is the second page of instructions.',
    'This is the final page.'
    ],
    show_clickable_nav: true
}
.
.
.//実験内容が書いてある
.
.
timeline.push(endmessage);
</script>
//!ここまで!

<script type="text/javascript">
    var jsPsych = initJsPsych();
</script>

<script src="main.js"></script>
<script type="text/javascript">

    /* 実験開始 */
    jsPsych.run(timeline);
</script>

VSCodeなどお好みのエディタで、新規に「main.js」というファイルを作成します。main.jsにjsPsychのJavascriptの部分、上記がexperiment.htmlファイルだとすると、「!ここから!」から「!ここまで!」に相当する部分をコピペします。

(同じ)main.jsの画像のパスを変更

var repo_site = "https://自分のサーバーのアドレス/フォルダ名/";

次に、main.js内の画像のパスを変更します。main.js内に画像のファイルを置いたURLを変数名として保存。一番上に上の一行を追加しましょう。自サーバーに保存する場合とGitHubにアップする場合でそれぞれ上のリンクは改変してください。

  //教示文を読んで入力してもらう
  var iat_instruction_test = {
    type: "jsPsychInstructions",
    pages:["<img src='" + repo_site + "Picture03.png' width='60%'>"+"<p style = 'font-size:1.5em; text-align: center'>練習です</p>"+"<p>キーボードを利用した単語の分類課題を行います。</p>"+"<p>画面中央に表示される単語が、左上の<b>「よい」</b>または<b>「虫」</b>のカテゴリーに当てはまると思ったら<b>「E」</b>キーを、<br>右上の<b>「わるい」</b>のカテゴリーに当てはまると思ったら<b>「I」</b>キーを押してください。</p>"+ "<p><b>左右のカテゴリーは固定で、中央の単語が変わります。</b></p>"+"<p>間違えるとX(バツ)が中央に表示されるので、押したキーと反対のキーを押してください</br><b>スペースキー</b>を押すと開始します。</p>"+"<p style = 'font-size: 1.5em;'>単語が表示されたら、なるべく早く回答してください。</p>"],
    show_clickable_nav: true
  };

main.js内にある画像を、さっき設定した変数名を含む形へ、つまり画像のリンクを

//こっちから
<img src='Picture03.png' width='60%'>
//こっちへ下
<img src='" + repo_site + "Picture03.png' width='60%'>

の様に設定しなおしましょう。

(同じ)experiment_updated.htmlの作成

<script src="main.js"></script>

experiment.htmlを複製し、「experiment_updated.html」というファイルを作成します。experiment_updated.htmlから、先程main.jsでコピーした部分を削除します。上の記述をそのファイルのjsPsychを読み込んでいる部分に追加します。

ここでローカルでexperiment_updated.htmlをダブルクリックして問題なく実験を行うことができればOK。これはあくまでmain.jsが動作するかどうかを確認するためのファイル。データが保存されているかどうかは気にしなくて大丈夫です。

(同じ)jsPsychの実験をサーバーに置く


ここまで来たらサーバーにファイルを設置します。main.jsとjsPsych本体、画像をFTPクライアントなどを使ってアップロードします。アクセスできれば自分が運営しているWordPressをインストールしているサーバーにサブドメインなどを作ってそこにおいてしまうのもいいでしょう。

(変更)Qualtrics.jsの準備:jsPsych ver8用

適宜自分のサーバーリンクやプラグインに置き換えて使用してください。自分で修正すべきポイントとしては、サーバーのリンクプラグインのリンクignoreやfilterColumnsで保存するデータ列の選択です。

Qualtricsの新しいアンケート回答エクスペリエンスには、20KB制限があります。以下のコードでは、一つの埋め込みデータに収まるようにignoreやfilterColumnsでデータサイズを小さくしています。データサイズを一切小さくしたくない場合は、分割して保存することもできます

Qualtrics.SurveyEngine.addOnload(function () {
    /*Place your JavaScript here to run when the page loads*/

});

Qualtrics.SurveyEngine.addOnReady(function () {
    /*Place your JavaScript here to run when the page is fully displayed*/
    var qthis = this;
    qthis.hideNextButton();

    var task_github = "自分のサーバーのリンクを設定してください"; // https://<your-github-username>.github.io/<your-experiment-name>/
    var taskBase = task_github.charAt(task_github.length - 1) === "/" ? task_github : task_github + "/";
    var assetVersion = "20260615_03";
    var maxEmbeddedDataChars = 12000;
    var debugDataSize = true;

    var jsPsychScripts = [
        assetUrl("jspsych/dist/jspsych.js"),
        assetUrl("jspsych/dist/plugin-html-keyboard-response.js"),
        assetUrl("jspsych/dist/plugin-instructions.js"),
        assetUrl("jspsych/dist/plugin-iat-html.js"),
        assetUrl("jspsych/dist/plugin-fullscreen.js"),
        assetUrl("jspsych/dist/plugin-survey-text.js")
    ];

    function assetUrl(path) {
        return taskBase + path + "?v=" + assetVersion;
    }

    function loadJsScript(src) {
        console.log("Loading ", src);
        return new Promise(function (resolve, reject) {
            var script = document.createElement("script");
            script.src = src;
            script.async = false;
            script.onload = function () {
                resolve(src);
            };
            script.onerror = function () {
                reject(new Error("Failed to load " + src));
            };
            document.head.appendChild(script);
        });
    }

    function loadScriptsInOrder(scripts) {
        return scripts.reduce(function (promise, src) {
            return promise.then(function () {
                return loadJsScript(src);
            });
        }, Promise.resolve());
    }

    function loadCss(href) {
        console.log("Loading ", href);
        return new Promise(function (resolve, reject) {
            var anchor = document.createElement("a");
            anchor.href = href;

            var existingLinks = document.querySelectorAll('link[rel="stylesheet"]');
            for (var i = 0; i < existingLinks.length; i++) {
                if (existingLinks[i].href === anchor.href) {
                    resolve(href);
                    return;
                }
            }

            var link = document.createElement("link");
            link.rel = "stylesheet";
            link.type = "text/css";
            link.href = href;
            link.onload = function () {
                resolve(href);
            };
            link.onerror = function () {
                reject(new Error("Failed to load " + href));
            };
            document.head.appendChild(link);
        });
    }

    function showNextButton() {
        if (typeof qthis.showNextButton === "function") {
            qthis.showNextButton();
        }
    }

    function removeElement(id) {
        var element = document.getElementById(id);
        if (element) {
            element.parentNode.removeChild(element);
        }
    }

    function createDisplayElement(id) {
        var existing = document.getElementById(id);

        if (existing) {
            return existing;
        }

        var element = document.createElement("div");
        element.id = id;
        document.body.appendChild(element);
        return element;
    }

    function showLoadError(message, error) {
        console.error(message, error);

        var displayStage = document.getElementById("display_stage");
        if (displayStage) {
            displayStage.innerHTML = "";
            var paragraph = document.createElement("p");
            paragraph.textContent = "実験プログラムの読み込みに失敗しました。お手数ですが管理者までご連絡ください。";
            displayStage.appendChild(paragraph);
        }

        showNextButton();
    }

    function getUtf8Bytes(text) {
        if (window.TextEncoder) {
            return new TextEncoder().encode(text).length;
        }

        return unescape(encodeURIComponent(text)).length;
    }

    function showDataSizeDebug(mode, datajs) {
        var debugInfo = {
            mode: mode,
            characters: datajs.length,
            bytes: getUtf8Bytes(datajs),
            preview: datajs.slice(0, 300)
        };

        console.log("datajs debug:", debugInfo);
        console.log("datajs export mode: " + mode + " characters: " + datajs.length);

        if (!debugDataSize) {
            return;
        }

        var displayStage = document.getElementById("display_stage");
        if (!displayStage) {
            return;
        }

        var debugBox = document.createElement("pre");
        debugBox.id = "datajs_debug";
        debugBox.style.whiteSpace = "pre-wrap";
        debugBox.style.textAlign = "left";
        debugBox.style.margin = "24px auto";
        debugBox.style.maxWidth = "900px";
        debugBox.style.padding = "16px";
        debugBox.style.border = "1px solid #999";
        debugBox.style.background = "#f7f7f7";
        debugBox.textContent =
            "datajs export mode: " + mode + "\n" +
            "characters: " + debugInfo.characters + "\n" +
            "bytes_utf8: " + debugInfo.bytes + "\n" +
            "preview:\n" + debugInfo.preview;

        displayStage.innerHTML = "";
        displayStage.appendChild(debugBox);
    }

    function buildDataJson(data) {
        var exportMode = "DataSizeCheck";

        //旧アンケート回答エクスペリエンスはこっち(不要列を数行削除する)
        // var datajs = data
        //     .ignore("不要な列名1")
        //     .ignore("不要な列名2")
        //     .json();

        // 新アンケート回答エクスペリエンス
        var datajs = data
            .ignore([
                "time_elapsed",
                "trial_index",
                "success",
                "key_press",
                "internal_node_id",
                "view_history",
                "plugin_version",
                "不要な列名"
            ])
            .filterColumns(["stimulus", "response", "correct", "rt"])
            .json();

        if (datajs.length > maxEmbeddedDataChars) {
            console.warn("datajs is still large for Qualtrics after filtering columns.", datajs.length);
        }

        showDataSizeDebug(exportMode, datajs);
        return datajs;
    }

    function saveQualtricsData(key, value) {
        if (typeof Qualtrics.SurveyEngine.setJSEmbeddedData === "function") {
            Qualtrics.SurveyEngine.setJSEmbeddedData(key, value);
            return;
        }

        if (typeof Qualtrics.SurveyEngine.setEmbeddedData === "function") {
            Qualtrics.SurveyEngine.setEmbeddedData(key, value);
            return;
        }

        throw new Error("Qualtrics embedded data API is not available.");
    }

    var displayStage = null;

    function initExp() {
        window.jsPsych = initJsPsych({
            display_element: displayStage,
            on_finish: function (data) {
                try {
                    var datajs = buildDataJson(data);
                    saveQualtricsData("datajs", datajs);
                } catch (error) {
                    showLoadError("Failed to save jsPsych data", error);
                    return;
                }

                removeElement("display_stage");
                removeElement("display_stage_background");

                window.setTimeout(function () {
                    qthis.clickNextButton();
                }, debugDataSize ? 5000 : 1000);
            }
        });

        loadJsScript(assetUrl("main.js")).then(function () {
            jsPsych.run(timeline);
        }).catch(function (error) {
            showLoadError("Failed to load main.js", error);
        });
    }

    if (window.Qualtrics && (!window.frameElement || window.frameElement.id !== "mobile-preview-view")) {
        if (window.__jsPsychQualtricsV2Started) {
            return;
        }

        window.__jsPsychQualtricsV2Started = true;
        createDisplayElement("display_stage_background");
        displayStage = createDisplayElement("display_stage");

        Promise.all([
            loadCss(assetUrl("jspsych/dist/jspsych.css")),
            loadScriptsInOrder(jsPsychScripts)
        ]).then(initExp).catch(function (error) {
            showLoadError("Failed to load jsPsych v8 resources", error);
        });
    }
});

Qualtrics.SurveyEngine.addOnUnload(function () {
    /*Place your JavaScript here to run when the page is unloaded*/

});

新規ファイルとして「qualtrics.js」というファイルを作成し、task_githubの部分は適宜、自分がファイルを置くリンクの場所に変更してください。これは後でQualtrics上にコピペします。

変更点としては、jsPsych.init() を initJsPsych() + jsPsych.run() に変更したり、main.js の読み込みタイミングを変更したり、変更を加えています。

また、適宜コメントアウトを参照し、新アンケート回答エクスペリエンスか、旧アンケート回答エクスペリエンスかによって、データ保存部分は修正してください。

(変更)Qualtrics.jsの準備:jsPsych ver6用

適宜自分のサーバーリンクやプラグインに置き換えて使用してください。自分で修正すべきポイントとしては、サーバーのリンクプラグインのリンクignoreやfilterColumnsで保存するデータ列の選択です。

Qualtricsの新しいアンケート回答エクスペリエンスには、20KB制限があります。以下のコードでは、一つの埋め込みデータに収まるようにignoreやfilterColumnsでデータサイズを小さくしています。データサイズを一切小さくしたくない場合は、分割して保存することもできます

Qualtrics.SurveyEngine.addOnload(function () {
    /*Place your JavaScript here to run when the page loads*/

});

Qualtrics.SurveyEngine.addOnReady(function () {
    /*Place your JavaScript here to run when the page is fully displayed*/
    var qthis = this;
    qthis.hideNextButton();

    var task_github = "サーバーのリンクを設定"; // https://<your-github-username>.github.io/<your-experiment-name>/
    var taskBase = task_github.charAt(task_github.length - 1) === "/" ? task_github : task_github + "/";
    var assetVersion = "20260615_01";
    var maxEmbeddedDataChars = 12000;
    var debugDataSize = true;

    var requiredResources = [
        assetUrl("jspsych-6.1.0/jspsych.js"),
        assetUrl("jspsych-6.1.0/plugins/jspsych-html-keyboard-response.js"),
        assetUrl("jspsych-6.1.0/plugins/jspsych-instructions.js"),
        assetUrl("jspsych-6.1.0/plugins/jspsych-iat-html.js"),
        assetUrl("jspsych-6.1.0/plugins/jspsych-fullscreen.js"),
        assetUrl("jspsych-6.1.0/plugins/jspsych-survey-text.js"),
        assetUrl("main.js")
    ];

    function assetUrl(path) {
        return taskBase + path + "?v=" + assetVersion;
    }

    function loadJsScript(src) {
        console.log("Loading ", src);
        return new Promise(function (resolve, reject) {
            var script = document.createElement("script");
            script.src = src;
            script.async = false;
            script.onload = function () {
                resolve(src);
            };
            script.onerror = function () {
                reject(new Error("Failed to load " + src));
            };
            document.head.appendChild(script);
        });
    }

    function loadScriptsInOrder(scripts) {
        return scripts.reduce(function (promise, src) {
            return promise.then(function () {
                return loadJsScript(src);
            });
        }, Promise.resolve());
    }

    function loadCss(href) {
        console.log("Loading ", href);
        return new Promise(function (resolve, reject) {
            var anchor = document.createElement("a");
            anchor.href = href;

            var existingLinks = document.querySelectorAll('link[rel="stylesheet"]');
            for (var i = 0; i < existingLinks.length; i++) {
                if (existingLinks[i].href === anchor.href) {
                    resolve(href);
                    return;
                }
            }

            var link = document.createElement("link");
            link.rel = "stylesheet";
            link.type = "text/css";
            link.href = href;
            link.onload = function () {
                resolve(href);
            };
            link.onerror = function () {
                reject(new Error("Failed to load " + href));
            };
            document.head.appendChild(link);
        });
    }

    function showNextButton() {
        if (typeof qthis.showNextButton === "function") {
            qthis.showNextButton();
        }
    }

    function removeElement(id) {
        var element = document.getElementById(id);
        if (element) {
            element.parentNode.removeChild(element);
        }
    }

    function createDisplayElement(id) {
        var existing = document.getElementById(id);

        if (existing) {
            return existing;
        }

        var element = document.createElement("div");
        element.id = id;
        document.body.appendChild(element);
        return element;
    }

    function showLoadError(message, error) {
        console.error(message, error);

        var displayStage = document.getElementById("display_stage");
        if (displayStage) {
            displayStage.innerHTML = "";
            var paragraph = document.createElement("p");
            paragraph.textContent = "実験プログラムの読み込みに失敗しました。お手数ですが管理者までご連絡ください。";
            displayStage.appendChild(paragraph);
        }

        showNextButton();
    }

    function getUtf8Bytes(text) {
        if (window.TextEncoder) {
            return new TextEncoder().encode(text).length;
        }

        return unescape(encodeURIComponent(text)).length;
    }

    function showDataSizeDebug(mode, datajs) {
        var debugInfo = {
            mode: mode,
            characters: datajs.length,
            bytes: getUtf8Bytes(datajs),
            preview: datajs.slice(0, 300)
        };

        console.log("datajs debug:", debugInfo);
        console.log("datajs export mode: " + mode + " characters: " + datajs.length);

        if (!debugDataSize) {
            return;
        }

        var displayStage = document.getElementById("display_stage");
        if (!displayStage) {
            return;
        }

        var debugBox = document.createElement("pre");
        debugBox.id = "datajs_debug";
        debugBox.style.whiteSpace = "pre-wrap";
        debugBox.style.textAlign = "left";
        debugBox.style.margin = "24px auto";
        debugBox.style.maxWidth = "900px";
        debugBox.style.padding = "16px";
        debugBox.style.border = "1px solid #999";
        debugBox.style.background = "#f7f7f7";
        debugBox.textContent =
            "datajs export mode: " + mode + "\n" +
            "characters: " + debugInfo.characters + "\n" +
            "bytes_utf8: " + debugInfo.bytes + "\n" +
            "preview:\n" + debugInfo.preview;

        // displayStage.innerHTML = "";
        // displayStage.appendChild(debugBox);
    }

    function filterColumnsToJson(dataCollection, columns) {
        if (typeof dataCollection.filterColumns === "function") {
            return dataCollection.filterColumns(columns).json();
        }

        var rows = dataCollection.values().map(function (trial) {
            var row = {};

            columns.forEach(function (column) {
                if (typeof trial[column] !== "undefined") {
                    row[column] = trial[column];
                }
            });

            return row;
        });

        return JSON.stringify(rows);
    }

    function buildDataJson() {
        var exportMode = "DataSizeCheck";
        var filteredData = jsPsych.data.get()
            .ignore("time_elapsed")
            .ignore("trial_index")
            .ignore("success")
            .ignore("key_press")
            .ignore("internal_node_id")
            .ignore("view_history")
            .ignore("plugin_version")
            .ignore("不要な列名");
        var datajs = filterColumnsToJson(filteredData, ["stimulus", "response", "correct", "rt"]);

        if (datajs.length > maxEmbeddedDataChars) {
            console.warn("datajs is still large for Qualtrics after filtering columns.", datajs.length);
        }

        showDataSizeDebug(exportMode, datajs);
        return datajs;
    }

    function saveQualtricsData(key, value) {
        if (typeof Qualtrics.SurveyEngine.setJSEmbeddedData === "function") {
            Qualtrics.SurveyEngine.setJSEmbeddedData(key, value);
            return;
        }

        if (typeof Qualtrics.SurveyEngine.setEmbeddedData === "function") {
            Qualtrics.SurveyEngine.setEmbeddedData(key, value);
            return;
        }

        console.warn("Qualtrics embedded data API is not available.");
    }

    function initExp() {
        jsPsych.init({
            timeline: timeline,
            display_element: "display_stage",
            on_finish: function () {
                try {
                    var datajs = buildDataJson();
                    saveQualtricsData("datajs", datajs);
                } catch (error) {
                    showLoadError("Failed to save jsPsych data", error);
                    return;
                }

                window.setTimeout(function () {
                    removeElement("display_stage");
                    removeElement("display_stage_background");
                    qthis.clickNextButton();
                }, debugDataSize ? 5000 : 1000);
            }
        });
    }

    if (window.Qualtrics && (!window.frameElement || window.frameElement.id !== "mobile-preview-view")) {
        if (window.__jsPsychQualtricsV6Started) {
            return;
        }

        window.__jsPsychQualtricsV6Started = true;
        createDisplayElement("display_stage_background");
        createDisplayElement("display_stage");

        Promise.all([
            loadCss(assetUrl("jspsych-6.1.0/css/jspsych.css")),
            loadScriptsInOrder(requiredResources)
        ]).then(initExp).catch(function (error) {
            showLoadError("Failed to load jsPsych v6 resources", error);
        });
    }
});

Qualtrics.SurveyEngine.addOnUnload(function () {
    /*Place your JavaScript here to run when the page is unloaded*/

});

(変更)Qualtrics上での設定


新しい質問を作成し、質問タイプを「テキスト/グラフィック」にします。テキストを編集画面に切り替え、リッチコンテンツエディターからhtmlの編集モードに切り替えます。

<span style="font-size: 24px;">もしこのメッセージが <span style="color: rgb(255, 0, 0);"><b>5分以上表示されていたら、</b></span><br>
お手数ですが管理者までご連絡ください。 </span><br>
<br>
<style type="text/css">#display_stage_background {
  position: fixed;
  inset: 0;
  z-index: 999998;
  background-color: #fff;
}

#display_stage {
  position: fixed;
  inset: 0;
  z-index: 999999;
  width: 100vw;
  height: 100vh;
  box-sizing: border-box;
  background-color: #fff;
}

#display_stage.jspsych-display-element {
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  overflow-x: hidden;
}

#display_stage .jspsych-content-wrapper {
  width: 100%;
  min-height: 100%;
}

#display_stage .jspsych-content {
  width: min(100%, 1100px);
  box-sizing: border-box;
  padding: 24px;
}
</style>

そしてそこに上記の内容をペースト。一番上のリンクは自分のものに合わせて改変しましょう。

※また、Qualtricsの仕様として、”link href~~~”から始まるcss読み込みのコードは、再度編集画面を開くと削除される仕様になっています。jsPsych.cssの中身を丸ごとコピペして、”/* ↓jsPsych.cssの中身を以下に貼り付ける↓ */”の下に貼り付けておいてください。


後は、横にある歯車アイコンをクリックし、「javascriptを追加」を選択、 中身をすべて消しqualtrics.jsで記述した内容をコピペすればOK。

アンケートフローの設定


アンケートフローをクリックし、IATのブロックの下に「埋め込みデータ」を追加します。「新しいフィールド名を定義するか、ドロップダウンから選択」に「datajs」と入力し保存します。

これでQualtrics上でIATを実行できるようになります。

GitHubに置く場合


自前のレンタルサーバーではなく、GitHubにおいて実行する場合、該当レポジトリのSetting→Option→GitHub Pageの部分を上記のように設定しておかないと、設定したリンクにファイルをおいても実行されないので注意が必要です。

ちなみにこの記事で紹介していたmain.jsなどの「https://自サーバー」の部分は「https://ユーザー名.github.io/レポジトリ名/」に変更する必要があります。

モバイルバージョンを終了