An introduction to details-on-demand.
Reading, Recording, Notebook, Rmarkdown
{/* Define behavior of hover and click selections */
const hover = vl.selectSingle()
.on('mouseover') // select on mouseover
.nearest(true) // select nearest point to mouse cursor
.empty('none'); // empty selection should match nothing
const click = vl.selectMulti()
.empty('none'); // empty selection matches no points
/* Define encodings for full and filtered data */
const all_points = vl.markCircle().encode(
.x().fieldQ('Rotten_Tomatoes_Rating'),
vl.y().fieldQ('IMDB_Rating')
vl
)
const filter = vl.filter(vl.or(hover, click)),
= all_points.transform(filter);
subset_points
// mark properties for new layers
const halo = {size: 100, stroke: 'firebrick', strokeWidth: 3},
= {dx: 4, dy: -8, align: 'right'},
label = {stroke: 'white', strokeWidth: 2};
white
// layer scatter plot points, halo annotations, and title labels
return vl.data(movies)
.layer(
.select(hover, click),
all_points.markPoint(halo),
subset_points.markText(label, white).encode(vl.text().fieldN('Title')),
subset_points.markText(label).encode(vl.text().fieldN('Title'))
subset_points
).width(600)
.height(450)
.render();
}
robservable("@krisrs1128/week-3-3", include = 4, height = 480)
This kind of labeling is a good middle ground between overloading the user with all details (putting all labels in the plot) and hiding the information completely (no labels). This is one of the benefits of interactive visualization — we can adapt the complexity of a visualization based on user inputs.
Aside: Notice that we didn’t need to directly hover over a point in order for the label to be revealed. Behind the scenes, a the nearest-neighbor (Voronoi) tessellation has been computed, and the labels change as soon as you transition from one cell to another. This article provides a detailed description, if you are curious (we won’t refer to this again, though).
{const selector = vl.selectInterval().encodings('x');
const x = vl.x().fieldT('date').title(null);
// reusable time series encoding
const base = vl.markArea()
.encode(x, vl.y().fieldQ('price'))
.width(700);
const overview = base.select(selector).height(60);
const detail = base.encode(
.scale({domain: selector})
x;
)
// return layered view
return vl.vconcat(detail, overview)
.data(sp500)
.render();
}
robservable("@krisrs1128/week-3-3", include = 5, height = 475)
The visualization is implemented by re-using the same base visual encoding (markArea
time series) and (a) binding a selector to a smaller (60 pixel high) full time series and (b) adapting the scale of the larger time series, so that it only displays years within the current selection interval.
Finally, we note that dynamic queries can be chained together in a sequence. They allow the user to navigate between static visualizations that display information across subsets and at different resolution.
These visualizations are more complex to implement, but it’s worth sharing an example. One of the earliest examples of details on demand was the film finder app.
It let’s you transition between an overview of all movies and details about selected ones. The key is that the transition is effortless — we can focus in on a few movies without losing our bearings in the broader context.
You can watch a whole (delightfully retro) video about this visualization here.
Visualization people really like abstract compound words↩︎