PhET-iO Developer's Guide

When you obtain a license for PhET-iO development, you also receive account credentials that provide access to the PhET-iO simulations and their supporting materials. One simulation is provided as an example that can be accessed without an account:

Density 1.1

Take a look around, kick the tires, ask questions and read on to learn more!

What's in the Box?

The entry point to each PhET-iO simulation is an index page which provides:

  • A link to the simulation
  • Documentation
  • A suite of wrappers that demonstrate the simulation's features and capabilities
  • A template for creating your own wrapper

What is a Wrapper?

A wrapper is an HTML page that embeds a PhET-iO simulation in an iframe and provides added value, such as simulation customization, event logging, dynamic feedback, classroom activities, etc. To develop with PhET-iO, you will create and host your own wrapper. Each PhET-iO simulation provides a wrapper template which loads the simulation and provides hooks and callbacks that are easy to build on. Follow the link above to see the index page for the example simulation and its template wrapper. Read through the template and its documentation, then:

  1. Launch the template by pressing the corresponding button. Observe how the simulation is embedded in an iframe and messages are output to the developer tools console.
  2. Download the template and try running it from your own localhost or server. If it behaves like the template in the previous step, you are ready to begin iterating and adding your own features.

Wrappers begin by loading the PhET-iO JavaScript library and running code like:

const phetioClient = new phetio.Client(...);
phetioClient.launchSim(...);

After the phetioClient is used to launch the simulation, it can send commands to PhET-iO Elements in the simulation for customization or control, and receive messages from the PhET-iO Elements in the simulation about what is happening within the simulation.

What is a PhET-iO Element?

Each simulation contains numerous PhET-iO Elements, each of which represents an object within the simulation. Each PhET-iO Element has a unique identifier, called a phetioID, which can be used to send messages to the PhET-iO Element or receive messages from it. For example, in Density 1.1, the first screen has a reset button identified by the phetioID:

'density.introScreen.view.resetAllButton'

Most phetioIDs begin with the name of the simulation, then the name of the screen and either model or view.

An important PhET-iO Element which appears in every PhET-iO simulation is:

'phetioEngine'

which provides system-wide methods, such as getting or setting the state of the entire simulation, or capturing a screenshot. You can see its full API by selecting it in Studio.

How can I find out what PhET-iO Elements are in a simulation?

A good starting point is to launch the Studio wrapper, which is linked from the simulation index page. By default, Studio only shows the most important PhET-iO Elements for a simulation, which are labeled as Featured. There is a selection option that can show All of the PhET-iO Elements.

How can I send or receive messages to/from a PhET-iO Element?

The wrapper template defines a variable phetioClient which can be used to invoke methods on PhET-iO Elements. For example:

phetioClient.invoke(
  'density.introScreen.view.resetAllButton.visibleProperty',
  'setValue',
  [ false ]
);

The first argument to phetioClient.invoke is the phetioID. The second argument is the method name. An optional third argument is an array of values to be passed to the method. An optional fourth callback option can be supplied to support methods that return a value, as well as error handling. The API is described more completely in the "API Docs", linked from the simulation's index page.

How do I know what methods each PhET-iO Element supports?

Each PhET-iO Element has a type which ends in the suffix IO, such as NodeIO. The Studio wrapper can be used to show the type hierarchy and all methods associated with a particular PhET-iO Element.

Why do so many phetioIDs end with the suffix *Property?

In the previous example, we hid the Reset All button by setting the value of its visibleProperty to false. PhET-iO simulations use a pattern where stateful PhET-iO Elements have type PropertyIO which means they have a current value, a way to change the value, and a way to observe when the value has changed.


Data Stream

Several of the example wrappers demonstrate the simulation's data stream. A good way to understand the data stream is to launch one of the data stream wrappers and interact with the simulation while watching the data stream. For example, when pressing the "Reset All" button on the first screen, you may see a JSON data stream like so:

Example JSON Data Stream
{
  "index": 55,
  "time": 1686334368500,
  "type": "user",
  "phetioID": "density.general.controller.input.clickAction",
  "name": "executed",
  "componentType": "PhetioActionIO<EventContextIO>",
  "data": {
    "context": {
      "domEvent": {
        "constructorName": "PointerEvent",
        "altKey": false,
        "button": 0,
        "charCode": null,
        "clientX": 1511,
        "clientY": 299,
        "code": null,
        "ctrlKey": false,
        "deltaMode": null,
        "deltaX": null,
        "deltaY": null,
        "deltaZ": null,
        "key": null,
        "keyCode": null,
        "metaKey": false,
        "pageX": 1511,
        "pageY": 299,
        "pointerId": 1,
        "pointerType": "mouse",
        "scale": null,
        "shiftKey": false,
        "target": {},
        "type": "click",
        "relatedTarget": null,
        "which": 1
      }
    }
  },
  "metadata": {
    "dataKeys": [
      "context"
    ],
    "playback": true
  },
  "children": [
    {
      "index": 56,
      "time": 1686334368500,
      "type": "user",
      "phetioID": "density.general.controller.input.clickAction.executedEmitter",
      "name": "emitted",
      "componentType": "EmitterIO<EventContextIO>",
      "data": {
        "context": {
          "domEvent": {
            "constructorName": "PointerEvent",
            "altKey": false,
            "button": 0,
            "charCode": null,
            "clientX": 1511,
            "clientY": 299,
            "code": null,
            "ctrlKey": false,
            "deltaMode": null,
            "deltaX": null,
            "deltaY": null,
            "deltaZ": null,
            "key": null,
            "keyCode": null,
            "metaKey": false,
            "pageX": 1511,
            "pageY": 299,
            "pointerId": 1,
            "pointerType": "mouse",
            "scale": null,
            "shiftKey": false,
            "target": {},
            "type": "click",
            "relatedTarget": null,
            "which": 1
          }
        }
      }
    }
  ]
}

A wrapper can also emit to the data stream through the PhET-iO api, in which case the type will be wrapper. This is important if you want to interleave data from events in the wrapper with events in the simulation. Also notice that events are hierarchical and depict causality. Child events occur while the parent event is executing. The example shows how the user pressing the resetAllButton causes the quadraticTermVisibleProperty to change from true to false.

Here is a brief description of each of the fields in an event:

Attribute Description
index The number of the event, 0-indexed. Can be used to verify that the correct number of events was received.
type {user|model|wrapper} whether the event was originated by a user action (user) or by the simulation itself (model). For instance, a button press is a user event while a state change in the simulation is a model event (even if it was triggered by a user event). Wrappers can also trigger events to be added into the data stream, creating a single consistent log for the entire application.
phetioID Unique identifier for a PhET-iO Element. Described above.
componentType PhET-iO Type for the PhET-iO Element. Described above.
name The name of the event that was fired by the object, like "pressed" or "changed"
time The number of milliseconds since the epoch (in 1970) as reported by the client machine. No effort is made to verify that this matches any other clock, and note that the time may step back if the user manipulates the machine clock, or due to daylight savings time.
data (optional) Key value pairs that provide extra information about the event.
children (optional) Events that were triggered while this event was being processed are nested under this object.
metadata (optional) Supplemental information about the nature of the event.

JSON Simulation States

PhET-iO supports customization and interaction through the state of a simulation. The state of a simulation is collected in JSON format, where phetioIDs are the keys holding data about each PhET-iO Element. The state can be used to set the initial configuration of a simulation, or at any time during runtime. It can also be retrieved as a snapshot of a simulation, to be set on that version of the simulation at any time. PhET-iO supports emitting states on the data stream as well. When accessing fields of a state object, do not access any fields that are under the "private" field. Fields under "private" are part of the private implementation of the type. They are not considered to be part of the public API, and are included in the state only to support save/restore.


Startup Sequence

PhET-iO provides a startup sequence with a step where the wrapper can communicate with the simulation before it launches. This can be useful to set certain parts of the configuration, such as the random number sequence or to add listeners before the simulation launches, so that no event is missed. The various phases are documented in the simulation's wrapper template.


PhET-iO Simulation Versions

The wrapper template points to a {{MAJOR}}.{{MINOR}} version of a simulation, such as Density 1.1, which automatically uses the latest maintenance version. As maintenance release of the simulations are published, your wrapper will automatically use the newer versions. Maintenance releases are backward compatible and your wrapper will not require any changes. When PhET-iO publishes a newer MAJOR or MINOR version, it could entail feature changes, new features and a potentially different API which may not be compatible with all wrappers. In that case, you will need to opt-in to the new version by specifying it in the wrapper, update the wrapper code to be compatible, and thoroughly test.


FAQ

Q: What is the easiest way to customize a simulation?
A: Studio can be used to customize the simulation in two ways: (a) interacting with the simulation and (b) selecting PhET-iO Elements and configuring them with their control panel. After setting up the simulation in the desired state, you can generate an HTML file that will launch the simulation in the customized state.

Q: Which simulations are instrumented for PhET-iO?
A: Not all PhET-iO simulations are instrumented at the moment, but we are working to make every PhET simulation available in PhET-iO. When you receive a license, you will also receive links to the PhET-iO simulations. Here is a current list of simulations that have some level of PhET-iO support: PhET-iO Simulations Status .

Q: I'm using a much older PhET-iO simulation that doesn't match the patterns in this developer's guide. What should I do?
A: Documentation for the prior generation is available at the Legacy Developer's Guide

Q: Some of my customizations are undone when the user presses the "Reset All" button. What should I do?
A: In some cases, it is appropriate and simplest to just remove the "Reset All" button by making it invisible. In other cases, it is best to create a function that applies the customization and to call it both during startup and when the "Reset All" button is pressed.

What Will You Build?

We're excited to see what powerful learning experiences you will create with PhET-iO. Contact us today to get started!