דילוג לתוכן
uiify

Line chart

תרשים קו מסתגל שמרונדר באמצעות CSS בלבד.

דפדפנים
Chrome 123+Firefox 121+Safari 17.5+
שכבה
0
מגבלת פריסה כפויה
0 ms
מגבלת Axe
0
תואם SSR
כן
חוזה מפורסם
סביבת בדיקות

מגרש ניסויים

Line chart

48 weekly values with CSS geometry, automatic axes, and an optional interactive client entry.

CSS-only chart

Weekly revenue
TimeValue
2025-01-0189
2025-01-0892.3
2025-01-1595.5
2025-01-2298.3
2025-01-29100.6
2025-02-05102.4
2025-02-12103.5
2025-02-19104.1
2025-02-26104.1
2025-03-05103.7
2025-03-12102.9
2025-03-19102
2025-03-26101
2025-04-02100.2
2025-04-0999.8
2025-04-1699.7
2025-04-23100.2
2025-04-30101.3
2025-05-07103
2025-05-14105.2
2025-05-21107.8
2025-05-28110.8
2025-06-04114
2025-06-11117.3
2025-06-18120.4
2025-06-25123.3
2025-07-02125.8
2025-07-09127.8
2025-07-16129.3
2025-07-23130.3
2025-07-30130.8
2025-08-06130.9
2025-08-13130.8
2025-08-20130.5
2025-08-27130.3
2025-09-03130.2
2025-09-10130.5
2025-09-17131.2
2025-09-24132.4
2025-10-01134.2
2025-10-08136.5
2025-10-15139.3
2025-10-22142.6
2025-10-29146.1
2025-11-05149.8
2025-11-12153.4
2025-11-19156.8
2025-11-26159.9

Interactive chart

Interactive weekly revenue
2025-11-26159.9
  1. Jan 25
  2. Feb 25
  3. Mar 25
  4. Apr 25
  5. May 25
  6. Jun 25
  7. Jul 25
  8. Aug 25
  9. Sep 25
  10. Oct 25
  11. Nov 25
  1. 159.9
  2. 145.72
  3. 131.54
  4. 117.36
  5. 103.18
  6. 89
TimeValue
2025-01-0189
2025-01-0892.3
2025-01-1595.5
2025-01-2298.3
2025-01-29100.6
2025-02-05102.4
2025-02-12103.5
2025-02-19104.1
2025-02-26104.1
2025-03-05103.7
2025-03-12102.9
2025-03-19102
2025-03-26101
2025-04-02100.2
2025-04-0999.8
2025-04-1699.7
2025-04-23100.2
2025-04-30101.3
2025-05-07103
2025-05-14105.2
2025-05-21107.8
2025-05-28110.8
2025-06-04114
2025-06-11117.3
2025-06-18120.4
2025-06-25123.3
2025-07-02125.8
2025-07-09127.8
2025-07-16129.3
2025-07-23130.3
2025-07-30130.8
2025-08-06130.9
2025-08-13130.8
2025-08-20130.5
2025-08-27130.3
2025-09-03130.2
2025-09-10130.5
2025-09-17131.2
2025-09-24132.4
2025-10-01134.2
2025-10-08136.5
2025-10-15139.3
2025-10-22142.6
2025-10-29146.1
2025-11-05149.8
2025-11-12153.4
2025-11-19156.8
2025-11-26159.9

Live stream

A simulated trade feed appends one finished one-minute close every second; existing points never move. Every point lives in a horizontal VirtualScroll sized to show 20 at a time: scroll back through the history, and the chart follows new points again once you return to the newest one.

Live ETH / USD one-minute closes
10:293152.46
  1. 10:05
  2. 10:10
  3. 10:15
  4. 10:20
  5. 10:25
  1. 3,166.96
  2. 3,161.74
  3. 3,156.52
  4. 3,151.31
  5. 3,146.09
  6. 3,140.87
TimeValue
10:103166.96
10:113160.48
10:123154.88
10:133151.27
10:143156.95
10:153156.64
10:163153.31
10:173151.54
10:183148.18
10:193146.15
10:203144.78
10:213145.29
10:223143.28
10:233146.33
10:243142.35
10:253140.87
10:263149.75
10:273152.42
10:283154.24
10:293152.46
הכנסה שנתית
TimeValue
Jan42
Feb48
Mar45
Apr58
May62
Jun59
Jul71
Aug78
Sep74
Oct86
Nov91
Dec98
import { useEffect, useState } from "react";
import { LineChart } from "@gunkin/uiify";
import { LineChartClient } from "@gunkin/uiify/components/line-chart/client";

const data = Array.from({ length: 48 }, (_, index) => ({
  time: new Date(Date.UTC(2025, 0, 1 + index * 7)).toISOString().slice(0, 10),
  value: Math.round((86 + index * 1.4 + Math.sin(index / 3.5) * 7) * 10) / 10,
}));

const lineChartSlotProps = {
  caption: {
    className:
      "absolute z-[1] [inset-block-start:0.75rem] [inset-inline-start:1rem] [color:var(--line-axis)] [font-size:0.75rem] [font-weight:650] [letter-spacing:0.04em] [text-transform:uppercase]",
  },
  readout: {
    className:
      "absolute z-[1] [inset-block-start:0.75rem] [inset-inline-end:1rem] flex [gap:0.625rem] items-baseline [color:var(--line-axis)] [font-size:0.7rem] [font-variant-numeric:tabular-nums] leading-none whitespace-nowrap",
  },
  readoutTime: { className: "[font-weight:650]" },
  plot: {
    className:
      "relative grid [grid-template-columns:minmax(0,_1fr)_4.5rem] [grid-template-rows:minmax(18rem,_1fr)_calc(1.5rem_+_var(--line-scrollbar-size,_0px))] [gap:0_0.75rem] [min-inline-size:0] [min-block-size:18rem] [overscroll-behavior:none]",
  },
  scrollViewport: {
    className:
      "[grid-column:1] [grid-row:1_/_span_2] [min-inline-size:0] [min-block-size:18rem] [overflow:auto_hidden] [container-type:inline-size] [direction:rtl] group-data-[interactive]/line-chart:[overscroll-behavior-inline:none]",
  },
  canvas: {
    className:
      "grid [grid-template-rows:minmax(18rem,_1fr)_1.5rem] [inline-size:calc(100%_*_var(--line-scale))] [min-inline-size:100%] [min-block-size:100%] [direction:ltr]",
  },
  viewport: {
    className:
      "[container-type:inline-size] relative [grid-column:1] [grid-row:1] [min-inline-size:0] [min-block-size:18rem] overflow-clip [overscroll-behavior:none]",
  },
  points: {
    className:
      "[container-type:size] absolute [inset:1rem_0] block m-0 p-0 list-none [background-image:linear-gradient(to_right,_var(--line-grid)_1px,_transparent_1px),_linear-gradient(to_bottom,_var(--line-grid)_1px,_transparent_1px)] [background-size:var(--line-segment-slot)_100%,_100%_20%]",
  },
  crosshair: {
    className:
      "absolute z-[4] inset-0 [inline-size:var(--line-content-size,_100%)] [translate:calc(-1_*_var(--line-scroll-x,_0px))_0] [block-size:100%] [color:light-dark(oklch(52%_0.01_0deg),_oklch(72%_0.01_0deg))] pointer-events-none",
  },
  crosshairMarker: {
    className:
      "absolute [inset-block-start:calc(var(--line-cursor-y,_50%)_+_1rem)] [inset-inline-start:var(--line-crosshair-x)] [font-size:1rem] leading-none [transform:translate(-50%,-50%)]",
  },
  timeViewport: {
    className: "relative [grid-column:1] [grid-row:2] [min-inline-size:0] overflow-clip [overscroll-behavior:none]",
  },
  timeAxis: {
    className:
      "relative [inset-block:0] [inline-size:var(--line-content-size,_100%)] [translate:calc(-1_*_var(--line-scroll-x,_0px))_0] [min-inline-size:0] m-0 p-0 list-none",
  },
  timeAxisLabel: {
    className:
      "absolute [inset-block-start:0.375rem] [inset-inline-start:var(--line-axis-x)] [color:var(--line-axis)] [font-size:0.7rem] whitespace-nowrap [transform:translateX(-50%)] first:[transform:none] last:[transform:translateX(-100%)] only:[transform:translateX(-50%)]",
  },
  crosshairTime: {
    className:
      "[inset-inline-start:var(--line-crosshair-x)] [padding:0.25rem_0.5rem] [background:var(--line-axis)] [color:Canvas] [font-weight:650] [transform:translateX(-50%)]",
  },
  valueAxis: { className: "relative [grid-column:2] [grid-row:1] [min-inline-size:0] m-0 p-0 list-none" },
  valueAxisLabel: {
    className:
      "absolute [inset-inline:0] [inset-block-start:var(--line-axis-y)] overflow-hidden [color:var(--line-axis)] [font-size:0.65rem] [font-variant-numeric:tabular-nums] leading-none text-end [text-overflow:ellipsis] whitespace-nowrap [transform:translateY(-50%)] first:[transform:none] last:[transform:translateY(-100%)]",
  },
  crosshairValue: {
    className:
      "[inset-block-start:calc(var(--line-cursor-y,_50%)_+_1rem)] [inset-inline-end:0] [padding:0.25rem_0.5rem] [background:var(--line-axis)] [color:Canvas] [font-weight:650] [transform:translateY(-50%)]",
  },
  dataTable: {
    className: "absolute [inline-size:1px] [block-size:1px] overflow-hidden [clip-path:inset(50%)] whitespace-nowrap",
  },
};

export default function Example() {
  return (
    <>
      <LineChart className="" slotProps={lineChartSlotProps} data={data} aria-label="Weekly revenue" />
      <LineChartClient
        className=""
        slotProps={lineChartSlotProps}
        data={data}
        aria-label="Interactive weekly revenue"
      />
      <LiveChart />
    </>
  );
}

function LiveChart() {
  const [points, setPoints] = useState(data);
  const [running, setRunning] = useState(false);

  useEffect(() => {
    if (!running) return;
    const timer = setInterval(() => {
      setPoints((current) => {
        const last = current.at(-1);
        if (!last) return current;
        const value = Math.round(last.value * (1 + (Math.random() - 0.5) * 0.02) * 10) / 10;
        return [...current, { time: String(current.length), value }];
      });
    }, 1000);
    return () => clearInterval(timer);
  }, [running]);

  return (
    <>
      <button type="button" onClick={() => setRunning((value) => !value)}>
        {running ? "Stop" : "Start"}
      </button>
      {/* All points, virtualized; 20 fit the viewport and new ones stay in view at the end. */}
      <LineChartClient
        className=""
        slotProps={lineChartSlotProps}
        data={points}
        visibleCount={20}
        aria-label="Live revenue"
      />
    </>
  );
}
@property --line-y {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 50%;
}

@property --line-prev-y {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 50%;
}

@property --line-count {
  syntax: "<integer>";
  inherits: false;
  initial-value: 1;
}

@property --line-scale {
  syntax: "<number>";
  inherits: true;
  initial-value: 1;
}

@layer components {
  [data-uiify-line-chart] {
    --line-grid: color-mix(in srgb, currentcolor 10%, transparent);
    --line-up: oklch(67% 0.14 166deg);
    --line-down: oklch(64% 0.21 24deg);
    --line-flat: light-dark(oklch(43% 0.05 165deg), oklch(72% 0.04 165deg));
    --line-axis: light-dark(
      color-mix(in oklch, var(--muted-foreground) 70%, var(--foreground)),
      var(--muted-foreground)
    );
    --line-width: 0.12rem;

    position: relative;
    display: block;
    inline-size: 100%;
    min-block-size: 21rem;
    padding: 1rem 0.75rem 0.5rem;
    overflow: clip;
    overscroll-behavior: none;
    isolation: isolate;

    /* No color-scheme declaration: it inherits, so light-dark() colors and the
       live scrollbar follow the page theme. An explicit `inherit` is rewritten
       to `normal` by Lightning CSS in consumer bundlers such as Next. */
    background: transparent;
    color: light-dark(oklch(28% 0.04 165deg), oklch(82% 0.04 165deg));
  }

  /* Interactive charts own touch and horizontal wheel gestures (crosshair,
     zoom, native scroll); a static chart leaves page scrolling alone. */
  [data-uiify-line-chart][data-interactive] {
    touch-action: none;
  }

  [data-uiify-line-chart][data-interactive] [data-part="scroll-viewport"] {
    overscroll-behavior-inline: none;
  }

  /* The time-axis row also reserves the horizontal scrollbar, measured by the
     client entry (0 for overlay scrollbars and static charts), so the flexible
     candle row does not absorb it and stays aligned with the value axis. */
  [data-uiify-line-chart] > [data-part="plot"] {
    position: relative;
    display: grid;
    grid-template-columns: minmax(0, 1fr) 4.5rem;
    grid-template-rows: minmax(18rem, 1fr) calc(1.5rem + var(--line-scrollbar-size, 0px));
    gap: 0 0.75rem;
    min-inline-size: 0;
    min-block-size: 18rem;
    overscroll-behavior: none;
  }

  [data-uiify-line-chart] [data-part="scroll-viewport"] {
    grid-column: 1;
    grid-row: 1 / span 2;
    min-inline-size: 0;
    min-block-size: 18rem;
    overflow: auto hidden;
    container-type: inline-size;
    direction: rtl;
  }

  [data-uiify-line-chart] [data-part="canvas"] {
    display: grid;
    grid-template-rows: minmax(18rem, 1fr) 1.5rem;
    inline-size: calc(100% * var(--line-scale));
    min-inline-size: 100%;
    min-block-size: 100%;
    direction: ltr;
  }

  /* Inline-size container: live-mode segments measure their run in cqw. */
  [data-uiify-line-chart] [data-part="viewport"] {
    container-type: inline-size;
    position: relative;
    grid-column: 1;
    grid-row: 1;
    min-inline-size: 0;
    min-block-size: 18rem;
    overflow: clip;
    overscroll-behavior: none;
  }

  [data-uiify-line-chart] > figcaption {
    position: absolute;
    z-index: 1;
    inset-block-start: 0.75rem;
    inset-inline-start: 1rem;
    color: var(--line-axis);
    font-size: 0.75rem;
    font-weight: 650;
    letter-spacing: 0.04em;
    text-transform: uppercase;
  }

  [data-uiify-line-chart] [data-part="readout"] {
    position: absolute;
    z-index: 1;
    inset-block-start: 0.75rem;
    inset-inline-end: 1rem;
    display: flex;
    gap: 0.625rem;
    align-items: baseline;
    color: var(--line-axis);
    font-size: 0.7rem;
    font-variant-numeric: tabular-nums;
    line-height: 1;
    white-space: nowrap;
  }

  [data-uiify-line-chart] [data-part="readout-time"] {
    font-weight: 650;
  }

  /* Size container for segment slopes. Its height comes from insets, so it is
     definite (a height from min-block-size alone reports 0cqh in Chromium).
     One container for all points keeps every line below every dot. */
  [data-uiify-line-chart] [data-part="points"] {
    container-type: size;
    position: absolute;
    inset: 1rem 0;
    display: block;
    margin: 0;
    padding: 0;
    list-style: none;
    background-image:
      linear-gradient(to right, var(--line-grid) 1px, transparent 1px),
      linear-gradient(to bottom, var(--line-grid) 1px, transparent 1px);
    background-size:
      var(--line-segment-slot) 100%,
      100% 20%;
  }

  /* Live mode: a horizontal VirtualScroll replaces the static list. The grid sits
     on the viewport, so it holds still while points scroll past and a classic
     scrollbar cannot squeeze it. */
  [data-uiify-line-chart] [data-part="viewport"] > [data-uiify-virtual-scroll] {
    position: absolute;
    inset: 0;
  }

  [data-uiify-line-chart] [data-part="viewport"]:has(> [data-uiify-virtual-scroll]) {
    background-image:
      linear-gradient(to right, var(--line-grid) 1px, transparent 1px),
      linear-gradient(to bottom, var(--line-grid) 1px, transparent 1px);
    background-position: 0 1rem;
    background-size:
      10% 100%,
      100% calc((100% - 2rem) / 5);
  }

  [data-uiify-line-chart] [data-part="point"] {
    position: absolute;
    min-inline-size: 0;
    color: var(--line-flat);
    list-style: none;
  }

  [data-uiify-line-chart] [data-part="points"] > [data-part="point"] {
    inset-block: 0;
    inset-inline-start: var(--line-prev-x, var(--line-x));
    inline-size: var(--line-slot);
    min-block-size: 100%;
  }

  /* A virtual row is one point slot; the point element spans from the previous
     slot's center to this one's, so the segment ends on the row center. Its
     block size comes from the plot, since a classic scrollbar shortens rows. */
  [data-uiify-line-chart] [data-uiify-virtual-scroll] [data-part="point"] {
    --line-point-block-size: calc(var(--line-plot-height, 18rem) - 2rem);

    inset-block: 1rem auto;
    inset-inline-start: -50%;
    inline-size: 100%;
    block-size: var(--line-point-block-size);
  }

  /* Each segment is a bar of constant thickness rotated to its slope, so steep
     jumps stay as thick as flat runs. The run is one slot (`--line-run`% of the
     plot width); the rise is `--line-rise`% of the point box height — the points
     list in the static chart, the explicit point size in live mode. */
  [data-uiify-line-chart] [data-part="point"]::before {
    --line-dx: calc(var(--line-run, 0) * 1cqw);
    --line-dy: calc(var(--line-rise, 0) / 100 * var(--line-point-block-size, 100cqh));

    position: absolute;
    z-index: 1;
    inset-block-start: calc(var(--line-prev-y) - var(--line-width));
    inset-inline-start: 0;
    display: block;
    inline-size: hypot(var(--line-dx), var(--line-dy));
    block-size: calc(var(--line-width) * 2);
    border-radius: var(--line-width);
    background: currentcolor;
    content: "";
    transform-origin: 0 50%;
    rotate: atan2(var(--line-dy), var(--line-dx));
  }

  [data-uiify-line-chart] [data-part="point"][data-first]::before {
    display: none;
  }

  [data-uiify-line-chart] [data-part="point"]::after {
    position: absolute;
    z-index: 2;
    inset-block-start: var(--line-y);
    inset-inline-start: 100%;
    display: block;
    inline-size: 0.55rem;
    block-size: 0.55rem;
    border: 0;
    border-radius: 50%;
    background: currentcolor;
    content: "";
    transform: translate(-50%, -50%);
  }

  [data-uiify-line-chart] [data-part="points"] > [data-part="point"][data-first]::after {
    inset-inline-start: 0;
  }

  [data-uiify-line-chart] [data-part="point"][data-trend="up"] {
    color: var(--line-up);
  }

  [data-uiify-line-chart] [data-part="point"][data-trend="down"] {
    color: var(--line-down);
  }

  [data-uiify-line-chart] [data-part="point-hit"] {
    position: absolute;
    z-index: 3;
    inset: 0;
    inline-size: 100%;
    block-size: 100%;
    margin: 0;
    padding: 0;
    border: 0;
    appearance: none;
    background: transparent;
    color: transparent;
    cursor: crosshair;
    font: inherit;
  }

  [data-uiify-line-chart][data-interactive] [data-part="point"][data-point-selected]::after {
    box-shadow: 0 0 0 0.2rem color-mix(in srgb, currentcolor 24%, transparent);
  }

  [data-uiify-line-chart][data-interactive] [data-part="point-hit"]:focus-visible {
    outline: 2px solid currentcolor;
    outline-offset: -2px;
  }

  /* The crosshair and time axis span the scrolled content in live mode:
     `--line-content-size` is its width, `--line-scroll-x` its scroll offset. */
  [data-uiify-line-chart] [data-part="crosshair"] {
    position: absolute;
    z-index: 4;
    inset: 0;
    inline-size: var(--line-content-size, 100%);
    translate: calc(-1 * var(--line-scroll-x, 0px)) 0;
    block-size: 100%;
    color: light-dark(oklch(52% 0.01 0deg), oklch(72% 0.01 0deg));
    pointer-events: none;
  }

  [data-uiify-line-chart] [data-part="crosshair"]::before,
  [data-uiify-line-chart] [data-part="crosshair"]::after {
    position: absolute;
    display: block;
    background: currentcolor;
    content: "";
  }

  [data-uiify-line-chart] [data-part="crosshair"]::before {
    inset-block: 0;
    inset-inline-start: var(--line-crosshair-x);
    inline-size: 1px;
    background-image: repeating-linear-gradient(
      to bottom,
      currentcolor 0 0.4rem,
      transparent 0.4rem 0.8rem
    );
    background-color: transparent;
  }

  [data-uiify-line-chart] [data-part="crosshair"]::after {
    inset-block-start: calc(var(--line-cursor-y, 50%) + 1rem);
    inset-inline: 0;
    block-size: 1px;
    background-image: repeating-linear-gradient(
      to right,
      currentcolor 0 0.4rem,
      transparent 0.4rem 0.8rem
    );
    background-color: transparent;
  }

  [data-uiify-line-chart] [data-part="crosshair-marker"] {
    position: absolute;
    inset-block-start: calc(var(--line-cursor-y, 50%) + 1rem);
    inset-inline-start: var(--line-crosshair-x);
    font-size: 1rem;
    line-height: 1;
    transform: translate(-50%, -50%);
  }

  [data-uiify-line-chart] [data-part="value-axis"],
  [data-uiify-line-chart] [data-part="time-axis"] {
    margin: 0;
    padding: 0;
    list-style: none;
  }

  [data-uiify-line-chart] [data-part="value-axis"] {
    position: relative;
    grid-column: 2;
    grid-row: 1;
    min-inline-size: 0;
  }

  [data-uiify-line-chart] [data-part="value-axis"] > li {
    position: absolute;
    inset-inline: 0;
    inset-block-start: var(--line-axis-y);
    overflow: hidden;
    color: var(--line-axis);
    font-size: 0.65rem;
    font-variant-numeric: tabular-nums;
    line-height: 1;
    text-align: end;
    text-overflow: ellipsis;
    white-space: nowrap;
    transform: translateY(-50%);
  }

  [data-uiify-line-chart] [data-part="value-axis"] > li:first-child {
    transform: none;
  }

  [data-uiify-line-chart] [data-part="value-axis"] > li:last-child {
    transform: translateY(-100%);
  }

  [data-uiify-line-chart] [data-part="time-viewport"] {
    position: relative;
    grid-column: 1;
    grid-row: 2;
    min-inline-size: 0;
    overflow: clip;
    overscroll-behavior: none;
  }

  [data-uiify-line-chart] [data-part="time-axis"] {
    position: relative;
    inset-block: 0;
    inline-size: var(--line-content-size, 100%);
    translate: calc(-1 * var(--line-scroll-x, 0px)) 0;
    min-inline-size: 0;
  }

  [data-uiify-line-chart] [data-part="time-axis"] > li {
    position: absolute;
    inset-block-start: 0.375rem;
    inset-inline-start: var(--line-axis-x);
    color: var(--line-axis);
    font-size: 0.7rem;
    white-space: nowrap;
    transform: translateX(-50%);
  }

  [data-uiify-line-chart] [data-part="time-axis"] > li:first-child {
    transform: none;
  }

  [data-uiify-line-chart] [data-part="time-axis"] > li:last-child {
    transform: translateX(-100%);
  }

  [data-uiify-line-chart] [data-part="time-axis"] > li:only-child {
    transform: translateX(-50%);
  }

  [data-uiify-line-chart] [data-part="time-axis"] > li[data-part="crosshair-time"] {
    inset-inline-start: var(--line-crosshair-x);
    padding: 0.25rem 0.5rem;
    background: var(--line-axis);
    color: Canvas;
    font-weight: 650;
    transform: translateX(-50%);
  }

  [data-uiify-line-chart] [data-part="value-axis"] > li[data-part="crosshair-value"] {
    inset-block-start: calc(var(--line-cursor-y, 50%) + 1rem);
    inset-inline-end: 0;
    padding: 0.25rem 0.5rem;
    background: var(--line-axis);
    color: Canvas;
    font-weight: 650;
    transform: translateY(-50%);
  }

  [data-uiify-line-chart] > table {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  @media (prefers-reduced-motion: no-preference) {
    [data-uiify-line-chart] [data-part="point"]::after {
      transition: scale 180ms ease;
    }

    [data-uiify-line-chart][data-interactive] [data-part="point"][data-point-selected]::after {
      scale: 1.15;
    }
  }
}
import { useEffect, useState } from "react";
import { LineChart } from "@gunkin/uiify";
import { LineChartClient } from "@gunkin/uiify/components/line-chart/client";

const data = Array.from({ length: 48 }, (_, index) => ({
  time: new Date(Date.UTC(2025, 0, 1 + index * 7)).toISOString().slice(0, 10),
  value: Math.round((86 + index * 1.4 + Math.sin(index / 3.5) * 7) * 10) / 10,
}));

const lineChartSlotProps = {
  caption: {
    className:
      "absolute z-[1] [inset-block-start:0.75rem] [inset-inline-start:1rem] [color:var(--line-axis)] [font-size:0.75rem] [font-weight:650] [letter-spacing:0.04em] [text-transform:uppercase]",
  },
  readout: {
    className:
      "absolute z-[1] [inset-block-start:0.75rem] [inset-inline-end:1rem] flex [gap:0.625rem] items-baseline [color:var(--line-axis)] [font-size:0.7rem] [font-variant-numeric:tabular-nums] leading-none whitespace-nowrap",
  },
  readoutTime: { className: "[font-weight:650]" },
  plot: {
    className:
      "relative grid [grid-template-columns:minmax(0,_1fr)_4.5rem] [grid-template-rows:minmax(18rem,_1fr)_calc(1.5rem_+_var(--line-scrollbar-size,_0px))] [gap:0_0.75rem] [min-inline-size:0] [min-block-size:18rem] [overscroll-behavior:none]",
  },
  scrollViewport: {
    className:
      "[grid-column:1] [grid-row:1_/_span_2] [min-inline-size:0] [min-block-size:18rem] [overflow:auto_hidden] [container-type:inline-size] [direction:rtl] group-data-[interactive]/line-chart:[overscroll-behavior-inline:none]",
  },
  canvas: {
    className:
      "grid [grid-template-rows:minmax(18rem,_1fr)_1.5rem] [inline-size:calc(100%_*_var(--line-scale))] [min-inline-size:100%] [min-block-size:100%] [direction:ltr]",
  },
  viewport: {
    className:
      "[container-type:inline-size] relative [grid-column:1] [grid-row:1] [min-inline-size:0] [min-block-size:18rem] overflow-clip [overscroll-behavior:none]",
  },
  points: {
    className:
      "[container-type:size] absolute [inset:1rem_0] block m-0 p-0 list-none [background-image:linear-gradient(to_right,_var(--line-grid)_1px,_transparent_1px),_linear-gradient(to_bottom,_var(--line-grid)_1px,_transparent_1px)] [background-size:var(--line-segment-slot)_100%,_100%_20%]",
  },
  crosshair: {
    className:
      "absolute z-[4] inset-0 [inline-size:var(--line-content-size,_100%)] [translate:calc(-1_*_var(--line-scroll-x,_0px))_0] [block-size:100%] [color:light-dark(oklch(52%_0.01_0deg),_oklch(72%_0.01_0deg))] pointer-events-none",
  },
  crosshairMarker: {
    className:
      "absolute [inset-block-start:calc(var(--line-cursor-y,_50%)_+_1rem)] [inset-inline-start:var(--line-crosshair-x)] [font-size:1rem] leading-none [transform:translate(-50%,-50%)]",
  },
  timeViewport: {
    className: "relative [grid-column:1] [grid-row:2] [min-inline-size:0] overflow-clip [overscroll-behavior:none]",
  },
  timeAxis: {
    className:
      "relative [inset-block:0] [inline-size:var(--line-content-size,_100%)] [translate:calc(-1_*_var(--line-scroll-x,_0px))_0] [min-inline-size:0] m-0 p-0 list-none",
  },
  timeAxisLabel: {
    className:
      "absolute [inset-block-start:0.375rem] [inset-inline-start:var(--line-axis-x)] [color:var(--line-axis)] [font-size:0.7rem] whitespace-nowrap [transform:translateX(-50%)] first:[transform:none] last:[transform:translateX(-100%)] only:[transform:translateX(-50%)]",
  },
  crosshairTime: {
    className:
      "[inset-inline-start:var(--line-crosshair-x)] [padding:0.25rem_0.5rem] [background:var(--line-axis)] [color:Canvas] [font-weight:650] [transform:translateX(-50%)]",
  },
  valueAxis: { className: "relative [grid-column:2] [grid-row:1] [min-inline-size:0] m-0 p-0 list-none" },
  valueAxisLabel: {
    className:
      "absolute [inset-inline:0] [inset-block-start:var(--line-axis-y)] overflow-hidden [color:var(--line-axis)] [font-size:0.65rem] [font-variant-numeric:tabular-nums] leading-none text-end [text-overflow:ellipsis] whitespace-nowrap [transform:translateY(-50%)] first:[transform:none] last:[transform:translateY(-100%)]",
  },
  crosshairValue: {
    className:
      "[inset-block-start:calc(var(--line-cursor-y,_50%)_+_1rem)] [inset-inline-end:0] [padding:0.25rem_0.5rem] [background:var(--line-axis)] [color:Canvas] [font-weight:650] [transform:translateY(-50%)]",
  },
  dataTable: {
    className: "absolute [inline-size:1px] [block-size:1px] overflow-hidden [clip-path:inset(50%)] whitespace-nowrap",
  },
};

export default function Example() {
  return (
    <>
      <LineChart
        className={
          "[--line-grid:color-mix(in_srgb,_currentcolor_10%,_transparent)] [--line-up:oklch(67%_0.14_166deg)] [--lin" +
          "e-down:oklch(64%_0.21_24deg)] [--line-flat:light-dark(oklch(43%_0.05_165deg),_oklch(72%_0.04_165deg))] [-" +
          "-line-axis:light-dark(_color-mix(in_oklch,_var(--muted-foreground)_70%,_var(--foreground)),_var(--muted-f" +
          "oreground)_)] [--line-width:0.12rem] relative block [inline-size:100%] [min-block-size:21rem] [padding:1r" +
          "em_0.75rem_0.5rem] overflow-clip [overscroll-behavior:none] isolate bg-transparent [color:light-dark(oklc" +
          "h(28%_0.04_165deg),_oklch(82%_0.04_165deg))] group/line-chart data-[interactive]:[touch-action:none]"
        }
        slotProps={lineChartSlotProps}
        data={data}
        aria-label="Weekly revenue"
      />
      <LineChartClient
        className={
          "[--line-grid:color-mix(in_srgb,_currentcolor_10%,_transparent)] [--line-up:oklch(67%_0.14_166deg)] [--lin" +
          "e-down:oklch(64%_0.21_24deg)] [--line-flat:light-dark(oklch(43%_0.05_165deg),_oklch(72%_0.04_165deg))] [-" +
          "-line-axis:light-dark(_color-mix(in_oklch,_var(--muted-foreground)_70%,_var(--foreground)),_var(--muted-f" +
          "oreground)_)] [--line-width:0.12rem] relative block [inline-size:100%] [min-block-size:21rem] [padding:1r" +
          "em_0.75rem_0.5rem] overflow-clip [overscroll-behavior:none] isolate bg-transparent [color:light-dark(oklc" +
          "h(28%_0.04_165deg),_oklch(82%_0.04_165deg))] group/line-chart data-[interactive]:[touch-action:none]"
        }
        slotProps={lineChartSlotProps}
        data={data}
        aria-label="Interactive weekly revenue"
      />
      <LiveChart />
    </>
  );
}

function LiveChart() {
  const [points, setPoints] = useState(data);
  const [running, setRunning] = useState(false);

  useEffect(() => {
    if (!running) return;
    const timer = setInterval(() => {
      setPoints((current) => {
        const last = current.at(-1);
        if (!last) return current;
        const value = Math.round(last.value * (1 + (Math.random() - 0.5) * 0.02) * 10) / 10;
        return [...current, { time: String(current.length), value }];
      });
    }, 1000);
    return () => clearInterval(timer);
  }, [running]);

  return (
    <>
      <button type="button" onClick={() => setRunning((value) => !value)}>
        {running ? "Stop" : "Start"}
      </button>
      {/* All points, virtualized; 20 fit the viewport and new ones stay in view at the end. */}
      <LineChartClient
        className={
          "[--line-grid:color-mix(in_srgb,_currentcolor_10%,_transparent)] [--line-up:oklch(67%_0.14_166deg)] [--lin" +
          "e-down:oklch(64%_0.21_24deg)] [--line-flat:light-dark(oklch(43%_0.05_165deg),_oklch(72%_0.04_165deg))] [-" +
          "-line-axis:light-dark(_color-mix(in_oklch,_var(--muted-foreground)_70%,_var(--foreground)),_var(--muted-f" +
          "oreground)_)] [--line-width:0.12rem] relative block [inline-size:100%] [min-block-size:21rem] [padding:1r" +
          "em_0.75rem_0.5rem] overflow-clip [overscroll-behavior:none] isolate bg-transparent [color:light-dark(oklc" +
          "h(28%_0.04_165deg),_oklch(82%_0.04_165deg))] group/line-chart data-[interactive]:[touch-action:none]"
        }
        slotProps={lineChartSlotProps}
        data={points}
        visibleCount={20}
        aria-label="Live revenue"
      />
    </>
  );
}

API

LineChart

מאפייןטיפוסברירת מחדלתיאור
data*readonly LineChartData[]—רשומות זמן וערך המשמשות לציור הקו.
slotPropsLineChartSlotProps—מאפיינים מובנים לצומתי התרשים שנוצרים. נקודות ושורות או תאים חוזרים בטבלה ממוקדים באינדקס הנתון המקורי באמצעות byItemIndex; תוויות הצירים משתמשות באינדקס שלהן במערך הציר. המצב החי חושף גם את אזור הגלילה, המרווח ומעטפת השורה.
aria-labelstring"Line chart"שם נגיש המשמש לתרשים ולטבלת הנתונים שלו.
xAxisreadonly LineChartAxisLabel[]—תוויות מותאמות אישית ואופציונליות לציר התחתון. כל פריט מספק תוכן React חופשי ומיקום בין 0 ל-100 אחוזים.
yAxisreadonly LineChartAxisLabel[]—תוויות מותאמות אישית ואופציונליות לציר הימני. כל פריט מספק תוכן React חופשי ומיקום בין 0 ל-100 אחוזים.
interactiveboolean—מפעיל אזורי פגיעה לנקודות, מצב כוונת, הפעלה במקלדת ותצוגת ערכים באמצעות React; LineChartClient מגדיר זאת אוטומטית.
renderReactElement | ((props, state) => ReactElement)—החלפת האלמנט המוצג תוך שימור ההתנהגות וקישורי הנגישות.

LineChartClient (@gunkin/uiify/components/line-chart/client)

מאפייןטיפוסברירת מחדלתיאור
data*readonly LineChartData[]—אותם נתוני זמן וערך כמו ב-LineChart; הלקוח מוסיף שינוי קנה מידה בגלגלת, גלילה אופקית, כוונת הנצמדת לנקודות הקצה, בחירת נקודות במקלדת ותצוגת ערכים.
visibleCountnumber—מצב חי: N נקודות ממלאות את אזור התצוגה; ההיסטוריה מוצגת באופן וירטואלי. הצירים, תצוגת הערכים והטבלה עוקבים אחר הנקודות הגלויות; התרשים עוקב אחר נקודות חדשות אלא אם גוללים לאחור, עם שינוי קנה מידה והכרזות מושבתים.
slotPropsLineChartSlotProps—מאפיינים מובנים לצומתי התרשים שנוצרים. נקודות ושורות או תאים חוזרים בטבלה ממוקדים באינדקס הנתון המקורי באמצעות byItemIndex; תוויות הצירים משתמשות באינדקס שלהן במערך הציר. המצב החי חושף גם את אזור הגלילה, המרווח ומעטפת השורה.

מקלדת

מקשפעולה
Tabמיקוד נקודה אינטראקטיבית
Enter / Spaceבחירת הנקודה הממוקדת ועדכון תצוגת הנתונים

נגישות

הקו הסטטי דקורטיבי, והאיור כולל טבלה מוסתרת חזותית עם צמדי הזמן והערך המקוריים. נקודת הכניסה האופציונלית ללקוח מאפשרת מיקוד נקודות במקלדת ומוסיפה שינוי קנה מידה בגלגלת, גלילה אופקית מובנית, כוונת נצמדת ותצוגת ערך חיה, תוך שימור הטבלה.

נקודות חיבור לעיצוב

data-uiify-line-chartdata-interactivedata-trenddata-timedata-crosshair-activedata-axis--line-y--line-prev-y--line-slot--line-count--line-scale--line-axis-x--line-axis-y--line-crosshair-x--line-cursor-y
דף זה תורגם אוטומטית וטרם נבדק.