Skip to content

packages/core/src/features/viz/lib/axis-title.tsx

Read as Markdown

This is the source snapshot used to build these API details. View this revision on GitHub.

Back to reference

1 import { Group, Text, resolveSignalValue, type SignalValue } from "@pibbl/core";
2 import type { SignalStyle } from "@pibbl/core/internal";
3 import type { AxisProps, AxisStyle, ScaleId } from "./types.js";
4 import { lookupScale } from "./scale-context.js";
5 import { opacity } from "./paint.js";
6 /** Scale-aligned axis text; callers reserve the margin outside the plot. See {@link AxisTitle}. */
7 export interface AxisTitleProps {
8   /** Ancestor scale whose range supplies the title midpoint. */
9   readonly scale: ScaleId;
10   /** Plot edge; left/right titles rotate along the axis. */
11   readonly position: AxisProps["position"];
12   /** Single-line title. */
13   readonly label: SignalValue<string>;
14   /** Optional unit appended in parentheses; empty string omits it. */
15   readonly unit?: SignalValue<string>;
16   /** Nonnegative finite distance outside the plot edge, default 40 pixels. */
17   readonly offset?: SignalValue<number>;
18   /** Font defaults to 12px sans-serif, fill to #617783, opacity to 1. */
19   readonly style?: SignalValue<SignalStyle<Pick<AxisStyle, "font" | "fill" | "opacity">>>;
20 }
21 /** Paint an axis title with optional units, without allocating margins or changing ticks.
22  * @param props - Scale geometry and presentation. See {@link AxisTitleProps}.
23  * @returns Decorative chart content in the caller's coordinate space.
24  */
25 export function AxisTitle(props: AxisTitleProps) {
26   if (!["top", "bottom", "left", "right"].includes(props.position)) throw new TypeError("AxisTitle position must be top, bottom, left, or right.");
27   const source = lookupScale(props.scale, "AxisTitle");
28   if (!source) throw new TypeError("AxisTitle requires a scale ID.");
29   const scale = source.get(), label = resolveSignalValue(props.label), unit = resolveSignalValue(props.unit) ?? "";
30   if ([label, unit].some(value => typeof value !== "string" || /[\r\n]/.test(value))) throw new TypeError("AxisTitle label and unit must be single-line strings.");
31   const offset = resolveSignalValue(props.offset) ?? 40;
32   if (typeof offset !== "number" || !Number.isFinite(offset) || offset < 0) throw new RangeError("AxisTitle offset must be finite and nonnegative.");
33   const horizontal = props.position === "top" || props.position === "bottom";
34   const midpoint = scale.range[0] / 2 + scale.range[1] / 2;
35   const x = horizontal ? midpoint : props.position === "left" ? -offset : scale.frame.width + offset;
36   const y = horizontal ? props.position === "top" ? -offset : scale.frame.height + offset : midpoint;
37   const style = resolveSignalValue(props.style) ?? {};
38   return <Group style={{ translateX: x, translateY: y }}><Group style={{ rotationDegrees: horizontal ? 0 : props.position === "left" ? -90 : 90 }}>
39     <Text pointerEvents="none" style={{ textAlign: "center", textBaseline: "middle", font: resolveSignalValue(style.font) ?? "12px sans-serif", fill: resolveSignalValue(style.fill) ?? "#617783", opacity: opacity(resolveSignalValue(style.opacity) ?? 1, "AxisTitle") }}>{unit ? `${label} (${unit})` : label}</Text>
40   </Group></Group>;
41 }
42 

Documentation built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.