Front end for zooniverse/Panoptes
Clone or download
eatyourgreens and srallen Workflow selection updates and bug fixes (#5198)
* Workflow selection: use loadWorkflows action
Remove the API client from workflow selection. Use the loadWorkflows action to load the selected workflow ID.
Update tests.
Reload if the stored workflow changes and does not match user preferences.

* HomeWorkflowButton: rules for routing to classify
Home workflow buttons should load the classify page immediately if the stored workflow matches the selected workflow, or wait for the new workflow to load and then change the page.

* Check workflow assignment state before prompt
Don't show the workflow assignment prompt if we're already showing it.

* Tidy up valid workflow code
Preferences are updated by the redux action, so remove the preference change from workflow selection.
Tidy up the logic that checks project links for valid workflow IDs.

* Handle preferences change
A new workflow should be selected when new user preferences load, or when preferences are reset to null on logout. Reset the classifier when preferences ID changes, not when the user login changes.

* Fix typo

* Guard against updating null classifications
Check that a classification exists before saving annotations.

* Guard against updating null annotations
Check that an annotation exists before saving annotations from HidePreviousMarksToggle.
NB. why is that toggle storing data on the annotation anyway?

* Validate workflows for projects
Validate the project against workflow.links.project before rendering the classifier from workflow selection.
Latest commit da2aa6f Jan 16, 2019
Type Name Latest commit message Commit time
Failed to load latest commit information.
.github Updated contributing documentation about merge conflicts (#4311) Jan 19, 2018
app Workflow selection updates and bug fixes (#5198) Jan 16, 2019
bin Add config for docker-compose (#4620) May 16, 2018
css Add bug warning to exports (#5161) Jan 8, 2019
public Added Samuel Aroney bio (#4876) Sep 11, 2018
test Install Sinon-Chai (#5087) Nov 27, 2018
views Tidy up index view (#4863) Sep 6, 2018
.dockerignore Add .dockerignore file Jan 7, 2016
.editorconfig Add editorconfig Jun 12, 2017
.eslintrc Add babel to ESlint (#4678) Jun 8, 2018
.gitignore Install nyc and coveralls to measure test coverage (#4882) Sep 17, 2018
.mention-bot Update ignore list (#3568) Mar 8, 2017
.npmrc Set default save prefix to "~", switch existing dependencies Nov 18, 2015
.travis.yml Install nyc and coveralls to measure test coverage (#4882) Sep 17, 2018
Dockerfile Add Jenkinsfile to automatically stage branches (#4753) Oct 22, 2018
Jenkinsfile Add production deploy via jenkinsfile - test jenkins (#5006) Oct 23, 2018
LICENSE Create LICENSE Apr 16, 2015 add link to deployment docs (#5020) Oct 25, 2018
babel.config.js Create a global Babel config object (#4972) Oct 8, 2018
docker-compose.yml Add config for docker-compose (#4620) May 16, 2018
package-lock.json Bump papaparse from 4.6.2 to 4.6.3 (#5200) Jan 15, 2019
package.json Bump papaparse from 4.6.2 to 4.6.3 (#5200) Jan 15, 2019
static-server.js Clean up npm scripts Jun 2, 2016 Update to Babel 7 and Webpack 4 (#4924) Sep 28, 2018 Connect the dev classifier to the classify store (#4978) Oct 15, 2018

Panoptes (front end)

Build Status

Coverage Status


Requires Node.js. It's recommended you manage your Node installations with nvm.

npm install installs dependencies.

npm start builds and runs the site locally.

npm run stage builds and optimizes the site, and then deploys it to

For testing with production data, you can add env=production to your development url, e.g. localhost:3735/projects?env=production. Note that it is removed on every page refresh.

All the good stuff is in ./app. Start at ./app/main.cjsx

While editing, do your best to follow style and architecture conventions already used by the project, unless you have a reason not to. If in doubt, ask.

Development with Docker

To avoid having to install Node.js or any other dependencies, you can also run everything with Docker and Docker Compose.

docker-compose build will build a local Docker image and run npm install. Run this whenever you change dependencies in package.json.

docker-compose run --service-ports dev npm start builds and runs the site locally on port 3735.

docker-compose run dev npm run stage builds and optimizes the site, and then deploys it to

What to do if it doesn't run

Try rm -rf ./node_modules && npm install to freshen up your dependencies. And read the warnings, they should tell you if you're using the wrong version of Node or npm or if you're missing any dependencies. If you use docker-compose to build and test the site, you shouldn't run into any problems with the Node version, but docker-compose build will build a new image with a fresh npm install.


If you write a new component, write a test. Each component should have its own .spec.js file. The test runner is Mocha and Enzyme is available for testing React components. Mocha throws an error (Illegal import declaration) when compiling coffeescript files that contain ES6 import statements with template strings. Convert these imports to require statements. You can run the tests with npm test.


See for details on how PFE deploys.

Directory structure

  • ./app/classifier/

    All things classifier-related.

  • ./app/collections/

    Collections-related components.

  • ./app/components/

    Misc generic, reusable components.

  • ./app/layout/

    App-level layout stuff goes here. If it affects the main site header, the main site footer, or the layout of the main site content, this is where it lives.

  • ./app/lib/

    Individual functions and data that are reused across components.

  • ./app/pages/

    This is where the bulk of the app lives. Ideally, each route points to a page component responsible for fetching data and handling any actions the user can perform on that data. That page component uses that data to render the UI with dumb components, passing actions down as necessary.

  • ./app/partials/

    Originally intended to hold isolated components that wouldn't actually be reused anywhere. These probably belong closer to where they're actually used.

  • ./app/subjects/

    Subject views (TODOC: How's this related to Talk/collections?)

  • ./app/talk/

    Talk-related components.

  • ./public

    Files here will get copied to the output directory during build.

Classifier tasks

Each task component class should have a couple static components:

  • Summary: Shows the post-classification summary of the tasks's annotation.

  • Editor: The component used to edit the workflow task in the project builder.

There are also a few hooks into the rest of the classification interface available, if the task needs to render outside the task area.

  • BeforeSubject: HTML Content to appear before the subject image during the task.

  • InsideSubject: SVG Content to appear over the subject image during the task.

  • AfterSubject HTML Content to appear after the subject image during the task.

These hooks can be prefixed with Persist, which will cause them to appear with the task and persist even after the user has moved on to the next task.

Persist{Before,After}Task work the same way, but for the task area. Non-persistent hooks are unnecessary for the task area.

Each component also needs a few static methods:

  • getDefaultTask: Returns the task description to be used as the default when a user adds the task to a workflow in the project builder.

  • getTaskText: Given a task, this returns a basic text description of the task (e.g. the question in a question task, the instruction in a drawing task, etc.)

  • getDefaultAnnotation: The annotation to be generated when the classifier begins the task

  • isAnnotationComplete: Given a task and an annotation, this determines whether or not the classifier will allow the user to move on to the next task.

  • testAnnotationQuality: Given the user's annotation and a known-good "gold standard" annotation for the same task, this returns a number between 0 (totally wrong) and 1 (totally correct) indicating how close the user's annotation is to the standard.

Task editors

Make sure you call this.props.onChange with the updated task when it changes.

Drawing task tools

Some static methods, called from the MarkInitializer component, which controls the mark's values during the user's first mark-creating action:

  • defaultValues: Just some defaults for the mark.

  • initStart: For every mousedown/touchstart until isComplete returns true, return the values for the mark.

  • initMove" For every mousemove/touchmove, return new values for the mark.

  • initRelease: For every mouseup/touchend, return new values for the mark.

  • isComplete: Is the mark complete? Some marks require multiple interactions before the initializer gives up control.

  • initValid: If a mark is invalid (e.g. a rectangle with zero width or height), it'll be destroyed automatically.

A couple helper components are the DrawingToolRoot which handles selected/disabled states and renders sub-task popups, and the DeleteButton and DragHandle, which render consistent controls for drawing tools. There's also a deleteIfOutOfBounds function that should be called after any whole-mark drags.


React requires each component in an array to have a sibling-unique key. When rendering arrays of things that do not have IDs (annotations, answers), provide a random _key property if it doesn't exist. Ensure underscore-prefixed properties aren't persisted. That's automatic with the JSONAPIClient.Model class.

  {for item in things
    item._key ?= Math.random()
    <li key={item._key}>{item.label}</li>}

Async helper components

There are some nice unfortunate (in hindsight) components to help with async values. They take a function as @props.children, which looks a little horsey but works rather nicely. Most requested data is cached locally, so these are usually safe, but if you notice the same request being made multiple times in a row, these are a good place to start looking for the redundant calls. Here's an example of re-rendering when a project changes, which results in checking the projects owners.

<ChangeListener target={@props.project}>{=>
  <PromiseRenderer promise={@props.project.get('owners')}>{([owner]) =>
    if owner is @props.user
      <p>This project is yours.</p>
      <p>This project belongs to {owner.display_name}.</p>

Do not write new code using ChangeListener or PromiseRenderer.

If it's reasonable, replace ChangeListener and PromiseRenderer instances with component state in code you work on. It's more verbose, but it's more readable, and it'll get us closer to rendering on the server in the future.

CSS conventions

Include any CSS required for a component's functionality inline in with component, otherwise keep it in a separate file, one per component. For a given component, pick a unique top-level class name for that component and nest child classes under it. Keep common base styles and variables in common.styl. Stylus formatting: Yes colons, no semicolons, no braces. @extends up top, then properties (alphabetically), then descendant selectors. Prefer use of display: flex and flex-wrap: wrap to explicit media queries wherever possible.

Our CSS has gotten really huge, so we're trying out BEM for organization.

// <special-button.styl>
  background: red
  color: white

  width: 1em;

// <special-container.styl>
  margin: 1em 1vw

    border: 1px solid

Writing components in ES6/ES2015

We're migrating from coffeescript to ES6. This can be done incrementally by writing a new component or rewriting an existing component in ES6. A few gotchas should be mentioned:

  • The existential operator does not exist in ES6. Either compare explicitly to null or use !!thing if it just needs to be truthy.

  • Native ES6 classes are preferred since React.createClass() is deprecated, however, if the existing component is relying on mixins, then consider using createReactClass().

  • Mixins are being deprecated and not supported with native classes, so do not use them in new components.

  • Use backticks to import ES6 components into coffeescript components:

`import NewComponent from './new-component'`

An ESLint configuration file is setup in the root of the repository for you to use with your text editor to lint both ES6 and use Airbnb's React style guide.

A guide on writing native classes versus using createReactClass()

Custom projects

See the panoptes-client library:

Format of annotation values

The format of an annotation's value depends on the task used to generate it.

  • single: The index of the chosen answer.

  • multiple: An array of the indices of the chosen answers (in the order they were chosen).

  • drawing: An array of drawing tool marks (descriptions of which follow below).

  • survey: An array of identifications as objects. Each identification a choice (the ID of the identified animal) and answers, an object. Each key in answers is the ID of a question. If that question allows multiple answers, the value will be an array of answer IDs, otherwise just a single answer ID.

  • crop: An object containing the x, y, width, and height of the cropped region.

  • text: A string.

  • combo: A sub-array of annotations.

  • dropdown: An array of objects where the string value refers to the answer to the corresponding question and the boolean option indicates that the answer was in the list of options.

Drawing tool marks

All coordinates are relative to the top-left of the image.

All marks have a tool, which is the index of the tool (e.g.[0]) used to make the mark.

All marks contain a frame, which is the index of the subject frame (e.g. subject.locations[0]) the mark was made on.

If details tasks are defined for a tool, its marks will have a details array of sub-classifications (each with a value, following the descriptions above).

Drawing annotation value are as follows:

  • point: The x and y coordinates.

  • line: The start (x1, y1) and end (x2, y2) coordinates.

  • polygon: An array of objects, each containing the x and y coordinate of a vertex. If the mark was not explicitly closed by the user, auto_closed is true.

  • rectangle: The x, y coordinate of the top-left point of the rectangle along with its width and height.

  • circle: The x and y coordinate of the center of the circle and its radius r.

  • ellipse: The x and y coordinate of the center of the ellipse, its radii rx and ry, and the angle of rx relative to the x axis in degrees (counterclockwise from 3:00).

  • bezier: The same as polygon, but every odd-indexed point is the coordinate of the control point of a quadratic bezier curve.

  • column: The left-most x pixel and the width of the column selection.


Thanks to BrowserStack for supporting open source and allowing us to test this project on multiple platforms.

BrowserStack logo