packages/core/src/features/viz/lib/axis-title.tsx
This is the source snapshot used to build these API details. View this revision on GitHub.
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 version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision 2dccb19. ALPHA — NOT FOR PRODUCTION USE.