Skip to content

Renderers

A renderer draws one mark. The package comes with two: shapesRenderer, the default, with five shapes, and dotsRenderer, where every mark is a dot and shape carries nothing. Pass the same renderer to the legends, so their swatches match the marks.

Running the code below: built-in-renderers.tsx
built-in-renderers.tsx
import { useMemo, useState } from "react";
import {
dotsRenderer,
FingerprintCanvas,
FingerprintLegend,
shapesRenderer,
useFingerprint,
} from "@data-profile/fingerprint/react";
import "@data-profile/fingerprint/styles.css";
import { dimensions, library } from "./library";
const renderers = { shapes: shapesRenderer, dots: dotsRenderer };
export default function BuiltInRenderers() {
const [name, setName] = useState<keyof typeof renderers>("shapes");
const renderer = renderers[name];
const items = useMemo(() => library(200), []);
const fp = useFingerprint({
items,
dimensions,
// Colour by genre, shape by reading state.
views: [{ id: "v", label: "Genre and state", encoding: { arrange: "genre", shape: "state" } }],
});
return (
<div>
<div role="group" aria-label="Renderer" className="demo-toolbar">
{(["shapes", "dots"] as const).map((option) => (
<button key={option} aria-pressed={name === option} onClick={() => setName(option)}>
{option}Renderer
</button>
))}
</div>
<div className="demo-split">
<FingerprintCanvas {...fp.canvasProps} title="A library" renderer={renderer} />
<div>
{fp.layout.legends.map((group) => (
<FingerprintLegend
key={group.dimensionId}
{...fp.legendProps(group)}
renderer={renderer}
/>
))}
</div>
</div>
</div>
);
}

A renderer’s mark draws one mark, centred on the origin, node.size across, with the ridge running along the x axis; the canvas positions and rotates it. Each mark arrives with its properties resolved:

node. Holds
color The colour dimension’s colour, or currentColor when nothing drives colour
shape The index of its category in the shape dimension (0, 1, 2, …); 0 when nothing drives shape; null when unknown
lightness 0 (faintest) to 1 along the lightness dimension, or null. markOpacity(node) turns it into an opacity
categories Its category in every encoded dimension, by dimension id
size, weight, itemIds Its footprint, summed weight and the items it stands for

Here every book is a spine, and the reading state is a band’s position along it:

Running the code below: custom-renderer.tsx
custom-renderer.tsx
import { useMemo } from "react";
import { markOpacity } from "@data-profile/fingerprint";
import {
FingerprintCanvas,
FingerprintLegend,
useFingerprint,
type FingerprintRenderer,
} from "@data-profile/fingerprint/react";
import "@data-profile/fingerprint/styles.css";
import { dimensions, library } from "./library";
// A book spine: a rounded bar along the ridge, with a band whose position shows the reading
// state. The mark is drawn centred on the origin, `node.size` across; the canvas positions and
// rotates it. A renderer must depend on its props alone, since marks are memoised.
const spines: FingerprintRenderer = {
name: "spines",
mark: ({ node }) => {
const length = node.size;
const width = node.size * 0.42;
const band = node.shape === null ? null : (node.shape - 1) * length * 0.28;
return (
<g opacity={markOpacity(node)}>
<rect
x={-length / 2}
y={-width / 2}
width={length}
height={width}
rx={width * 0.25}
fill={node.color}
/>
{band !== null && (
<rect
x={band - length * 0.06}
y={-width / 2}
width={length * 0.12}
height={width}
fill="white"
opacity={0.75}
/>
)}
</g>
);
},
};
export default function CustomRenderer() {
const items = useMemo(() => library(260), []);
const fp = useFingerprint({
items,
dimensions,
views: [
{
id: "v",
label: "Combined",
encoding: { arrange: "genre", shape: "state", lightness: "decade" },
},
],
});
return (
<div className="demo-split">
<FingerprintCanvas {...fp.canvasProps} title="A library of spines" renderer={spines} />
<div>
{fp.layout.legends.map((group) => (
<FingerprintLegend key={group.dimensionId} {...fp.legendProps(group)} renderer={spines} />
))}
</div>
</div>
);
}

A renderer can also return SVG defs once per drawing (gradients, patterns), receiving an idPrefix to keep its ids unique on the page. Marks are memoised, so a renderer must depend on its props alone.