Skip to content
design
Components

Chart range

Navigate a controlled numeric or time window with keyboard and touch controls.

On this page

Shared observation window

Illustrative data · relative sample coordinates

Navigate a controlled numeric or time window with keyboard and touch controls.
Code
import { useState, type ReactElement } from "react";
import { ChartRange, type ChartRangeValue } from "@bosunkit/components/chart-range";
import { LineChart } from "@bosunkit/components/line-chart";
import { ChartPanel } from "@bosunkit/components/chart-panel";
// Fixed illustrative display strings; consumers supply their quantity formatter.
export const description: string =
  "Two illustrative channels share a controlled viewport and synchronized inspection; the caller owns range state.";
const labels: Readonly<Record<number, string>> = {
  "0": "0",
  "1": "1",
  "2": "2",
  "3": "3",
  "4": "4",
  "5": "5",
  "6": "6",
  "7": "7",
  "8": "8",
  "9": "9",
  "10": "10",
  "11": "11",
  "12": "12",
  "13": "13",
  "14": "14",
  "15": "15",
  "16": "16",
  "17": "17",
  "18": "18",
  "19": "19",
  "20": "20",
  "21": "21",
  "22": "22",
  "23": "23",
  "24": "24",
  "25": "25",
  "26": "26",
  "27": "27",
  "28": "28",
  "29": "29",
  "30": "30",
  "31": "31",
  "32": "32",
  "33": "33",
  "34": "34",
  "35": "35",
  "36": "36",
  "37": "37",
  "38": "38",
  "39": "39",
  "40": "40",
  "41": "41",
  "42": "42",
  "43": "43",
  "44": "44",
  "45": "45",
  "46": "46",
  "47": "47",
  "48": "48",
  "49": "49",
  "50": "50",
  "51": "51",
  "52": "52",
  "53": "53",
  "54": "54",
  "55": "55",
  "56": "56",
  "57": "57",
  "58": "58",
  "59": "59",
  "60": "60",
  "61": "61",
  "62": "62",
  "63": "63",
  "64": "64",
  "65": "65",
  "66": "66",
  "67": "67",
  "68": "68",
  "69": "69",
  "70": "70",
  "71": "71",
  "72": "72",
  "73": "73",
  "74": "74",
  "75": "75",
  "76": "76",
  "77": "77",
  "78": "78",
  "79": "79",
  "80": "80",
  "81": "81",
  "82": "82",
  "83": "83",
  "84": "84",
  "85": "85",
  "86": "86",
  "87": "87",
  "88": "88",
  "89": "89",
  "90": "90",
  "91": "91",
  "92": "92",
  "93": "93",
  "94": "94",
  "95": "95",
  "96": "96",
  "97": "97",
  "98": "98",
  "99": "99",
  "100": "100",
};
const format: (value: string | number) => string = (value: string | number): string => labels[Number(value)] ?? "";
export default function SharedWindow(): ReactElement {
  const [range, setRange] = useState<ChartRangeValue>([0, 100]);
  const [following, setFollowing] = useState<boolean>(false);
  const data = Array.from({ length: 21 }, (_: unknown, index: number): { time: number; observed: number } => ({
    time: index * 5,
    observed: [8, 9, 10, 9, 8, 7, 6][index % 7] ?? 8,
  }));
  return (
    <ChartPanel
      title="Shared observation window"
      description="Illustrative data · relative sample coordinates"
      controls={
        <ChartRange
          value={range}
          onValueChange={setRange}
          bounds={[0, 100]}
          minSpan={10}
          label="Observation window"
          labels={["Earlier", "Later", "Zoom in", "Zoom out", "Follow live"]}
          following={following}
          onFollowingChange={(next: boolean): void => {
            setFollowing(next);
            if (next) setRange([50, 100]);
          }}
        />
      }
    >
      {["Primary", "Comparison"].map((name: string): ReactElement => (
        <LineChart
          key={name}
          data={data}
          categoryKey="time"
          categoryScale="linear"
          categoryDomain={range}
          onCategoryDomainChange={setRange}
          syncId="range-example"
          config={{ observed: { label: name, formatValue: format } }}
          series={["observed"]}
          formatCategory={format}
          formatAxisValue={(value: number): string => format(value)}
          label={`${name} observations`}
          categoryLabel="Coordinate"
          dataTableLabel={`View ${name.toLowerCase()} observations`}
          missingValueText="Unavailable"
        />
      ))}
    </ChartPanel>
  );
}

The caller owns the visible range, available-history bounds and follow-live state. Earlier and later pan by half a window; zoom changes the span around its center and preserves span at history edges. Every action emits a finite increasing range. Pass that range as categoryDomain on a numeric LineChart or AreaChart. Provide onCategoryDomainChange to allow drag-to-zoom and Shift-drag pan on the numeric plot. The buttons provide equivalent range navigation by keyboard and touch. Shared syncId values synchronize inspection by category, and syncTolerance bounds nearest-time matching. Application wiring fetches and validates the requested window, cancels superseded requests and exposes loading or failure beside the chart. The controls have no clock or network dependency.

01Preserve meaningObservation spacing, validity and comparison basis remain explicit.
02Caller display textLabels and formatted values come from the consumer.
03Inspect without hoverNamed controls and source values remain accessible with a keyboard.

Examples

Composition

Shared observation window

Illustrative data · relative sample coordinates

Two illustrative channels share a controlled viewport and synchronized inspection; the caller owns range state.
Code
import { useState, type ReactElement } from "react";
import { ChartRange, type ChartRangeValue } from "@bosunkit/components/chart-range";
import { LineChart } from "@bosunkit/components/line-chart";
import { ChartPanel } from "@bosunkit/components/chart-panel";
// Fixed illustrative display strings; consumers supply their quantity formatter.
export const description: string =
  "Two illustrative channels share a controlled viewport and synchronized inspection; the caller owns range state.";
const labels: Readonly<Record<number, string>> = {
  "0": "0",
  "1": "1",
  "2": "2",
  "3": "3",
  "4": "4",
  "5": "5",
  "6": "6",
  "7": "7",
  "8": "8",
  "9": "9",
  "10": "10",
  "11": "11",
  "12": "12",
  "13": "13",
  "14": "14",
  "15": "15",
  "16": "16",
  "17": "17",
  "18": "18",
  "19": "19",
  "20": "20",
  "21": "21",
  "22": "22",
  "23": "23",
  "24": "24",
  "25": "25",
  "26": "26",
  "27": "27",
  "28": "28",
  "29": "29",
  "30": "30",
  "31": "31",
  "32": "32",
  "33": "33",
  "34": "34",
  "35": "35",
  "36": "36",
  "37": "37",
  "38": "38",
  "39": "39",
  "40": "40",
  "41": "41",
  "42": "42",
  "43": "43",
  "44": "44",
  "45": "45",
  "46": "46",
  "47": "47",
  "48": "48",
  "49": "49",
  "50": "50",
  "51": "51",
  "52": "52",
  "53": "53",
  "54": "54",
  "55": "55",
  "56": "56",
  "57": "57",
  "58": "58",
  "59": "59",
  "60": "60",
  "61": "61",
  "62": "62",
  "63": "63",
  "64": "64",
  "65": "65",
  "66": "66",
  "67": "67",
  "68": "68",
  "69": "69",
  "70": "70",
  "71": "71",
  "72": "72",
  "73": "73",
  "74": "74",
  "75": "75",
  "76": "76",
  "77": "77",
  "78": "78",
  "79": "79",
  "80": "80",
  "81": "81",
  "82": "82",
  "83": "83",
  "84": "84",
  "85": "85",
  "86": "86",
  "87": "87",
  "88": "88",
  "89": "89",
  "90": "90",
  "91": "91",
  "92": "92",
  "93": "93",
  "94": "94",
  "95": "95",
  "96": "96",
  "97": "97",
  "98": "98",
  "99": "99",
  "100": "100",
};
const format: (value: string | number) => string = (value: string | number): string => labels[Number(value)] ?? "";
export default function SharedWindow(): ReactElement {
  const [range, setRange] = useState<ChartRangeValue>([0, 100]);
  const [following, setFollowing] = useState<boolean>(false);
  const data = Array.from({ length: 21 }, (_: unknown, index: number): { time: number; observed: number } => ({
    time: index * 5,
    observed: [8, 9, 10, 9, 8, 7, 6][index % 7] ?? 8,
  }));
  return (
    <ChartPanel
      title="Shared observation window"
      description="Illustrative data · relative sample coordinates"
      controls={
        <ChartRange
          value={range}
          onValueChange={setRange}
          bounds={[0, 100]}
          minSpan={10}
          label="Observation window"
          labels={["Earlier", "Later", "Zoom in", "Zoom out", "Follow live"]}
          following={following}
          onFollowingChange={(next: boolean): void => {
            setFollowing(next);
            if (next) setRange([50, 100]);
          }}
        />
      }
    >
      {["Primary", "Comparison"].map((name: string): ReactElement => (
        <LineChart
          key={name}
          data={data}
          categoryKey="time"
          categoryScale="linear"
          categoryDomain={range}
          onCategoryDomainChange={setRange}
          syncId="range-example"
          config={{ observed: { label: name, formatValue: format } }}
          series={["observed"]}
          formatCategory={format}
          formatAxisValue={(value: number): string => format(value)}
          label={`${name} observations`}
          categoryLabel="Coordinate"
          dataTableLabel={`View ${name.toLowerCase()} observations`}
          missingValueText="Unavailable"
        />
      ))}
    </ChartPanel>
  );
}

Props

PropTypeDefaultDescription
valuerequiredChartRangeValue–Controlled visible range; time ranges use epoch milliseconds.
onValueChangerequired(value: ChartRangeValue) => void–Receives the requested range; callers load the data asynchronously.
boundsChartRangeValue | undefined–Available history bounds; navigation preserves the span at these edges.
minSpannumber | undefined1Smallest permitted span in axis units; one by default.
labelrequiredstring–Names the group of range controls.
labelsrequiredreadonly [string, string, string, string, string]–Display labels for earlier, later, zoom in, zoom out and follow live.
followingboolean | undefinedfalseControlled follow-live state; a range interaction requests false.
onFollowingChange((following: boolean) => void) | undefined–Receives follow-live requests; the caller supplies the clock and newest range.
disabledboolean | undefinedfalseDisables navigation while the caller cannot accept a range request.
testIdstring | undefined–Becomes data-testid on the group.
refRef<HTMLDivElement> | undefined–The control group element.
Do

Shared observation window

Illustrative data · relative sample coordinates

Keep the comparison basis and missing information visible.
Don't

Preserve gaps and supply the source or validity information needed to interpret the observations.

Missing history must not appear to be a measured zero or a known state.