Temporal Interaction

Interactivity in time series visualization

Code, Recording

  1. Temporal data often have high information density, making interactivity worthwhile. In these notes, we’ll review some of the general principles of temporal interactivity discussed in the reading before illustrating with a few examples.

  2. It is often useful to think about interactive visualization in terms of Norman’s “gulfs.” The gulf of interaction refers to the mental effort required to go from a visual query (in our heads) to a physical interaction (on the screen). The gulf of evaluation refers to the reader’s attempt to understand what has happened when the interface changes. Users need to be able to traverse both gulfs easily. If there is too much difficulty in either defining the query or making sense of the result, the interaction will be ineffective.

  3. One of the most useful types of interactivity in time series visualization is the overview + detail technique. In this approach, the reader is first presented with an overview of the full time series. They can then interactively query for details depending on their interest. The overview + detail technique is often referred to as Shneiderman’s mantra: “Overview first, then details on demand.”

  4. Tooltips can be viewed as implementing the overview + detail principle. Let’s see how we can implement a tooltip for the electricity time series example from the previous notes. We will use a Voronoi mouseover, so that it’s not necessary to hover exactly over each path (which can be quite narrow). We define the Voronoi neighborhoods using d3.Delaunay together with the x and y scales, just like in our earlier scatterplot notes. One subtlety here is that we had to reshape the data structure containing all the timepoints, because d3.Delaunay only knows how to work with ordinary arrays (not arrays of arrays, like how our original temporal data are stored).

    function add_voronoi(flat_data, scales) {
      let delaunay = d3.Delaunay.from(flat_data, d => scales.x(d.time_of_day), d => scales.y(d.Demand));
      d3.select("svg").on("mousemove", (ev) => update_series(ev, flat_data, delaunay, scales))
    }
    
  5. In update_series(), we use delaunay.find() to look up the date of the closest time series so that it can be highlighted. The first block updates the stroke color and width. The second block replaces the previous tooltip text and positions it at the closest measurement location. Note that the index refers to the flattened array, not the original array of arrays.

    function update_series(ev, flat_data, delaunay, scales) {
      let ix = delaunay.find(ev.pageX, ev.pageY);
      d3.select("#series")
        .selectAll("path")
        .attrs({
          stroke: e => e[0].Date_string == flat_data[ix].Date_string ? "red" : "#a8a8a8",
          "stroke-width": e => e[0].Date_string == flat_data[ix].Date_string ? "4px" : "1px"
        })
    
      d3.select(ev.target).raise()
      d3.select("#tooltip text")
        .attr("transform", `translate(${scales.x(flat_data[ix].time_of_day) + 5}, ${scales.y(data[ix].Demand) - 5})`)
        .text(data[ix].Date_string)
    }
    
  6. Here is another example of the overview + detail principle. Below, we’ve built an collapsable version of the previous notes’ Gantt chart. The idea is to reduce multiple events into a larger group. For example, we could use this to collapse many short subtasks (the detail) in a project into one larger, longer-term task (the overview). We’ll implement a simple version of this that collapses all events in the chart. The main idea is to define a variable, called collapsed below, that stores whether or not the chart is currently collapsed. Whenever a button is clicked, we trigger an event that reverses the value of that variable. We then either expand or overlap the events.

    function toggle_collapse(scales) {
      if (collapsed) {
        collapsed = false;
        uncollapse(scales);
      } else {
        collapsed = true;
        collapse();
      }
    }
    
  7. To implement the collapse, we have to select all the rectangles and then change their y coordinate attribute. We similarly move the group element containing the x-axis to the top of the plot and clear the tooltip. The uncollapse() function simply reverses this operation.

    function collapse() {
      d3.select("#rects")
        .selectAll("rect")
        .transition()
        .duration(1000)
        .attr("y", 0)
    
      d3.select("#x_axis")
        .transition()
        .duration(1000)
        .attr("transform", "translate(0, 8)")
    
      d3.select("#tooltip").select("text").text("")
    }
    
  8. Another form of overview + detail can be implement by linked brushing. We can brush to focus in on specific time windows of interest, while never losing the context of the overall time series shape. This is implemented in the following example (due to Mike Bostock). The main idea is to update the domain of the upper (focus) time series’ x-axis scale based on the currently brushed time window from the lower (context) series. This is the one-dimensional analog of the approach used in the scatterplot zooming example in our earlier notes.

  9. It is also common to use linked brushing to define queries based on attributes of the time series. This was used in our earlier bike sharing example, where we linked daily bike demand with the day’s weather.

  10. Finally, we can query series based on summaries of each series. For example, we could have queried the time series based on their overall trend. The histogram below gives the slopes across series in the Opioid Atlas dataset. Hovering over points on the left hand histogram highlights countries with the largest changes over time.

    Indeed, slope is just one statistic that can be used to navigate a collection of time series. There is a small literature on summary statistics for time series. feasts is a useful R package for extracting these kinds of statistics.