Skip to content
Domphy

chartLineLabelCustom

A Charts block/component from shadcn/ui — clean-room reimplemented for Domphy (see methodology). Call chartLineLabelCustom() with no arguments for a working demo, or edit the code below live.

Props

PropTypeDescription
titlestring
descriptionstring
seriesColorThemeColor
dataCategoryPoint[]
trendHeadlinestring
trendSubtitlestring
trendDirection"up" | "down"
Implementation notes

Colored per-point dots via the same staticPointMarkersOverlay technique as chartLineDotsColors (see its notes on why a custom overlay is needed). The always-on display-name label reuses the engine's native label.formatter (a genuine built-in feature) with a formatter that looks up each point's friendly name from the data array by dataIndex, rather than showing the raw category key or numeric value. Direct-source-diff fix (2026-07-05): Line stroke was neutral gray (should be secondary/chart-2) and per-point dots were multicolored by browser — upstream's 'custom' variant is the per-point NAME label, with uniform chart-2 dots, not multicolor ones. Both fixed.

Status: ported · Reference: shadcn/ui original

// shadcn/ui "charts/line-label-custom" block — clean-room reimplementation.
//
// A categorical single line (uniform accent color, uniform-color dots) with
// an always-visible text label above every point showing that category's
// display name (looked up from the data, not the raw numeric value) instead
// of hiding the axis with no label at all. Unlike chartLineDotsColors, the
// dots here are one uniform accent color — the "custom" part is the label,
// not per-point dot colors.
//
// Implemented purely from the block's public functional/visual spec — no
// upstream shadcn/ui source was viewed or copied.

import type { ChartOption, TooltipParams } from "@domphy/chart";
import type { DomphyElement } from "@domphy/core";
import { type ThemeColor, themeColor } from "@domphy/theme";
import {
  BROWSER_CATEGORY_DATA,
  type CategoryPoint,
  chartCard,
  chartLineSeriesColor,
  chartPlot,
  computeYDomain,
  HIDDEN_AXIS_LINE_GRID,
  hiddenLabelYAxis,
  hiddenXAxis,
  hoverDotOverlay,
  staticPointMarkersOverlay,
  tooltipRow,
  trendFooter,
} from "./chart-line-shared.js";

const REST_DOT_RADIUS = 5;
// Upstream activeDot={{ r: 6 }} — the hover dot is 6px, not larger.
const ACTIVE_DOT_RADIUS = 6;

function escapeHtml(text: string): string {
  return text
    .replace(/&/g, "&")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;");
}

/** Props for {@link chartLineLabelCustom}. */
export interface ChartLineLabelCustomProps {
  title?: string;
  description?: string;
  seriesColor?: ThemeColor;
  data?: CategoryPoint[];
  trendHeadline?: string;
  trendSubtitle?: string;
  trendDirection?: "up" | "down";
}

/**
 * shadcn/ui "charts/line-label-custom" — the categorical colored-dots chart
 * with an always-on display-name label above each point. Call with no
 * arguments for a fully working demo.
 */
function chartLineLabelCustom(
  props: ChartLineLabelCustomProps = {},
): DomphyElement<"div"> {
  const {
    title = "Line Chart - Custom Label",
    description = "Browser share for the last 6 months",
    seriesColor = "primary",
    data = BROWSER_CATEGORY_DATA,
    trendHeadline = "Trending up by 4.8% this period",
    trendSubtitle = "Showing browser share across five platforms",
    trendDirection = "up",
  } = props;

  const categories = data.map((point) => point.key);
  const values = data.map((point) => point.value);
  const yDomain = computeYDomain(values);
  // Upstream's uniform stroke/dots are var(--chart-2) — the ramp's second step.
  const ramp = chartLineSeriesColor(1);
  const dotFill = themeColor(null, ramp.tone, seriesColor);

  // Upstream ChartTooltipContent (indicator="line", nameKey="visitors",
  // hideLabel) colors the swatch with item.payload.fill — the HOVERED point's
  // own per-browser fill (chart.tsx L205) — and renders the metric label
  // beside the value (chart.tsx L251-253). The engine's TooltipParams.color is
  // the uniform series color, so resolve the point's own color by dataIndex
  // here (matching renderMarker below), and keep the series name in the line.
  function perPointSwatchTooltipFormatter(
    params: TooltipParams | TooltipParams[],
  ): string {
    const point = Array.isArray(params) ? params[0] : params;
    if (!point) return "";
    const pointColorRaw = data[point.dataIndex]?.color ?? seriesColor;
    const pointColor =
      pointColorRaw.startsWith("#") ||
      pointColorRaw.startsWith("rgb") ||
      pointColorRaw.startsWith("var(")
        ? pointColorRaw
        : themeColor(null, ramp.tone, pointColorRaw as ThemeColor);
    const swatch = `<span style="display:inline-block;width:3px;height:12px;border-radius:2px;background:${pointColor};"></span>`;
    const label = escapeHtml(String(point.seriesName ?? point.name ?? ""));
    return tooltipRow(swatch, label, escapeHtml(String(point.value ?? "")));
  }

  const option: ChartOption = {
    grid: HIDDEN_AXIS_LINE_GRID,
    xAxis: hiddenXAxis(categories),
    yAxis: hiddenLabelYAxis(yDomain),
    tooltip: {
      trigger: "axis",
      axisPointer: { type: "none" },
      formatter: perPointSwatchTooltipFormatter,
    },
    series: [
      {
        type: "line",
        name: "Share",
        data: values,
        smooth: true,
        showSymbol: false,
        lineStyle: { width: 2, opacity: ramp.strokeOpacity },
        color: seriesColor,
        // Look up the friendly display name from `data` by dataIndex — the
        // label shows the category's name, not its raw value. Upstream
        // <LabelList className="fill-foreground"> renders it at full card
        // foreground (not the engine's muted default point-label tone).
        label: {
          show: true,
          color: "neutral",
          formatter: (params) => data[params.dataIndex]?.label ?? "",
        },
      },
    ],
  };

  return chartCard({
    title,
    description,
    plot: chartPlot({
      option,
      overlays: [
        staticPointMarkersOverlay({
          categories,
          values,
          yDomain,
          grid: HIDDEN_AXIS_LINE_GRID,
          renderMarker({ cx, cy, group }) {
            const circle = document.createElementNS(
              "http://www.w3.org/2000/svg",
              "circle",
            ) as SVGCircleElement;
            circle.setAttribute("cx", String(cx));
            circle.setAttribute("cy", String(cy));
            circle.setAttribute("r", String(REST_DOT_RADIUS));
            circle.setAttribute("fill", dotFill);
            group.appendChild(circle);
          },
        }),
        hoverDotOverlay({
          categories,
          values,
          yDomain,
          grid: HIDDEN_AXIS_LINE_GRID,
          color: seriesColor,
          tone: ramp.tone,
          radius: ACTIVE_DOT_RADIUS,
        }),
      ],
    }),
    footer: trendFooter({
      headline: trendHeadline,
      subtitle: trendSubtitle,
      direction: trendDirection,
    }),
  });
}

export { chartLineLabelCustom };

← Back to shadcn/ui catalog