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.
Welcome (back)!

Working with HTML

Once you know your way around, learning HTML gives you greater flexibility and control over design.

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
(in excrutiating detail)

Contributing

Seriously, you're awesome. Suggestions, examples, even code are all super-welcome.
Come join us!


Project website

This is where we show off all our features in glossy pictures. Show this to your friends, and boss!

Support

Here's where you'll find help.
Do join our community chat and say hello, maybe even help someone else, too?

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 world­wide 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!

Introduction to HTML

Screen design in HTML

Attributes and dubious elements

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

Pre-made styles

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.


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:

Page with just a container div
<!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:

Screen divided into sections
<!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
Container with fullscreen class

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.

Sections with fullscreen class

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.

Text styles
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

Example: Stroop task instruction

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.

Alert styles
<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 styles
<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.

Text styles

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>

Width

To force elements to use all available width, add the w-100 class.

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

Custom layout

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.

Add media to a study

Many experiments use media files such as images and vector graphics, sounds and videos, all of which are supported by lab.js. In this section, we discuss how to embed and use media in a study.

Working with files

Using files as stimuli

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!

Introduction to HTML forms

Form elements

Forms in practice

Write custom logic

We do our best to provide as much functionality as we can in the builder interface, but sometimes that’s not enough: Your ingenuity and creativity are tough to keep up with.

In these cases, when you’d like to go beyond the features that the builder provides, you can do so through small custom programs written in JavaScript. These vastly extend the functionality provided by lab.js, and allow you to customize any possible aspect of your study.

In this section, we’d like to provide a brief introduction to JavaScript, the programming language used by browsers, and show you how to use it in service of your research.


A lightning-quick introduction to JavaScript

Writing scripts in the builder

Note

We’ve made a change in a recent version of lab.js that isn’t yet reflected in the video.

In the video, we describe how to set parameters by assigning values to this.options.parameters.greeting to setup a parameter with the name greeting. However, recent versions of lab.js allow you to do the same by writing this.parameters.greeting and omit the options part.

We’re highlighting this because

  • This change saves you some typing
  • You can now use this.parameters and this.state consistently both in your scripts and in placeholders elsewhere in the experiment. In either case, you can extract parameter and state values, and save data to the state.

Following the example in the video, you could write:

this.parameters.greeting = this.random.choice([
  'As-salāmu ʿalaykum', 'shalom'
])

and extract the value thus generated on a screen or in any builder setting through the placeholder ${ this.parameters.greeting }. See how this saves the value and retrieves it from the same place?

We hope that this makes things more consistent, and will update the video as soon as we get the chance. 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.

See also

This page covers integration with third-party tools in the abstract. 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, please export it using the generic survey software integration. This will give you a zip archive with all the necessary files to run the study, pre-configured to send the data to a third-party service.

After exporting the study, the next step is to make it accessible to the survey software. In most scenarios this means uploading the study files to a hosting provider or webspace. Some tools, notably JATOS and Open Lab, will host the study files for you, but others, you’ll need to host the study yourself, outside of the survey.
If your institution provides a server, or you have a personal webspace, you can unpack the study files there; if you don’t, we like to use Netlify Drop, which makes uploading and hosting a study a drag-and-drop operation (you’ll need to setup a free account to permanently store your files with Netlify).

Whereever you upload your study, please make a note of the URL at which it is accessible, you’ll need it for the next step.

Note

We’re happy to help you with this! We recognize that hosting a study can be a tricky step; please join our support community if there’s anything we can support you with.


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 above.


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;"
  allowfullscreen
></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', function _labjs_data_handler(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)
      // ...

      // ... finally, stop listening for further data
      window.removeEventListener('message', _labjs_data_handler)
    }
  })
</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.

Before you go through the steps below, please make sure that you’ve prepared your study for use in third-party tools.

Danger

Qualtrics is prone to deleting data without warning. Specifically, as of March 2021, Qualtrics will truncate data saved from outside tools such as lab.js. This means that especially longer experiments run the risk of Qualtrics throwing away parts of the experimental data. They do not provide any indication that this is happening.

If you use Qualtrics, we implore you to make sure that your collected data is exported completely. Even if your study works perfectly in the lab.js builder, Qualtrics may still render the collected data unusable. We would very much like to spare you the pain of losing data.

Especially if you are new to online data collection, please consider using a service such as Open Lab instead; the experience is much smoother and the caveats fewer.


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;"
  allowfullscreen
></iframe>

<!-- Adjust the page style slightly -->
<style>
  /* 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.

const page = this
page.hideNextButton()

// Listen for the study sending data
window.addEventListener('message', function _labjs_data_handler(event) {
  // Make sure that the event is from lab.js, then ...
  if (event.data.type === 'labjs.data') {
    // ... extract the JSON data lab.js is sending.
    const data = event.data.json

    // ... save data and submit page
    Qualtrics.SurveyEngine.setEmbeddedData('labjs-data', data)
    window.removeEventListener('message', _labjs_data_handler)
    page.clickNextButton()
  }
})

Caution

If you deploy a study to Qualtrics, please make absolutely sure that you’ve thoroughly checked the collected data, especially if you’ve made changes to the data storage code.


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.

Note

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.

See also

The ambition and the powers of the Experiment Factory go way beyond hosting lab.js studies. Please also check out:


Design and export your experiment

Expfactory export option

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!

Expfactory export modal

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!

  1. Create a container repository on Docker Hub to correspond to the name you want to build
  2. Commit and push the code to Github
  3. Connect the repository to Circle Ci, and
  4. Add this name to the variable CONTAINER_NAME, along with DOCKER_USER and DOCKER_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!

Expfactory Stroop Task

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() %>%
  # ... filter empty rows ...
  discard(function(x) x == '') %>%
  # ... parse JSON into a data.frame
  map_dfr(fromJSON, flatten=T) -> data

# Optionally save the resulting dataset
#write_csv(data, 'labjs_data_output.csv')

Managing studies with Open Lab

Open Lab is a web application that makes study hosting easy and aims to provide a secure framework for data collection. Its direct integration with lab.js makes deploying your study a breeze.


Exporting a task to Open Lab

To export a task to Open Lab, please select the Upload to Open Lab… option in the builder interface. After confirming the upload, you’ll see a button that allows you to manage the task within Open Lab. Clicking it will take you to the Open Lab site, where you can add further configuration options. If you aren’t already, you may have to log into the Open Lab system or create a new account.


Running your study

To create a new study, navigate to the Studies tab in Open Lab, where you will find a New study section. Give your study a name, click Enter, and continue to the Select tasks tab. The empty panel at the top represents your study (currently devoid of tasks), whereas the available tasks are listed toward the bottom of the page. Add the task you previously uploaded to the study by clicking the green plus on the corresponding task card. Your study is now ready to go, and you can find links to distribute to your participants on the Invitations tab.

Working with data

The participants’ data can be found in the Data tab. You can download data as CSV files, optionally filtered by participant or task. The Participants page shows the table of all participants, allowing you to inspect or export the data for any participant. On the Results by task page, you can select data for any of the tasks in your study.

Open Lab supports collaboration on studies, as well as the re-use and adaptation of pre-made tasks. It can also manage participants, by distributing invitations and scheduling notifications for repeated participation. For more information, please consult the documentation, or visit the publicly available library of tasks.

[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 programming experiments in JavaScript 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

So how do you build a ``lab.js`` study in pure code? This is what we’d like to show you in the following, by building a very simple experiment. We’ll cover more details in the subsequent parts of the tutorial.

First, let’s get you set up: 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 Visual Studio Code, 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 assist you! The lab.js community convenes in the community support channel, where you’ll find kind folks to help answer questions and discuss how things work. You’re very warmly invited to join, and after going through this tutorial, you’ll be able to help others, too! Finally, we are also constantly trying to make the tutorial clearer and more helpful – if you have comments or suggestions, we would genuinely love to hear them.


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. However, you’ll always need the files in the ``lib`` folder of the starter kit, because that’s where the lab.js library files live.

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.

Screenshot of default starterkit page

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, and because we haven’t provided a study for it to run, it will wait indefinitely at this point. The experiment we are about to 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 and CSS. These are provided with lab.js and contain the library code and default styles. You might have also spotted a reference to study.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 a div tag of the container fullscreen class. This is what provides the rectangular frame you may have spotted on the page.
  • Within the container div, the content is subdivided into header, main, and footer elements. These correspond to the three areas on the page. Feel free to adjust the content as you see fit!
  • Finally, you might have spotted that the main element has an attribute data-labjs-section with the value main. That because the experiment content will go inside that element, and the surrounding parts of the page will remain unchanged. You can move this attribute, for example, to the surrounding div, which will allow you to replace the entire container content with every new screen.

So that’s the page structure we’re going to work within. Next, let’s go get an experiment to work!

See also

If you would like to find out more about how the design works, we discuss specifics of page layouts in the section 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 study.js file included in the starter kit – that’s where the actual structure of the experiment is set up. In particular, we would like to draw your attention to a specific part of the code:

var study = 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 study variable. Some additional options are provided in the brackets, notably some content (omitted here). You might have noticed that the content is included in square brackets, which indicate that the content is 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, there’s one more thing that happens: The study is started … or rather it isn’t yet, 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 as you experiment: It can’t harm your computer. If something goes wrong, you can find the original version in the repository.

As before, we’d love to support you 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

Stroop task screenshot

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, if the word and color correspond (are congruent), participants can perform the task faster.


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 kicked off the study by adding or uncommenting study.run(). We’ll build upon the same code, so please make sure you have the files and a text 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.

Individual component timeline

As we just discussed, every component’s moment in the spotlight is when it runs. This will very often mean showing some information for a fixed amount of time, or waiting for the participants’ response. A typical experiment will often consist of many such components strung together, for example like this:

Multiple components in sequence

When we build experiments, components will not only be responsible for presenting stimuli and collecting responses: We will also 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.

Components nested in a sequence

In many ways, a sequence component behaves exactly as a standard component would: It prepares by signaling to all nested components to get ready 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 string, enclosed in quotation marks to indicate that the browser should treat it as literal text rather than as a command. 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 content option. Second, it’s worth noting briefly that the the quotation marks around and within 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 – that’s something to look out for.

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!


Combining screens to build a trial

In the previous section, we’ve filled our Stroop screen with the minimal content it needs. In this one, we’d like to build a single trial with you, adding more screens, and then teaching the software to move between them.

Right now, your study code will most likely look somewhat like this, in that it consists of a single sequence, containing a single screen:

const study = new lab.flow.Sequence({
  content: [
    new lab.html.Screen({
      content: '<div style="color: red"> blue </div>',
    }),
  ],
})

Let’s now expand on that by adding a couple more screens: A fixation cross prior to the stroop screen, and an inter-stimulus-interval thereafter. You can build these by duplicating the Stroop screen code twice, and placing it in front of and below the existing trial screen, defining a sequence of several screens, somewhat like this:

const study = new lab.flow.Sequence({
  content: [
    new lab.html.Screen({ /* Fixation cross options */ }),
    new lab.html.Screen({ /* Stroop stimulus options */ }),
    new lab.html.Screen({ /* Inter-stimulus interval options */ }),
  ],
})

Each of these screens differs with regard to its content – for example, the fixation cross might contain just a single plus sign for the moment, and the inter-stimulus interval might remain entirely empty.


Moving between screens

If you start the study at this point, you’ll see that the study hangs at the fixation cross, and won’t continue beyond it. Let’s change that!

Setting timeouts

What we’d like to happen is for the study to move on from the fixation cross after a fixed amount of time, and do likewise in the inter-trial interval. To implement this, we’ll need to add a second option to the respective components, the timeout. This sets a time in milliseconds after which a component ends automatically, and cedes control to the subsequent screen (if there is one). With a timeout in place, you should see the study moving to the stimulus at least.

Defining responses

On the stimulus screen, we’d like to wait for our participant’s decision before moving on. For this to work, lab.js needs to know about the permissible responses on the screen, which are defined in the responses option.

The responses map the actions the participant can take onto the meanings they convey. For example, in the Stroop task, participants might press the r, g and b keys, corresponding to the the responses red, green and blue. This mapping is added to the screen settings:

new lab.html.Screen({
  content: '<div style="color: red"> blue </div>',
  responses: {
    'keypress(r)': 'red',
    'keypress(g)': 'green',
    'keypress(b)': 'blue',
  }
})

With this, all parts of your study know when to move on automatically or wait for partipant input, allowing the study to run through a single trial. We’ll build on that in the next step!


So to recap briefly, we hope to have shown you how to setup different components and their options, and how to run through different components in sequence. In the next part, we’ll put everything you now know to use and define an experiment that varies information across trials.


[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.


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.html.Screen(
    '+',
    {
      '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.flow.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 log function is part of the console object. Grouping of functions in objects is often used for tidiness – you might have noticed that all functions belonging to lab.js are contained in the lab object, as in lab.HTMLScreen.

Similarly, functions that pertain to a specific element in the experiment are also linked to the element’s variable with a period, like experiment.run(), which runs a specific element. This indicates that the function is linked to, and operates on, the object it comes with. Such functions are often called methods.

[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


Recipes

Optimizing for timing performance

For many fields of research, accuracy with regard to presentation timing and response time measurement is critical. ``lab.js`` can achieve a very high level of both if a little care is taken during study construction.

In this document, we provide actionable guidance for ensuring that your study operates at the highest possible level of performance. We regularly monitor the timing characteristics of our software and provide our benchmark results online. We also strongly encourage you to measure the performance of your study if timing is critical to your research, and are happy to assist with assessment and optimization. That being said, the steps described in the following should get you very close to an optimum level of performance. Here are our recommendations:


Use canvas-based screens where possible
New component modal with canvas screen selected

For optimal performance, our first recommendation is to use the canvas-based screens that are designed using the visual builder interface for presentation.

This is because, compared to HTML-based stimuli, these take load off the browser by reducing the need for computing complex layouts on the fly (which is necessary for HTML), instead allowing lab.js to preload screens ahead of time so that they can be displayed as efficiently as possible.

HTML-based screens absolutely have their place in your toolbox: They are the most adaptable type of screen, and extremely helpful for formatting questionnaires and text-based screens such as instructions. However, for fast-paced stimuli, we recommend using canvas-based screens.


Add frames around performance-critical parts of the study
Stroop task with frame around task loop

Our second recommendation is to place frames around fast-paced, performance-critical parts of the study. In most cases, this is easily achieved by inserting a frame around the trial loop, as illustrated in the figure to the right.

Frames further increase performance by reducing changes to the page content, ensuring that only the relevant parts of the screen are updated when its content changes.


Unroll unnecessary loops

As a final recommendation to achieve the utmost performance, we recommend unrolling loops so that timing-critical transitions do not take place in-between loop iterations. The vast majority of studies do not require this, but it is worth checking just in case.

In most if not all experiments, the technical structure of the study corresponds to the natural progression of stimuli. For example, in the Stroop Task pictured above, timing is most critical on the Stroop screen and in the progression from fixation cross to stimulus and finally to the inter-trial interval. Because these are part of the same sequence, lab.js knows to optimize transitions between these immediately subsequent screens, and will provide maximum timing accuracy inside of the trial sequence, while allowing for some potential slack between trials.

You might be tempted to spread a set of subsequent stimuli across multiple loop iterations. For example, you might use a loop to repeatedly present a single screen while varying its color. In this case, subsequent screens are not immediately adjacent in the study timeline, but broken across multiple loop iterations. Because switching from one iteration to the next incurs a slight overhead, these breaks may lead to slight delays in stimulus presentation. This potential delay between loop iterations is a well-known issue in computer science, as is its solution, unrolling loops. Unrolling means to take stimuli that were previously split across iterations of a loop, and placing them in direct succession. Following the above example, our recommendation would be to convert the loop into a string of individual screens with varying contents. That way, all screens will be placed in immediate succession, and delays will be minimized.

Note

This is a temporary measure, and we are actively working to improve cross-iteration performance. Stay tuned, and remember to unroll where necessary in the meantime!


Example studies

We also maintain an extensive collection of examples studies in the project repository; all of these are also 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 throughout lab.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 of responses and a timeout:

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 necessary media required later.

The prepare() method can, but need not be called manually: The preparation phase will be executed automatically when the the component is run(). 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 as tardy.

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. Calling run() will trigger prepare() 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’s data, evaluate it against the ideal response as specified in correctResponse, and then end() 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 accumulated data, commits it to the specified datastore, 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 a Sequence() 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 the parameters 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 not color, 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 is run(), it will be set to undefined. Then, until the end() 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 its end(), 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 and 1 (read-only)

The progress attribute indicates whether a component has successfully completed its run(), and (for more complex components) to which degree. For example, a basic html.Screen() will report its progress as either 0 or 1, depending whether it has completed its turn. Nested components such as the flow.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 attribute data-labjs-section with the value main.

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, an html.Screen() will insert custom HTML, whereas a canvas.Screen() will supply a Canvas 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 or document.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 and data is that the former are retained at all times, while the data 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 the parameters, whereas all data collected later should be (and is automatically) collected in the data attribute.

Behavior

options.skip

End immediately after running (false).

options.tardy

Ignore automated attempts to prepare() the component, defaults to false.

Setting this attribute to true will mean that the component needs to be prepared manually through a call to prepare(), or (failing this) that it will be prepared immediately before it is run(), 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, the end() method is called immediately.

For example, if the possible responses are to press the keys s and l, 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 alternatively Space and Enter 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 value Space instead of a single whitespace for clarity, as well as Comma so as not to confuse this key with the separator. Note also, however, that some browsers do not fire keypress 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 is 0, the middle button is 1, and so on, and vice versa for a left-handed mice. (please note that you may also need to catch and handle the contextmenu 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).

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 and data.

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() method

const 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 the skip option is set to true 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, the content 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, the debug 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 varying parameters 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 a flow.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) a flow.Sequence() or even another flow.Loop(). This component will be cloned for each iteration, and the parameters 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.
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.sample.n

The number of samples to draw from the templateParameters. If not specified, the number of samples defaults to the number of available parameter sets.

Thus, in sequential, draw and draw-shuffle mode, leaving this option unset ensures that all parameter sets are used.

flow.Loop.options.sample.mode

How to sample from the iterations while constructing the loop. Several distinct modes are available (the default is draw-shuffle):

  • sequential: Run through the parameter sets in order. If all parameter sets have been run through, but more samples are required, re-start from the top.
  • draw: Sample from all parameter sets without replacement. When all sets have been drawn, start over.
  • draw-shuffle: Like draw, but shuffle the result when oversampling.
  • draw-replace: Sample from the parameter sets with replacement.

The draw and draw-shuffle modes only differ when the number of samples, n exceeds the number of available parameter sets. In this case, the draw algorithm will lead to blocks of shuffled parameter sets. For example, given three parameter sets [1, 2, 3], over-sampling five sets might lead to a resulting order of [2, 3, 1, 2, 1] (notice that all sets are exhausted before the first repetition occurs). The draw-shuffle mode adds a shuffle step after sampling that breaks this restiction (which might lead to the result of [2, 3, 3, 2, 1]). Thus in draw-shuffle mode, the block structure is broken up, but the frequency of parameter sets will be roughly equal.

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 entire flow.Parallel() component ends as soon as the first nested component ends. In this case, any remaining components are shut down automatically (by calling end()). 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.


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 the content, which is the string of text and HTML inserted into the document. Additional options correspond to those of a core.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 using el option.

Using placeholders

You can access the parameters available through the html.Screen() to insert placeholders within its content. These are filled when the screen is prepared. 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 format parameters.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 via state.column_name. For example, you might want to use the veracity of the last response to provide feedback via state.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 the core.Component. For example, one might capture responses 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 load HTML content as text. The content is loaded when the screen is prepared. Replaces the screen’s content.


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 the html.Screen() does, with a few additions:

See also

A html.Form() is derived from the html.Screen(), and therefore also accepts the content and contentUrl 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 the validator. Returns true or false.

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 returning true or false depending on its decision. Only if it returns true will the html.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 returns true, 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 a HTML surrounding, a context, for nested components, its content. 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 nested content is embedded (required).

html.Frame.options.contextSelector

CSS selector (as string, required) which specifies the element inside the context within which the content is shown. It is passed onto the nested component as el attribute.

html.Frame.options.content

Single component (required) that is run within the content provided by the context, and given control of the HTML element defined by the selector.

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.”


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:


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 a renderFunction , which is a function responsible for filling the canvas, or an array of shapes as content, 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 the canvas.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 the canvas.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()
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 the 2d 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 the viewport 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 .


[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 the datastore 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 (see handMeDowns).

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 column variable.

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 the data.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 the sender column in the data set (which contains the title 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 the sender 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 or json 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 the payload argument is set to 'staging') encoded as a JSON string, (under the key data), the current page URL (as url), and any additional metadata 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 a Response instance representing the server’s response. The status of the exchange can be accessed via the response.ok attribute, or through the status code, which is available through response.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 a loop or sequence 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, are canvas.Screen, html.Form or flow.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 number 0, and a loop following it the number 1. However, inside of the loop or sequence, the counter starts anew, so the first repetition would be represented as 1_0, the second as 1_1, and so on. If you have a sequence inside of the loop, the first screen inside of that sequence would be 1_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 the Metadata plugin, and records the URL 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 a response was recorded, or that the component was terminated by a timeout; less commonly, the component might have been skipped or aborted. Sequences, loops and other flow control components end when their content is completed.

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 presented
  • time_render records the frame at which information is shown
  • time_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 and time_run or time_render, if available)
  • time_commit when the data was saved to the data 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) to 1 (exclusive).
util.Random.range(a[, b])
Returns:If only a single value is given, a random integer between 0 and ceiling - 1; if two values are passed, an integer value between offset and ceiling - 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 an array with or without replacement (default).
util.Random.shuffle(array)
Returns:A shuffled copy of the input array.
util.Random.constrainedShuffle(array, constraints[, helpers={}, maxIterations=10**4, failOnMaxIterations=false])
Arguments:
  • array (array) – Array to be shuffled
  • constraints – Constraint specification as an object, or a check function (see below)
  • helpers (object) – Optional specification of equality check or hash function used while checking constraints.
  • maxIterations (int) – Maximum number of shuffle iterations to go through before giving up.
  • failOnMaxIterations (Boolean) – If max iterations are reached, throws an exception if true, else warns in the console.
Returns:

A shuffled copy of the input array, subject to specified constraints.

This method will shuffle an array similar to the shuffle function described above, but will check whether constraints are met before returning the result.

Defining constraints

The constraints argument can be used to define desired properties of the shuffled result, specifically the maximum number of repetitions of the same value in series, and the minimum distance between repetitions of the same value. These are defined using the maxRepSeries and minRepDistance parameters, respectively.

maxRepSeries restricts the number of repetitions of the same value in immediate succession. For example, maxRepSeries: 1 ensures that no value appears twice in sequence:

// Create a new RNG for demo purposes. Inside a component,
// scripts can use the built-in RNG via this.random
const rng = new lab.util.Random()

rng.constrainedShuffle( // (I was a terror since the public school era)
  ['party', 'party', 'bullsh!*', 'bullsh!*'],
  { maxRepSeries: 1 }
)
// ➝ ['party', 'bullsh!*', 'party', 'bullsh!*']

Similarly, minRepDistance ensures a minimum distance between successive repetitions of the same value (and implies maxRepSeries: 1). Note that maxRepDistance: 2 requires that there is at least one other entry in the shuffled array between subsequent repetitions of the same entry, 3 requires two entries in between, and so on:

rng.constrainedShuffle(
  ['dj', 'dj', 'fan', 'fan', 'freak', 'freak'],
  { minRepDistance: 3 }
)
// ➝ ['dj', 'fan', 'freak', 'dj', /* ... */]

Custom constraint checkers

As an alternative to desired properties of the shuffled result, it’s possible to define a custom constraint checker. This is a function that evaluates a shuffled candidate array, and returns true or false to accept or reject the shuffled candidate, depending on whether it meets the desired properties:

// Function that evaluates to true only if
// the first array entry matches the provided value.
const firstThingsFirst = array => array[0] === "I'm the realest"

rng.constrainedShuffle(
  [
    "I'm the realest",
    "givin' lessons in physics",
    "put my name in bold",
    "bring the hooks in, where the bass at?",
    // ... who dat, who dat?
  ],
  firstThingsFirst
)
// ➝ Shuffled result with fixed first entry
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 the shuffleTable 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:

const rng = new lab.util.Random()
rng.shuffleTable(stroopTable, [['word'], ['color']])

Here, the word and color 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']]).
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 is JSON-encoded and contains the following keys:

  • location: URL under which the study was accessed
  • userAgent: Browser identification
  • platform: Operating system, if provided by the browser
  • language: 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 to UTC, in minutes, e.g. -60
  • screen_width and screen_height: Monitor resolution
  • scroll_width and scroll_height: Size of the window content (in pixels)
  • window_innerWidth and window_innerHeight: Size of the browser viewport, that is, the portion of the page that is visible
  • devicePixelRatio: 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 are transmit() to a url supplied in the options (required), along with any metadata, which can be specified in the options as an object (optionally). At the end() 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!


Share studies

A great way to support fellow researchers is to share your studies. We maintain a set of example studies that can serve as a starting point for other scientists, and we’re continuously collecting more.

  • If you have a study you’d like to contribute, we’re all ears! It needn’t be perfect by any means (though we’d be glad to make it so).
  • We’re also always looking for ideas for studies that are missing from the collection, so if you would like to see a study for your research or teaching, let us know! It’s easiest to work from a published study, so any references you can provide are hugely helpful.

Tip

If you’re constructing a study you would be willing to contribute to the examples, we’ll help you build it if we can. We’re careful with promises, but because it makes us really happy to help you out and other users too, we’ll do our best to help you build polish the study. Do reach out!


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/dist 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.

Please note that the builder requires a copy of the library to work, so please compile the library starterkit as described above before modifying the builder. With the library in place, please then navigate into the packages/builder directory. From there, 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.

Environment variables

During the build step, you can set several environment variables that customize the builder’s behavior.

  • PUBLIC_URL provides the URL at which the builder will run, e.g. https://labjs.felixhenninger.com
  • REACT_APP_EXAMPLE_PATH sets the URL from which the builder loads its repository of example studies. By default, this points to the tasks directory in the project repository, and thus the builder loads the example collection metadata from therein.
  • REACT_APP_TEMPLATE_PATH works just like the EXAMPLE_PATH above, but instead of loading examples, it loads the templates offered to users when they add new components. This path, too, is expected to contain a metadata JSON file that provides further information about the templates, like the one in the project repository.

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 classes

This code defines the core user-facing parts of the library, notably the core.Component() and its simplest derivative, the core.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() and html.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() and canvas.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() and flow.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 handlers

This 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 central sequence 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.