@flourish/facets
v4.2.0
Published
Create small-multiple grid layouts
Downloads
124
Maintainers
Keywords
Readme
Flourish facets module
A module for creating "small multiples" or "facetted" visualizations. Creates a configurable responsive grid layout in a given SVG. Each "facet" (each cell in the grid) gets a title and a container for the visualization itself, and the module provides editor settings for grid layout and title formatting.
As described below, this module is often used in conjunction with @flourish/chart-layout (or @flourish/axes) to add axes to each mini facet visualization, but you could also use it in other ways, e.g. to lay out small multiple maps.
Summary
The module creates and positions a group (g
) element for each current facet, which contains a text element for the title and a child group, known as the facet's primary
group. For simple use cases, place your visualization elements (data marks, axes, etc) into this group.
In some cases, however, you might prefer your visualization elements to be in a separate layer from the facet groups themselves. For example, putting the dots of a scatter plot in a separate layout enables animating points between different mini charts in the grid. The module supports this approach too by providing the positional information of each facet, so you can draw your data points in a separate layer from the facet groups and simply offset each one so that it aligns with the relevant facet.
In addition to adding facets, the module also handles removing unused ones, and animating facets into new positions when required.
Installation
1. Install the module
npm install -s @flourish/facets
2. Add to state
First, add an object to your template state to hold the module's state, e.g.:
export var state = {
facets: {},
...
}
3. Initialize the module
Initialize the module (only once) outside any function:
import initFacets from "@flourish/facets";
var facets = initFacets(state.facets);
4. Append to an SVG group
Next, in the draw
function, append the facets grid to an SVG group element in your visualization. When setting up a new template with facets, it can be useful to enable debug
mode so you can see the facet grid.
facets.appendTo(my_svg_g_element).debug(true);
5. Import the settings
Although not required, in most cases you would want to import the settings into your template.yml
settings, e.g.:
- Grid of charts
- property: facets
import: "@flourish/facets"
6. Add a data binding
Although not required, it is common for the facets grid to be driven by a data binding (typically pointing at a categorical column, with each unique value in the column becoming a facet). For example:
- dataset: data
key: facet
name: Grid of charts
type: column
optional: true
column: 'Data::F'
Typical usage
You can use the module however you like, given the API below. But the typical flow is:
- At the beginning of your template's update function, during any data processing step, compute which facets are needed – e.g. by identifying the unique values in the column bound to the facets data binding. Create an array of facet data where each facet is represented by an object containing the facet name and data e.g.
// facet_data
[
{ name: "A", data_points: [...] },
{ name: "B", data_points: [...] },
...
]
Tip: If the template allows the user to turn facets on and off (e.g. by setting and unsetting a facets binding), it's usually best to treat the non-faceted version as a facet grid with only one cell. (When you have only one facet to display, if you pass the name of this "facet" as an empty string, or any string set using the hideTitle
method, the title will be hidden, so it looks the same as an unfacetted chart).
- Inside the template's
update
function, pass the facet data and an accessor function (which returns the facet name) to the initialized facets instance. Also pass in other required properties such as the containerwidth
and either: a) the overall gridheight
, b) the targetfacet_height
in pixels or, c) the targetfacet_aspect
. Finally call the module'supdate
function, passing in a callback to operate on each facet. For example, it might look something like:
function update()
facets
.data(facet_data, d => d.name)
.width(container_width)
.height(container_height)
.update(function(facet, index) {
// Here we can position the data points as circles
var circles = d3.select(facet.node).selectAll("circle").data(facet.data.data_points);
var enter = circles.enter().append("circle");
circles.merge(enter)
.x(function(d) { return myXScale(data_point.x); ))
.y(function(d) { return myYScale(data_point.y); ));
});
You can also pass an array of facet names to .data
in which case you don't need to provide an accessor function.
Combing with other modules
In core Flourish templates this module is typically used with @flourish/layout
, which adds chart-levels titles, footers and themes. A simple use (not using Flourish.setHight
) might be something like:
function update() {
layout.update() // updates the layout module
facets
.width(layout.getPrimaryWidth())
.height(layout.getPrimaryHeight())
.data(facet_data, accessor)
.update(function(facet) {
// Here we update each facet
});
}
A more complex case using setHeight
might be something like this:
function update() {
layout.update() // updates the layout module
facets
.width(layout.getPrimaryWidth())
.facetAspect(1) // For square facets
.data(facet_data, accessor)
.update(function(facet) {
const grid_height = facets.height(); // Gets the computed height of the grid
layout.setHeight(grid_height); // Convenient wrapper for Flourish.update
// Here we update each facet
});
For charts with axes, this module is often used with @flourish/chart-layout
(@flourish/radial-axis
) or @flourish/axes
. One way of doing this is to attach a new chart layout (or axes) instance once to each new facet. You could do this by maintaining a list of instances…
const axes_instances = {};
const chart_layout_props = { x: state.x, y: state.y, background: state.chart_bg };
function update() {
function updateFacets() {
facets
.data(facet_data, accessor)
.update(function(facet) {
axes_instances[facet.name] = axes_instances[facet.name] || initChartLayout(facet.node, chart_layout_props);
axes_instances[facet.name]
.width(facet.width)
.height(facet.height)
.xData(facet.data, d => d.x)
.yData(facet.data, d => d.y)
.update();
});
}
}
… or perhaps by attaching the instance directly to the facet's node:
const chart_layout_props = { x: state.x, y: state.y, background: state.chart_bg };
facets
.width(layout.getPrimaryWidth())
.height(layout.getPrimaryHeight())
.data(facet_data, accessor)
.update(function(facet) {
if (!facet.node.__chart_layout) facet.node.__chart_layout = initChartLayout(facet.node, chart_layout_props);
facet.node.__chart_layout
.width(facet.width)
.height(facet.height)
.xData(facet.data, d => d.x)
.yData(facet.data, d => d.x)
.update()
});
API reference
The facets
object has a number of methods:
Getter/setters
facets.height(number)
When called with number
, sets the overall height of the facet grid in pixels (clearing any value passed in by facets.facetHeight
or facets.facetAspect
) and returns the instance. When called without an argument returns the computed height of the grid.
Setters
facets.appendTo(container)
Appends the SVG group element containing the facet groups to the specified container
(which should be an SVG group element), and returns the instance.
facets.data(data, accessor)
Sets the facets in the grid and returns the instance. data
should be an array of strings containing the names of the facets. Alternatively, data
can be a richer array, in which case also provide an accessor
function that takes each entry as an argument and returns the name.
facets.width(number)
Sets the overall width of the facet grid, and returns the instance.
facets.facetHeight(number)
When called with number
, sets the height of each facet in the grid in pixels (clearing any value passed in by facets.height
or facets.facetAspect
) and returns the instance. The specified height excludes the facet header, if present.
facetAspect(number, padding)
When called with number
, sets the aspect ratio of each facet in the grid (clearing any value passed in by facets.height
or facets.facetAspect
) and returns the instance. The aspect applies to the main part of the facet (i.e. excluding the header, if present) and should be in the format where 1 is square. The optional padding
object, if provided, can contain any combination of top
, right
, left
and right
properties. These will be taken account of when setting the aspect ratio - useful for example to set a square plot with uneven margins around the edge for the axes.
facets.duration(number)
Sets the duration, in milliseconds, of the animations in the update function, and returns the instance.
facets.autoTitleAlign(callback)
Sets how the titles should be aligned when the relevant setting is set to “Auto”, and returns the instance. callback
is a function that should return "left", "center" or "right".
facets.titleColor(function)
By default facet titles are all the same and can be specified in the settings. This methods allows you to change that by passing in a function
that takes the facet name as an argument and returns a color. A typical use case is to pass in the getColor
function from an instance of @flourish/colors
.
facets.axisSpaceTop(number)
, facets.axisSpaceBottom(number)
, facets.axisSpaceLeft(number)
, facets.axisSpaceRight(number)
Creates optional blank space around the grid, typically used to house axes that area shared across whole rows/columns of facets, and returns the instance. number
is the size in pixels. The margins default to zero.
facets.hideTitle(string)
Sets the string
to be a special facet title that – if it's the only title in the grid – hides the title. Useful when the module is associated with a optional facet binding, in which case when the binding is unset you may want to treat all the data as a single facet and give that a special name that you don't want to display. Alternatively you can name that facet as an empty string and it will have the same effect.
facets.readDirection("ltr" or "rtl")
This method allows you to override the facet title text read direction. Otherwise, this is automatically inferred from the document body read direction.
facets.maxFacets(number)
Sets a cap on the number of facets the module should render.
facets.debug(debug)
If debug
is truthy, adds fills to the facets to make it possible to see the grid, and returns the instance.
facets.update(callback)
Renders/updates the facet grid based on the current state, and returns the instance. The callback, if provided, is called once for each facet with two arguments: the facet object (see below) and the facet index.
Getters
facets.numColumns()
Returns the number of columns in the grid.
facets.numRows()
Returns the number of rows in the grid.
facets.facets()
Returns an array of objects representing each facet.
facets.facetWidth()
Returns the width of the facets in the grid.
facets.getFacet(name)
Returns an object representing the facet with the specified name
.
facets.isRagged()
Returns false
if the grid is "complete" (i.e. the last row has an cell for every column). Otherwise returns true
.
The facet object
When calling facets.update
, the callback function is called once for each facet, and receives as its first argument an object representing that facet. This object includes the following properties:
{
node // the facet's "primary" group, an SVG g element for placing/positioning visualization elements
data // the data you passed in to this facet
name // the display name
width // the "primary" group's width in pixels
height // the "primary" group's height in pixels
x // the "primary" group's left offset in pixels
y // the "primary" group's top offset in pixels
cx // the "primary" group's centre-x offset in pixels
cy // the "primary" group's centre-y offset in pixels
row // the index of the facet's row in the grid
column // the index of the facet's column in the grid
}