← Files DataARCHIVED FILE

templates/data-app/base/src/charting/TemporalXAxis.jsx

4.24 KB · Sep 30, 2026 · 23:19 UTC

↓ Download file

import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { XAxis, useChartWidth, usePlotArea } from "recharts";
import { temporalAxisLayout } from "./chart-data-shape.js";
import { tick } from "./chart-theme.js";

export function TemporalAxisTick({ x, y, payload, formatLabel, onMeasure }) {
  const textRef = useRef(null);
  useLayoutEffect(() => {
    const width = textRef.current?.getComputedTextLength?.();
    if (Number.isFinite(width) && width > 0) onMeasure?.(payload.value,width);
  });
  return <text ref={textRef} x={x} y={y + 11} fill="var(--secondary)" textAnchor="middle" fontSize={12}
    data-temporal-axis-tick data-axis-value={payload.value}>{formatLabel(payload.value)}</text>;
}

export function TemporalXAxis({ values, banded = false, continuous = false, ...props }) {
  const plot = usePlotArea();
  const chartWidth = useChartWidth() ?? 0;
  const probeRef = useRef(null), measuredSignature = useRef("");
  const [widths,setWidths] = useState({});
  const textMeasure = useRef(null);
  const valuesKey = JSON.stringify(values);
  const includeYear = new Set(values.map(value => String(value).slice(0,4))).size > 1;
  const times = continuous ? values.map(value => Date.parse(value)) : [];
  const start = Math.min(...times), end = Math.max(...times);
  const includeTime = continuous && (end > start && end - start < 86_400_000 || times.some(time => time % 86_400_000));
  const timeFormat = useMemo(() => includeTime ? new Intl.DateTimeFormat("en-US", {
    month:"short",day:"numeric",...(includeYear ? {year:"numeric"} : {}),
    hour:"2-digit",minute:"2-digit",...(end-start < 60_000 ? {second:"2-digit",fractionalSecondDigits:3} : {}),
    hourCycle:"h23",timeZone:"UTC",
  }) : null, [includeTime,includeYear,end-start < 60_000]);
  const formatLabel = value => continuous
    ? timeFormat ? timeFormat.format(new Date(value)) : tick(new Date(value).toISOString(),{includeYear})
    : tick(value,{includeYear});
  const recordWidth = (value,width) => setWidths(current => width > (current[value] ?? 0) + .5
    ? {...current,[value]:width} : current);
  const measure = (force = false) => {
    const probe = probeRef.current, view = probe?.ownerDocument?.defaultView;
    if (!view) return;
    const style = view.getComputedStyle(probe);
    const font = `${style.fontStyle} ${style.fontWeight} ${style.fontSize} ${style.fontFamily}`;
    const signature = `${valuesKey}|${font}|${style.letterSpacing}`;
    if (!force && signature === measuredSignature.current) return;
    const context = probe.ownerDocument.createElement("canvas").getContext("2d");
    if (!context) return;
    context.font = font;
    const spacing = Number.parseFloat(style.letterSpacing) || 0;
    textMeasure.current = label => context.measureText(label).width + Math.max(0,label.length-1)*spacing;
    measuredSignature.current = signature;
    setWidths({});
  };
  // Read the actual inherited SVG font, including theme changes, before paint.
  useLayoutEffect(() => { measure(); });
  useEffect(() => {
    const fonts = probeRef.current?.ownerDocument?.fonts;
    let active = true;
    const update = () => { if (active) measure(true); };
    fonts?.ready?.then(update);
    fonts?.addEventListener?.("loadingdone",update);
    return () => { active=false; fonts?.removeEventListener?.("loadingdone",update); };
  }, [valuesKey]);
  const layout = temporalAxisLayout(values,{
    plotWidth:plot?.width ?? chartWidth,
    leftRoom:plot?.x ?? 0,
    rightRoom:plot ? Math.max(0,chartWidth-plot.x-plot.width) : 0,
    banded, continuous,
    measureLabel:value => widths[value] ?? textMeasure.current?.(String(formatLabel(value))) ?? String(formatLabel(value)).length*8,
  });
  return <>
    <text ref={probeRef} visibility="hidden" aria-hidden="true" pointerEvents="none" fontSize={12}
      data-temporal-axis-start={values[0]} data-temporal-axis-end={values.at(-1)}
      data-temporal-axis-scale={continuous ? "time" : "bucket"}>M</text>
    <XAxis {...props} ticks={layout.ticks} padding={layout.padding} interval={0} minTickGap={0}
      {...(continuous ? {type:"number",scale:"linear",domain:start === end ? [start-43_200_000,end+43_200_000] : [start,end]} : {})}
      tick={props.tick === false ? false : <TemporalAxisTick formatLabel={formatLabel} onMeasure={recordWidth} />} />
  </>;
}

SHA-256: ef2ba3b476b1887416ede87fb577450ec0d54524b54c3cec3e2dbbc659c752d6