npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@flourish/facets

v4.2.0

Published

Create small-multiple grid layouts

Downloads

178

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:

  1. 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).

  1. 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 container width and either: a) the overall grid height, b) the target facet_height in pixels or, c) the target facet_aspect. Finally call the module's update 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
}