Line chart
תרשים קו מסתגל שמרונדר באמצעות CSS בלבד.
- דפדפנים
- Chrome 123+Firefox 121+Safari 17.5+
- שכבה
- 0
- מגבלת פריסה כפויה
- 0 ms
- מגבלת Axe
- 0
- תואם SSR
- כן
- חוזה מפורסם
- סביבת בדיקות
מגרש ניסויים
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[] | — | רשומות זמן וערך המשמשות לציור הקו. |
slotProps | LineChartSlotProps | — | מאפיינים מובנים לצומתי התרשים שנוצרים. נקודות ושורות או תאים חוזרים בטבלה ממוקדים באינדקס הנתון המקורי באמצעות byItemIndex; תוויות הצירים משתמשות באינדקס שלהן במערך הציר. המצב החי חושף גם את אזור הגלילה, המרווח ומעטפת השורה. |
aria-label | string | "Line chart" | שם נגיש המשמש לתרשים ולטבלת הנתונים שלו. |
xAxis | readonly LineChartAxisLabel[] | — | תוויות מותאמות אישית ואופציונליות לציר התחתון. כל פריט מספק תוכן React חופשי ומיקום בין 0 ל-100 אחוזים. |
yAxis | readonly LineChartAxisLabel[] | — | תוויות מותאמות אישית ואופציונליות לציר הימני. כל פריט מספק תוכן React חופשי ומיקום בין 0 ל-100 אחוזים. |
interactive | boolean | — | מפעיל אזורי פגיעה לנקודות, מצב כוונת, הפעלה במקלדת ותצוגת ערכים באמצעות React; LineChartClient מגדיר זאת אוטומטית. |
render | ReactElement | ((props, state) => ReactElement) | — | החלפת האלמנט המוצג תוך שימור ההתנהגות וקישורי הנגישות. |
LineChartClient (@gunkin/uiify/components/line-chart/client)
| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
data* | readonly LineChartData[] | — | אותם נתוני זמן וערך כמו ב-LineChart; הלקוח מוסיף שינוי קנה מידה בגלגלת, גלילה אופקית, כוונת הנצמדת לנקודות הקצה, בחירת נקודות במקלדת ותצוגת ערכים. |
visibleCount | number | — | מצב חי: N נקודות ממלאות את אזור התצוגה; ההיסטוריה מוצגת באופן וירטואלי. הצירים, תצוגת הערכים והטבלה עוקבים אחר הנקודות הגלויות; התרשים עוקב אחר נקודות חדשות אלא אם גוללים לאחור, עם שינוי קנה מידה והכרזות מושבתים. |
slotProps | LineChartSlotProps | — | מאפיינים מובנים לצומתי התרשים שנוצרים. נקודות ושורות או תאים חוזרים בטבלה ממוקדים באינדקס הנתון המקורי באמצעות 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