Skip to content
Domphy

chartLineDots

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

Props

PropTypeDescription
titlestring
descriptionstring
seriesLabelstring
seriesColorThemeColor
dataMonthlyPoint[]
dotRadiusnumber
activeDotRadiusnumber
trendHeadlinestring
trendSubtitlestring
trendDirection"up" | "down"
Implementation notes

Resting dots use @domphy/chart's built-in line-symbol renderer (showSymbol/symbolSize). The hover-enlarging active dot is a genuine custom feature (not a static approximation): a companion SVG overlay (hoverDotOverlay in chart-line-shared.ts) listens for mousemove on the plot wrapper, finds the nearest column using the SAME public scale factories (createOrdinalScale/createLinearScale, exported from @domphy/chart) and an explicit shared grid/y-domain so it lands pixel-exact on the line, then grows a circle into view — because @domphy/chart's built-in symbol renderer has no per-point hover state or size override. Minor gap: the overlay's fill color is resolved once at mount via themeColorToken(null, ...) (design-time), so it will not live-update on a runtime light/dark theme toggle — the same non-reactivity already exists throughout @domphy/chart's own axis/grid color resolution, so this isn't a new regression. Direct-source-diff fix (2026-07-05): Resting point markers used the chart engine's built-in symbol, which is a hardcoded hollow white-fill donut — upstream's dots are solid-filled. Replaced with a solid-filled marker overlay.

Status: ported · Reference: shadcn/ui original

// shadcn/ui "charts/line-dots" block — clean-room reimplementation.
//
// The default smooth single-line chart with a filled circular dot at every
// data point, plus a larger marker that grows in near the cursor on hover.
// The resting dots come from @domphy/chart's built-in line-symbol renderer;
// the enlarging hover marker is a small companion SVG overlay (see
// ./chart-line-shared.ts for why one is needed) positioned with the same
// public scale factories the engine itself uses.
//
// Implemented purely from the block's public functional/visual spec — no
// upstream shadcn/ui source was viewed or copied.

import type { ChartOption } from "@domphy/chart";
import type { DomphyElement } from "@domphy/core";
import { type ThemeColor, themeColor } from "@domphy/theme";
import {
  chartCard,
  chartLineSeriesColor,
  chartPlot,
  computeYDomain,
  DEFAULT_LINE_GRID,
  hiddenLabelYAxis,
  hoverDotOverlay,
  lineSwatchLabelValueTooltipFormatter,
  MONTHLY_VISITOR_DATA,
  type MonthlyPoint,
  monthCategoryXAxis,
  staticPointMarkersOverlay,
  trendFooter,
} from "./chart-line-shared.js";

// Recharts renders `dot={{ fill }}` (no explicit r) at its default radius of 3,
// and `activeDot={{ r: 6 }}` grows the hover marker to 6.
const REST_DOT_RADIUS = 3;
const ACTIVE_DOT_RADIUS = 6;

/** Props for {@link chartLineDots}. */
export interface ChartLineDotsProps {
  title?: string;
  description?: string;
  seriesLabel?: string;
  seriesColor?: ThemeColor;
  data?: MonthlyPoint[];
  dotRadius?: number;
  activeDotRadius?: number;
  trendHeadline?: string;
  trendSubtitle?: string;
  trendDirection?: "up" | "down";
}

/**
 * shadcn/ui "charts/line-dots" — the default single-line chart with a dot at
 * every point and a hover-enlarging active marker. Call with no arguments
 * for a fully working demo.
 */
function chartLineDots(props: ChartLineDotsProps = {}): DomphyElement<"div"> {
  const {
    title = "Line Chart - Dots",
    description = "January - June 2026",
    seriesLabel = "Desktop",
    seriesColor = "primary",
    data = MONTHLY_VISITOR_DATA,
    dotRadius = REST_DOT_RADIUS,
    activeDotRadius = ACTIVE_DOT_RADIUS,
    trendHeadline = "Trending up by 5.2% this month",
    trendSubtitle = "Showing total visitors for the last 6 months",
    trendDirection = "up",
  } = props;

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

  const option: ChartOption = {
    grid: DEFAULT_LINE_GRID,
    xAxis: monthCategoryXAxis(categories),
    yAxis: hiddenLabelYAxis(yDomain),
    tooltip: {
      trigger: "axis",
      axisPointer: { type: "none" },
      // Upstream `<ChartTooltipContent hideLabel />`: hides the axis (month)
      // label but still shows the color swatch + "Desktop" series label + value.
      formatter: lineSwatchLabelValueTooltipFormatter,
    },
    series: [
      {
        type: "line",
        name: seriesLabel,
        data: values,
        smooth: true,
        // Resting dots are drawn by the overlay below as solid filled circles
        // (upstream `dot={{ fill: color }}`); the engine's built-in line symbol
        // is a hollow white-fill circle, so it is disabled here.
        showSymbol: false,
        lineStyle: { width: 2, opacity: ramp.strokeOpacity },
        color: seriesColor,
      },
    ],
  };

  return chartCard({
    title,
    description,
    plot: chartPlot({
      option,
      overlays: [
        staticPointMarkersOverlay({
          categories,
          values,
          yDomain,
          grid: DEFAULT_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(dotRadius));
            circle.setAttribute("fill", dotFill);
            group.appendChild(circle);
          },
        }),
        hoverDotOverlay({
          categories,
          values,
          yDomain,
          grid: DEFAULT_LINE_GRID,
          color: seriesColor,
          tone: ramp.tone,
          radius: activeDotRadius,
        }),
      ],
    }),
    footer: trendFooter({
      headline: trendHeadline,
      subtitle: trendSubtitle,
      direction: trendDirection,
    }),
  });
}

export { chartLineDots };

← Back to shadcn/ui catalog