{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chart-line",
  "title": "Chart Line",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A line chart with four curves, dots or value labels, dashed series, labels at the end of each line instead of a legend and gaps for missing data, in a frame with a table view, a toggling legend, patterns and states.",
  "dependencies": [
    "recharts@^3"
  ],
  "registryDependencies": [
    "https://ui.ballmac.com/r/chart.json",
    "https://ui.ballmac.com/r/chart-frame.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/chart-line.tsx",
      "content": "// Ballmac UI: Chart Line. https://ui.ballmac.com/components/chart-line\n\"use client\"\n\nimport * as React from \"react\"\nimport { CartesianGrid, LabelList, Line, LineChart, ReferenceLine, usePlotArea, useXAxisScale, useYAxisScale, XAxis, YAxis } from \"recharts\"\n\nimport { ChartTooltip, ChartTooltipContent } from \"@/components/ballmac/chart\"\nimport { ChartFrame, domainTicks, niceScale, type ChartFrameProps, type ChartRow, type ChartSeries } from \"@/components/ballmac/chart-frame\"\n\ntype ChartLineProps = Omit<ChartFrameProps, \"children\" | \"summaryMode\" | \"skeleton\" | \"table\"> & {\n  /** How the line moves between points. */\n  curve?: \"monotone\" | \"linear\" | \"step\" | \"natural\"\n  /** Draw a dot on every point. */\n  dots?: boolean\n  /** Series keys drawn dashed, for forecasts or targets: dashes tell lines apart without color. */\n  dashed?: string[]\n  /** Write each series' name at the end of its line, so people read it without finding the legend. */\n  directLabels?: boolean\n  /** Write the value on every point. */\n  labels?: boolean\n  /** Keep the gap where a value is missing instead of joining the points either side. */\n  gaps?: boolean\n  grid?: \"horizontal\" | \"both\" | \"none\"\n  yAxis?: boolean\n  yFormatter?: (value: number) => string\n  yDomain?: [number | \"auto\", number | \"auto\"]\n  referenceLines?: { y: number; label: string }[]\n  tooltip?: \"line\" | \"dot\" | \"dashed\" | \"none\"\n}\n\nconst DASH = \"6 5\"\n\n/** Each line's name at its last point, pushed apart where two lines end close together. Lives inside the chart to read its scales. */\nfunction EndLabels({ data, xKey, series, rtl, color }: { data: ChartRow[]; xKey: string; series: ChartSeries[]; rtl: boolean; color: (key: string) => string }) {\n  const xScale = useXAxisScale()\n  const yScale = useYAxisScale()\n  const plot = usePlotArea()\n  const last = data.at(-1)\n  if (!xScale || !yScale || !plot || !last) return null\n  const x = xScale(last[xKey] as never)\n  if (typeof x !== \"number\") return null\n  const gap = 15\n  const items = series\n    .map((s) => ({ s, y: yScale(last[s.key] as never) }))\n    .filter((i): i is { s: ChartSeries; y: number } => typeof i.y === \"number\" && Number.isFinite(i.y))\n    .sort((a, b) => a.y - b.y)\n  const placed = items.map((i) => ({ ...i, at: i.y }))\n  for (let i = 1; i < placed.length; i++) if (placed[i]!.at - placed[i - 1]!.at < gap) placed[i]!.at = placed[i - 1]!.at + gap\n  const bottom = plot.y + plot.height\n  for (let i = placed.length - 1; i >= 0; i--) {\n    const limit = i === placed.length - 1 ? bottom : placed[i + 1]!.at - gap\n    if (placed[i]!.at > limit) placed[i]!.at = limit\n  }\n  return (\n    <g aria-hidden=\"true\">\n      {placed.map(({ s, at }) => (\n        <text key={s.key} x={rtl ? x - 10 : x + 10} y={at} dy={4} textAnchor={rtl ? \"end\" : \"start\"} className=\"text-xs font-medium\" fill={color(s.key)}>\n          {s.label}\n        </text>\n      ))}\n    </g>\n  )\n}\n\n/**\n * A line chart in a ChartFrame: four curves, dots or value labels, dashed series, labels at the end of each line instead of a legend, a table view,\n * a toggling legend, patterns (as dash styles), loading, empty and error states and mirroring in right-to-left.\n */\nfunction ChartLine({\n  data,\n  xKey,\n  series,\n  curve = \"monotone\",\n  dots = false,\n  dashed = [],\n  directLabels = false,\n  labels = false,\n  gaps = false,\n  grid = \"horizontal\",\n  yAxis = true,\n  yFormatter,\n  yDomain,\n  referenceLines,\n  tooltip = \"line\",\n  legend,\n  ...frame\n}: ChartLineProps) {\n  return (\n    <ChartFrame data={data} xKey={xKey} series={series} summaryMode=\"trend\" skeleton=\"lines\" legend={legend ?? (directLabels ? \"none\" : undefined)} {...frame}>\n      {(chart) => {\n        const compact = new Intl.NumberFormat(chart.locale, { notation: \"compact\", maximumFractionDigits: 1 })\n        const last = data.length - 1\n        const scale = yDomain ? undefined : niceScale({ data, keys: chart.visible, zero: false })\n        return (\n          <LineChart data={data} margin={chart.margin({ top: labels ? 22 : 8, right: directLabels ? 88 : 12, left: 0, bottom: 0 })} accessibilityLayer>\n            <defs>{chart.defs}</defs>\n            <CartesianGrid vertical={grid === \"both\"} horizontal={grid !== \"none\"} />\n            <XAxis dataKey={xKey} tickLine={false} axisLine={false} tickMargin={8} minTickGap={24} reversed={chart.rtl} tickFormatter={(v: unknown) => chart.formatCategory(v)} padding={{ left: 8, right: 8 }} />\n            {yAxis ? <YAxis width=\"auto\" tickLine={false} axisLine={false} tickMargin={8} orientation={chart.rtl ? \"right\" : \"left\"} domain={yDomain ?? scale?.domain} ticks={scale?.ticks ?? domainTicks(yDomain)} tickFormatter={(v: number) => (yFormatter ? yFormatter(v) : compact.format(v))} /> : null}\n            {referenceLines?.map((r) => (\n              <ReferenceLine key={r.label} y={r.y} stroke=\"var(--muted-foreground)\" strokeDasharray=\"4 4\" strokeOpacity={0.7} label={{ value: r.label, position: chart.rtl ? \"insideTopLeft\" : \"insideTopRight\", fill: \"var(--muted-foreground)\", fontSize: 11 }} />\n            ))}\n            {tooltip !== \"none\" ? <ChartTooltip cursor={{ strokeDasharray: \"3 3\" }} content={<ChartTooltipContent indicator={tooltip} />} /> : null}\n            {series.map((s, i) => (\n              <Line\n                key={s.key}\n                dataKey={s.key}\n                type={curve}\n                hide={chart.isHidden(s.key)}\n                stroke={chart.color(s.key)}\n                strokeWidth={2.25}\n                // with patterns on, lines are told apart by their dashes\n                strokeDasharray={dashed.includes(s.key) ? DASH : chart.patterns && i % 2 === 1 ? \"2 4\" : undefined}\n                strokeLinecap=\"round\"\n                dot={dots ? { r: 3, strokeWidth: 2, fill: \"var(--card)\" } : false}\n                activeDot={{ r: 4.5 }}\n                connectNulls={!gaps}\n                isAnimationActive={chart.animate}\n                animationDuration={700}\n              >\n                {labels ? <LabelList dataKey={s.key} position=\"top\" offset={8} className=\"fill-foreground text-xs\" formatter={(v: unknown) => (typeof v === \"number\" ? chart.format(v, s.key) : \"\")} /> : null}\n              </Line>\n            ))}\n            {directLabels ? <EndLabels data={data} xKey={xKey} series={series.filter((s) => !chart.isHidden(s.key))} rtl={chart.rtl} color={chart.color} /> : null}\n          </LineChart>\n        )\n      }}\n    </ChartFrame>\n  )\n}\n\nexport { ChartLine, type ChartLineProps }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/chart-line.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "chart",
      "line",
      "trend",
      "timeseries",
      "forecast",
      "recharts",
      "graph"
    ],
    "version": "1.0.0",
    "updated": "2026-10-18",
    "ai": {
      "summary": "<ChartLine title description data xKey series={[{ key, label, color? }]} />. curve: monotone | linear | step | natural. Also dots, dashed (series keys), directLabels (write names at the line ends and drop the legend), labels (values on points), gaps (null breaks the line), yFormatter, yDomain, referenceLines, legend, patterns, state and every ChartFrame prop.",
      "whenToUse": [
        "Trends where the exact path matters and fills would get in the way",
        "Several series that cross (use directLabels)",
        "Forecast against actual (dashed)"
      ],
      "whenNotToUse": [
        "Part-to-whole over time (use chart-area with stack)",
        "Comparing a few categories (use chart-bar)"
      ],
      "composesWith": [
        "chart-frame",
        "stat-card",
        "segmented-control"
      ],
      "a11y": [
        {
          "keys": "Arrow keys",
          "action": "Move between data points (accessibilityLayer)"
        },
        {
          "keys": "Tab, Enter / Space",
          "action": "Legend buttons switch series; the Chart / Table switch shows the numbers as a table"
        },
        {
          "keys": "Dashes",
          "action": "Dashed lines tell series apart without color; patterns add dash styles to every other line"
        }
      ],
      "customization": [
        "curve, dots and strokeWidth",
        "dashed for forecasts",
        "directLabels instead of a legend",
        "gaps to keep missing data visible"
      ]
    },
    "examples": [
      "chart-line-demo",
      "chart-line-dots",
      "chart-line-dashed",
      "chart-line-direct",
      "chart-line-labels",
      "chart-line-step",
      "chart-line-gaps",
      "chart-line-compare"
    ],
    "url": "https://ui.ballmac.com/components/chart-line"
  },
  "categories": [
    "data-display"
  ],
  "type": "registry:ui"
}