If you’re the author of an htmlwidget package and would like to add Crosstalk support to your widget, this document will help you get started.

Should you integrate with Crosstalk?

Before you begin, know that not all HTML widgets are good candidates for integration with Crosstalk. The htmlwidgets framework itself makes minimal assumptions, so as to encourage a diverse ecosystem of widgets. Crosstalk, on the other hand, is highly opinionated; it is designed to operate on certain kinds of data, with certain kinds of interactivity. Some widgets will be difficult or impossible to integrate with Crosstalk; other widgets might be easy to integrate but offer a poor overall user experience.

A widget is a good candidate for Crosstalk if it:

Operates on data frames, or data frame-like objects

Crosstalk works on a row-by-row basis. When you select a bunch of data points in Crosstalk, each data point is considered a row (or observation), and row IDs are broadcast to the rest of the Crosstalk group. If your data isn’t inherently tabular in nature, or at the very least a flat list of values, you’re going to suffer from impedance mismatch. (That doesn’t mean your widget’s visualization needs to be tabular, just that the underlying data should have a by-row structure to it.)

Examples of appropriate data: data frame, data.table, spatial objects (e.g. SpatialPolygonsDataFrame).

Examples of inappropriate data: raster images, text corpora, hierarchy/trees (lists of lists).

Displays individual rows/observations/data points

Crosstalk works best when each data point is displayed independently in a visualization. This makes it straightforward to highlight/fade or filter out arbitrary sets of points.

Visualizations that aggregate or summarize rows—for example, grouping rows by a categorical variable and showing the averages in a bar chart—will have a harder time. There’s no hope of this working correctly unless all of the row-level data is passed to the widget’s JavaScript binding, plus all of the parameters of how the aggregation was performed, plus the actual logic to perform said aggregation. Without all that information, there’s no way to partially highlight a bar based on a set of points that were brushed in a linked scatter plot, for example.

It’s not impossible, but I haven’t explored this area enough to make recommendations for how such operations should be expressed, and therefore suggest you steer clear for now. If you have an urgent scenario, feel free to contact me (joe@rstudio.com).

Supports highlighting/fading and hiding/showing of data points

Crosstalk widgets need to support highlighting/fading (or emphasizing/de-emphasizing through other means) of data points to support linked brushing, and they need to support hiding/showing to support filtering. Ideally, this will happen quickly, without disturbing other aspects of the widget (e.g. if the user has zoomed in on one part of the widget, or repositioned some nodes in a network graph, those aspects should not be affected by selecting or filtering).

Provides a gesture for selecting data points, and API for dismissing selection

Ideally, each Crosstalk widget would not only highlight/fade to reflect selections made on linked widgets, but also allow you to make a selection directly on the widget itself. Further, after a selection is made on your widget, Crosstalk may alert you to a selection made on linked widget, at which time you must programatically remove the selection box (or whatever your brush looks like) from your widget.

This is easy if the underlying JavaScript library has already provided these facilities for you, but if not, it can be quite tricky to implement. Unfortunately, doing the JavaScript UI programming necessary to support a nice selection experience is outside the scope of this article.

Modify your package

If you’ve read those criteria and are still with us, it’s time to modify your package to support Crosstalk!

  1. Modify your DESCRIPTION file’s Imports field to include crosstalk and htmltools.
  2. In your widget’s R function, modify the call to createWidget to include the argument dependencies = crosstalk::crosstalkLibs().
# Before

myWidget <- function(data, width = NULL, height = NULL) {
  # Some logic goes here...

  createWidget("myWidget", x, width=width, height=height)
}


# After

myWidget <- function(data, width = NULL, height = NULL) {
  # Some logic goes here...

  createWidget("myWidget", x, width=width, height=height,
    dependencies = crosstalk::crosstalkLibs()
  )
}

This ensures that Crosstalk’s JavaScript runtime dependencies are loaded at the appropriate time.

Now we’re ready to begin customizing your widget-specific logic to work with Crosstalk.

Modify the R binding

The R binding is the R function that constructs an instance of your widget (usually it is named after your widget). I’ll assume you already take a data frame as an argument.

Users signal their intention to use Crosstalk by passing your widget a SharedData instance instead of a regular data frame.

df <- read.csv("mydata.csv", stringsAsFactors=FALSE)
sd <- SharedData$new(df)

# No Crosstalk
myWidget(df)

# Crosstalk enabled
myWidget(sd)

When your R binding detects a SharedData instance, it will extract the data frame, key vector, and group name, and include them in the widget payload that is passed to createWidget.

Before:

myWidget <- function(data, width = NULL, height = NULL) {
  x <- list(
    data = data
  )

  createWidget("myWidget", x, width=width, height=height)
}

After:

myWidget <- function(data, width = NULL, height = NULL) {
  if (is.SharedData(data)) {
    # Using Crosstalk
    key <- data$key()
    group <- data$groupName()
    data <- data$origData()
  } else {
    # Not using Crosstalk
    key <- NULL
    group <- NULL
  }

  x <- list(
    data = data,
    settings = list(
      crosstalk_key = key,
      crosstalk_group = group
    )
  )

  createWidget("myWidget", x, width=width, height=height,
    dependencies = crosstalk::crosstalkLibs()
  )
}

Now in your JavaScript binding’s renderValue method, you can access x.settings.crosstalk_key and x.settings.crosstalk_group. To tell if Crosstalk is enabled for the current renderValue invocation, test one of these values for truthiness.

This example happens to pass the entire data frame to createWidget. That’s not necessary and in fact is probably not that common. Usually a subset of columns, possibly transformed, is all that’s included in the payload. That’s fine, as far as Crosstalk is concerned. All that matters is that you keep your data in sync with the key vector; if you subset or reorder the data, you must do the same to the key vector.

Modify the JavaScript binding

Now the more difficult part: actually implementing the linked interactions on the JavaScript side.

Currently, Crosstalk supports two types of inter-widget interaction: linked brushing, and filtering. By “supports” I mean that Crosstalk will help visualizations and filter controls communicate intentions to each other. But actually implementing the selection, highlighting, and filtering behaviors for each widget type is the job of the widget author.

The sections below will discuss the types of interactions that are expected from each Crosstalk-compatible widget, and how to use Crosstalk’s JavaScript APIs to communicate.

Linked brushing

Linked brushing lets the end user select data points in any visualization, to highlight the corresponding data points in all linked visualization.

At any given moment, only one visualization may have a selection; for example, if widget A has an active selection, and then the user begins making a selection on widget B, then widget A should immediately clear its selection.

Ideally, each visualization should be capable of the following:

  1. Allow the user to make a selection, usually through clicking on data points, or better, making a rectangular or lasso-shaped selection. (The result of the selection must be a subset of the rows in the dataset—that’s the only type of selection that is useful in Crosstalk.)
  2. Clear a selection interactively in response to a user gesture (i.e. clicking on an inert, unselected area of the visualization).
  3. Clear a selection programmatically, in response to another visualization (in the same Crosstalk group) starting a selection operation.
  4. Highlight points selected by another visualization. A common way to do this is to lower the opacity of data points that are not selected (and use the maximum opacity value for all data points when no selection is active in the group).

In my experience so far, I’ve found 4 to be reasonably straightforward to achieve. But 1-3 can be difficult if you’re building on top of a JavaScript visualization library that isn’t designed to accomodate interactively selecting data points. Though it’s far preferable to support all four of these features, it’s possible to only support 4 to create what is essentially a “listen-only” Crosstalk participant.

Selection JavaScript API

Each Crosstalk-enabled visualization instance (i.e. each call to renderValue) should create a new crosstalk.SelectionHandle instance. Use SelectionHandle to read and write (and listen for changes to) the selection state for a Crosstalk group.

Construction
var ct_sel = new crosstalk.SelectionHandle();

If you know the Crosstalk group name at construction time, you can pass it as an argument to the constructor. If not, you can set it (and re-set it) later:

ct_sel.setGroup(x.settings.crosstalk_group);
Events

Listen on the "change" event to be notified whenever the selection changes.

ct_sel.on("change", function(e) { ... });

The event handler will be called back with an object argument (e in the above example) that contains these properties:

  • value - Either falsy (meaning no selection is active) or a string array, where each element is the key of a selected data point/row. Any data point whose key does not appear in the string array should be considered deselected.
  • oldValue - The previous value of the selection. A convenience for widgets that care to calculate the difference between the old and new values.
  • sender - The SelectionHandle instance that made this change. Use === to compare to your own SelectionHandle to determine if the change is in response to an action you yourself took.

You can unregister a previously registered event handler using either the return value of on, or with the function/closure itself.

function myEventListener(e) { ... }
var subscription = ct_sel.on("change", myEventListener);

// These two lines are equivalent
ct_sel.off("change", subscription);
ct_sel.off("change", myEventListener);
Getting the selection

It may also be useful to get the selection value from places other than the change event handler. You can do this using the value property.

var selected_keys = ct_sel.value;
Setting the selection

And of course, we need the ability to modify the selection. The set method changes the value and automatically triggers any change event listeners.

ct_sel.set(["Alaska", "Rhode Island", "Wyoming"]);

You can also easily clear the selection.

ct_sel.clear();
Cleaning up

Close the handle to remove all event handlers and remove the handle from the current group.

ct_sel.close();

Filtering

Filtering causes a subset of data points to be shown, while all others are hidden.

With Crosstalk’s filtering API, multiple widgets simultaneously contribute to the current “filter set”. Crosstalk will determine which data points are permitted by all of the actively filtering widgets.

Earlier, we said that visualizations would ideally support both reading and writing of linked brushing state; that is, each plot should allow users to set the selection via direct manipulation, and also highlight points based on selections on other linked plots. While it’s possible to envision such functionality for filtering as well (see dc.js), it’s not currently recommended to do so with Crosstalk. Instead, the current design is geared toward distinct “filter controls” for input and “filtered visualizations” for output. Both types of controls will use the same crosstalk.FilterHandle class, but they will interact with it in different ways.

Filter JavaScript API

Construction, event handling, and setting of crosstalk.FilterHandle are almost identical to SelectionHandle:

// Make instance
var ct_filter = new crosstalk.FilterHandle();

// Choose group
ct_filter.setGroup(x.settings.crosstalk_group);

// Get notified when this group's filter changes
ct_filter.on("change", function(e) {
  // e.value gives the filter
});

// Set this handle's filter keys
ct_filter.set(["Washington", "Oregon", "Idaho"]);

// Clear this handle's filter
ct_filter.clear();

FilterHandle doesn’t have a value property. Instead, it has a filteredKeys property. This property doesn’t simply return the most recently set filter value in the group; rather, each filter handle maintains its own set of keys (or none) and filteredKeys returns the intersection of them all.

For example:

var filter1 = new crosstalk.FilterHandle("groupA");
var filter2 = new crosstalk.FilterHandle("groupA");
var filter3 = new crosstalk.FilterHandle("groupA");

filter1.set(["New York", "New Jersey", "Connecticut"]);
filter1.filteredKeys // ["New York", "New Jersey", "Connecticut"]

filter2.set(["New York", "Connecticut"]);
filter1.filteredKeys // ["New York", "Connecticut"];

filter3.set(["New Jersey"]);
filter3.filteredKeys // []

filter2.clear();
filter1.filteredKeys // ["New Jersey"]

Note that it doesn’t matter which handle you call filteredKeys on. All filter handles that belong to the same group will return the same value for filteredKeys.

Putting it together

Here’s what the htmlwidget binding code for d3scatter looks like, without Crosstalk support:

HTMLWidgets.widget({

  name: 'd3scatter',

  type: 'output',

  factory: function(el, width, height) {

    var firstRun = true;
    var scatter = d3scatter(el).width(width).height(height);

    return {
      renderValue: function(x) {
        var value = x.data;
        scatter
          .x_var(value.x_var)
          .y_var(value.y_var)
          .color_var(value.color_var)
          .color_spec(value.color_spec)
          .x_label(value.x_label)
          .y_label(value.y_label)
          .x_lim(value.x_lim)
          .y_lim(value.y_lim);

        scatter(!firstRun);
        firstRun = false;
      },
      resize: function(width, height) {
        scatter.width(width).height(height)(false);
      }
    };
  }
});

The d3scatter object is created at the scope of the factory function. Then, during renderValue, it’s updated with the value object.

The first step is to create a crosstalk.SelectionHandle object at the factory function level.

var sel_handle = new crosstalk.SelectionHandle();

We haven’t yet specified what group this handle should belong to. In fact, we won’t know the group until we receive a value via renderValue (and in some circumstances, renderValue might be called multiple times with different groups).

But we do have both scatter and sel_handle objects at this point, so we can wire them together. The d3scatter object has a "brush" event that we can use to update the Crosstalk selection handle, and the Crosstalk selection handle has a "change" event we can use to highlight the d3scatter data points appropriately.

scatter.on("brush", function(keys) {
  sel_handle.set(keys);
});

sel_handle.on("change", function(e) {
  if (e.sender !== sel_handle) {
    scatter.clearBrush();
  }
  scatter.selection(e.value);
});

With these relationships established inside of factory, we can now move on to renderValue. The only modifications we need are to pass the key data to the d3scatter object, and update the group of the sel_handle object.

The SelectionHandle change event provides a value property that indicates the currently selected keys as a string array; or, value can be null to indicate that no selection is active.

Notice the comparison e.sender !== sel_handle; this lets us distinguish between selection operations initiated by this widget instance versus by other instances, and to clear any active selection boundaries in the latter case.

The fully selection-enabled binding code is here:

HTMLWidgets.widget({

  name: 'd3scatter',

  type: 'output',

  factory: function(el, width, height) {

    var firstRun = true;
    var scatter = d3scatter(el).width(width).height(height);

    var sel_handle = new crosstalk.SelectionHandle();

    scatter.on("brush", function(keys) {
      sel_handle.set(keys);
    });

    sel_handle.on("change", function(e) {
      if (e.sender !== sel_handle) {
        scatter.clearBrush();
      }
      scatter.selection(e.value);
    });

    return {
      renderValue: function(x) {
        var value = x.data;
        scatter
          .x_var(value.x_var)
          .y_var(value.y_var)
          .color_var(value.color_var)
          .color_spec(value.color_spec)
          .x_label(value.x_label)
          .y_label(value.y_label)
          .x_lim(value.x_lim)
          .y_lim(value.y_lim)
          .key(x.settings.crosstalk_key);

        sel_handle.setGroup(x.settings.crosstalk_group);

        scatter(!firstRun);
        firstRun = false;
      },
      resize: function(width, height) {
        scatter.width(width).height(height)(false);
      }
    };
  }
});