Line Chart

A composable line chart with tooltips, markers, and hover interactions

Preview

Installation

pnpm dlx shadcn@latest add https://ui.bklit.com/r/line-chart.json

Usage

The chart uses a composable API where you build charts by combining components. This design allows you to mix and match features while keeping your code clean and maintainable.

Basic Example

The simplest chart with a single line:

import { LineChart, Line, ChartTooltip } from "@bklitui/ui/charts";

const data = [
  { date: new Date("2025-01-01"), users: 1200 },
  { date: new Date("2025-01-02"), users: 1350 },
  { date: new Date("2025-01-03"), users: 1100 },
  // ... more data points
];

export default function SimpleChart() {
  return (
    <LineChart data={data}>
      <Line dataKey="users" />
      <ChartTooltip />
    </LineChart>
  );
}

Multiple Lines

Display multiple metrics on the same chart:

import { LineChart, Line, Grid, XAxis, ChartTooltip } from "@bklitui/ui/charts";

const data = [
  { date: new Date("2025-01-01"), users: 1200, pageviews: 4500, sessions: 800 },
  { date: new Date("2025-01-02"), users: 1350, pageviews: 4800, sessions: 920 },
  // ... more data
];

export default function MultiLineChart() {
  return (
    <LineChart data={data}>
      <Grid horizontal />
      <Line dataKey="users" stroke="var(--chart-line-primary)" />
      <Line dataKey="pageviews" stroke="var(--chart-line-secondary)" />
      <Line dataKey="sessions" stroke="#10b981" />
      <XAxis />
      <ChartTooltip />
    </LineChart>
  );
}

Custom Data Key

By default, the chart uses date as the x-axis key. Use xDataKey for different field names:

// Data with timestamp instead of date
const apiData = [
  { timestamp: new Date("2025-01-01"), value: 100 },
  { timestamp: new Date("2025-01-02"), value: 150 },
];

<LineChart data={apiData} xDataKey="timestamp">
  <Line dataKey="value" />
  <ChartTooltip />
</LineChart>

Chart Sizing

Control the chart's aspect ratio and add custom styling:

// Wide chart (3:1 aspect ratio)
<LineChart data={data} aspectRatio="3 / 1">
  <Line dataKey="users" />
</LineChart>

// Square chart
<LineChart data={data} aspectRatio="1 / 1">
  <Line dataKey="users" />
</LineChart>

// With custom margins for more breathing room
<LineChart
  data={data}
  margin={{ top: 60, right: 60, bottom: 60, left: 60 }}
>
  <Line dataKey="users" />
</LineChart>

// Compact margins for dense layouts
<LineChart
  data={data}
  margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
>
  <Line dataKey="users" />
</LineChart>

Line Customization

Fine-tune each line's appearance:

import { curveStep, curveMonotoneX, curveBasis } from "@visx/curve";

<LineChart data={data}>
  {/* Thick primary line */}
  <Line
    dataKey="users"
    stroke="var(--chart-line-primary)"
    strokeWidth={3}
  />

  {/* Stepped line for discrete data */}
  <Line
    dataKey="sessions"
    stroke="#10b981"
    curve={curveStep}
    strokeWidth={2}
  />

  {/* Smooth monotonic curve */}
  <Line
    dataKey="pageviews"
    stroke="var(--chart-line-secondary)"
    curve={curveMonotoneX}
  />

  {/* Disable edge fading */}
  <Line
    dataKey="revenue"
    stroke="#f59e0b"
    fadeEdges={false}
  />

  {/* No hover highlight */}
  <Line
    dataKey="conversions"
    stroke="#ef4444"
    showHighlight={false}
  />

  <ChartTooltip />
</LineChart>

Grid Variations

Configure grid lines for different visual styles:

// Horizontal grid only (default)
<LineChart data={data}>
  <Grid horizontal />
  <Line dataKey="users" />
</LineChart>

// Vertical grid only
<LineChart data={data}>
  <Grid vertical horizontal={false} />
  <Line dataKey="users" />
</LineChart>

// Both horizontal and vertical
<LineChart data={data}>
  <Grid horizontal vertical />
  <Line dataKey="users" />
</LineChart>

// Custom grid density
<LineChart data={data}>
  <Grid
    horizontal
    vertical
    numTicksRows={10}
    numTicksColumns={15}
  />
  <Line dataKey="users" />
</LineChart>

// Solid grid lines instead of dashed
<LineChart data={data}>
  <Grid
    horizontal
    strokeDasharray=""
    stroke="var(--border)"
  />
  <Line dataKey="users" />
</LineChart>

Animation Control

Customize or disable animations:

// Faster animation (500ms)
<LineChart data={data} animationDuration={500}>
  <Line dataKey="users" animate />
  <ChartTooltip />
</LineChart>

// Slower, dramatic entrance (2 seconds)
<LineChart data={data} animationDuration={2000}>
  <Line dataKey="users" animate />
  <ChartTooltip />
</LineChart>

// Disable all line animation
<LineChart data={data}>
  <Line dataKey="users" animate={false} />
  <ChartTooltip />
</LineChart>

Tooltip Customization

Hide Specific Tooltip Elements

// Minimal tooltip - just the content box
<LineChart data={data}>
  <Line dataKey="users" />
  <ChartTooltip
    showCrosshair={false}
    showDots={false}
    showDatePill={false}
  />
</LineChart>

// Crosshair only, no date pill
<LineChart data={data}>
  <Line dataKey="users" />
  <ChartTooltip showDatePill={false} />
</LineChart>

Custom Row Labels

<LineChart data={data}>
  <Line dataKey="users" stroke="var(--chart-line-primary)" />
  <Line dataKey="pageviews" stroke="var(--chart-line-secondary)" />
  <ChartTooltip
    rows={(point) => [
      {
        color: "var(--chart-line-primary)",
        label: "Active Users",
        value: point.users.toLocaleString(),
      },
      {
        color: "var(--chart-line-secondary)",
        label: "Page Views",
        value: point.pageviews.toLocaleString(),
      },
    ]}
  />
</LineChart>

Fully Custom Content

<LineChart data={data}>
  <Line dataKey="users" />
  <Line dataKey="pageviews" />
  <ChartTooltip
    content={({ point, index }) => (
      <div className="flex flex-col gap-2 p-3">
        <div className="text-sm font-medium">
          {point.date.toLocaleDateString("en-US", {
            weekday: "short",
            month: "short",
            day: "numeric",
          })}
        </div>
        <div className="grid grid-cols-2 gap-x-4 gap-y-1 text-sm">
          <span className="text-muted-foreground">Users</span>
          <span className="font-mono">{point.users.toLocaleString()}</span>
          <span className="text-muted-foreground">Views</span>
          <span className="font-mono">{point.pageviews.toLocaleString()}</span>
          <span className="text-muted-foreground">Ratio</span>
          <span className="font-mono">
            {(point.pageviews / point.users).toFixed(2)}x
          </span>
        </div>
      </div>
    )}
  />
</LineChart>

Real-World Examples

Dashboard Metrics Card

import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { LineChart, Line, ChartTooltip } from "@bklitui/ui/charts";

export function MetricsCard({ title, data, dataKey, trend }) {
  const latestValue = data[data.length - 1]?.[dataKey] ?? 0;

  return (
    <Card>
      <CardHeader className="pb-2">
        <CardTitle className="text-sm font-medium text-muted-foreground">
          {title}
        </CardTitle>
        <div className="flex items-baseline gap-2">
          <span className="text-2xl font-bold">
            {latestValue.toLocaleString()}
          </span>
          <span className={trend >= 0 ? "text-green-500" : "text-red-500"}>
            {trend >= 0 ? "↑" : "↓"} {Math.abs(trend)}%
          </span>
        </div>
      </CardHeader>
      <CardContent>
        <LineChart
          data={data}
          aspectRatio="3 / 1"
          margin={{ top: 10, right: 10, bottom: 10, left: 10 }}
        >
          <Line dataKey={dataKey} strokeWidth={2} />
          <ChartTooltip showDatePill={false} />
        </LineChart>
      </CardContent>
    </Card>
  );
}

Comparative Analytics

import { LineChart, Line, Grid, XAxis, ChartTooltip } from "@bklitui/ui/charts";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";

export function AnalyticsDashboard({ data }) {
  return (
    <Tabs defaultValue="overview">
      <TabsList>
        <TabsTrigger value="overview">Overview</TabsTrigger>
        <TabsTrigger value="traffic">Traffic</TabsTrigger>
        <TabsTrigger value="engagement">Engagement</TabsTrigger>
      </TabsList>

      <TabsContent value="overview">
        <LineChart data={data}>
          <Grid horizontal />
          <Line dataKey="users" stroke="var(--chart-line-primary)" />
          <Line dataKey="pageviews" stroke="var(--chart-line-secondary)" />
          <XAxis />
          <ChartTooltip
            rows={(point) => [
              { color: "var(--chart-line-primary)", label: "Users", value: point.users },
              { color: "var(--chart-line-secondary)", label: "Views", value: point.pageviews },
            ]}
          />
        </LineChart>
      </TabsContent>

      <TabsContent value="traffic">
        <LineChart data={data}>
          <Grid horizontal vertical />
          <Line dataKey="pageviews" stroke="var(--chart-line-primary)" strokeWidth={3} />
          <XAxis />
          <ChartTooltip />
        </LineChart>
      </TabsContent>

      <TabsContent value="engagement">
        <LineChart data={data}>
          <Grid horizontal />
          <Line dataKey="sessions" stroke="#10b981" />
          <Line dataKey="bounceRate" stroke="#ef4444" />
          <XAxis />
          <ChartTooltip />
        </LineChart>
      </TabsContent>
    </Tabs>
  );
}

With Loading State

import { LineChart, Line, Grid, ChartTooltip } from "@bklitui/ui/charts";
import { Skeleton } from "@/components/ui/skeleton";

export function ChartWithLoading({ data, isLoading }) {
  if (isLoading) {
    return <Skeleton className="h-[300px] w-full" />;
  }

  if (!data || data.length === 0) {
    return (
      <div className="flex h-[300px] items-center justify-center text-muted-foreground">
        No data available
      </div>
    );
  }

  return (
    <LineChart data={data}>
      <Grid horizontal />
      <Line dataKey="value" />
      <ChartTooltip />
    </LineChart>
  );
}

Data Formatting Tips

Working with API Responses

// Transform API data with string dates
const apiResponse = [
  { timestamp: "2025-01-01T00:00:00Z", count: 1200 },
  { timestamp: "2025-01-02T00:00:00Z", count: 1350 },
];

const chartData = apiResponse.map(item => ({
  date: new Date(item.timestamp),
  users: item.count,
}));

<LineChart data={chartData}>
  <Line dataKey="users" />
</LineChart>

Aggregating Data

// Group hourly data into daily averages
function aggregateToDaily(hourlyData) {
  const grouped = {};

  for (const point of hourlyData) {
    const dateKey = point.date.toDateString();
    if (!grouped[dateKey]) {
      grouped[dateKey] = { date: new Date(dateKey), values: [] };
    }
    grouped[dateKey].values.push(point.value);
  }

  return Object.values(grouped).map(group => ({
    date: group.date,
    value: group.values.reduce((a, b) => a + b, 0) / group.values.length,
  }));
}

const dailyData = aggregateToDaily(hourlyData);

<LineChart data={dailyData}>
  <Line dataKey="value" />
</LineChart>

Accessibility

The chart includes several accessibility features:

  • SVG elements are marked with aria-hidden="true" as they are decorative
  • Keyboard users can navigate the tooltip using standard browser interactions
  • Color alone is not used to convey information—values are always displayed in the tooltip

For maximum accessibility, provide a text summary or data table alongside the chart:

export function AccessibleChart({ data }) {
  return (
    <div>
      <LineChart data={data}>
        <Line dataKey="users" />
        <ChartTooltip />
      </LineChart>

      {/* Screen reader summary */}
      <p className="sr-only">
        Chart showing user growth from {data[0].users.toLocaleString()} to{" "}
        {data[data.length - 1].users.toLocaleString()} users over{" "}
        {data.length} days.
      </p>
    </div>
  );
}

Components

LineChart

The root component that provides context to all children.

PropTypeDefaultDescription
dataRecord<string, unknown>[]requiredArray of data points
xDataKeystring"date"Key in data for x-axis values
marginPartial<Margin>{ top: 40, right: 40, bottom: 40, left: 40 }Chart margins
animationDurationnumber1100Animation duration in ms
aspectRatiostring"2 / 1"CSS aspect ratio
classNamestring""Additional CSS class

Line

Renders a line on the chart.

PropTypeDefaultDescription
dataKeystringrequiredKey in data for y values
strokestringvar(--chart-line-primary)Line color
strokeWidthnumber2.5Line width
curveCurveFactorycurveNaturalD3 curve function
animatebooleantrueEnable grow animation
fadeEdgesbooleantrueFade line at edges
showHighlightbooleantrueShow highlight on hover

Grid

Renders grid lines.

PropTypeDefaultDescription
horizontalbooleantrueShow horizontal lines
verticalbooleanfalseShow vertical lines
numTicksRowsnumber5Number of horizontal lines
numTicksColumnsnumber10Number of vertical lines
strokestringvar(--chart-grid)Line color
strokeDasharraystring"4,4"Dash pattern

XAxis

Renders x-axis labels that fade when the crosshair passes.

PropTypeDefaultDescription
numTicksnumber6Number of tick labels
tickerHalfWidthnumber50Fade radius for labels

ChartTooltip

Renders the tooltip with crosshair, dots, and content box.

PropTypeDefaultDescription
showDatePillbooleantrueShow animated date ticker
showCrosshairbooleantrueShow vertical crosshair
showDotsbooleantrueShow dots on lines
content(props) => ReactNode-Custom content renderer
rows(point) => TooltipRow[]-Custom row generator

Markers

Add markers to annotate specific dates on the chart:

import { LineChart, Line, ChartTooltip, ChartMarkers, MarkerTooltipContent, useActiveMarkers, type ChartMarker } from "@bklitui/ui/charts";

const markers: ChartMarker[] = [
  {
    date: new Date("2025-01-05"),
    icon: "🚀",
    title: "v1.2.0 Released",
    description: "New chart animations",
  },
  {
    date: new Date("2025-01-05"), // Same day - will stack!
    icon: "🐛",
    title: "Bug Fix",
    description: "Fixed tooltip positioning",
  },
];

function MyChart({ data }) {
  return (
    <LineChart data={data}>
      <Line dataKey="users" />
      <ChartMarkers items={markers} />
      <ChartTooltip>
        <MarkerContent markers={markers} />
      </ChartTooltip>
    </LineChart>
  );
}

// Use the hook to get markers for the hovered date
function MarkerContent({ markers }) {
  const activeMarkers = useActiveMarkers(markers);
  if (activeMarkers.length === 0) return null;
  return <MarkerTooltipContent markers={activeMarkers} />;
}

ChartMarker Interface

interface ChartMarker {
  date: Date;           // Date for marker position
  icon: React.ReactNode; // Icon (emoji or component)
  title: string;        // Tooltip title
  description?: string; // Optional description
  content?: React.ReactNode; // Custom tooltip content
  color?: string;       // Background color override
  onClick?: () => void; // Click handler
  href?: string;        // URL to navigate to
  target?: "_blank" | "_self"; // Link target
}

ChartMarkers Props

PropTypeDefaultDescription
itemsChartMarker[]requiredArray of markers
sizenumber28Marker circle size
showLinesbooleantrueShow vertical guide lines
animatebooleantrueAnimate markers on entrance

Custom Tooltip Content

You can fully customize the tooltip content:

<ChartTooltip
  content={({ point, index }) => (
    <div className="p-3">
      <h3 className="font-bold">{point.date.toLocaleDateString()}</h3>
      <p>Users: {point.users}</p>
      <p>Views: {point.pageviews}</p>
    </div>
  )}
/>

Or customize just the rows:

<ChartTooltip
  rows={(point) => [
    { color: "var(--chart-line-primary)", label: "Active Users", value: point.users },
    { color: "var(--chart-line-secondary)", label: "Page Views", value: point.pageviews },
  ]}
/>

Segment Selection

Add click-drag and touch segment selection to your chart with composable components. The line highlight automatically shows the selected path segment.

2,400users
+1,200 (+100.0%)Feb 11Mar 12

Basic Usage

Click and drag (or two-finger touch on mobile) to select a range. The three segment components are fully composable -- use all of them or just the ones you need:

import { LineChart, Line, Grid, XAxis, ChartTooltip, SegmentBackground, SegmentLineFrom, SegmentLineTo } from "@bklitui/ui/charts";

<LineChart data={data}>
  <Grid horizontal />
  <Line dataKey="users" />
  <SegmentBackground />
  <SegmentLineFrom />
  <SegmentLineTo />
  <XAxis />
  <ChartTooltip />
</LineChart>

Line Variants

Each segment boundary line supports three visual styles:

{/* Dashed lines (default) */}
<SegmentLineFrom variant="dashed" />
<SegmentLineTo variant="dashed" />

{/* Solid lines */}
<SegmentLineFrom variant="solid" />
<SegmentLineTo variant="solid" />

{/* Gradient fade (transparent at top/bottom edges) */}
<SegmentLineFrom variant="gradient" />
<SegmentLineTo variant="gradient" />

Background Only

Use just the background highlight without boundary lines:

<LineChart data={data}>
  <Line dataKey="users" />
  <SegmentBackground />
  <ChartTooltip />
</LineChart>

Lines Only

Use just the boundary lines without the background:

<LineChart data={data}>
  <Line dataKey="users" />
  <SegmentLineFrom variant="solid" />
  <SegmentLineTo variant="solid" />
  <ChartTooltip />
</LineChart>

Reading Selection Data

Use the useChart hook inside a child component to read the active selection and update external UI (like a header card):

import { useChart } from "@bklitui/ui/charts";

function SelectionStats({ onSelectionChange }) {
  const { selection, data, xAccessor } = useChart();

  useEffect(() => {
    if (!selection?.active) {
      onSelectionChange(null);
      return;
    }

    const startPoint = data[selection.startIndex];
    const endPoint = data[selection.endIndex];
    // Compute and report stats...
    onSelectionChange({ startPoint, endPoint });
  }, [selection, data, xAccessor, onSelectionChange]);

  return null;
}

SegmentBackground

PropTypeDefaultDescription
fillstringvar(--chart-segment-background)Fill color for the selected region

SegmentLineFrom / SegmentLineTo

PropTypeDefaultDescription
strokestringvar(--chart-segment-line)Line color
strokeWidthnumber1Line width
variant"dashed" | "solid" | "gradient""dashed"Line style

Theming

The chart uses CSS variables for theming. Define these in your CSS:

:root {
  --chart-background: oklch(1 0 0);
  --chart-foreground: oklch(0.145 0.004 285);
  --chart-foreground-muted: oklch(0.55 0.014 260);
  --chart-line-primary: oklch(0.623 0.214 255);
  --chart-line-secondary: oklch(0.705 0.015 265);
  --chart-crosshair: oklch(0.4 0.1828 274.34);
  --chart-grid: oklch(0.9 0 0);
  --chart-tooltip-foreground: oklch(0.985 0 0);
  --chart-tooltip-muted: oklch(0.65 0.01 260);
  --chart-marker-background: oklch(0.97 0.005 260);
  --chart-marker-border: oklch(0.85 0.01 260);
  --chart-marker-foreground: oklch(0.3 0.01 260);
  --chart-marker-badge-background: oklch(0 0 0);
  --chart-marker-badge-foreground: oklch(1 0 0);
  --chart-segment-background: oklch(0.5 0 0 / 0.06);
  --chart-segment-line: oklch(0.5 0 0 / 0.25);
}

.dark {
  --chart-background: oklch(0.145 0 0);
  --chart-foreground: oklch(0.45 0 0);
  --chart-crosshair: oklch(0.45 0 0);
  --chart-grid: oklch(0.25 0 0);
  --chart-marker-background: oklch(0.25 0.01 260);
  --chart-marker-border: oklch(0.4 0.01 260);
  --chart-marker-foreground: oklch(0.9 0 0);
  --chart-marker-badge-background: oklch(1 0 0);
  --chart-marker-badge-foreground: oklch(0.15 0 0);
  --chart-segment-background: oklch(1 0 0 / 0.06);
  --chart-segment-line: oklch(1 0 0 / 0.25);
}

Dependencies

This component requires the following packages:

pnpm add @visx/shape @visx/curve @visx/scale @visx/gradient @visx/responsive @visx/event @visx/grid d3-array motion react-use-measure