diff --git a/docs/features/marks.md b/docs/features/marks.md index 4d9cb90cd4..0f6bb67b15 100644 --- a/docs/features/marks.md +++ b/docs/features/marks.md @@ -547,6 +547,8 @@ All marks support the following [transform](./transforms.md) options: The **sort** option, when not specified as a channel value (such as a field name or an accessor function), can also be used to [impute ordinal scale domains](./scales.md#sort-mark-option). +The **render** option allows to override or extend the default mark’s [rendering](#rendering) method. + ### Insets Rect-like marks support insets: a positive inset moves the respective side in (towards the opposing side), whereas a negative inset moves the respective side out (away from the opposing side). Insets are specified in pixels using the following options: @@ -588,3 +590,140 @@ Plot.marks( ``` A convenience method for composing a mark from a series of other marks. Returns an array of marks that implements the *mark*.plot function. See the [box mark](../marks/box.md) implementation for an example. + +## Rendering + +A mark’s render method is called for each facet, unless its data for that facet is empty. The render method is responsible for drawing the mark by producing an SVG element. + +:::warning +We do not recommend using this low-level interface when a more high-level option exists such as a [data transform](https://observablehq.com/plot/features/transforms). It is meant for use by extension developers more than by users. +::: + +The mark’s render function is called with the following five arguments: + +* *index*: the index of the facet +* *scales*: the scale functions and descriptors +* *values*: the scaled and raw channels +* *dimensions*: the dimensions of the facet +* *context*: the context + +The function is expected to return a single SVG node, or null or undefined if no output is desired for the current facet. Typically, it returns a [G element](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g), with a child node (say, a [circle element](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/circle)) for each valid data point. + +You can extend or replace this method by specifying a render transform with the mark’s **render** option. The render transform will be called with the five arguments described above and a sixth argument: + +* *next*: the next render method in the chain + +The *index* is an array of indices in the channels, that represent the points to be drawn in the current facet. The *scales* object contains the scale functions, indexed by name, and an additional scales property with the scales descriptors, also indexed by name. + +For example, the following code will log the color associated with the Torgersen category ("#e15759") and the [instantiated color scale object](./plots.md#plot_scale), and will not render anything to the chart. + +```js +Plot.dot(penguins, { + x: "culmen_length_mm", + y: "culmen_depth_mm", + fill: "island", + render(index, scales) { + console.log(scales.color("Torgersen")); // "#e15759" + console.log(scales.scales.color); // {type: "ordinal", …} + } +}).plot() +``` + +The *values* object contains the scaled channels, indexed by name, and an additional channels property with the unscaled channels, also indexed by name. For example: + +```js +Plot.dot(penguins, { + x: "culmen_length_mm", + y: "culmen_depth_mm", + fx: "species", + fill: "island", + render(index, scales, values) { + const i = index[0]; + console.log(i, values.fill[i], values.channels.fill.value[i]); + } +}).plot() +``` + +will output the following three lines to the console, with each line containing the index of the first penguin of the current facet, its fill color, and the underlying (unscaled) category: + +```js +0 '#e15759' 'Torgersen' +152 '#f28e2c' 'Dream' +220 '#4e79a7' 'Biscoe' +``` + +The *dimensions* object contains the marginTop, marginRight, marginLeft,marginBottom, and width and height of the chart. For example, to draw an ellipse that extends to the edges: + +```js +Plot.plot({ + marks: [ + function (index, scales, values, dimensions, context) { + const e = context.document.createElementNS("http://www.w3.org/2000/svg", "ellipse"); + e.setAttribute("rx", (dimensions.width - dimensions.marginLeft - dimensions.marginRight) / 2); + e.setAttribute("ry", (dimensions.height - dimensions.marginTop - dimensions.marginBottom) / 2); + e.setAttribute("cx", (dimensions.width + dimensions.marginLeft - dimensions.marginRight) / 2); + e.setAttribute("cy", (dimensions.height + dimensions.marginTop - dimensions.marginBottom) / 2); + e.setAttribute("fill", "red"); + return e; + } + ] +}) +``` + +The *context* contains several useful globals: +* document - the [document object](https://developer.mozilla.org/en-US/docs/Web/API/Document) +* ownerSVGElement - the chart’s bare svg element +* className - the [class name](./plots.md#other-options) of the chart (*e.g.*, "plot-d6a7b5") +* projection - the [projection](./projections.md) stream, if any + +:::tip +When you write a plugin, using *context*.document allows your code to run in different contexts such as a server-side rendering environment. +::: + +Render transforms are called with a sixth argument, *next*, a function that can be called to continue the render chain. For example, if you wish to animate a mark to fade in, you can render it as usual, immediately set its opacity to 0, then bring it to life with D3: + +```js +Plot.dot(penguins, { + x: "culmen_length_mm", + y: "culmen_depth_mm", + fill: "island", + render(index, scales, values, dimensions, context, next) { + const g = next(index, scales, values, dimensions, context); + d3.select(g) + .selectAll("circle") + .style("opacity", 0) + .transition() + .delay(() => Math.random() * 5000) + .style("opacity", 1); + return g; + } +}).plot() +``` + +:::info +Note that Plot’s marks usually set the attributes of the nodes. As styles have precedence over attributes, the output can be customized with [CSS](https://developer.mozilla.org/en-US/docs/Web/CSS). +::: + +Here is another example, where we render the dots one by one: +```js +Plot.dot(penguins, { + x: "culmen_length_mm", + y: "culmen_depth_mm", + fill: "island", + render(index, scales, values, dimensions, context, next) { + let node = next(index, scales, values, dimensions, context); + let k = 0; + requestAnimationFrame(function draw() { + const newNode = next(index.slice(0, ++k), scales, values, dimensions, context); + node.replaceWith(newNode); + node = newNode; + if (node.isConnected && k < index.length) requestAnimationFrame(draw); + }); + return node; + } +}).plot() +``` + +:::info +A similar technique is used by Plot’s [pointer](../interactions/pointer.md) transform to render the point closest to the pointer. +:::