Skip to content

davenquinn/d3-ternary

Repository files navigation

D3 Ternary Plot

npm version

d3-ternary is a JavaScript library and D3.js module that makes it easy to create ternary plots, its API exposes configurable functions in the manner of other D3 modules.

Ternary plots are a type of triangular diagram that depict components proportions in three-component systems. Each point in the triangle corresponds to a unique composition of those three components.

Try d3-ternary your browser, view the introductory notebook on Observable and see the 'Ternary Plots' notebook collection for examples. Or make ternary plots in the browser on TernaryPlot.com which is built using d3-ternary.

Example ternary plot

Installing

If you use npm

npm install d3-ternary

You can also download the latest release on GitHub. For vanilla JS in modern browsers, import d3-ternary from jsDelivr:

<script type="module">
  import {
    barycentric,
    ternaryPlot,
  } from "https://cdn.jsdelivr.net/npm/d3-ternary@3/+esm";

  const b = barycentric();
  const t = ternaryPlot(b);
</script>

API Reference

barycentric()

barycentric() <>

Constructs a new default ternary converter that converts ternary data to Cartesian coordinates. By default, it makes an equilateral triangle on the unit circle centered at the origin.

# barycentric(data) <>

Computes [x,y] coordinates from a ternary values (a single three-element array). Note that the [x, y] coordinates here are unscaled (radius of 1). All values are normalized by default.

# barycentric.invert(coordinates) <>

Computes ternary values from coordinates (a two-element array [x, y]). Note that the [x, y] coordinates here are unscaled i.e. a radius of 1.

# barycentric.a([a]) <>

If a is specified, sets the a-accessor to the specified function and returns this barycentric converter. If a is not specified, returns the current a-value accessor, which defaults to:

const a = (d) => d[0];

# barycentric.b([b]) <>

If b is specified, sets the b-accessor to the specified function and returns this barycentric converter. If b is not specified, returns the current b-value accessor, which defaults to:

const b = (d) => d[1];

# barycentric.c([c]) <>

If c is specified, sets the c-accessor to the specified function and returns this barycentric converter. If c is not specified, returns the current c-value accessor, which defaults to:

const c = (d) => d[2];

# barycentric.domains([domains]) <>

If domains is specified, sets the domains for each axis to the specified domains in order of [A, B, C] and returns this barycentric converter. Each domain should be a two-element array [min, max]. All domains must have equal lengths. This method allows you to create "partial" ternary plots that zoom in on a specific region of the full triangle. For example, setting domains to [[0.2, 0.4], [0.2, 0.4], [0.2, 0.4]] will show only the portion of the triangle where each component is between 20-40%.

If domains is not specified, returns the current domains for each axis.

// Create a zoomed ternary plot showing only values where
// each component is between 20% and 40%
barycentric.domains([
  [0.2, 0.4], // A axis
  [0.2, 0.4], // B axis
  [0.2, 0.4], // C axis
]);

# barycentric.scales() <>

Returns an array of the three d3.scaleLinear() scale functions used internally by the barycentric converter, in order of [A, B, C]. These scale functions map the input domains to normalized values between 0 and 1.

# barycentric.unscaled(data) <>

Similar to the standard conversion function, but bypasses the domain scaling. Takes a three-element array of ternary values and returns [x,y] coordinates on the unit circle. This is primarily used internally for plotting the triangle bounds and grid lines.

# barycentric.rotation([angle]) <>

If angle is specified, sets the rotation angle in degrees and returns this barycentric converter. If angle is not specified, returns the current rotation angle, which defaults to 0. Positive angles rotate clockwise.

ternaryPlot()

ternaryPlot(barycentric) <>

Constructs a new default ternary plot generator with the default options.

# ternaryPlot(data) <>

Computes [x, y] coordinates that are scaled by the plot radius from ternary data. Unlike the barycentric method, this method takes the plot radius into account.

# ternaryPlot.invert(coordinates) <>

Computes ternary values from [x, y] coordinates that are scaled by the radius. Unlike the barycentric.invert() method this method takes the plot radius into account. Note that for inverting mouse positions, the ternary plot should centered at the origin of the containing SVG element.

Configuration methods

# ternaryPlot.radius([radius]) <>

If radius is specified, sets the radius of the ternary plot to the specified number. If radius is not specified, returns the current radius, which defaults to 300 (px).

To set domains without these extra checks, use ternaryPlot.setDomains(domains).

Layout methods

# ternaryPlot.labels([labels]) <>

If labels is specified, sets the axis labels to the labels in order of [A, B, C] and returns the ternary plot. If labels is not specified, returns the current labels, which defaults to [[A, B, C]].

# ternaryPlot.labelAngles([angles]) <>

If angles is specified, sets the angles of the axis labels to the specified angles in order of [A, B, C] and returns the ternary plot. If angles is not specified, returns the current label angles, which defaults to [0, 60, -60].

# ternaryPlot.labelOffsets([offsets]) <>

If offsets is specified and is an array, sets the axis label offsets to the specified offsets in order of [A, B, C] and returns the ternary plot. If offsets is a number, sets all label offsets to that value. If offsets is not specified, returns the current label offsets, which defaults to [45, 45, 45] px.

# ternaryPlot.tickAngles([angles]) <>

If angles is specified, sets the angle of the ticks of each axis to the specified angles in order [A, B, C] and returns the ternary plot. If angles is not specified, returns the current tick angles, which defaults to [0, 60, -60].

# ternaryPlot.tickTextAnchors([textAnchors]) <>

If textAnchors is specified, sets the axis tick text-anchor to the specified text-anchors in order of [A, B, C] and returns the ternary plot. If textAnchors is not specified, returns the current tick text-anchors, which defaults to ["start", "start", "end"].

# ternaryPlot.tickSizes([sizes]) <>

If sizes is specified and is an array, sets the axis tick sizes to the specified tick sizes in order of [A, B, C] and returns the ternary plot. If sizes is a number, sets the tick sizes of all axes to sizes. If sizes is not specified, returns the current tick sizes, which defaults to [6, 6, 6] (px).

# ternaryPlot.tickFormat([format]) <>

If format is specified, sets the tick format. format can either be a format specifier string that is passed to d3.tickFormat(). To implement your own tick format function, pass a custom formatter function, for example const formatTick = (x) => String(x.toFixed(1)). If format is not specified, returns the current tick format, which defaults to "%".

Plot Methods

# ternaryPlot.gridLines([count]) <>

Generates and returns an array of arrays containing grid line coordinates for each axis. If count is not specified, it defaults to 10. count can be a number or an array of numbers, one for each axis in order of [A, B, C]. Each array contains count elements of two-element arrays with the start- and end coordinates of the grid line.

Grid lines are generated using d3.scaleLinear.ticks(). The specified count is only a hint; the scale may return more or fewer values depending on the domain.

# ternaryPlot.ticks([count]) <>

Generates and returns an array of tick objects for each axis. If count is not specified, it defaults to 10. count can be a number or an array of numbers, one for each axis in order of [A, B, C].

Each tick object contains:

  • tick: The formatted tick text
  • position: An array of [x,y] coordinates
  • angle: The tick rotation angle
  • textAnchor: The SVG text-anchor value
  • size: The length of the tick line

Ticks are generated using d3.scaleLinear.ticks(). The specified count is only a hint; the scale may return more or fewer values depending on the domain.

# ternaryPlot.axisLabels([options]) <>

Generates and returns an array containing axis label objects. Each axis label object contains:

  • position: An array of [x,y] coordinates
  • angle: The rotation angle of the label
  • label: The axis label text

Takes an optional configuration object:

{
  center: false; // If true, places labels at center of axes instead of vertices
}

# ternaryPlot.triangle() <>

Returns an SVG path command for a the outer triangle. This is used for the bounds of the ternary plot and its clipPath.

Transform Functions

# domainsFromTransform(transform) <>

Converts a transform into domain ranges for the ternary plot axes. This can be used to handle zooming and panning using d3-zoom. The transform object contains

  • k: The zoom scale factor (1 = no zoom, >1 = zoomed in)
  • x: The x-translation
  • y: The y-translation

Returns an array of [start, end] domain ranges for axes A, B, and C. For example:

const transform = { k: 1.4285, x: -0.3711, y: -0.2142; }
const domains = domainsFromTransform(transform);
// Returns :
// [
//   [0, 0.7],
//   [0, 0.7],
//   [0.3, 1],
// ]

Throws an error if the transform would create invalid domains (outside the [0,1] range) or if trying to zoom out beyond the original triangle.

# transformFromDomains(domains) <>

The inverse of domainsFromTransform - converts domain ranges into a d3.zoom transform. This is useful when you want to programmatically set the zoom/pan state to focus on specific domain ranges.

Takes an array of [start, end] domain ranges for axes A, B, and C. Returns a transform object with:

  • k: The zoom scale factor
  • x: The x-translation (unscaled by radius)
  • y: The y-translation (unscaled by radius)

Example usage:

// Zoom in to show only values where each component is between 20-70%
const partialDomains = [
  [0, 0.7],
  [0, 0.7],
  [0.3, 1],
];

const b = barycentric().domains(partialDomains);

const { x, y, k } = transformFromDomains(b.domains());

// We need to sync d3-zoom with the tranform of the partial domains
const initialTransform = d3.zoomIdentity
  .translate(x * radius, y * radius)
  .scale(k);

chart.call(zoom).call(zoom.transform, initialTransform);

Note that the translations returned are unscaled by the plot radius - they should be scaled by the radius before being used with SVG transforms.

Rendering Examples

For detailed examples of how to render ternary plots:

Acknowledgments

Several projects have served as a starting point for this module.

All authors are thanked.