Welcome to the lab.js documentation!¶
lab.js makes building in-browser experiments easy. It’s a simple, graphical tool to help you build studies for the web and the laboratory.
Thank you for checking out our project! We’ve collected a few links below to get you started, but we’re happy to help with any additional questions or ideas you have. We’d love to hear from you!
Introductory tutorial
For a first overview or a refresher, this is the place to start. |
Working with HTML
Once you know your way around, learning |
||
Recipes and examples
Because someone might have figured out that tricky thing before. |
Online data collection
When you've built your study, you'll want to run it and collect data. Here's how to do that. |
||
Developer reference
All library internals |
Contributing
Seriously, you're awesome. Suggestions, examples, even code are all super-welcome. |
Project website
This is where we show off all our features in glossy pictures. Show this to your friends, and boss! |
Support channel
Here's where you'll find help. |
||
GitHub repository
Work happens over on GitHub: Releases are cut, changes are logged, and issues reported. |
Twitter
Join our growing fan club and keep up-to-date with our worldwide sticker distribution efforts. |
Get started building studies¶
The lab.js builder is the easiest way to get started designing studies. You’ll build experiments using a graphical, drag-and-drop interface. We’d like to show it to you in this short tutorial – you’ll have your first study running in less than an hour.
Thanks so much for checking out lab.js
! We would love to support you and your work. This tutorial will walk you through the main features of our software.
Throughout this tutorial, you’ll be using the builder interface in your browser. It’s free to use, and always will be.
We’d love to help you if you have any questions; likewise, if you have suggestions for things we could explain better, we’re there for you.
Design a stimulus¶
In this section, we’ll take a look around the interface, and very briefly visit the different sections. By the end, you’ll know you way around, and be able to build a basic screen. We’ll build on that in the following parts.
If you haven’t already, you’ll need to open the builder interface.
Move between screens¶
In this part, we’ll explore how to move forward through a series of screens. This can happen automatically through a timeout, or following a participant’s response.
Trials (no tribulations)¶
What data is collected¶
Run studies locally¶
Grade responses¶
Providing feedback¶
Design screens with HTML¶
Note
This documentation page is currently under development. Sorry for that!
We’re actively working on this, so please check back in a bit. Also, please be invited to send us a line or two, we’ve probably got a half-baked working version that we can share, or we can help you get started directly.
Sorry for the trouble!
Experiment in style¶
Note
This documentation page is currently under development. Sorry for that!
We’re actively working on this, so please check back in a bit. Also, please be invited to send us a line or two, we’ve probably got a half-baked working version that we can share, or we can help you get started directly.
Sorry for the trouble!
The style attribute¶
Styling your study¶
Making a study look neat is helpful in several ways: A clear design helps participants navigate through the study, and it shows the professionalism of its creators. There are, of course, many ways to achieve this, and if you have built web-based experiments before, you might well have a preferred layout that is tested and proven.
With lab.js
, we’ve included a basic set of styles with the starter kit.
These are provided to get you started quickly, and to save you some hassle when
you’re building your first experiment. The following section describes the
styles that are available, and shows you how to apply them.
Important
You are in no way bound to the styles provided in the starterkit and described here – you’re very welcome to replace them, or extend and adapt them to your needs: These styles are designed to give users a head start, and are in no way mandatory.
If you notice something that’s missing or should be working differently, please let us know – we’re really happy to extend the built-in styles, and to make them more useful.
Contents
Including the styles¶
Note
If you’re working from the starter kit, the default styles have already been set up for you – you’re good to go, and ready to set up your page!
If you’ve been working on an existing study and would like to use the styles,
please download the latest starter kit and include the file lib/lab.css
in your project. You’ll also need to include a link tag in the head
section
of your document, with a reference to the file:
<link rel="stylesheet" href="lib/lab.css">
You might need to adjust the path in the href
attribute depending on the
placement of the downloaded style sheet file.
Setting up the page¶
Container¶
On the most coarse level, all content on the page is gathered inside a
container. This element holds all of the content and determines its width.
In the default style, it provides a thin outer border for the content. You can
create a container by applying the container
class to a div
or another
block element:

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Example Experiment</title>
<!-- Load styles -->
<link rel="stylesheet" href="lib/lab.css">
<!-- Load additional styles and scripts -->
</head>
<body>
<!-- Define the container -->
<div class="container">
<!-- Container content -->
</div>
</body>
</html>
Page sections¶
You’ll often want to subdivide the page into different sections containing different parts of the visible information. For example, you might want to include a header with your university’s logo, a footer with contact info or navigation buttons, and of course the main experiment content.
You can achieve this directly by placing header
, main
and footer
elements within the container:

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Example Experiment</title>
<link rel="stylesheet" href="lib/lab.css">
</head>
<body>
<div class="container">
<header>
Header
</header>
<main>
Main
</main>
<footer>
Footer
</footer>
</div>
</body>
</html>
Fullscreen styles¶

By adding the fullscreen
class to the container element, you can make it
expand to fill the entire width and height of the browser window.

Of course, any sections included in the container are positioned accordingly.
Text styles¶
The bulk of a study’s content will often be pure text. HTML
provides many
tags for text markup (such as headings, paragraphs, lists, etc.) out of the box,
and the stylesheet provides matching settings for many, even some exotic tags
like the keyboard button <kbd>key</kbd>
.
However, sometimes tags alone are not sufficient, and therefore we have added some helper classes to provide frequently used layout adjustments.

Alignment¶
The text-left
, text-center
and text-right
classes align text to
the left, center and right of its containing block.
Helper classes¶
The font-weight-bold
and font-italic
classes change the formatting of
an element’s text content.
Contextual formatting¶
Like the alerts shown above, there is often the need to mark text as secondary.
The text-muted
class achieves, applied to an element, will color its content
in gray.
Styling content¶

Beyond styles for regular text, we’ve tried to include CSS classes for purposes that we often use, and which we hope will come in handy in may studies. These are described in the following.
Alerts¶
Alerts help you highlight information that should not go unnoticed.
The basic alert
class, applied to a <div>
tag, will emphasize its
content by placing it on a grey background. Adding the alert-warning
or
alert-danger
class will change the color to yellow and red for drawing
further attention.

<div class="alert">
Let me draw your attention to this
</div>
<div class="alert alert-warning">
You have been warned
</div>
<div class="alert alert-danger">
Something is deeply wrong here
</div>
Tables¶
The default stylesheet adds horizontal dividers between the rows of tables
(this deviates from the bootstrap defaults, which require the table
class
for styling). Adding the table-striped
class to the table adds striped rows.
Any additional styles can be removed by adding the table-plain
class to the
table.

<table>
<tr>
<th>Table header 1</th>
<th>Table header 2</th>
</tr>
<tr>
<td>Table data 1a</td>
<td>Table data 2a</td>
</tr>
<tr>
<td>Table data 1b</td>
<td>Table data 2b</td>
</tr>
</table>
Positioning things¶
Alignment of block elements¶
The most common challenge encountered in building an experiment is the alignment of stimuli and other content. By default, content will be positioned in the top left of its containing element, but this need not always be the case.
The content-vertical-center
, content-horizontal-center
and
content-horizontal-right
classes place a single element in the vertical
center of it surrounding element, and, independently, in the horizontal center
and at the right border. Both sets of classes can be used in conjunction.

Block alignment examples
Note how the classes are applied to the surrounding elements, and not directly to the elements which whose position is changed.
Also, only the directly nested elements are aligned; their content must be positioned independently.
<div class="container">
<main class="content-horizontal-center
content-vertical-center">
<div>
The center of attention
</div>
</main>
<main class="content-horizontal-right
content-vertical-center">
<div style="width: 100px">
To the right
</div>
</main>
<main class="content-vertical-center">
<div>
Only one possibility left
</div>
</main>
<main class="content-vertical-center">
<div class="w-100">
Full width
</div>
</main>
</div>
Element visibility¶
The invisible
class hides an element from view, but still includes it in
the layout. Thereby, an empty space remains where the element would otherwise
have been rendered.
The hidden
class excludes an element from rendering, meaning that it will
not affect the page display in any way.
The hide-if-empty
class removes an element from the page if it does not
contain content.
Beyond the default styles¶

The default styles presented above are designed to be neutral and as widely applicable as possible. That very fact, however, makes them slightly boring.
If you like, you can do away with the default styles entirely. Nothing in the Javascript library dictates what your study should look like – it will happily exchange and display content regardless of structure of the page and the styles applied.
Alternatively, you can extend the default styles [1]. We often include a second stylesheet in the page header, which contains some a few rules that supplement and overwrite the defaults. In the screenshot on the right, the fonts have been changed slightly, and a dash of color added. Here’s what the additional style sheet looked like:
/* Add a dark page background,
and highlight the content */
body {
background-color: rgb(6, 21, 38);
}
div.container {
background-color: white;
border-width: 2px;
}
/* Use a serif font for the headers,
and add a bottom border to h1 elements */
h1, h2, h3 {
font-family: "Georgia", serif;
font-weight: normal;
}
h1 {
text-align: center;
border-bottom: 1px dotted lightgray;
padding-bottom: 0.8rem;
}
See also
Many of the selectors used here correspond (on purpose) to those used in the Bootstrap framework, which provides far more comprehensive styles for many more applications.
To a large degree, the supplied styles are a simplified subset and facsimile of bootstrap’s many and beautiful styles. Please check them out if you find the included stylesheet lacking – because the class names are, where possible, identical, switching should not be to big an effort.
There are several more such frameworks that cater to different tastes and programming styles, for example Semantic UI or Material Design.
[1] | You could, of course, also modify the stylesheet directly if you like. We caution against this approach, because you’ll loose the ability to update the default library stylesheet independently of your modifications. By overwriting the defaults explicitly, it will be easier to see exactly which adjustments you’ve made. |
Include questionnaires¶
Note
This documentation page is currently under development. Sorry for that!
We’re actively working on this, so please check back in a bit. Also, please be invited to send us a line or two, we’ve probably got a half-baked working version that we can share, or we can help you get started directly.
Sorry for the trouble!
Write custom logic¶
Note
This documentation page is currently under development. Sorry for that!
We’re actively working on this, so please check back in a bit. Also, please be invited to send us a line or two, we’ve probably got a half-baked working version that we can share, or we can help you get started directly.
Sorry for the trouble!
Deploy studies online¶
While building studies is (we hope) a pleasure in itself, the main aim of every study is to collect data in pursuit of a substantive question. Though the studies you build with lab.js
will run perfectly in the laboratory, collecting data over the web is often attractive [1].
In this section, we’ll teach you how to collect data online: We discuss the basics of how the internet works, and show you how to make it work for your research. Whether you’re running your own server, share some webspace with your university or employer, or rely on questionnaire tools, we’ve got you covered: lab.js
can run in all of these scenarios (and probably more).
How the net works¶
Setting up on a webspace with PHP¶
Interfacing with third-party tools¶
Many researchers collect data through questionnaire or survey tools, such as Qualtrics, SoSci Survey, or the like. These are great for collecting questionnaire-type data, but often limited with regard to experimental research.
Studies built in lab.js
integrate well with external tools, and will happily send their results to an external data collection service. This enables you to build the survey-based part of your study in the questionnaire software of your choice, while constructing the experimental part in lab.js
.
Steps
See also
This page covers integration with third-party tools in general. We cover the most popular tools individually:
Before diving into specifics which depend on the software you’re working with, let’s take a look at the basic ideas common to all solutions of this kind – It’s worth discussing how they work in general.
The mechanism for collecting experimental data in a questionnaire setting is the same regardless of the specific software: Basically, we pretend that the information collected during the experiment is one large free-form answer, along all the other responses in the survey. The experiment runs inside of the questionnaire, and places its data in a text input field that is hidden to the user.
This means that you’ll need a couple of things for this to work:
- Survey software that can create hidden input fields for you, and save the collected data.
- A place to host your experiment files so that the survey can embed them (a static webspace will do, you won’t need
PHP
support), and - Access to the code of the survey page that will contain your experiment so that you can embed it, and include the binding between study and survey in the
HTML
.
Prepare the experiment¶
The first step is to prepare the experiment you’ve built for use within external software. Once you have a working study, all you need to do is export it using the generic survey software integration. This will give you a zip file; we’d ask you to unpack and upload it to a hosting provider of your choice. From there, you should be able to run the study using your browser; please make a note of the URL
at which you accessed the study.
Prepare the survey¶
Inside the survey, you’ll need to add a new text input field that will serve to capture and store the experiment’s data. Because we’ll fill it with lots of strange-looking experimental data, it should ideally be hidden from participants (and not limited in length). You probably know best how to create this; we’ve provided pointers for a few tools we’ve worked with below.
Embed the experiment within the survey¶
The final step is to integrate the experiment within the survey. First, we’ll want the experiment to fit in the confined space of a questionnaire page.
The most straightforward way to achieve this is in a screen-in-screen approach, using an <iframe>
tag. This dedicates an area on the page to a page loaded from elsewhere. This is where you’ll need the link to the study you uploaded earlier: It will be the source of the iframe
, and referenced in the src
attribute. Here’s a snippet for you to use – you’ll notice that it additionally sets up the frame to use as much horizontal space as is available, and sets a minimum height to make sure the study is adequately visible.
<iframe
src="https://example.com/link/to/study"
style="width: 100%; min-height: 600px; border: none;"
></iframe>
If you include this code on a survey page, you should see the study embedded. We’re almost there: the final missing step is to catch the information generated and save it.
Store the data in the survey¶
An experiment exported for survey software and embedded in a external questionnaire will send its data to the surrounding page after when the experiment is complete. The responsability to capture and store the data thus lies with the surrounding page that is created in the survey software. In the case of questionnaire tools, the surrounding page needs to make sure that the data are saved within the survey.
To process and save the data, the surrounding page needs to capture the results and convert them into a format that the survey software understands. Depending on the setup of the page, it might also need to submit the page and move on to the next. This will take another small piece of code — it will look somewhat like this, depending on the specific questionnaire tool involved:
<script>
// Listen for the study sending data
window.addEventListener('message', (event) => {
// Make sure that the event is from lab.js, then ...
if (event.data.type === 'labjs.data') {
// ... extract the data lab.js is sending.
// The collected data is available via:
// - event.data.json for json-encoded data
// - event.data.csv for csv-formatted data
// - event.data.raw for the raw data array
const data = event.data.csv
// ... process data and submit page
// (the specific code here will depend on the tool
// you're using to process and store the data)
// ...
}
})
</script>
Process the collected data¶
Many third-party tools, and specifically those that are focussed on questionnaires, limit every participant’s data to a single row, enforcing a wide data format. This is at odds with most experimental data, where every dataset occupies many rows, resulting in a long-format dataset.
Because of this restriction, lab.js
may need to store all of the collected data in a single data cell for it to be compatible with other tools. We typically use the JSON encoding for this task, which may look unfamiliar at first, but is an established format for storing complex data structures.
Prior to analysis, it’s often useful to reverse this compression, and restore the full tabular dataset you’re probably used to getting from your experimental software. Thankfully, all major analysis tools can deal with JSON easily. We collect scripts for various tools, such as the following one for the R
programming language.
# This code relies on the pacman, tidyverse and jsonlite packages
require(pacman)
p_load('tidyverse', 'jsonlite')
# We're going to assume that the data coming from
# the third-party tool has been loaded into R,
# for example from a CSV file.
data_raw <- read_csv('raw_data_from_external_tool.csv')
# Please also check that any extraneous data that
# an external tool might introduce are stripped
# before the following steps. For example, Qualtrics
# introduces two extra rows of metadata after the
# header. Un-commenting the following command removes
# this line and re-checks all column data types.
#data_raw <- data_raw[-c(1, 2),] %>% type_convert()
# One of the columns in this file contains the
# JSON-encoded data from lab.js
labjs_column <- 'labjs-data'
# Unpack the JSON data and discard the compressed version
data_raw %>%
# Provide a fallback for missing data
mutate(
!!labjs_column := recode(.[[labjs_column]], .missing='[{}]')
) %>%
# Expand JSON-encoded data per participant
group_by_all() %>%
do(
fromJSON(.[[labjs_column]], flatten=T)
) %>%
ungroup() %>%
# Remove column containing raw JSON
select(-matches(labjs_column)) -> data
# The resulting dataset, available via the 'data'
# variable, now contains both the experimental
# data collected by lab.js, as well as any other
# columns introduced by the software that collected
# the data. Values from the latter are repeated
# to fill added rows.
# As a final step, you might want to save the
# resulting long-form dataset
#write_csv(data, 'labjs_data_output.csv')
Working with Qualtrics¶
Qualtrics is a popular proprietary questionnaire and survey tool that is easy to pick up. Studies built with lab.js
can be embedded in Qualtrics with a few simple steps; setting up the connection should take you about 10 minutes.
Steps
Set up data storage¶
As a first step, you’ll need to create a place to store the data. In Qualtrics, most data will come from survey questions; our external study will need an embedded data field where it can place the collected data.
You’ll can add an embedded data field in the survey flow dialogue, like this:
Caution
The following steps assume that you’ve called the field labjs-data
. If you’d prefer a different name, or if you’re combining several experiments in a single survey, please adjust the JavaScript snippet below.
Embed the study¶
Next, you’ll need to pull in the study you’ve built. As described in the introduction, you’ll need to host your study externally and embed it in the survey through an <iframe>
tag.
Because the data is saved in the embedded field you set up above, the study is best inserted in a Descriptive Text component in Qualtrics, rather than a question of its own. After you’ve inserted the new component, please click on its contents and change to the HTML View to insert the snippet below. You’ll need to change the URL
at the top to point to the study you’d like to embed.
<!-- Embed the study -->
<iframe
src="https://labjs-qualtrics.netlify.com"
style="width: 100%; min-height: 600px; border: none;"
></iframe>
<!-- Adjust the page style slightly -->
<style>
/* Hide button for skipping the page */
.NextButton {
visibility: hidden;
}
/* Remove border from last question */
.QuestionOuter:last-of-type .QuestionText {
border: none;
}
</style>
Note
Qualtrics requires that the study be accessed via an encrypted connection, so please make sure that the link you insert starts with https
.
Connect the study and the questionnaire¶
The next step is to link the behavior of the questionnaire and that of the study. The questionnaire should collect and store the generated data, and move to the next page after participants have completed the experiment. This requires a bit of logic, which is added to the question you created in the last step.
To achieve the connection, you’ll need to add JavaScript logic to the Descriptive Text question, inserting the following code inside the curly braces of the addOnReady
block. This snippet can stay as-is, unless you’d like to store the study data in a different embedded data field.
// Listen for the study sending data
window.addEventListener('message', function(event) {
// Make sure that the event is from lab.js, then ...
if (event.data.type === 'labjs.data') {
// ... extract the data lab.js is sending.
// We're going to work with JSON data
const data = event.data.json
// ... save data and submit page
Qualtrics.SurveyEngine.setEmbeddedData('labjs-data', data)
document.querySelector('.NextButton').click()
}
})
Working with the collected data¶
After setting up the survey and study as described, and going through the survey, you should see the collected data in the ‘Data & Analysis’ tab. It should appear as a single column of somewhat unwieldy data, named labjs-data
(unless, that is, you’ve changed this name).
The somewhat garbled appearance is because, like other questionnaire-focussed tools, Qualtrics enforces a wide data format, requiring a conversion step to decompress the data from lab.js
before further analyses can be done. This step is also required with other, similar tools, and therefore described in the general documentation.
Caution
If you can see the experiment embedded in the survey, but aren’t redirected to the next survey page after completing the experiment, or if you don’t see the collected data, please make sure that your experiment doesn’t get stuck on the last screen. For example, you might set a timeout on the last screen, or allow participants to respond to your goodbye message.
Without this, Qualtrics will not count the dataset as a complete response, and will exclude it from the data export.
Reproducible studies with the Experiment Factory¶
The Experiment Factory, or “expfactory” for short, is is an open source infrastructure for building and deploying reproducible experiment containers. You can export a lab.js
experiment and build it into a container (or combine multiple experiments, even if they are built with different tools) that is ready for deployment on a webserver with technologies like SSL, and extensive data collection options ranging from flat files to database support.
Steps
See also
The ambition and the powers of the Experiment Factory go way beyond hosting lab.js
studies. Please also check out:
- Vanessa Sochat’s (et al.) papers outlining the vision of standardizing experiments and using reproducible experiment containers.
- Their extensive library of pre-made paradigms, all of which are free to use.
- Their documentation on integrating with
lab.js
Design and export your experiment¶

After building your experiment with lab.js
, you’ll need to export it to the Experiment Factory by selecting the corresponding bundle from the dropdown menu in the toolbar.
The builder will present you with a pop-up window that allows you to select between exporting the study to a new container or adding it to an existing one. If you’re new to expfactory
, you’ll likely want to select the first option, creating a new container. This will export a zip
archive of all the files you need to plug into the Experiment Factory!

To help you learn and get started, we’ve provided an exported example of a Stroop task as a zip
file in the repository if you’d rather use a premade study.
In the following, we’ll show you how to get this experiment running in an experiment container, but you can go through the exact same steps with your own exported experiment.
Preparing a container¶
At this point, you should have a zip
file downloaded from the builder. It should include an experiments
directory containing your task, and a .circleci
directory with build instructions. If you don’t see this last directory, you might need to check your file manager’s settings (because it starts with a dot, Unix systems, Linux and Mac OS might treat it as a hidden folder).
Checking the task metadata¶
Just to be sure, please next check the file config.json
in the extracted experiment folder. It will contain further information about your task if you’ve provided it in the builder interface. Otherwise, please add at least the name
and exp_id
fields, and update the time
to a value in minutes. Here’s what the metadata looks like for the Stroop task:
$ cat experiments/stroop-task/config.json
{
"name": "Stroop task",
"exp_id": "stroop-task",
"url": "https://github.com/felixhenninger/lab.js/examples/",
"description": "An implementation of the classic paradigm introduced by Stroop (1935).",
"contributors": [
"Felix Henninger <mailbox@felixhenninger.com> (http://felixhenninger.com)"
],
"template": "lab.js",
"instructions": "",
"time": 5
}
Adding more tasks (optional)¶
You can add any additional tasks you’d like to include in your container by exporting them for use in an existing container, and extracting the resulting zip
file in the experiments/[taskname]
directory. The expfactory
will automatically pick up these tasks in the next step.
If you’d like to add additional experiments from the library you could add their names in a single line (separated by spaces) to an experiments.txt
file in the main folder lie so:
tower-of-london test-task
If you change your mind, you can add or remove tasks later and go through the following steps to update your container.
Building the container¶
We now will recruit the builder to turn our folder into a reproducible experiment container! Guess what? You don’t actually need to do any working with Docker (or other) locally! All you need to do is connect your repository to Github and create a container repository on Docker Hub, and then push. Let’s review these steps!
- Create a container repository on Docker Hub to correspond to the name you want to build
- Commit and push the code to Github
- Connect the repository to Circle Ci, and
- Add this name to the variable
CONTAINER_NAME
, along withDOCKER_USER
andDOCKER_PASS
to the set of encrypted environment variables in our CircleCI project settings.
Once you’ve done those steps, that’s it! The container will be built and pushed to Docker Hub on each commit.
See also
The expfactory homepage provides far more detailed information regarding the container internals, and also covers several more ways of generating containers.
Running the study¶
Once your container is deployed, you can run and use it! Read the Experiment Factory documentation to learn of all the ways that you can do this. You can deploy a headless battery, one that is interactive (requiring the experimenter to input an identifier), one with SSL, or use database backends ranging from the filesystem to a postgresql database. Regardless of your choice, the experiment container that you build, by way of being a container, can be reproducibly deployed and shared.
Here is an example of how you might run the example container that we described here:
docker run -d -p 80:80 vanessa/expfactory-stroop start
Where vanessa
is the user who created the container, expfactory-stroop
the container name, and 80
the port on which the port on which the webserver is hosting the study. If you open this port on your local machine, you’ll see the familiar, the beautiful, the Stroop task – or any other tasks you’ve included in your container. From the overview screen, assemble the series of tasks you want your participants to go through, and you’re good to go!

Finding help¶
Do you have a question? You can ask for help for an experiment or for anything related to the Experiment Factory software. We can help you with all steps along the way to assemble a reproducible container for others to also be empowered to deploy your paradigms.
Collecting data with JATOS¶
JATOS, a modest abbreviation for Just Another Tool for Online Studies, is much more than that – it’s a powerful, open source study hosting and data collection service that’s super-easy to use and provides many useful features. Among these are an excellent integration with Amazon Mechanical Turk and support for group studies in which participants interact online.
lab.js
integrates seamlessly with JATOS, so that studies can be set up in seconds.
Importing a study to JATOS¶
To import a study to JATOS, please export it using the JATOS integration from the builder interface. This will provide you with a zip archive that JATOS can import directly. Here’s the procedure in well under a minute:
There is no step two!¶
Seriously, JATOS is that awesome. You can run your study directly from the Worker and Batch Manager interface, which provides links you can distribute to participants.
With what we’ve discussed so far, however, we’ve only scratched the surface of what JATOS can do – it’s much more than a mere study runner: It will help you with participant and data management, and provides comprehensive privacy features for critical data. Please check out the JATOS paper and the online documentation for more information about its many capabilities.
Post-processing the data¶
Data access and download are available in JATOS via the results interface. For studies built with lab.js
, JATOS stores the raw, JSON-encoded data. The following snippet for R
imports this data format for analysis.
In the resulting data.frame
, the JATOS participant ID is available through the srid
column.
# This code relies on the pacman, tidyverse and jsonlite packages
require(pacman)
p_load('tidyverse', 'jsonlite')
# Read the text file from JATOS ...
read_file('jatos_results.txt') %>%
# ... split it into lines ...
str_split('\n') %>% first() %>%
# ... parse JSON into a data.frame
map_dfr(fromJSON, flatten=T) -> data
# Optionally save the resulting dataset
#write_csv(data, 'labjs_data_output.csv')
[1] | In practice, the location does not fully determine the deployment method: Even if you run your study in a laboratory, it can often be useful to collect data centrally on a server. We also know of colleagues who have asked their participants to collect and send in csv files, so that’s also possible if you’re in a pinch. |
Code a study from scratch¶
Welcome to the lab.js tutorial, and thank you for checking out our library! We hope you like it, and are excited to see how you are going to use it in your research.
On the following pages, we’re going to provide an introduction to building
experiments using lab.js
, and show you how the library works. In our
experience, it will take between 30 minutes and an hour to get a feel for how to
work with the library, and probably an afternoon to build your first experiment.
After that, our experience is that things get progressively easier, and students
can often build a complete experiment in an hour or two.
The experiments will be built as web pages, so the tutorial presupposes some familiarity with HTML and CSS, and some (minimal) experience in programming (not necessarily in Javascript – R users, in our experience, quickly feel at home).
If you are not familiar with HTML and CSS, it is well worth it to spend some time learning these skills, which are handy regardless of how you will build your experiments, and useful far beyond the domain of online experiments. These topics warrant their own tutorials; thankfully, Codecademy offers an excellent course on HTML and CSS that will teach you everything you need to know for building experiments online and more. If you are just getting started with making web pages, we warmly recommend this course. Similarly, there is a very good introductory Javascript course offered on Codecademy and another on Khan Academy. However, having detailed Javascript knowledge is not necessary for following the tutorial: If you have a little experience in programming (especially with R), or if you are willing to experiment and mess with the code, please be invited to jump right in and consult further resources as required.
With that, let’s get started!
Note
We’re actively working on the tutorials – things might be slightly ajar still. If you spot something that might be improved, please let us know.
Getting Started¶
Thank you so much for checking out lab.js! It’s a great pleasure to have you here, and we hope you will enjoy building experiments using this little library as much as we have enjoyed creating it.
The purpose of this initial tutorial is to get you up to speed as quickly as possible. We’ll have you building a very simple experiment in the next half hour or so, and we’ll examine more details in the following parts of the tutorial.
You’ll need a browser, and a basic understanding of how web pages are built
using HTML
. In addition, a good text editor with syntax highlighting can be
an enormous support: It helps us distinguish the different parts of our code
visually. If you’re using a text editor already in your daily work, we’d
recommend to stick to that for the moment. If you haven’t used a text editor
before, we would encourage you to try out Atom, which
works great out of the box.
If you run into difficulties in the tutorial, that’s our fault: Please let us know how we can support you! We are also constantly trying to make the tutorial clearer and more helpful – if you have comments or suggestions, we would love to hear them!
Contents
Downloading the starter kit¶
To get up and running, the first thing you’ll need to do is download the starter
kit attached to the latest release
of lab.js
.
The starter kit is a zip archive containing all necessary files for building a simple experiment. Please extract it in a convenient location on your computer, and navigate to the folder containing the extracted files. That’s it!
Whenever you are building a new experiment in the future, you can start from a clean slate by downloading and building upon the latest starter kit. As you gather more experience, you might build your own starter kit using the code that helps you get to speed quickest – you are by no means limited to the template provided.
A web page about to be turned into an experiment¶
Among the extracted files, you’ll find a file named index.html
[1] . This
is the web page that contains the initial experiment. Please open this file in a
browser, by double-clicking the file or dragging it onto your browser window.

The page should look very similar to the example on the right, but please don’t anxiously wait for something to happen: it won’t. That’s because right now, there is no experiment to run – the file we opened just contains the loading screen. The experiment we build will replace this content, as we will see in the next step.
Before we move on, you might want to have a brief look at the code of the file you just opened. If you view it in your editor instead of the browser, you’ll see the underlying source code. If you like, take a closer look – here are some things you might notice:
- In the
head
tag, there are quite a few references to outside files. In particular, we’re loading some external Javascript andCSS
. These are provided withlab.js
and contain the library code and default styles. You might have also spotted a reference toexperiment.js
– that’s where we’ll define the actual study. - The
body
tag contains the page content. A closer look will reveal that everything is contained within adiv
tag of thecontainer
class. This is what provides the rectangular frame you saw on the page. - Within the container div, the content is subdivided into
header
,main
, andfooter
elements. These correspond to the three areas on the page. Feel free to adjust the content as you see fit! - Finally, inside the
main
element, there’s adiv
with an attributedata-labjs-section
with the valuemain
. That’s where the actual experimental content will go.
With that, let’s go get this experiment to work!
See also
If you’re not quite sure exactly how the design works, please don’t worry – we’ll come back to the specifics of layout when we think about styling your study
How an experiment is built¶
The experiment runs on top of the basic HTML
file you’ve just seen, by
exchanging content when appropriate, and collecting and reacting to
participants’ responses. This interaction requires Javascript.
Let’s take a closer look at the experiment.js
file included in the starter
kit – that’s where the actual structure of the experiment is set up. In
particular, let us draw your attention to a specific part of the code:
var experiment = new lab.flow.Sequence({
content: [
/* ... */
]
})
As you may have guessed, this snippet defines the experiment as a sequence
of things. To be exact, the sequence component is retrieved from the flow
control part of the lab
library. Then, a new sequence is created and saved
in the experiment
variable. Some additional options are provided in the
brackets, notably some content
(omitted here). You might have noticed that
the content is included in brackets, which indicate a list of things (or, to use
the common technical term, an array).
So what goes into the sequence content? Again, there’s an example in the starter kit:
new lab.html.Screen({
content: 'Hello world!'
})
We hope that the similarities to the previous example become apparent: We’re
building a new screen which is provided by the HTML
part of the library.
Again, there’s some content, this time a text string, which is more appropriate
as content for a single screen than the list of things used in the sequence
above.
This basic structure is worth taking another look at, because we’re going to come across it over and over again: We’re going to build components, specify some content (and possibly a few more options), and nest them within one another to build even complex experiments.
So why isn’t this working yet?¶
We apologize for keeping you in suspense for this long! If you take another look at the remainder of the code in the file, two more things happen: A data store is set up to collect the information gathered in the experiment, and then the experiment is run … or rather it isn’t, because us spoilsports have commented out the final line of code.
By uncommenting the final line and reloading the HTML
page in the browser,
you should see the code in action: Instead of the loading screen you saw before,
the page should now contain the content you specified above.
Feel free to change the content to see that your changes to the code are
reflected in the display. You might also try adding a second screen to the
sequence – make sure that you don’t forget a comma to separate the two as you
list them in the sequence content. Also, you might need to add an additional
option like timeout: 1000
to the first screen to make sure that the
experiment progresses beyond it!
Tip
Please don’t worry about breaking the code: It can’t harm your computer. If something goes wrong, you can find the original version in the repository.
If you have questions at this point, please don’t hesitate to reach out; we’d be thrilled to hear from you and happy to help as best we can.
Where to go from here¶
In this section, we hope that you’ve gained some familiarity with the starter
kit, that you’ve seen that experiments in lab.js
operate by exchanging
page content, and that experiments consist of components with a regular
structure, and that can be nested to create even complex experiments.
As a next step, we’ll build upon your new knowledge and create more useful experiments using the exact same technique. We hope you’ll join us!
[1] | Traditionally, the landing page visitors see first when navigating to
a web page is called index.html . It is solely out of convention that this
naming scheme has been adopted here, you are welcome to change it! |
Building a working study¶
Note
This documentation page is currently under development. Sorry for that!
We’re actively working on this, so there might be parts that are missing or incomplete. Please be invited to start with the tutorial nonetheless, the additional parts are coming very soon.
If you find something awry or missing content, please don’t hesitate to send us a line or two, we’re happy to explain things further or to give you a personal tutorial via Skype/Hangouts/etc. or in person.
Sorry for the trouble!

This is where we build our first working study! Specifically, we’re going to create an experiment that demonstrates the Stroop effect. This effect describes the interference between a written word’s content and its visual characteristics: John Ridley Stroop demonstrated that naming the color of a word is harder (takes longer) when the word denotes a different color. An example for such an incongruent display might be the word red. Conversely, in the word and color can correspond (be congruent), which makes the task easier.
Picking back up¶
This section builds on the previous one, in which you downloaded the starter
kit and took at first look
how a minimal ‘experiment’ was constructed from individual components. You also
made the code run by adding or uncommenting experiment.run()
. We’ll build
upon the same code, so please make sure you have the files and an editor handy.
If didn’t go through the initial steps and don’t feel confident looking at the starter kit code, please go back and take a quick look. You’re always welcome to reach out if you need help right now or in any of the following steps.
With that, let’s get going!
Thinking about a study’s structure¶
When we build our studies, we’ll think about them in a particular way: As a sequence of individual building blocks. What does that mean?
Every component performs a particular function – it might show some information onscreen, play a sound, or do some processing in the background. Each component prepares, often at the beginning of the experiment, readying for its task, and will run later, to perform its main function.
As just noted, every component’s moment in the spotlight is when it runs. This will very often mean showing some information, and then waiting for a response. A typical experiment will consist of many such components strung together like this:
When we build experiments, components will not only be responsible for presenting stimuli and collecting responses. We will use different components to tie the structure of our experiment together. For example, the stimuli above are shown sequentially, and therefore together constitute a sequence. Accordingly, we’ll use a sequence component to group them together.
In many ways, a sequence component behaves exactly as a standard component would: It prepares by signaling all nested components to prepare themselves, and it runs by running them in sequence.
A sequence differs from a stimulus component in that it does not provide any new information to the viewers. Instead, it is in charge of flow control: It makes sure that other components run when they are supposed to. These nested components can then do the actual work of presenting information, or they might themselves organize the flow of yet another set of components.
We’ll always combine both types, presentational components and flow control components, to build studies.
Building a Stroop screen¶
Knowing what you now know, what might be a good component to start building a Stroop experiment? We’re going to start with the main stimulus display itself, the part that displays the word and color, and collects the response.
First, let’s think about how to design the stimulus. For the purposes of this
tutorial, we’ll use HTML
to tell the browser what we’d like to show
onscreen [1]. We’d like to show a word, and give it a color. The syntax
required to do this will probably look somewhat like the following:
<div style="color: red">
blue
</div>
Given this content, let’s build a component that will make it visible to the
participants by inserting the HTML
syntax into the page. This is the purpose
of the html.Screen()
component that you may have noticed in the
starter kit code. By extending our earlier ‘hello world’ example, we might
create the following snippet:
new lab.html.Screen({
content: '<div style="color: red"> blue </div>',
})
This creates a new html.Screen()
with our content. When it runs, the
short HTML
code will be inserted into the page, specifically into the
element whose data-labjs-section
attribute is main
(this default can be
changed).
There are a few details to note here: First, the screen is constructed using
options which are supplied in brackets – and not only regular ones, but also
curly braces. This is because the options are defined by a dictionary (you
might also use the term object) which has pairs of keys and values, separated by
a colon. Right now, only one option is provided: The content in form of our
HTML
text. If we were to add further options, we would need to insert commas
between them, a fact that is hinted at by the comma behind the option. Second,
it’s worth noting that the the quotation marks around and with the HTML
code
are different. This is because the simple quotation marks denote the beginning
and the end of the string, whereas the double quotation marks are part of its
content. Using single quotation marks within the HTML
code would end the
string prematurely and cause an error.
If you’ve changed the code to correspond to the above example and reloaded the page in your browser, you should see the word blue on the screen, written in red. It’s not (yet) as pretty as it could be, but it’ll do for the moment: We’ll get around to styling our study later!
[1] | This is not the only way to design the display. If you’re used to writing code that draws shapes and text at exact screen coordinates, don’t worry: That is also possible using canvas-based displays. Both approaches have their advantages and disadvantages: We’ll discuss these at a later point. For now, we decided to give up some control over the precise display in return for a simpler method of stimulus construction. |
Simplifying code using functions¶
Note
This documentation page is currently under development. Sorry for that!
This page is undergoing major revisions and updates, and parts are missing. We’re actively working on this, so please check back in a bit. Also, please be invited to send us a line or two, we’ve probably got a better working version that we can share, or we can help you get started directly.
Sorry for the trouble!
As you probably noticed in the last section, duplicating and modifying code to create elements that differ only in details quickly becomes fairly tedious. There must be a better way! Indeed, there is: Computers are very good at carrying out simple, repetitive tasks for us. In this section, we will explore how we can harness [1] their diligence.
Contents
Introduction to functions¶
The base for all our efforts will be functions. These are series of steps that we can teach a computer to perform, similar to a recipe. This means that, instead of talking through many individual steps every time we need something done, we can ask them to complete the entire task, and the computer will handle the details for us, having been taught the necessary steps. Other ways of thinking about functions include magical spells, or very specialized machines that we can build, that do our bidding at the press of a single button.
Besides reducing the need for repetition, there are several other advantages of
using functions in our code. One related plus is that the code becomes much more
readable and manageable, which is increasingly important as an experiment grows.
This is because functions hide complexity from programmers: Imagine, for
example, the massive complexity of writing text to the browser’s console when we
issue an instruction like console.log('Hello world')
! There are a lot of
bits being twiddled in the background for this to work, and we don’t need to
care about almost any of them.
Let’s take a closer look at the instruction above. The technical term for it is
a function call, in the sense that we call upon a predefined function to do
our bidding. We can separate it into two parts, the one outside and the other
inside the parentheses. The first part is the name of the function, which is
really a variable name under which the function is stored [2]. The second
part are the arguments, which are included in the brackets. They further
specify what the function does. You might think of them as knobs on your newly
constructed machine, or the things you point your wand at while you recite your
incantation. The arguments allow you to do similar things in slightly differing
variations using the same function, thus providing some flexibility. In the
above example, the function console.log
can write different pieces of text
to the console depending on the arguments provided, rather than being limited
to a single value.
The parentheses at the end of the function call are required, like a magic wand has to be swished just right for the spell to work. Just saying the word will not do, which makes things a little harder, but also (in the case of incantations) prevents unintended catastrophes in latin classes worldwide.
Like a machine might produce, say, pancakes, functions also often return
values as results. In doing so, they ideally abstract a more complex operation
and act as a shortcut. Like the pancake machine makes pancakes a matter of
pressing a button, thereby absolving the user of the need to understand its
complex inner workings, a function provides a shorthand for a series of steps or
calculations. We will see how to use this feature in an experiment in a moment,
but as an abstract example for the time being, you might imagine the work
required to make a function call like Math.sqrt(9)
seem effortless. Any
other effects a function might have (besides producing a return value) are
referred to as side effects.
As just mentioned, a function call can hide very complex operations from us,
saving us from having to calculate a square root on our own, as in the last
example. Thus, a function can replace any other code by returning an equivalent
value. If we had a function called plusTwo
, typing 1 + 2
and
plusTwo(1)
, and analogously let new_number = 1 + 2
and let new_number
= plusTwo(1)
are for our purposes entirely equivalent. A function call can act
as a stand-in for an expression that results in the same value, or a variable
name that represents the same value.
Where do functions come from? Many, like the above examples, are built-in,
and come with the browser. Others are provided by libraries, which are
external collections of functions loaded with the page. This latter way is how
lab.js
gets loaded onto the web page containing the experiment.
Both methods provide a range of variables representing useful functions. So as
not to use up to many variable names, the functions are often grouped together
using a common ‘stem’, such as Math.
for many math-related functions,
console.
for functions pertaining to console output, and lab.
for
everything provided by the present library.
So now we know how to invoke functions, but we can’t rely on other programmers to supply just the right ones for our purposes. How do we make our own?
Defining our own functions¶
A simple example¶
We just saw that functions can be thought of as miniature machines inside a program, built to serve a specific purpose, and to encapsulate a more complex process. Many are offered by the browser itself so that we may use them, they might be added through the libraries we load on our pages, or we can define our own.
One of the simplest possible functions can be defined as follows:
const greetFelix = function() {
console.log('Hello Felix!')
}
If you have a browser window handy, please be invited to copy the code into the browser console! (feel free to substitute your own name)
There are several parts to this function definition. The final, and largest
part, located within the curly braces, delimits the block of code that
contains the function’s inner workings, the recipe that is run when the function
is called. In this case, all our function does is call another function in turn,
writing a greeting on the console. You might recognize this block structure from
other elements of programs, for example if
statements, where blocks of code
are run only if a certain condition is met, or loops, where blocks of code are
run repeatedly. This block of code is preceded by the function
keyword,
which marks it as a function. The very first part represents the assignment of
the function to the greetFelix
variable, allowing us to retrieve the
function at some later point.
If you now call the function using greetFelix()
(typed in the console or as
a line within a larger script), the code contained in the function block will be
executed, and the greeting will be shown.
Using return values¶
In our last example, all our function did was produce a console output as a side
effect. In a way, it acted as a shortcut for another function call. However,
functions are capable of far more, and can substitute not only other function
calls, but also more complex calculations (such as the Math.sqrt
example
above). We can also make use of this in our own functions, using the return
keyword to return a value:
const makeTwo = function() {
return 2
}
A call of this makeTwo
function now produces the integer value 2
, and
both can be substituted for one another. For example, 1 + makeTwo()
would
produce the value three, and console.log(2 * makeTwo())
would output the
number four onto the console.[#f3]_
Of course, this is not a very useful function, because the value it returns
is easier to produce through other means (by writing 2
directly); it does
not make our lives easier. However, there are many cases in which long blocks of
code can be substituted by a function call. Take, for example, the humble
fixation cross. It is used often, rarely varies, and therefore a prime candidate
for abstraction using a function:
const fixationCross = function() {
return new lab.HTMLScreen(
'+',
{
'timeout': 500
}
)
}
This function, when called, returns an HTMLScreen
containing nothing but a
plus character that, for our purposes, will double as a fixation cross. Like a
call of makeTwo
would provide the number two for further use, a call of
the fixationCross
function provides a fixation cross screen, and accordingly
may be substituted wherever we would otherwise have defined such a screen by
hand.
For example, one might construct a simple experiment as follows:
const experiment = lab.Sequence([
// First trial
fixationCross(),
// Stimulus 1
new lab.HTMLScreen(
'Press A!',
{ // Options
responses: {
'keypress(a)': 'correct'
}
}
),
// Second trial
fixationCross(),
// Stimulus 2
new lab.HTMLScreen(
'Press B!',
{ // Options
responses: {
'keypress(b)': 'correct'
}
}
),
// ...
])
experiment.prepare()
experiment.run()
Please note how the calls to the fixationCross
function replaces the
otherwise unwieldy and repetitive direct construction of the corresponding
screen. Nice, isn’t it?
Adding parameters¶
Up to now, the functions we have defined always perform the exact same task, whether producing side effects or returning values. Once defined, they never wavered in their stoic performance of the recipe they have been programmed to perform. This would mean that we would have to program a new function for each set of tasks we would like to encapsulate. If the sets of tasks vary only in minutiae, this would also quickly become repetitive.
Parameters allow us vary the behavior of a single function across calls, by
specifying the details of its’ execution. For example, rather than a makeTwo
function, we might define a plusTwo
function that, as you might imagine,
increments a given value by two. We do so by adding a parameter in the brackets
following the function keyword. In this case, it is called x
, but any other
variable name would also be possible. The central trick is that whatever we pass
along as a parameter value will be available within the function block through
this variable, and can be used for our further calculations.:
const plusTwo = function(x) {
return x + 2
}
In this case, the variable x
takes on the value of the parameter passed to
the function, which adds two before returning the result. Thus, plusTwo(3)
would return the value five, and so on.
Again, this is not a particularly useful example, so how can we apply this to our experiments?
[1] | An earlier version of this tutorial read ‘take advantage of their diligence’, but we would never do that, right? The author, for one, welcomes his silicon overlords. |
[2] | You might have noticed that the name, in this case, is also split into
two parts, separated by the period. This signifies that the Similarly, functions that pertain to a specific element in the experiment are
also linked to the element’s variable with a period, like
|
[3] | Note that, unlike this example might suggest, return values need not
be deterministic. For example, the function Math.random() will return a
different floating point number between zero and one with each call (well,
most of the time). |
Examples & recipes¶
Note
This documentation page is currently under development. Sorry for that!
We’re actively working on this, so please check back in a bit. Also, please be invited to send us a line or two, we’ve probably got at half-baked working version that we can share, or we can help you get started directly.
Sorry for the trouble!
The current directory of examples studies is hosted in a repository and available from within the builder interface.
Contributions are very welcome!
Library reference¶
This section provides documentation and reference for all library components. Keep this under your pillow!
Conceptual overview¶
The core idea of lab.js
is that experiments can be broken down into components that are fundamentally alike. From these building blocks, even complex experiments can then be constructed.
For example, a typical experiment can probably be broken down into several screens
which are presented to participants sequentially. These behave similarly: Each likely needs some preparation before it can be presented (e.g. preloading of content), before it can be run. It might wait for a response and then end, or terminate automatically after a certain time period, or both.
On a higher level, screens might be combined into sequences
: The experiment itself is most likely a sequence of screens, but on a finer level, trials are also often composed of several distinct phases that appear in sequence, for example a fixation cross that precedes stimulus presentation, and an ISI that follows it.
Sequences, too, need to be prepared (and any components they contain), and when they are run, they execute any contained content.
The two aforementioned components, screens
and sequences
, make up the core of lab.js. Even though they represent fundamentally different building blocks, they behave very similarly: As just noted, both are prepared and run in a similar way. In addition, they share much of the same internal structure. Specifically, both emit and are sensitive to several types of events, of which preparation and presentation are just two.
These similarities are reflected in the internal structure of lab.js, in which all components are descendants of the same core prototype, often with very few additional changes.
Library core¶
The core
module contains the foundations of the library. As with many types of foundations, you’ll probably not see much of this module. However, you’ll rely on it with everything you do, and so it’s worth getting familiar with the machinations underlying the library, especially if you are looking to extend it.
Because all other components in the library build on the core.Component()
class, they share many of the same options
. This is why we’ll often refer to this page when discussing other parts of the library.
Component¶
The core.Component()
class is the most basic provided by lab.js
. It is the foundation for all other building blocks, which extend and slightly modify it. As per the philosophy of lab.js
, experiments are composed entirely out of these components, which provide the structure of the study’s code.
In many cases, you will not include a core.Component()
directly in your experiment. Instead, your experiment will most likely consist of the other building blocks which lab.js
provides – but because all of these derive from this fundamental class, there are many similarities: Many components share the same behavior, and accept the same options
, so that you will (hopefully) find yourself referring to this part of the documentation from time to time.
Behavior¶
Following its creation, every component will go through several distinct stages.
The preparation stage is designed to prepare the component for its later use in the experiment in the best possible way. For example, a display might be prerendered during this phase, and any necessary media
loaded. Importantly, by the time a component is prepared, its settings need to have been finalized.
The run stage is the big moment for any component in an experiment. Upon running, the component assumes (some degree of) control over the study: It starts capturing and responding to events triggered by the user, it might display information or stimuli on screen, through sound, or by any other means.
The end marks the close of an component’s activity. It cedes control over the output, and stops listening to any events emitted by the browser. If there are data to log, they are taken care of after the component’s run is complete. Similarly, any housekeeping or cleaning-up is done at this point.
Usage¶
Instantiation¶
-
class
core.
Component
([options])¶ Arguments: - options (object) – Component options
The
core.Component()
does not, by itself, display information or modify the page. However, it can be (and is throughoutlab.js
) extended to meet even very complex requirements.A component is constructed using a set of
options
to govern its behavior, which are specified as name/value pairs in an object. For example, the following component is given a set ofresponses
and atimeout
:const c = new lab.core.Component({ 'responses': { 'keypress(s)': 'left', 'keypress(l)': 'right', }, 'timeout': 1000, }) c.run()
These options are available after construction from within the
options
property. For example, the timeout of the above component could be changed later like so:c.options.timeout = 2000
All other options can be modified later using the same mechanism. However, options are assumed to be fixed when a component is prepared.
Event API¶
During a study, a component goes through several distinct stages, specifically prepare
, run
and end
. Much of the internal logic revolves around these events. The most important events are:
prepare
run
end
commit
(data sent to storage)
External functions can also tie into this logic, for example to collect and transmit data when an experiment (or a part of the same) is over. The following methods make this possible.
-
waitFor
(event)¶ Returns: A Promise that resolves when a specific event occurs. This helper makes it possible to plan actions for a later point during the study, using the Promise API, as visible in the following example:
const c = new lab.core.Component({ /* ... */ }) // Queue a dataset download when the component ends c.waitFor('end').then( () => c.options.datastore.download() ) // Run the component c.run()
-
on
(event, handler)¶ Like the
waitFor()
helper, this function will trigger an action at a later point. However, instead of a promise, it uses a callback function:c.on('end', () => this.options.datastore.download())
The callback is internally bound to the component, so that the value of
this
inside the function corresponds to the component on which the event is triggered.
-
once
(event, handler)¶ Equivalent to
on()
, but additionally ensures that the handler is only run on the first matching event.
-
off
(event, handler)¶ Remove a previously registered handler for an event.
See also
If you want to be notified of every event a component goes through, you’ll want to look into Plugins.
Methods¶
-
prepare
()¶ Trigger the component’s prepare phase.
Make the preparations necessary for
run()
-ning a component; for example, preload all necessarymedia
required later.The
prepare()
method can, but need not be called manually: The preparation phase will be executed automatically when the the component isrun()
. Therefore, it is usually omitted from the examples in the documentation.Flow control components such as the
Sequence()
will automatically prepare all subordinate components unless these are explicitly marked astardy
.Returns: A promise that resolves when the preparation is complete (e.g. when all media
have been loaded, etc.)
-
run
()¶ Run the component, giving it control over the participants’ screen until the
end()
method is called. Callingrun()
will triggerprepare()
if the component has not yet been prepared.Returns: A promise that resolves when the component has taken control of the display, and all immediate tasks have been completed (i.e. content inserted in the page, requests for rendering on the next animation frame filed)
-
respond
([response])¶ Collect a response and call
end()
.This is a shortcut for the (frequent) cases in which the component ends with the observation of a response. The method will add the contents of the
response
argument to the component’sdata
, evaluate it against the ideal response as specified incorrectResponse
, and thenend()
the component’s run.Returns: The return value of the call to end()
(see below).
-
end
([reason])¶ End a running component. This causes an component to cede control over the browser, so that it can be passed on to the next component: It stops monitoring
events
on the screen, collects all the accumulateddata
, commits it to the specifieddatastore
, and performs any additional housekeeping that might be due.Returns: A promise that resolves when all necessary cleanup is done: When all data have been logged, all event handlers taken down, etc.
-
clone
([optionsOverride])¶ Returns: A new component of the same type with the same options as the original. If an object with additional options
is supplied, these override the original settings.
Properties¶
-
aggregateParameters
¶ Superset of the component’s
parameters
and those of any superordinate components (read-only)Often, a component’s content and behavior is determined not only by its own
parameters
, but also by those of superordinate components. For example, a component might be contained within aSequence()
representing a block of stimuli of the same type. In this and many similar situations, it makes sense to define parameters on superordinate components, which are then applied to all subordinate, nested, components.The
aggregateParameters
attribute combines theparameters
of any single component with those of superordinate components, if there are any. Within this structure, parameters defined at lower, more specific, levels override those with an otherwise broader scope.Consider the following structure:
const experiment = lab.flow.Sequence({ 'title': 'Superordinate sequence', 'parameters': { 'color': 'blue', 'text': 'green', }, // ... additional options ... content: [ lab.core.Component({ 'title': 'Nested component', 'parameters': { 'color': 'red', }, }), ], })
In this case, the nested component inherits the parameter
text
from the superordinate sequence, but notcolor
, because the value of this parameter is defined anew within the nested component itself.
-
timer
¶ Timer for the component (read-only)
The
timer
attribute provides the central time-keeping instance for the component. Until the component isrun()
, it will be set toundefined
. Then, until theend()
of an component’s cycle, it will continuously provide the duration (in milliseconds) for which it has been running. Finally, once the cycle has reached itsend()
, it will provide the time difference between the start and the end of the component’s run cycle.
-
progress
¶ Progress indicator, as a number between
0
and1
(read-only)The
progress
attribute indicates whether a component has successfully completed itsrun()
, and (for more complex components) to which degree. For example, a basichtml.Screen()
will report its progress as either0
or1
, depending whether it has completed its turn. Nested components such as theflow.Sequence()
, on the other hand, will return a more nuanced value, depending on the status of subordinate components – specifically, the proportion that has passed at any given time.
Options¶
-
options
The vast majority of customizations are made possible through a component’s options, which govern its behavior in detail. In most cases, these options are set when a component is created:
const c = new lab.core.Component({
'exampleOption': 'value'
})
The options can also be retrieved and changed later through the options
property. For example, the current value of the option created above is available through the variable c.options.exampleOption
, and could be changed by altering its content.
Because the presentation of components is prepared when prepare()
is called, and the options factor into this step, changes should generally be made before the prepare phase starts (c.f. also the tardy
option).
Basic settings
-
options.
debug
¶ Activate debug mode (defaults to
false
)If this option is set, the component provides additional debug information via the browser console.
-
options.
el
¶ HTML
element within the document into which content is inserted. Defaults to the element with the attributedata-labjs-section
with the valuemain
.The
el
property determines where in the document the contents of the experiment will be placed. Most parts of an experiment will replace the contents of this element entirely, and substitute their own information. For example, anhtml.Screen()
will insert customHTML
, whereas acanvas.Screen()
will supply aCanvas
on which information is then drawn.To change the location of the content, you can pick out the element of the
HTML
document where you would like the content placed as follows:const c = new lab.core.Component({ 'el': document.getElementById('experiment_content_goes_here'), // ... additional options ... })
Selecting a target via
document.getElementById
ordocument.querySelector
requires that the document contains a matching element. For the example above, this would be the following:<div id="experiment_content_goes_here"></div>
Metadata
-
options.
title
¶ Human-readable title for the component, defaults to
null
This is included in any data stored by the component, and can be used to pick out individual components.
-
options.
id
¶ Machine-readable component identifier (
null
)This is often generated automatically; for example, flow control components will automatically number their nested components when prepared.
-
options.
parameters
¶ Settings that govern component’s behavior ({})
This object contains any user-specified custom settings that determine a component’s content and behavior. These may, for example, be used to fill placeholders in the information presented to participants, as a
html.Screen()
does.The difference between
parameters
anddata
is that the former are retained at all times, while thedata
may be reset at some later time if necessary. Thus, any information that is constant and set a priori, but does not change after the component’s preparation should be stored in theparameters
, whereas all data collected later should be (and is automatically) collected in thedata
attribute.
Behavior
-
options.
skip
¶ End immediately after running (
false
).
-
options.
tardy
¶ Ignore automated attempts to
prepare()
the component, defaults tofalse
.Setting this attribute to
true
will mean that the component needs to be prepared manually through a call toprepare()
, or (failing this) that it will be prepared immediately before it isrun()
, at the last minute.
Response handling
-
options.
responses
¶ Map of response events onto response descriptions ({})
The
responses
object maps the actions a participant might take onto the responses saved in the data. If a response is collected, theend()
method is called immediately.For example, if the possible responses are to press the keys
s
andl
, and these map onto the categories left and right, the response mapping might look as follows:'responses': { 'keypress(s)': 'left', 'keypress(l)': 'right', }
The left part, or the keys of this object, defines the browser event corresponding to the response. This value follows the event type syntax, so that any browser event can be caught. Additional (contrived) examples might be:
'responses': { 'keypress(s)': 'The "s" key was pressed', 'keypress input': 'Participant typed in a form field', 'click': 'A mouse click was recorded', 'click button.option_1': 'Participant clicked on option 1', }
As is visible in the first example, additional options for each event can be specified in brackets. These are:
- For
keypress
events, the letters corresponding to the desired keys, or alternativelySpace
andEnter
for the respective keys. Multiple alternate keys can be defined by separating letters with a comma. (for a full list, please consult the W3C keyboard event specification.lab.js
follows this standard where it is available, using only the valueSpace
instead of a single whitespace for clarity. Note also, however, that some browsers do not firekeypress
events for all keys; specifically, chrome-based browsers do not provide such events for arrow and navigation keys) - For
click
events, the mouse button used. Buttons are numbered from the index finger outwards, i.e. on a right-handed mouse, the leftmost button is0
, the middle button is1
, and so on, and vice versa for a left-handed mice. (please note that you may also need to catch and handle thecontextmenu
event if you would like to stop the menu from appearing when the respective button is pressed.)
Finally, a target element in the page can be specified for every event, as is the case in the last example. The element in question is identified through a CSS selector. If an element is specified in this manner, the response is limited to that element, so a click will only be collected if it hits this specific element, and a keyboard event will only be responded to if the element is selected when the button is pressed (for example if text is input into a form field).
- For
-
options.
correctResponse
¶ Label or description of the correct response (defaults to
null
)The
correctResponse
attribute defines the label of the normative response. For example, in the simple example given above, it could take the values'left'
or'right'
, and the corresponding response would be classified as correct.
Timing
-
options.
timeout
¶ Delay between component run and automatic end (null)
The component automatically ends after the number of milliseconds specified in this option, if it is set.
Data collection
-
options.
data
¶ Additional data (
{}
)Any additional data (e.g. regarding the current trial) to be saved alongside automatically generated data entries (e.g. response and response time). This option should be an object, with the desired information in its keys and values.
Please consult the entry for the
parameters
for an explanation of the difference between these anddata
.
-
options.
datastore
¶ Store for any generated data (
null
by default)A
data.Store()
object to handle data collection (and export). If this is not set, the data will not be collected in a central location outside the component itself.
-
options.
datacommit
¶ Whether to commit data by default (
true
)If you would prefer to handle data manually, unset this option to prevent data from being commit when the component ends.
Preloading media
-
options.
media
¶ Media files to preload (
{}
)Images and audio files can be preloaded in the background during the prepare phase, to reduce load times later during the experiment. To achieve this, supply an object containing the urls of the files in question, split into images and audio files as follows:
'media': { 'images': [ 'https://mydomain.example/experiment/stimulus.png' ], 'audio': [ 'https://mydomain.example/experiment/sound.mp3' ] }
Both image and audio arrays are optional, and empty by default.
Please note that this method has some limitations. In particular, the preloading mechanism is dependent upon the browser’s file cache, which cannot (yet) be controlled completely. The media file might have been removed from the cache by the time it is needed. Thus, this is a somewhat brittle mechanism which can improve load times, but is, for technical reasons, not guaranteed safe. In our experience, testing across several browsers reliably indicates whether preloading is dependable for a given experiment.
Caution
This is an experimental feature and might change at some later point. That’s because we are still gathering experience with it, and because we foresee that new browser technology may change the implementation.
Plugins
-
options.
plugins
¶ Array of plugins that interact with the component, and are automatically notified of events. For example, adding a
plugins.Logger()
instance will log event notifications onto the console:const c = new lab.core.Component({ plugins: [ new lab.plugins.Logger(), ], })
Similarly,
plugins.Debug()
provides the interface for data checking and debugging used in the builder preview.
Advanced options
-
options.
events
¶ Map of additional event handlers (
{}
)In many experiments, the only events that need to be handled are responses, which can be defined using the
responses
option described above. However, some studies may require additional handling of events before a final response is collected. In these cases, the events object offers an alternative.The events option follows the same format used for the responses, as outlined above. However, instead of a string response, the object values on the right-hand side are event handler functions, which are called whenever the specified event occurs. The functions are expected to receive the event in question as an argument, and process it as they see fit. They are automatically bound to the component in question, which is available within the function through the
this
keyword.As a very basic example, one might want to ask users not to change to other windows during the experiment:
'events': { 'visibilitychange': function(event) { if (document.hidden) { alert(`Please don't change windows while the experiment is running`) } }, }
-
options.
messageHandlers
¶ Map of internal component events to handler functions (
{}
)This is a shorthand for the
on()
methodconst c = new lab.core.Component({ messageHandlers: { 'run': () => console.log('Component running'), 'end': () => console.log('Component ended'), } })
Caution
This option is likely to be renamed at some later point; we are not happy with its current label. Ideas are very welcome!
Dummy¶
The core.Dummy()
component is a stand-in component that calls end()
immediately when the component is run. We use it for tests and demonstrations, and only very rarely in experiments.
-
class
core.
Dummy
([options])¶ Direct descendant of the
core.Component()
class, with the single difference that theskip
option is set totrue
by default.
Flow control¶
This part of the library provides components that control the sequence of events during the experiment. It is thus responsible for the flow of Components throughout the experiment. For example, a flow.Sequence()
groups several components together to be run sequentially, a flow.Loop()
repeats single components, and a flow.Parallel()
runs multiple components in parallel.
Sequence¶
-
class
flow.
Sequence
([options])¶ A
flow.Sequence()
runs a group of components one after another. These can be any type of component – screens or other stimuli, and even other sequences or loops.A typical experiment will often, on the highest, most coarse level, consist of a single sequence that encompasses the entirety of the experiment – instructions, experimental task, and debriefing – and runs it in sequence.
Sequences are, however, also useful on a much more granular level – for example, a single trial can be built as a sequence of an inter-stimulus interval, a fixation dot, and the stimulus itself.
-
flow.Sequence.options.
content
¶ List of components to run in sequence (
[]
)When a
flow.Sequence()
is constructed, the most important option is the content, which is a list of ‘sub-components’ that the sequence is comprised of. A basic example might be the following [1]:const proclaimers = new lab.flow.Sequence({ content: [ new lab.html.Screen({ content: 'And', timeout: 500 }), new lab.html.Screen({ content: 'I', timeout: 500 }), new lab.html.Screen({ content: 'will', timeout: 500 }), new lab.html.Screen({ content: 'walk', timeout: 500 }), new lab.html.Screen({ content: 'five', timeout: 500 }), new lab.html.Screen({ content: 'hun-', timeout: 500 }), new lab.html.Screen({ content: '-dred', timeout: 500 }), new lab.html.Screen({ content: 'miles', timeout: 500 }), ], }) proclaimers.run()
When the sequence is prepared or run, the constituent parts are prepared and run in sequence.
-
flow.Sequence.options.
shuffle
¶ Run the content components in random order (
false
)If this option is set to
true
, thecontent
of the sequence is shuffled during the prepare phase.
-
flow.Sequence.options.
handMeDowns
¶ List of options passed to nested components (
['datastore', 'el', 'debug']
)The options specified as
handMeDowns
are transferred to nested components during the prepare phase. This option is largely for convenience, and designed to decrease the amount of repetition when all nested components behave similarly – typically, nested components share the same data storage and output element, so these are passed on by default. Similarly, thedebug
mode is easiest to set on the topmost component, and will automatically propagate to include all other components.
-
Loop¶
-
class
flow.
Loop
([options])¶ A
flow.Loop()
repeats the same (single)template
component, while varyingparameters
between repetitions. Keeping with our example above:const template = new lab.html.Screen({ content: '${ parameters.lyrics }', // parameters substituted ... timeout: '${ parameters.beats * 600 }', // ... during preparation }) const spandauBallet = new lab.flow.Loop({ template: template, templateParameters: [ /* ... */ { lyrics: 'So true, funny how it seems', beats: 7 }, { lyrics: 'Always in time, but never in line for dreams', beats: 10 }, { lyrics: 'Head over heels when toe to toe', beats: 8 }, { lyrics: 'This is the sound of my soul', beats: 8 }, /* ... */ ] })
In many cases, the template will not be a single
html.Screen()
, but rather aflow.Sequence()
, so that multiple screens can be repeated on each iteration.-
flow.Loop.options.
template
¶ Content for each repetition of the loop.
There are several ways in which this option can be used:
- First it can be a single component of any type, an
html.Screen()
, (most likely) aflow.Sequence()
or even anotherflow.Loop()
. This component will becloned
for each iteration, and theparameters
substituted on each copy so that the repetitions can differ from another. - Second, it can be a function that creates and returns the component for each iteration. This function will receive each set of
templateParameters
in turn as a first argument (and, optionally, the index as a second argument, and the loop component itself as a third). The advantage of this method is a greater flexibility: Additional logic can be used at every step to customize every iteration.
- First it can be a single component of any type, an
-
flow.Loop.options.
templateParameters
¶ Array of parameter sets for each individual repetition (
[]
).This option defines the parameters for every repetition of the
template
. Each individual set of parameters is defined as an object with name/value pairs, and these objects are combined to an array:const stroopTrials = [ { color: 'red', word: 'red' }, { color: 'red', word: 'blue' }, /* ... */ ] const stroopTask = new lab.flow.Loop({ template: /* ... */, templateParameters: stroopTrials, })
-
flow.Loop.options.
shuffle
¶ Whether to shuffle iterations (
false
, seeflow.Sequence()
).
-
flow.Loop.options.
handMeDowns
¶ Options to pass to subordinate components (see
flow.Sequence()
).
-
Parallel¶
-
class
flow.
Parallel
([options])¶ A
flow.Parallel()
component runs other components concurrently, in that they are started together. Browser engines do not currently support literally parallel processing, but an effort has been made to approximate parallel processing as closely as possible.-
flow.Parallel.options.
content
¶ List of components to run in parallel (
[]
)
-
flow.Parallel.options.
mode
¶ How to react to nested elements ending (
'race'
)If this option is set to
'race'
, the entireflow.Parallel()
component ends as soon as the first nested component ends. In this case, any remaining components are shut down automatically (by callingend()
). If the mode is set to'all'
, it waits until all nested items have ended by themselves.
-
flow.Parallel.options.
handMeDowns
¶ Options passed to nested elements (see
flow.Sequence()
).
-
[1] | In apology to our British colleagues: This is, obviously, a grossly distorted version of the classic anthem: According to XKCD, the song has 131.9 beats per minute; the appropriate adjustment, as well as the Scottish accent, are left as an exercise for our esteemed readers. We hereby also pledge to award special prizes to any colleagues who use the library for interdepartmental karaoke (video proof required). |
HTML-based displays¶
The following elements use HTML
for showing content. That’s all there is to them! If you are new to lab.js
, these are the easiest way to get things in front of your participants.
Contents
Screen¶
The html.Screen()
is a component of the experiment that changes the content of an element on the page when it is run. Otherwise, it behaves exactly as a core.Component()
does.
-
class
html.
Screen
([options])¶ When an
html.Screen()
is constructed, it takes a single argument that specifies the component options. The most important of these is thecontent
, which is the string of text andHTML
inserted into the document. Additional options correspond to those of acore.Component()
.For example, a screen showing a simple text could be constructing as follows:
const screen = new lab.html.Screen({ content: '<p>Hello world!</p>', }) screen.run()
When this code is run, the screen content is shown, that is, the content string supplied is inserted into the page. Per default, the element with the attribute
data-labjs-section="main"
is used as an insertion point, however this may be changed usingel
option.Using placeholders
You can access the
parameters
available through thehtml.Screen()
to insert placeholders within itscontent
. These are filled when the screen isprepared
. Through this mechanism, the exact content of a screen need not be specified fully from the onset of the study, but can be assembled dynamically depending on the structure of the experiment, and participants’ behavior.Placeholders are delimited by
${
and}
. A parameter name can be placed within these limits in the formatparameters.parameter_name
, and the content stored in place of the parameter will replace the placeholder as soon as the screen is prepared. Similarly, the last value in every column of the data set can be accessed viastate.column_name
. For example, you might want to use the veracity of the last response to provide feedback viastate.correct
.The following screen would produce an output equivalent to the example above, using parameters:
const parameterScreen = new lab.html.Screen({ content: '<p>Hello ${ parameters.place }!</p>', parameters: { place: 'World' }, })
Placeholders can contain any JavaScript expression, so basic logic can be inserted directly into a placeholder. For example, you might use the boolean value contained in
state.correct
to provide feedback, using a conditional operator:const feedbackScreen = new lab.html.Screen({ content: '<p>${ state.correct ? "Well done!" : "Please have another go!" }</p>', })
Note
The placeholder syntax is deliberately chosen to be equivalent to JavaScript’s template literals. You might therefore be tempted to place the content options containing placeholders in backticks (
`
) instead of quotation marks ('
or"
). Doing so will introduce a subtle difference: The option you’re setting will no longer be a regular string, and your browser’s JavaScript engine will attempt to compute the content in placeholders and insert the result in their place as soon as it encounters them. Because the template literal mechanism prempts and bypasses the placeholders, they won’t perform their regular function.In sum: If your placeholders aren’t doing what they are supposed to, this might be worth checking.
Options
The options available for an
html.Screen()
are identical to those of thecore.Component
. For example, one might captureresponses
as in the following example:const screen = new lab.html.Screen({ content: '<p>Please press <kbd>s</kbd> or <kbd>l</kbd></p>', responses: { 'keypress(s)': 's', 'keypress(l)': 'l' }, }) screen.run()
Similarly, the screen might be shown only for a specified amount of time (in milliseconds):
const timedScreen = new lab.html.Screen({ content: '<p>Please press space, fast!</p>', timeout: 500, // 500ms timeout responses: { 'keypress(Space)': 'response' }, })
See also
If you are looking for very short or more precise timings, you will probably be better served using canvas-based displays such as the
canvas.Screen()
.Screens provide two new options that can be specified:
-
html.Screen.options.
content
¶ HTML
content to insert into the page, as text.
-
html.Screen.options.
contentUrl
¶ URL
from which to loadHTML
content as text. The content is loaded when the screen is prepared. Replaces the screen’scontent
.
-
Form¶
A html.Form()
is like the html.Screen()
described above, in that it uses HTML
to display information. However, it adds support for HTML
forms. This means that it will automatically react to form submission, and save form contents when it ends.
On a purely superficial level, a html.Form()
is handled, and behaves, almost exactly like an html.Screen()
: The content
option contains an HTML string which is rendered onscreen when the screen is shown. This is because a html.Form()
builds upon, and extends, the html.Screen()
. It merely handles HTML
form tags somewhat more intelligently.
HTML forms¶
HTML
forms make possible inputs of many kinds, ranging from free-form text entry, to checkboxes, to multiple-choice items and response buttons. This allows for a great variety of data collection methods, ranging far beyond the responses discussed so far.
As with the html.Screen()
discussed above, we assume some familiarity with HTML
forms in the following. If you would like to become familiar or reacquaint yourself with them, we have found the following resources helpful:
Form handling¶
Within HTML
forms, each field is represented by one or more HTML
tags. The name
attribute of these tags typically contains the variable in which the fields information is stored and transmitted.
For example, a very simple form containing only an input field for the participant id, and a button for submitting the form, might be represented as follows:
<form>
<input type="number" name="participant-id" id="participant-id">
<button type="submit">Save</button>
</form>
By inserting this snippet into an HTML
document, an input field is added which accepts numeric input, and also offers buttons to increment and decrease the contained value. In addition, the form can be submitted using a button. Please note that the input field is named, which means that any input present in the form field when the form is submitted will be represented by the key given in the name
attribute, in this case participant-id
(though it is common to reuse the value of the name
attribute as the element’s id
attribute, the two are unrelated and can be chosen independently).
By combining the above code with an html.Form()
, it can become part of an experiment:
const screen = new lab.html.Form({
content: '<form>' +
' <input type="number" name="participant-id" id="participant-id">' +
' <button type="submit">Save</button>' +
'</form>'
})
The above screen, inserted into an experiment, will display the form, and wait for the user to submit it using the supplied button. When this occurs, the form contents will automatically be transferred into the experiment’s data set, and whichever value was entered into the specified field will be saved into the variable participant-id
.
Caution
The html.Form()
differs from standard HTML
forms (those that are sent to a server, which responds with a new page) in one important point: It does not save information about the <button>
used to submit the <form>
data. This is because the information is not made available within the page itself.
If you are looking to capture a response to one of several buttons, we recommend using an html.Screen()
instead, and defining a distinct response
for clicks on every button.
-
class
html.
Form
([options])¶ An
html.Form()
accepts the same options and provides the same methods thehtml.Screen()
does, with a few additions:See also
A
html.Form()
is derived from thehtml.Screen()
, and therefore also accepts thecontent
andcontentUrl
options.-
html.Form.
serialize
()¶ Read the current form state from the page, and output it as a javascript object in which the keys correspond to the
name
attributes on the form fields, and the values correspond to their current states.
-
html.Form.
validate
()¶ serialize()
the current form content and check its validity using thevalidator
. Returnstrue
orfalse
.
-
html.Form.options.
validator
¶ Function that accepts the serialized form input provided by the
serialize()
method, and indicates whether it is valid or not by returningtrue
orfalse
depending on its decision. Only if it returnstrue
will thehtml.Form()
end following submission of the form content.The function is also responsible for generating an error message and showing it to the user, if this is desired.
The
validator
option defaults to a function that always returnstrue
, regardless of form content.
-
Frame¶
The html.Frame()
inserts pre-defined HTML
content into the page like a html.Screen()
does, but then runs
a nested component
within this new context, passing on control over a subsection of the screen. It thereby provides a ‘frame’ around the content of a subordinate component
.
-
class
html.
Frame
([options])¶ A
html.Frame()
provides aHTML
surrounding, acontext
, for nested components, itscontent
. This has two main use-cases:- Simplicity: Any content common to all nested components can be moved to the superordinate
frame
and need not be repeated. - Speed: Instead of exchanging the entire screen content, nested components swap out only a small part of the page, reducing the load on the browser and ensuring more consistent performance. A
frame
can also embed canvas-based components so that the most timing-critical parts of the screen, or visually complex and interactive stimuli, can be rendered through the more performant canvas.
A common application is when stimuli make up only a small part of the total screen content:
const stimuli = new lab.flow.Loop({ /* ... */ }) const frame = new lab.html.Frame({ context: ` <header> You have one job to do </header> <main> <!-- this is where stimuli will be inserted --> </main> <footer> You better / push the button / let me know. </footer> `, contextSelector: 'main', content: stimuli, })
-
html.Frame.options.
context
¶ HTML
code in which the nestedcontent
is embedded (required).
- Simplicity: Any content common to all nested components can be moved to the superordinate
Canvas-based displays¶
The canvas
is an alternate method of displaying content and stimuli on the
screen. The underlying principle are true to the canvas metaphor: A canvas is
a (rectangular) area on which lines, shapes and text can be drawn, to be shown
to the user. It is represented in HTML
using the <canvas>
tag.
As Mark Pilgrim has put it: “A canvas is a rectangle in your page where you can use JavaScript to draw anything you want.”
Contents
Introduction to the canvas¶
Why use a canvas?¶
You might be wondering: HTML
largely deals with showing rectangles and text
on screen, so if a canvas basically does the same thing, why, then, is it
useful? The primary reason is that browsers are often doing more work for us
than is directly visible. In particular, for any HTML
content, it is the
browser’s responsibility to maintain the layout of the page. Whenever a page’s
content changes, browsers need to recalculate the layout, to make sure that any
newly inserted text pushes the content below further downward, that all text is
wrapped neatly around newly inserted images, and that all style rules are
applied. You might imagine this process like continuously trying to layout a
newspaper’s front page while new content is added and deleted simultaneously.
Naturally, this process takes time, and if we rely on the browser to react very
quickly and update the display rapidly in response to our changing the content,
the resulting delay might be too long, resulting in lag.
A canvas
does away with continuous layout recalculation, and instead
provides us with space on which we can happily paint and collage the content
ourselves. The browser no longer assumes responsibility for our layout, but
leaves us to squiggle at our hearts’ content. If things overlap, no worries –
the browser can always paint on top! This simplifies things immensely for the
browser, but it requires a bit more thought from our side: We can no longer, for
example, ask the browser to kindly center content for us; instead, we need to
calculate its position and place it ourselves.
Canvas graphics are raster-based, that is, the browser remembers the color of
each pixel across the canvas, rather than the shapes and text that the colored
pixels represent. This means that a canvas cannot change its size easily; if it
does, the pixels will be warped, resulting in a blurry display. To achieve crisp
images, it is our responsibility to redraw content at different sizes depending
on the screen resolution. This sets it apart from vector graphics, which
represent a display through the shapes visible on it, and can be redrawn at
different sizes and resolutions without loss in quality. That being said, we
can make sure that we draw content at the appropriate resolution, and adjust
sizes depending on the client’s screen to achieve crisp rendering everywhere.
As you will see, the canvas.Screen()
component contains a few helpers
to make this easy.
Resources for learning¶
It would be impossible to cover the usage of the canvas in detail here, but luckily there are excellent resources available from more knowledgable authors. We have compiled a few in the following:
- Mozilla Developer Network: Canvas Tutorial – excellent introduction to the canvas, available in multiple languages
- Mozilla Developer Network: Canvas API – A comprehensive overview of the canvas API
Screen¶
Caution
The canvas.Screen()
API, while completely functional, is not
entirely settled yet. You are absolutely invited to use it, however please
bear in mind that some details might change over time, as everybody gathers
experience using it.
In particular, the part that is most likely to change is the handling of
animation. This is because this is the aspect of the canvas which the authors
have the least experience with. If you are using a canvas
to show animated
content within an experiment, and would be willing to share thoughts or even
code, please be warmly invited to drop us a line.
-
class
canvas.
Screen
([options])¶ A
canvas.Screen()
is a component in an experiment that provides a canvas element to draw on via Javascript. It automatically inserts a canvas into the page when it is run, and adjusts its size to cover the containing element.When a
canvas.Screen()
is constructed, it takes options as any other component. It expects either arenderFunction
, which is a function responsible for filling the canvas, or an array of shapes ascontent
, which is rendered automatically using a generic render function.Arguments: - options (object) – Options
-
canvas.Screen.options.
renderFunction
¶ The render function contains any code that draws on the canvas when the screen is shown. It is called with four arguments:
- The
timestamp
contains a timestamp which represents the point in time at which the function is called. It represents the interval since page load, measured in milliseconds. - The second argument,
canvas
, contains a reference to the Canvas object provided by thecanvas.Screen()
. - On third place, the
ctx
argument provides a canvas drawing context. This is used to actually place information on the canvas. - Finally, the
obj
argument provides a reference to thecanvas.Screen()
that is currently drawing the canvas.
The simplest possible
canvas.Screen()
might therefore be defined as follows:// Define a simple render function const renderFunction = function(ts, canvas, ctx, obj) { // The render function draws a simple text on the screen ctx.fillText( 'Hello world', // Text to be shown canvas.width / 2, // x coordinate canvas.height / 2 // y coordinate ) } // Define a canvas.Screen that uses the render function const example_screen = new lab.canvas.Screen({ renderFunction: renderFunction, }) // Run the component example_screen.run()
- The
-
canvas.Screen.options.
ctxType
¶ Drawing mode: String, defaults to
'2d'
Type of canvas context passed to the render function (via the
ctx
parameter, as described above). By default, the context will be of the2d
variety, which will probably be most commonly used in experiments. However, more types are possible, in particular if the content is three-dimensional or drawn using 3d hardware acceleration. [1]
-
canvas.Screen.options.
translateOrigin
¶ Shift the origin of the coordinate system to the center of the visible canvas. Boolean, defaults to
true
In conjunction with the
viewport
, this option helps in creating a coordinate system that is replicable across screen sizes.
-
canvas.Screen.options.
viewport
¶ Size of canvas content: Array, defaults to
[800, 600]
Specifies the dimensions of the central canvas content (as tuple of width and height in pixels). In conjunction with
viewportScale
, this can be used to design a screen at a specific size and then, during the study, automatically scale this area to fit participants’ screen dimensions.
-
canvas.Screen.options.
viewportScale
¶ Scale
viewport
to fit screen:'auto'
(default), or numeric scale factor.If set to
'auto'
, translates canvas coordinate system so that the visible area covered by the canvas is assigned a (virtual) width and height corresponding to theviewport
size. The aspect ratio is perserved, so that the entirety of the viewport is always shown (empty space may be added at the top and bottom or at the sides, depending on the available space).For any numeric value, the coordinate system is scaled so that n pixels on the canvas correspond to n * viewportScale browser pixel units.
-
canvas.Screen.options.
viewportEdge
¶ Draw viewport borders: Boolean, defaults to
false
-
canvas.Screen.options.
devicePixelScaling
¶ Use native rendering resolution for high-DPI (retina) displays: Boolean, defaults to
true
Examples and tricks¶
Drawing shapes¶
The most natural use of the canvas is to draw shapes on it. In comparison to
using HTML
and images, this approach will offer you greater flexibility and
likely slightly better timing properties: As noted above, a canvas will provide
faster drawing times since it does not need to load images and layout the page.
This is particularly important if you are drawing different shapes in rapid
succession.
A simple example, which shows a square, a circle and a triangle on screen, might be realized as follows:
const renderFunction = function(ts, canvas, ctx, obj) {
// Draw a *square* ------------------------------------
// (let's start easy!)
ctx.fillStyle = '#164f86'
ctx.fillRect(
canvas.width * 0.2 - 25, // x coordinate
canvas.height * 0.5 - 25, // y coordinate
50, // width
50 // height
)
// Draw a *circle* ------------------------------------
// Start a new path
ctx.beginPath()
ctx.arc(
canvas.width * 0.4, // x center
canvas.height * 0.5, // y center
27.5, // radius
0, // start angle
2 * Math.PI // end angle (in radians)
)
// Fill the newly defined shape
ctx.fillStyle = '#861001'
ctx.fill()
// Draw a *triangle* ----------------------------------
// (this is slightly more involved, as we
// need to draw all the edges manually)
let center_x = canvas.width * 0.6
let center_y = canvas.height * 0.5 + 8 // (moved downward slightly)
let r = 32 // radius
ctx.beginPath()
// Move to the apex
ctx.moveTo(
center_x + r * Math.cos((0/3 - 0.5) * Math.PI), // center + displacement
center_y + r * Math.sin((0/3 - 0.5) * Math.PI)
)
// First edge
ctx.lineTo(
center_x + r * Math.cos((2/3 - 0.5) * Math.PI),
center_y + r * Math.sin((2/3 - 0.5) * Math.PI)
)
// Second edge
ctx.lineTo(
center_x + r * Math.cos((4/3 - 0.5) * Math.PI),
center_y + r * Math.sin((4/3 - 0.5) * Math.PI)
)
// Fill the shape
ctx.fillStyle = '#bd5b0c'
ctx.fill()
// Draw a *polygon* -----------------------------------
// (this uses the same principles as the
// triangle above, but generalized and
// written as a loop)
center_x = canvas.width * 0.8
center_y = canvas.height * 0.5
r = 30
let edges = 5
ctx.beginPath()
// Draw the edges sequentially
for (let i = 0; i <= edges; i += 1) {
// Use trigonometry to calculate
// the position of each vertex
let x = center_x + r * Math.cos(i * 2 * Math.PI / edges - 0.5 * Math.PI)
let y = center_y + r * Math.sin(i * 2 * Math.PI / edges - 0.5 * Math.PI)
if (i === 0) {
// For the first point, merely move the drawing cursor
ctx.moveTo(x, y)
} else {
// Draw a line to each subsequent vertex
ctx.lineTo(x, y)
}
}
// Fill the shape spanned by the vertices
ctx.fillStyle = '#0b5d18'
ctx.fill()
}
Sharp lines¶
When you draw lines on a canvas, you might notice that vertical and horizontal lines are not as sharp as you might have expected, namely if these lines have integer coordinates in both dimensions (or, to be exact, in that dimension in which the line does not extend).
The reason for this behavior is that the canvas coordinate system does not place points into the center of pixels, but rather at their edge. This means that any given point with integer coordinates is placed at the point at which the four surrounding pixels meet. Therefore, a vertical or horizontal line with integer coordinates in one dimension will always follow the edge between two adjacent pixels, and the browser will attempt to do this situation justice by drawing a slightly coloring both of the pixels in a slightly lighter shade than the line would otherwise have been.
To achieve crisp rendering and draw lines along the coordinate system (for lines
where the width is an odd integer number), you’ll need offset the coordinates by
half a pixel. You could shift the x and y coordinates of each drawing
command by 0.5, or alternatively you might apply a global shift using
ctx.translate(0.5, 0.5)
.
Advanced text placement¶
If you run the example above, you will notice that the text is not actually centered, but rather placed to right of the center of the screen, and slightly above the vertical center. This is is because, by default, the coordinates define the leftmost point at the baseline of the text (the baseline is the bottom of letters without descenders, such as all letters in this set of brackets) This placement is not typically the most helpful when putting together a screen. Instead, it is often easier to define the (vertical and horizontal) center of a given text. A ‘corrected’ render function might look as follows:
const renderFunction = function(ts, canvas, ctx, obj) {
// Set a font size and family
ctx.font = '40px Helvetica,Arial,sans-serif'
// Center the text horizontally
// around the specified coordinates
ctx.textAlign = 'center'
// Center the text vertically
// around the center of lowercase letters
ctx.textBaseline = 'middle'
// Draw the text as before
ctx.fillText(
'Hello world',
canvas.width / 2, // x
canvas.height / 2 // y
)
}
Saving and resetting drawing options¶
In the last example, the code set several options for drawing on the canvas,
such as the font size and type, and the positioning of text. The above code
changes these attributes for the entire context, meaning that any later calls
of the fillText
method use the same alignment and font, until the respective
options are changed.
This behavior, however, is often not desirable. Often, options are used only
once, and should be reverted to a sensible default after their application. This
is possible through the ctx.save()
and .restore()
methods provided by a
2d drawing context. Invoking these methods saves the state of the current
settings to an internal stack, to be restored at any later point.
Again extending the above render function, this might be used as follows:
const renderFunction = function(ts, canvas, ctx, obj) {
// Set a font size and family as default
ctx.font = '24px Helvetica,Arial,sans-serif'
// Center the text horizontally and vertically
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
// Save the context state
ctx.save()
// Draw some larger text
ctx.font = '36px Helvetica,Arial,sans-serif'
ctx.fillText(
'Welcome!',
canvas.width / 2, // x
canvas.height * 0.4 // y
)
// Restore the previous state
ctx.restore()
// Draw text using the initially defined size
ctx.fillText(
'Thank you for participating in this experiment',
canvas.width / 2,
canvas.height * 0.6
)
}
Using external libraries for drawing¶
If you find yourself building very complex interactive graphics using a canvas, consider enlisting a helper library to simplify drawing, such as three.js .
Sequence¶
If a canvas.Screen()
reflects a single canvas-based display, a
canvas.Sequence()
represents a series of such screens strung together.
It is constructed exactly like a regular flow.Sequence()
, and behaves
identically, with the single exception that it inserts a canvas into the
document when it starts, and directs all nested screens to draw onto this
canvas.
The rationale for using a dedicated canvas.Sequence()
over a regular
one is that the canvas need only be inserted into the document once, when the
sequence runs, rather than before each nested screen individually. This results
in a significant increase in transition speed, and allows for seamless and
instant switches between adjacent screens. The canvas
is not cleared
automatically between nested elements, so progressive animations are also
possible.
-
class
canvas.
Sequence
([options])¶ See also
A
canvas.Sequence()
will accept and apply any of the options used by aflow.Sequence()
(e.g.shuffle
), as well asctxType
as accepted bycanvas.Screen()
.-
canvas.Sequence.
content
¶ Array of canvas-based components to be run in sequence.
Important
A
canvas.Sequence()
requires that all nested elements arecanvas
-based. This is because thecanvas
is shared between all elements in the sequence, and is assumed to be visible and available throughout. The code will therefore throw an error if this condition is not met.If you switch between
canvas
andHTML
-based elements, please use a regularflow.Sequence()
. This will allow nested elements to insert a canvas if they require one, at the cost of changing the document content rather than being able to reduce the samecanvas
continuously.-
[1] | If you ever do this, please let us know, we will award you the coveted lab.js brave soul award. |
Data storage¶
While the different kinds of elements are responsible for what happens on screen, data storage collects participants’ responses, records their actions, and keeps them in store for later retrieval and export.
Collected data can have many origins and takes many forms. Different types of data are separated into different variables, each of which can save a different indicator or type of data. For example, many experiments will require collection of observed behavior, decisions, or judgments alongside the time participants needed to respond to the stimuli presented. Each variable, in turn, can vary over time, taking on different values as the experiment proceeds. In many cases, variables will change from screen to screen, as every new display elicits new data to be recorded.
A data.Store()
provides two central functions. First, it maintains the
state of the experiment, which is comprised of the latest value of each
variable. Second, a store archives the history of all variables over the
entire course of an experiment.
The entire history in lab.js
is represented as a long-form dataset, in
which each variable is contained in a column, and the values over time are
stored in rows. All data can be exported at any time for further processing and
analysis, either as comma separated value (csv) file, or as JSON-serialized
data.
-
class
data.
Store
([options])¶ If a record of the generated data is required, a
data.Store()
object is passed to the component whose data should be captured via thedatastore
option. This component will then commit its internal data to the store when it ends (unless instructed otherwise). Flow control components automatically pass this setting on to nested components (seehandMeDowns
).Thus, the simplest possible way to use a data store is the following:
// Create a new DataStore const ds = new lab.data.Store() const screen = new lab.html.Screen({ content: 'Some information to display', // DataStore to send data to datastore: ds, // Additional variables to be recorded data: { 'variable': 'value' }, // The response will be saved automatically responses: { 'keypress(Space)': 'done' } })
This will record any data collected by the Screen into the newly created datastore. In addition, the value
value
will be placed in the columnvariable
.The stored data can then be accessed later during the experiment, for example as follows:
// Download the data after the screen // has run its course. screen.on('end', () => ds.download()) screen.run()
This command sequence runs the screen, and executes the
download()
method on thedata.Store()
upon completion, causing a csv file with the data to be offered for download at this point. Further methods and options are illustrated in the following.Data storage
-
data.Store.
set
(key[, value])¶ Set the value of a variable or a set of variables
The
set()
method will assign the included value to the variable with the name specified in the first argument:ds.set('condition', 'control')
Alternatively, if an object is passed as the first argument, multiple variables can be set simultaneously:
ds.set({ 'condition': 'control', 'color': 'red' })
-
data.Store.
get
(key)¶ Get the current value of a variable
Returns the latest value of a variable given its name.
-
data.Store.
commit
([key, value])¶ Commit the current set of variables to storage
This method commits the current state of variables to the tabular long-term storage. Any variables that have changed since the last commit will be stored in a new row in the dataset.
In addition, any values passed via the key and value parameters will be added to the dataset before this takes place. Arguments are treated as in the
set()
method.
Data retrieval
-
data.Store.
show
()¶ Display the stored data on the console in a tabular format
This method shows the accumulated data on the console for review and debugging.
-
data.Store.
keys
()¶ Extract all variable names
Returns the names of all variables present in the data as an array.
Several variables containing administrative data are pulled to the front of the array, and the remainder are sorted in alphabetical order.
-
data.Store.
extract
(column[, senderRegExp])¶ Extract all values of a single variable
Returns all values this variable has taken over the course of the experiment as an array. That is, all of the states the variable was in when the data were committed.
The optional argument
senderRegExp
takes a string or regular expression that is compared to thesender
column in the data set (which contains thetitle
attribute of the element that contributed the corresponding set of data). If this option is a string, an exact match is performed. If it contains a regular expression, this is compared to the values in thesender
column.
Data export
-
data.Store.
exportJson
()¶ Export data as JSON string
Returns a string containing the collected data encoded as a JSON string. The string is constructed as a JSON array which contains a JSON-encoded object of each row of the data.
-
data.Store.
exportCsv
(separator=', ')¶ Export data as CSV string
Returns a string of the data in comma separated value (CSV) format.
The result is a string in which each data row is in a separate row, and columns within rows are separated by the specified separator, which is a comma by default.
-
data.Store.
exportBlob
(filetype='csv')¶ Export data as Javascript blob object
Returns the data enclosed in a given filetype (
csv
orjson
as described above), but as a blob object.
Data download
-
data.Store.
download
(filetype='csv', filename='data.csv')¶ Download data as a file
Initiates a download of the data in a specified format (see above) with a given file name.
Caution
Direct data download is not available on all browsers due to browser-side bugs and incompatibilities. We rely on
FileSaver.js
for this functionality, which excellent, but not perfect. Please consult the FileSaver.js documentation for information regarding browser support.
Data transmission
-
data.Store.
transmit
(url, metadata={}, payload='full')¶ Transmit data to a given url
Sends a HTTP
POST
request to the specified URL, with either the full dataset (default), or the currently staged data (if thepayload
argument is set to'staging'
) encoded as a JSON string, (under the keydata
), the current page URL (asurl
), and any additionalmetadata
specified in the field of the same name.This method returns a promise that originates from the underlying
fetch
call. The promise will be rejected if no connection can be established, but will otherwise resolve to aResponse
instance representing the server’s response. The status of the exchange can be accessed via theresponse.ok
attribute, or through the status code, which is available throughresponse.code
. Please consult the Fetch API documentation for additional details.Caution
The signature of this method may change in one of the next major versions (it might be replaced with an options object, but that’s not yet decided). We aren’t quite happy with its current state – if you have ideas, we’d love to hear them!
For the most part, you will probably interact with the transmit method in a way similar to the following example:
// Define server URL and metadata for the current dataset const storage_endpoint = 'https://awesome_lab.prestigious.edu/study/storage.php' const storage_metadata = { 'participant_id': 77 } // Transmit data to server ds.transmit( storage_endpoint, storage_metadata ).then( () => experiment.end() // ... thank the participant, // explaining that it is now possible // to close the browser window. )
However, much more complex scenarios are possible, especially with regard to the detection and graceful handling of errors. These are generally rare, however, especially in a more controlled, laboratory, environment, safeguards can be helpful in case something does go wrong, as illustrated in the following example:
// Assuming we have established and used the DataStore 'ds' ds.transmit(storage_endpoint, storage_metadata) .then((response) => { if (response.ok) { // All is well: The server reported a successful transmission experiment.end() // As a simple example of a possible reaction } else { // A connection could be established, but something went // wrong along the way ... let the experimenter know alert( 'Transmission resulted in response' + response.code + '. ' + 'Please download data manually.' ) // Download data locally (onto lab computers) // If you are conducting distributed experiments online, // you might instead use a timeout to retry after a short // interval. However, errors at this stage should be a // very rare occurrence. ds.download() // End the experiment (as above) experiment.end() }) .catch((error) => { // The connection itself failed, probably due to connectivity // issues. (this second part, the catch, is optional -- in may cases // you will not run into this situation, and if you do, there is, // sadly, very little that can be done. Any traditional web survey // will have long failed at this point) alert( 'Could not establish connection to endpoint. ' + 'ran into error ' + error.message ) // Download data and end as before ds.download() experiment.end() }) )
Hint
If you’re looking to transmit data automatically during a study, you might want to look at the
plugins.Transmit()
plugin, which sets this up for you.
-
Data format¶
Most studies built with lab.js
use a very similar data structure. We hope that, once you’re familiar with the general setup, you’ll find your way around all kinds of different studies easily. Among general features you’ll encounter are the following:
- One line per component: Every component in a study is represented in the data by a single line that contains all of the information pertaining to that component. This line is saved when the study moves beyond the component. Thus, data is written not only when a
screen
’s presentation is over, but also when aloop
orsequence
come to their end. - Log all available information: We tend to err on the side of saving too much data, and rely on you to filter the relevant parts in the analysis. In our experience, you can never have enough records for an experiment!
In the following, we’ll walk you through the columns that are present in a typical study.
Default columns¶
For a line that represents a component, the following columns contain metadata that contextualizes the remaining information in the row:
sender
reflects the component’s title.sender_type
contains the type of the component that collected the data. It stores both the part of the library that the component comes from, and the type of component itself, separated by a period. Values you might see, for example, arecanvas.Screen
,html.Form
orflow.Sequence
sender_id
represents the position of the component in the experiment’s timeline. This might seem confusing at first, because it reflects the studies’ nested structure. The first component in the experiment (e.g. an instruction screen at the beginning) will receive the number0
, and aloop
following it the number1
. However, inside of theloop
orsequence
, the counter starts anew, so the first repetition would be represented as1_0
, the second as1_1
, and so on. If you have asequence
inside of theloop
, the first screen inside of thatsequence
would be1_0_0
when it’s shown for the first time,1_1_0
when it is displayed for the second time, and so on.timestamp
contains the absolute time at which the data were recorded. This column uses the ISO 8601 date format.meta
is added by theMetadata
plugin, and records theURL
used to access the study, as well as technical information about the participant’s browser, screen size, and language settings. This information is encoded as JSON so as not to clutter the remaining columns. There is usually a single entry in this column at the beginning of the study.
The remaining columns reflect participants’ behaviour:
response
encodes the response chosen by the participant, or more specifically the label associated with this response.correctResponse
contains the normative response, if one is specified.correct
compares the previous two values, and indicates whether they match.duration
reflects the time for which a component was active (in milliseconds). If a timeout was set, this shows for how long a component was presented; if the component ended because the participant responded, it will measure the time from stimulus presentation to response.ended_on
separates the ways in which a component can end: It might have been because aresponse
was recorded, or that the component was terminated by atimeout
; less commonly, the component might have beenskipped
oraborted
.Sequences
,loops
and other flow control components end when their content iscompleted
.
We are meticulous about recording timestamps during the study, which as measured as milliseconds since the page load. They get their own columns:
time_run
when a component is presentedtime_render
records the frame at which information is showntime_end
when it ends. If a response was recorded, this reflects the time of the response as closely as possible (duration
is computed from the difference between this andtime_run
ortime_render
, if available)time_commit
when the data was saved to thedata store
Additional information¶
Besides the columns described above (which should be present in any study), additional columns are create for all parameters you add to your study. That is, all loop variables and task parameters you vary during the study are logged in all components for which they are active.
Random data generation¶
In many studies, stimuli aren’t defined ahead of time, but generated randomly for every participant anew. For this purpose, lab.js
contains flexible (pseudo-)random data generation utilities.
All random data generation is handled by the util.Random()
class. Every component in a study has direct access this utility through its random
property. Thus, to generate, for example, a random integer up to n
in a component script, one would write this.random.range(n)
. (for the sake of completeness: Outside of a component, the class can be instantiated and used by itself).
As an example, to randomly compute a parameter (which you could later use inside your screen content, or anywhere else where placeholders are accepted), you might use the following code in a script that runs before the component is prepared:
this.options.parameters['greeting'] =
this.random.choice(['aloha', 'as-salamualaikum', 'shalom', 'namaste'])
This will select one of the greetings at random, and save it in the greeting
parameter. The value is then available for re-use whereever parameters can be inserted, and will be included in the dataset.
You can alternatively use these functions directly inside of a placeholder, such as ${ this.random.choice(['hej', 'hola', 'ciao']) }
, and include this placeholder in the screen content. This shows a random greeting without preserving the message in the data.
In practice of course, you’ll probably be randomly generating more useful information, such as the assignment to one of several conditions.
-
class
util.
Random
([options])¶ A set of utilities with (pseudo-)random behavior, all drawing on the same source of randomness. By default, the random source is the browsers built-in random number generator,
Math.random
.-
util.Random.
random
()¶ Returns: A floating-point number in the range from 0
(inclusive) to1
(exclusive).
-
util.Random.
range
(a[, b])¶ Returns: If only a single value is given, a random integer between 0
andceiling - 1
; if two values are passed, an integer value betweenoffset
andceiling - 1
.
-
util.Random.
choice
(array)¶ Returns: A random element from the array
provided.
-
util.Random.
sample
(array, n[, replacement=false])¶ Returns: n
elements drawn from anarray
with or without replacement (default).
-
util.Random.
shuffle
(array)¶ Returns: A shuffled copy of the input array
.
-
util.Random.
shuffleTable
(table[, columnGroups=[]])¶ Returns: A shuffled copy of the input table
.Shuffles the rows of a tabular data structure, optionally shuffling groups of columns independently.
This function assumes a tabular input in the form of an
array
of one or more objects, each of which represents a row in the table. For example, we might imagine the following tabular input:const stroopTable = [ { word: 'red', color: 'red' }, { word: 'blue', color: 'blue' }, { word: 'green', color: 'green' }, ]
Here, the
array
(in square brackets) holds multiple rows, which contain the entries for every column.This data structure is common in
lab.js
: The entire data storage mechanism relies on it (though we hope you wouldn’t want to shuffle your collected data!), and (somewhat more usefully) loops represent their iterations in this format. So you might imagine that each of the rows in the example above represents a trial in a Stroop paradigm, with a combination of word and color. However, you’d want to shuffle the words and colors independently to create random combinations. This is probably where theshuffleTable
function is most useful: Implementing a complex randomization strategy.Invoked without further options, for example as
shuffleTable(stroopTable)
, the function shuffles the rows while keeping their structure intact. This changes if groups of columns are singled out for independent shuffling, as in this example:shuffleTable(stroopTable, [['word'], ['color'])
Here, the
word
andcolor
columns are shuffled independently of one another: The output will have the same number of rows and columns as the input, but values that were previously in a row are no longer joined. Two more things are worth noting:- Any columns not specified in the
columnGroups
parameter are treated as a single group: They are also shuffled, but values of these columns in the same row remain intact. - Building on the example above, multiple columns can be shuffled together by combining their names, e.g.
shuffleTable(stroopTable, [['word', 'duration'], ['color']])
.
- Any columns not specified in the
-
util.Random.
uuid4
()¶ Returns: A version 4 universally unique identifier as a string, e.g. 2b4a88ca-52ba-4950-9ec2-06f07f944fed
-
Plugins¶
The components that lab.js
provides differ with regard to their behavior during a study – some might be responsable for presenting information to participants, others might be in charge of the overall study flow.
It is, however, sometimes desirable to provide functionality that can be combined with any type of component, regardless of the specific type and stimulus modality it represents. This is what plugins are for.
Overview and motivation¶
Plugins hook into any component, and are then notified of events on that component. They are then free to react to each of these events in any way.
For example, it might be helpful to create a plugin that ensures that part of the study is presented in fullscreen mode – part being be a single html.Screen()
or html.Form()
, or a larger chunk of the experiment, as represented by a flow.Sequence()
or flow.Loop()
. In each case, such a plugin would ensure that the fullscreen mode is entered at the beginning of its activity, that the standard display is restored afterwards, and it would respond gracefully if the user exits the fullscreen view.
The advantage of this setup is that similar functionality (in this case, fullscreen handling) need not be implemented anew with every component. It thereby reduces the need for specialized components that cover any possible combination of functionality, such as a (hypothetical) flow.FullscreenSequence
or the like. Similarly, plugins can be used to pull out functionality that is not universally used, and would add complexity to the core.Component()
. Thus, plugins serve to reduce the bulk of the library code, and offer a flexible method of implementing custom functionality with the existing components.
Usage¶
Any number of plugins can be added to a component upon initialization via the plugins
option:
const c = new lab.core.Component({
plugins: [
new lab.plugins.Logger(),
]
})
After construction, plugins can be added using the commands c.plugins.add()
and c.plugins.remove()
.
Caution
This API (plugin addition and removal after construction) is tentative and may change as the library evolves.
Built-in plugins¶
-
class
plugins.
Logger
([options])¶ This basic plugin outputs any events triggered on a specific component to the browser console. It accepts a single option, a
title
that is output with every debug message.
-
class
plugins.
Debug
()¶ This plugin provides a debug overlay for any study, specifically a real-time view of the study state and the collected data. It is added in the builder preview to provide a means of checking the data.
-
class
plugins.
Metadata
()¶ Collects technical metadata regarding the user’s browser and saves it in the
meta
column. The data isJSON
-encoded and contains the following keys:location
:URL
under which the study was accesseduserAgent
: Browser identificationplatform
: Operating system, if provided by the browserlanguage
: Browser language preferences, e.g.en-US
locale
: Active browser locale, e.g.en-UK
timeZone
: User time zone, e.g.Europe/Berlin
timezoneOffset
: Offset from local time toUTC
, in minutes, e.g.-60
screen_width
andscreen_height
: Monitor resolutionscroll_width
andscroll_height
: Size of the window content (in pixels)window_innerWidth
andwindow_innerHeight
: Size of the browser viewport, that is, the portion of the page that is visibledevicePixelRatio
: Scaling factor that maps virtual onto physical pixels, for example on high-resolution screens or when the page zoom level is changed. This affects most of the screen measurements reported above, which are in virtual pixels. To convert to physical pixels, multiply the values by this scaling ratio.
-
class
plugins.
Transmit
([options])¶ Transmits collected data over the course of the study. Whenever new data are
committed
, all changed columns aretransmit()
to aurl
supplied in the options (required), along with anymetadata
, which can be specified in the options as an object (optionally). At theend()
of the component, the entire dataset is saved in the same way.
User-defined plugins¶
Users can define their own plugins to provide custom functionality. Plugins are JavaScript objects that are defined by one commonality only: They provide a handle
method that his called whenever an event is triggered on the associated component. The method receives two parameters, the context
which represents the component on which the event was triggered, and the event
, a string representing the type of event (e.g. prepare
, run
etc.).
In addition to the component event, the handle method will be called with the plugin:init
event when the plugin is added to the component, and plugin:removal
when the plugin is removed. It is the responsability of the plug-in to take care of all intervening coordination with the document and the linked component.
As an example, consider the plugins.Logger()
, shown here in its entirety:
class Logger {
constructor(options) {
this.title = options.title
}
handle(context, event) {
console.log(`Component ${ this.title } received ${ event }`)
}
}
Caution
As with the above API, some details of the custom plugin messages might be subject to changes. In particular, the plugin:removal
event might be renamed.
Module index¶
This is a dummy page, as replacement for the genindex
Find help¶
Please be invited to reach out and discuss any questions or ideas you have, or changes you would like to make: We are happy to answer your questions!
Online support¶
- Our Slack channel is always available for quick questions – we try to be around as much as possible, and other community members will also pitch in. Please be invited to join the discussion, and help your fellow researchers too!
- For long-term proposals, more formal technical discussions and bug reports, please use GitHub issues. These allow all developers involved to triage and keep track of outstanding to-dos and any organize longer-term work.
In-person workshops¶
Our favorite way to help is to provide in-person workshops for research groups. This allows us to give you our undivided attention, and provide you with a more structured introduction to our tool. Workshops also help sustain development, and we bring stickers! Here’s some things to know, based on our experience:
- We find that we can give an appetizer in a two-hour session and get people started in half a day; it takes two days to provide an in-depth introduction into all the features
lab.js
provides. - A very productive add-on to a workshop is building a set of paradigms specific to the local research group.
Note
We’re researchers like you! Allow us a moment to (preemtively) set expectations and explain ourselves: This project is built by researchers for researchers; its authors teach like you, sit in meetings like you, review papers and conduct independent research as you do; we, too, have deadlines and career obligations. This project is a labor of love for us, something we built because we strongly believe it should exist. We are always happy to help to the best of our abilities, and are immensely grateful for our users who are share our joy of helping others, are mindful of our sincere intentions, but also potential limits of fellow academics. Let’s make this project sustainable together!
Contribute¶
Thank you for considering contributing to lab.js
! Whether you have an idea or suggestion, if you have spotted a bug or even have a correction handy, whether you would like to add new features or documentation, or improve what’s already there, your help is very welcome indeed. Similarly, if you enjoy the project and would like to become a contributor, you are very warmly invited to join; we’d be glad to help you find a contribution that fits your interests and resources.
Together, we’re building a tool to help scientists understand behavior and cognition in its many forms, and to conduct their research efficiently and transparently. We believe the world needs this project:
- Browser-based studies have been difficult to build. We’d like to make data collection in the browser accessible to everyone.
- Commercial tools impede sharing of research material, and thereby hamper open, reproducable research; our goal is to make sure that studies can be shared, reproduced and extended.
- Behavioral research is moving towards large-scale, collaborative projects. The future needs open, extensible, cross-platform research tools. That’s what we’re creating here!
Ways to contribute¶
Thank you for considering contributing to lab.js
! We’re thrilled to have you around. This page summarizes a few of the many different ways in which you can help.
We would like to stress at this point that contributions can take may forms, and often don’t require writing code – maybe something could be documented more clearly, maybe a feature could be more helpful, a design more inviting. Help is welcome in any of these areas!
If you’re searching for a place to contribute, please do let us know: There’s always things to do, and we’d be glad to help you find something that fits your interests and resources. If you’re writing a tool that might interoperate with this one, we’re more than happy to link things up; if you’re looking to extend or build on this project, we’d be proud to provide a stepping stone for you!
Report bugs or suggest improvements¶
Notice something amiss, or some room for improvement? You’re already helping by letting us know — we’d love to hear from you, and try to make things work for everyone. We track bugs and tasks using GitHub issues. Here are some steps you can take to help us fix things and help you quickly and more effectively:
Before submitting an issue
- Please take a quick look whether the problem or idea has been reported already (there’s a list of open issues). You can try the search function with some related terms for a cursory check. If you do find a previous report, please add a comment there instead of opening a new issue. If you’re unsure, we’d be glad to help!
Submitting a (great) bug report
- Pick a descriptive title that clearly identifies the issue.
- Describe the steps that led to the problem so that we can go through the same sequence. Does the problem reoccur when you go through the same steps once more? Is it specific to a particular browser? Can you share the study in which the error occurs? It is a massive help if you can provide us all the information needed to recreate the problem.
- Briefly describe what you had expected and how that differed from what happened, and possibly, why.
Making a suggestion
- Summarize your idea with a clear title.
- Describe your suggestion in as much detail as possible. How would it change the usage of the software?
- Explain how the suggestion would be useful to most users.
Note
This software was built to make our own research easier, and we’re always eager to make it more useful. If there’s an annoyance we can fix easily, we’re always glad to do so!
Contribute code and/or documentation¶
Wow, thank you for considering making a contribution to the code or documentation! You have won a special place in our heart already [1]. As an open project, we welcome contributions from everybody, and we will gladly help you make yours.
If you’re looking for a way to get started, you might find a task that interests and suits you, or an inspiration, in our collection of good first bugs or the list of upcoming milestones. We would be happy, though, to help you find something that works for you.
Note
We would like to encourage you to reach out before you start working: Between our contributors, we have a lot of ideas and code lying around, and might be able to give you a head start. If you are are planning to add significant amounts of additional functionality, we might ask you to build an external add-on or a plugin first before including your code in lab.js
itself. In any case, we would be thrilled to help you get started!
If you are familiar with Git and GitHub, please feel free to fork the repository and submit pull requests; otherwise, your contributions are welcome in any shape or form. If you would like to learn to use GitHub, a nice way to get started is the course How to Contribute to an Open Source Project on GitHub by Kent C. Dodds.
We would like contributions to conform to the Developer Certificate of Origin to make sure that the licensing works out. We encourage contributors to ‘sign off’ patches as the Linux kernel developers do. If you’re not familiar with the process, please don’t let that stop you; we’ll gladly walk you through the process when you submit a change.
[1] | Since you’ve gotten this far, would you mind if we included you in our worldwide swag distribution scheme? Seriously, do ping us, sending a few stickers your way is the absolute least we can do. |
Working together¶
We would like ours to be an open, welcoming and supportive community, and are committed to making this possible. We expect all members to meet our Code of Conduct in all their interactions, to be excellent to one another and to help others do the same.
The p5.js community statement, the Apache Foundation’s guidelines and the Public Lab Code of Conduct provide a blueprint for the kind of project we strive to be.
Please make sure you understand and accept the terms outlined in the code of conduct; if you have questions or suggestions, please do let us know. Welcome to our community!
Reaching out and finding help¶
Please be invited to reach out and discuss any questions or ideas you have, or changes you would like to make: We are happy to answer your questions!
Our Slack channel is always available for quick questions – we try to be around as much as possible. For long-term proposals, more formal technical discussions and bug reports, please use GitHub issues. You are also welcome to drop the main contributors a line or two by email if you prefer.
Building a local copy¶
The project repository contains the code underlying the lab.js
library and the builder interface. To condense both into a single library file for distribution with studies, and an uploadable version of the builder, please follow these additional steps after downloading. You’ll need a local installation of node.js and yarn.
You’ll notice that many of the commands start with yarn
– that’s because we use scripts as shortcuts for most build steps.
Downloading the code¶
The easiest way to create a local copy is by cloning the repository. If you use git, you can copy the following command:
git clone https://github.com/FelixHenninger/lab.js.git
If you’d prefer a direct download, that’s available too!
Bootstrapping the project¶
The library and builder interface are contained in the same repository because they share several pieces of code. Both are coordinated by Lerna, which can initialize all parts at once by running the following commands in the project directory:
yarn && yarn run bootstrap
Compiling the library¶
Changing to the packages/library
directory and running
yarn
will install all dependencies for the lab.js
library, whereafter
yarn run build:js
will output a transpiled version in the packages/library/build
directory. If you would like the transpiled output to be updated automatically as you make changes, yarn run watch:js
will do that for you.
yarn run build:starterkit
will build the library with all its components (the basic HTML
template, the stylesheet, and several other useful files), and assemble the result into a zip
file for easier distribution. This is the bundle that is included with every release.
There are a few more commands available, which you can see by typing yarn run
in the packages/library
folder.
Working on the builder¶
The builder interface is created using Facebook’s create-react-app template, and follows the conventions instituted there. If you’re looking for details, their documentation provides more information than we ever could.
The main code is found in the packages/builder/src
folder, where the components
subdirectories contain all user-facing interface code, and logic
holds the main application logic.
To install a copy of the builder locally, please download the repository, navigate into the packages/builder
folder, and run yarn
to download all dependencies. Then, typing
yarn start
will run the builder application in a local development server, and open it in a browser.
yarn run build
bundles all files necessary for deployment, and creates an optimized version of the application code in the packages/builder/build
folder for you to upload to a local server.
Important
For the lab.js
builder to work on a public server, it must be served over an encrypted connection (via HTTPS); please make sure that encryption is set up on the server you’re using.
Building the documentation¶
The library’s documentation is built using Sphinx, using the fabulous Read the Docs Theme. Both require a local python
installation, as well as the pip
package manager.
If you don’t have python
on your system, please consider the Anaconda python distribution; if you’re only missing pip
, you can install it on your system. Equipped with both, install the required Python modules:
pip install -r docs/requirements.txt
With everything at hand, you can run the following command from the project’s root directory:
yarn run build:docs
This will output the html documentation in the docs/_build
subdirectory. Running yarn run watch:docs
will update the documentation whenever you save changes.
Finding your way around the code¶
If you look into the library code, you’ll find annotations and explanations alongside the JavaScript source. However, it can be difficult to find the place you’re looking for. The following page is meant as an overview; if you have any further questions, do let us know.
Library¶
The source code underlying the lab.js
library is contained in the packages/library/src
folder of the repository. For ease of development, the code is split across several files.
User-facing code¶
core.js
· Core user-facing classesThis code defines the core user-facing parts of the library, notably the
core.Component()
and its simplest derivative, thecore.Dummy()
component.If you are looking to understand the internals of the library, this is the place to start – all the core functionality is defined here. We strive to keep this code especially well-commented and understandable, please do let us know if we can explain something better!
html.js
· HTML-based components- All elements that use
HTML
for showing content:html.Screen()
,html.Form()
andhtml.Frame()
. These are probably most commonly used in studies. canvas.js
· Canvas-based components- Components in this file rely on the
Canvas
for showing content, for extra performance:canvas.Screen()
,canvas.Sequence()
andcanvas.Frame()
. flow.js
· Flow control- These components are not so much for displaying information, but for controlling the overall flow of the experiment. In particular, this file includes the source for
flow.Sequence()
,flow.Loop()
andflow.Parallel()
. data.js
· Data handling- The code contained in this file takes care of data storage and export. It defines the
data.Store()
class that logs and formats the experiments’ output.
Utilities¶
The library also contains a range of utility functions and classes for internal use. These are generally not exposed to end-users, but are used extensively throughout the library code.
util/eventAPI.js
· Low-level helpers and event handlersThis file defines the
EventHandler()
class that provides a very basic publish-subscribe architecture to all other classes in the library.This is really the backbone of the library, which relies heavily on this design for everything that happens. This is the place to dig deepest into the inner machinations of
lab.js
.util/domEvents.js
· Document event handling- The code in this file deals with assigning handlers to document events, and establishing and removing the links between both. The resulting
DomConnection()
class encapsulates this functionality, and is used within each component to handle document events. util/fromObject.js
· Construct studies from serialized representations- Many of the studies built with
lab.js
– for example those constructed using the builder, aren’t programmed in JavaScript code directly. Instead, users provide a static representation of their study, and rely on the library to assemble the appropriate code. This is what the code in this file is for. util/fullscreen.js
· Fullscreen helpers- This file provides functions to enter and leave fullscreen mode across all browsers.
util/options.js
· Option parsing- The code in this file helps with substituting component
parameters
in the content and options of components. util/preload.js
· Media preloading- Preloading images and other static assets.
util/random.js
· Random number generation- Anything that needs to be sampled, drawn, suffled or generated randomly goes through this code.
util/tree.js
· Tree traversal- A more complex study built with
lab.js
will often resemble a tree structure, in that there is a centralsequence
as a stem, which contains other components. These child components may, in turn, contain others nested inside them. This nested, or tree-like structure, frequently needs to be navigated, and the utilities in this file help with that.
Builder¶
The graphical builder interface resides in the repository’s builder/src
directory. It is structured as a React application, building on the create-react-app template. The internal state is managed using Redux.
components
· User interface components- The application is broken down into distinct components, for example the editor or the sidebar, each of which contain their own logic and styles. If you are looking for a specific part of the user interface to improve, this is where you’ll find it.
logic
· Application logic- Besides the user interface, the builder contains a substantial amount of application logic that governs how studies are put together, saved into and loaded from files, and exported to a local preview mode as well as publishable study bundles.
Running tests¶
Don’t be fooled by us listing them last – tests are a vital part of our work and infrastructure. They are what allows us to sleep at night while colleagues the world over rely on our software. When you or any of us proposes a change, automated tests will verify it, and together, we’ll write new tests to cover any added functionality.
Library¶
You’ll find the tests for the core library in the packages/library/test
directory. After building the library, you can test its functionality by opening index.html
in any browser, which will run a series of checks to ensure that everything works as designed. You should (hopefully) see a lot of green tick marks!
During development, you might find it easier and faster to run automated tests from the command line. The command npm test
, run in the packages/library
folder, will do that for you, provided that you have a version of the chrome browser installed.
To run cross-browser tests, you’ll need an account at Sauce Labs, and setup your computer so that your login credentials are available. Then, you can run npm run test:sauce
to automatically run the entire test suite across the full range of supported browsers.
We also take great pride in our good test coverage, for which statistics can be generated using the command npm run test:coverage
.
Builder¶
Unit tests for the builder cover the core application logic. By running npm test
in the packages/builder
directory, you’ll get continuously updated test results.
Teach with lab.js¶
One of the original motivations in building lab.js was to provide a tool for teaching: It was initially designed as part of the first author’s course on Methods in Cognitive Psychology, taught to first-semester master students at the University of Koblenz-Landau.
If you are interested in teaching with lab.js, please be invited to contact the first author, who is happy to share course material and additional pointers. If you have considered using the library in class, or have actually employed it, we would be thrilled to hear from your experience and gladly receive any feedback you have.
Why use lab.js in class?¶
As noted above, the library was built for a graduate-level seminar on browser based (cognitive) experiments. The syllabus focussed on building these experiments from the ground up, starting with HTML, CSS and finally Javascript. The students, in general, responded very well to the intensely technical course, and enjoyed building experiments with web technologies. By the end of the semester-long weekly course, students were able to build basic experiments by themselves using the elements provided in the library, though delving further into the details and features of Javascript (custom functions in particular) proved to be overly challenging. The author confesses to having had unrealistic plans given the limited time frame, but was, upon reflection, still impressed by the progress the students made given their very limited exposure to programming prior to the course.
Because the library was built for teaching, we believe that it provides some unique features that make it particularly suited for use in class if pure Javascript experiments are the focus. First, it introduces much of Javascript’s syntax in a natural way, and exposes users to different data types, variables, collections of data (lists and objects), and object-oriented programming style (rather than some arbitrary declarative syntax). It also lends itself to functional programming, using maps to translate lists of stimuli into screen elements, however this should be taught only if the time or prior experience of students permit discussion of these advanced topics. While the library exposes users to a wider range of Javascript concepts, it strongly encapsulates and therefore hides browser-specific parts of Javascript, in particular any manipulation of the Document Object Model (DOM). This allows the focus to remain on the general concepts used while programming instead of the (verbose) DOM API.
Generalizability of knowledge¶
We feel strongly that the terminology and concepts should generalize beyond the confines of this particular library. We cannot foresee the methods and tools that our students will encounter over the course of their careers, and believe that a cookbook-style course limited to a single library or a commercial experimental software would do our students a disservice in the long run.
Because we teach psychological concepts (and review experimental methodology) alongside programming, we have attempted to match the vocabulary used in both domains. This particularly concerns the subdivision of an experiment into recurring sequences, units that handle stimulus display and flow control, and the hierarchical nesting of building blocks. Similarly, we have adopted ideas and nomenclature from our experience with other experimental software (particularly OpenSesame), hoping that students will be able to transfer their knowledge should they encounter different tools in the future.
Broad applicability¶
Students in our classrooms have chosen an elective course in cognitive psychology, but often focus on very different fields within psychology, both basic and applied. We feel that a good course should not only cater to students interested in basic research, and emphasize general experimental methods and the value of considering cognitive processes alongside specific results from cognitive psychology. The library assists us and our students by allowing for the easy construction not only of experiments, but also of questionnaires and simple presentations. As a web-based framework, it is not bound to the laboratory, but can also be used in the field, from mobile devices, as well as participants’ own hardware.
Reflections on library design and pedagogy¶
The origin of the library has heavily influenced its design. Specifically, in teaching, we attempt to strike a balance between, on one hand, giving students tailored tools to build experiments very quickly (so as not to overload students with technical information, to retain focus on the psychological content of the course, and provide students with the sense of achievement vital to technical work) and, on the other, teaching skills and knowledge that carry further than the specifics of the library itself, so as not to limit the course to cookbook-style programming.
Because the experiments are provided online and run in the browser, the course
as well as the library itself require and thus convey basic knowledge of the
technology underlying the web, such as HTML
and CSS
, and some basic
general-purpose programming concepts such as variables, lists and functions.
In our experience, demonstrating that the new skills are useful beyond the
narrow domain of constructing experiments helps to increase and maintain
students’ motivation.
Roadmap¶
To be honest, we’d be hoodwinking you if we pretended for a second that we had any idea where this project was going in the long run. We’ve changed tack several times now, from being a pure JavaScript library to having a full-blown builder interface; so please take this with more than just a grain of salt. If you’re interested in where the project is going in the mid-term, please be invited to talk to the team, we’ll gladly share our secret plans.
There are, however, a couple of things we feel strongly about, which we’ve tried to capture here (again, to questionable success).
Release schedule¶
The library aims for biannual major releases in a tick-tock pattern. The summer release will be allowed to break backward compatibility if necessary, but the API should remain stable for the remainder of the year, though features may be added. This is very similar to the concept of semantic versioning.
Philosophy and Scope¶
Many small decisions have to be made when building a library like this, and from time to time, on idle evenings, the urge makes itself known to imagine that some grand underlying principles governed its design. At other times, when thoughts go in circles over some minute detail, obsessing over some minor detail, one dreams of having guidelines that might inform API structure.
This section is an attempt at distilling principles for the design of the library, to serve as a benchmark and discussion tool for the interested, and for its developers. It is the result of both pathological grandiosity and rumination, and should not be taken too seriously: Pragmatism will always dominate the following ideas, and quite likely they will have to revised sooner or later, when we discover that our thinking has changed.
Built as a tool for teaching¶
lab.js
is built for researchers with broad experience in programming
experiments as much as it is built for novices to programming. This necessitates
maximum possible conceptual clarity. Interfaces and terminology should be as
consistent as possible throughout the library.
The original author’s courses in experimental design and programming are half practical, geared toward enabling students to build and run experiments, and half technical, intended to convey at least the most basic programming concepts. Therefore, the library should be representative of general programming practices, and avoid custom notation that might seem simpler at first, but would limit generalizability of the acquired knowledge.
Limited in scope¶
The central technical goal of the library is to provide a framework for handling the temporal progression of events over the course of a computer-based experiment that is run in the browser as a single-page application. It also offers helpers for working with the collected data.
The generation and sequencing of stimuli themselves should be left to the user,
or external libraries. A GaborScreen
, or anything similarly specific, would
be out of scope, and should be provided as a third-party-addon.
That being said, the project’s design should make possible the reuse and sharing of parts of studies, so that they can be easily incorporated into new research.
Based on web standards¶
Technical decisions are made on the assumption that the era of great differences between web browsers is over, and that future browsers will be updated at a steady pace to follow common standards. Antiquated browsers should not be a reason to compromise on features or performance. We have been reluctant to incorporate experimental features unique to any particular browser, but if a particular feature is slated for standardization, using a polyfill for the time being is fine.