Combo Chart

Combined bar and line chart card.

Combo

Bars and a line on shared axes.

Sessions · Conversion 3.7%

83,200

+9.4%
function ComboChartDemo() {
  return (
    <ComboChartCard
      data={COMBO_DATA}
      bar={COMBO_BAR}
      line={COMBO_LINE}
      ranges={COMBO_RANGES}
    />
  );
}
function ComboChartDemo() {
  return (
    <ComboChartCard
      data={COMBO_DATA}
      bar={COMBO_BAR}
      line={COMBO_LINE}
      ranges={COMBO_RANGES}
    />
  );
}

Installation

npx shadcn@latest add https://boardcn.dev/r/combo-chart-card.json
npx shadcn@latest add https://boardcn.dev/r/combo-chart-card.json

npm packages

  • recharts

BoardCN dependencies

The CLI installs these for you — you do not need to add them yourself.

Source

The file the CLI copies into your project.

components/charts/combo-chart-card.tsx
"use client";

import { useState, type ReactNode } from "react";
import {
  Bar,
  CartesianGrid,
  Cell,
  ComposedChart,
  Line,
  ResponsiveContainer,
  Tooltip,
  XAxis,
  YAxis,
} from "recharts";
import { Chip, type ChipProps } from "@/components/base/badges/chip";
import { ChartRangeControl } from "@/components/charts/chart-range-control";
import { useCountUp } from "@/hooks/use-count-up";
import { cx } from "@/utils/cx";

export type ComboChartDatum = {
  label: string;
  [key: string]: string | number;
};

export interface ComboChartSeries {
  key: string;
  label: string;
  color?: string;
  activeColor?: string;
  format?: (value: number) => string;
}

export interface ComboChartRange {
  id: string;
  label: string;
  data: ComboChartDatum[];
  headline?: number;
  delta?: number;
}

export interface ComboChartCardProps {
  title?: string;
  data: ComboChartDatum[];
  bar: ComboChartSeries;
  line: ComboChartSeries;
  headline?: number;
  delta?: number;
  /** A noninteractive period pill. */
  range?: string;
  /** Periods shown in the top-right dropdown. */
  ranges?: ComboChartRange[];
  defaultRange?: string;
  onRangeChange?: (id: string) => void;
  /** Adds the total and average tiles and gives the plot the preview's fixed height. */
  tiles?: boolean;
  className?: string;
}

type Tone = { color: string; activeColor: string };

const DEFAULT_TONES: Tone[] = [2, 6, 5, 3, 8, 7, 4, 1].map((tone) => ({
  color: `var(--color-chart-${tone})`,
  activeColor: `var(--color-chart-${tone}-active)`,
}));

const formatNumber = (value: number) => value.toLocaleString("en-US");
const formatCompact = (value: number) =>
  value >= 1000 ? `${Math.round(value / 100) / 10}K`.replace(".0K", "K") : String(Math.round(value));
const formatPercent = (value: number) => `${Math.round(value * 10) / 10}%`;

function useAnimatedNumber(value: number) {
  const precision = Number.isInteger(value)
    ? 1
    : Math.abs(value * 10 - Math.round(value * 10)) < 0.000001
      ? 10
      : 100;
  return useCountUp(Math.round(value * precision)) / precision;
}

function resolveTone(index: number, color?: string, activeColor?: string): Tone {
  if (color) {
    return {
      color,
      activeColor: activeColor ?? `color-mix(in srgb, ${color} 82%, black)`,
    };
  }
  return DEFAULT_TONES[index % DEFAULT_TONES.length];
}

function describeDelta(delta: number): { label: string; color: ChipProps["color"] } {
  const percent = Math.round(Math.abs(delta) * 1000) / 10;
  if (percent === 0) return { label: "0.0%", color: "neutral" };
  return {
    label: `${delta > 0 ? "+" : "-"}${percent}%`,
    color: delta > 0 ? "lime" : "rose",
  };
}

function ChartCard({ className, children }: { className?: string; children: ReactNode }) {
  return (
    <section
      className={cx(
        "flex h-[329px] w-full min-w-0 flex-col gap-4 rounded-2xl bg-background-secondary-default px-4 pt-4 pb-3",
        className,
      )}
    >
      {children}
    </section>
  );
}

function ChartHeader({
  label,
  value,
  format,
  delta,
  hovering,
  fadeKey,
  range,
  ranges,
  rangeId,
  onRangeChange,
}: {
  label: string;
  value: number;
  format: (value: number) => string;
  delta?: ReturnType<typeof describeDelta>;
  hovering: boolean;
  fadeKey: string;
  range?: string;
  ranges?: ComboChartRange[];
  rangeId?: string;
  onRangeChange: (id: string) => void;
}) {
  const animatedValue = useAnimatedNumber(value);

  return (
    <div className="flex w-full items-start justify-between gap-3">
      <div className="flex min-w-0 flex-1 flex-col gap-0.5">
        <p className="w-full truncate text-body-medium text-text-secondary">{label}</p>
        <div className="flex w-full items-center gap-2">
          <p
            key={fadeKey}
            className="animate-number-fade text-title-1-medium whitespace-nowrap text-text-primary tabular-nums"
          >
            {format(animatedValue)}
          </p>
          {delta && (
            <Chip variant="bold" color={delta.color} className={hovering ? "invisible" : undefined}>
              {delta.label}
            </Chip>
          )}
        </div>
      </div>
      {ranges && ranges.length > 0 ? (
        <ChartRangeControl ranges={ranges} value={rangeId} onValueChange={onRangeChange} />
      ) : range ? (
        <ChartRangeControl label={range} />
      ) : null}
    </div>
  );
}

function ChartStatTiles({
  items,
}: {
  items: Array<{ label: string; value: string; color: string; activeColor: string }>;
}) {
  return (
    <div className="-mx-2 -mb-1 grid grid-cols-2 gap-2 sm:grid-cols-6">
      {items.map((item, index) => (
        <div
          key={`${item.label}-${index}`}
          className="col-span-1 flex min-w-0 flex-col items-start gap-px rounded-2lg bg-background-inner-default px-2.5 py-2 sm:col-span-3"
        >
          <div className="flex min-w-0 max-w-full items-center gap-1.5">
            <span
              className="size-3 shrink-0 rounded-[4px] transition-colors duration-150 ease-out"
              style={{ backgroundColor: item.color }}
            />
            <span className="truncate text-body-regular text-text-secondary">{item.label}</span>
          </div>
          <span className="text-body-medium whitespace-nowrap text-text-primary tabular-nums">{item.value}</span>
        </div>
      ))}
    </div>
  );
}

function PulseDot({ cx: dotX, cy: dotY, color }: { cx?: number; cy?: number; color: string }) {
  if (dotX == null || dotY == null) return null;

  return (
    <g>
      <circle cx={dotX} cy={dotY} r={5} fill={color} opacity={0.3}>
        <animate attributeName="r" values="5;13" dur="1.4s" repeatCount="indefinite" />
        <animate attributeName="opacity" values="0.35;0" dur="1.4s" repeatCount="indefinite" />
      </circle>
      <circle
        cx={dotX}
        cy={dotY}
        r={5}
        fill={color}
        stroke="var(--color-background-secondary-default)"
        strokeWidth={3}
      />
    </g>
  );
}

const BAR_RADIUS: [number, number, number, number] = [8, 8, 8, 8];

export function ComboChartCard({
  title = "Sessions",
  data,
  bar,
  line,
  headline,
  delta,
  range,
  ranges,
  defaultRange,
  onRangeChange,
  tiles = false,
  className,
}: ComboChartCardProps) {
  const [activeIndex, setActiveIndex] = useState<number | null>(null);
  const availableRanges = ranges;
  const [selectedRangeId, setSelectedRangeId] = useState(defaultRange);
  const selectedRange =
    availableRanges?.find((item) => item.id === selectedRangeId) ?? availableRanges?.[0];

  const chartData = selectedRange?.data ?? data;
  const barSeries = bar;
  const lineSeries = line;
  const rangeHeadline = selectedRange?.headline ?? headline;
  const rangeDelta = selectedRange?.delta ?? delta;
  const barTone = resolveTone(0, barSeries.color, barSeries.activeColor);
  const lineTone = resolveTone(1, lineSeries.color, lineSeries.activeColor);
  const formatBar = barSeries.format ?? formatNumber;
  const formatLine = lineSeries.format ?? formatPercent;
  const valueAt = (key: string, index: number) => Number(chartData[index]?.[key] ?? 0);
  const barTotal = chartData.reduce((sum, row) => sum + Number(row[barSeries.key] ?? 0), 0);
  const lineAverage = chartData.length
    ? chartData.reduce((sum, row) => sum + Number(row[lineSeries.key] ?? 0), 0) / chartData.length
    : 0;
  const isHovering = activeIndex !== null && activeIndex < chartData.length;
  const currentLabel = isHovering ? String(chartData[activeIndex].label) : title;
  const currentHeadline = isHovering
    ? valueAt(barSeries.key, activeIndex)
    : (rangeHeadline ?? barTotal);
  const currentLineValue = isHovering ? valueAt(lineSeries.key, activeIndex) : lineAverage;

  const selectRange = (id: string) => {
    setActiveIndex(null);
    setSelectedRangeId(id);
    onRangeChange?.(id);
  };

  return (
    <ChartCard className={cx(tiles && "h-auto", className)}>
      <ChartHeader
        label={`${currentLabel} · ${lineSeries.label} ${formatLine(currentLineValue)}`}
        value={currentHeadline}
        format={formatBar}
        delta={rangeDelta === undefined ? undefined : describeDelta(rangeDelta)}
        hovering={isHovering}
        fadeKey={`${selectedRange?.id ?? ""}:${activeIndex}`}
        range={range}
        ranges={availableRanges}
        rangeId={selectedRange?.id}
        onRangeChange={selectRange}
      />

      <div className={cx("min-h-0 w-full flex-1", tiles && "h-[196px] flex-none")}>
        <ResponsiveContainer width="100%" height="100%">
          <ComposedChart
            data={chartData}
            margin={{ top: 4, right: 0, bottom: 0, left: 0 }}
            barCategoryGap="22%"
            onMouseMove={(state) => {
              const index = Number(state?.activeTooltipIndex);
              setActiveIndex(state?.isTooltipActive && Number.isInteger(index) ? index : null);
            }}
            onMouseLeave={() => setActiveIndex(null)}
          >
            <CartesianGrid
              vertical={false}
              stroke="var(--color-chart-track)"
              strokeDasharray="4 4"
            />
            <YAxis
              yAxisId="bars"
              width={44}
              tickCount={4}
              tickFormatter={formatCompact}
              tickLine={false}
              axisLine={false}
              tick={{ fontSize: 12, fill: "var(--color-text-tertiary)" }}
            />
            <YAxis
              yAxisId="line"
              orientation="right"
              width={40}
              tickCount={4}
              tickFormatter={formatLine}
              tickLine={false}
              axisLine={false}
              tick={{ fontSize: 12, fill: "var(--color-text-tertiary)" }}
            />
            <XAxis
              dataKey="label"
              tickLine={false}
              axisLine={false}
              tickMargin={12}
              interval="preserveStartEnd"
              tick={{ fontSize: 13, fill: "var(--color-text-tertiary)" }}
            />
            <Tooltip content={() => null} cursor={false} isAnimationActive={false} />
            <Bar
              yAxisId="bars"
              dataKey={barSeries.key}
              name={barSeries.label}
              radius={BAR_RADIUS}
              maxBarSize={34}
              activeBar={false}
              isAnimationActive
              animationDuration={450}
            >
              {chartData.map((row, index) => (
                <Cell
                  key={String(row.label)}
                  fill={index === activeIndex ? barTone.activeColor : barTone.color}
                  opacity={isHovering && index !== activeIndex ? 0.3 : 1}
                  className="transition-[fill,opacity] duration-200 ease-out"
                />
              ))}
            </Bar>
            <Line
              yAxisId="line"
              type="monotone"
              dataKey={lineSeries.key}
              stroke="var(--color-background-secondary-default)"
              strokeWidth={7}
              strokeLinecap="round"
              strokeLinejoin="round"
              dot={false}
              activeDot={false}
              legendType="none"
              isAnimationActive
              animationDuration={450}
            />
            <Line
              yAxisId="line"
              type="monotone"
              dataKey={lineSeries.key}
              name={lineSeries.label}
              stroke={lineTone.activeColor}
              strokeWidth={2}
              strokeLinecap="round"
              strokeLinejoin="round"
              dot={{
                r: 3,
                fill: lineTone.activeColor,
                stroke: "var(--color-background-secondary-default)",
                strokeWidth: 2,
              }}
              activeDot={<PulseDot color={lineTone.activeColor} />}
              isAnimationActive
              animationDuration={450}
            />
          </ComposedChart>
        </ResponsiveContainer>
      </div>

      {tiles && (
        <ChartStatTiles
          items={[
            {
              label: `${barSeries.label} · total`,
              value: formatBar(isHovering ? valueAt(barSeries.key, activeIndex) : barTotal),
              color: barTone.color,
              activeColor: barTone.activeColor,
            },
            {
              label: `${lineSeries.label} · ${isHovering ? "this month" : "average"}`,
              value: formatLine(currentLineValue),
              color: lineTone.color,
              activeColor: lineTone.activeColor,
            },
          ]}
        />
      )}
    </ChartCard>
  );
}
"use client";

import { useState, type ReactNode } from "react";
import {
  Bar,
  CartesianGrid,
  Cell,
  ComposedChart,
  Line,
  ResponsiveContainer,
  Tooltip,
  XAxis,
  YAxis,
} from "recharts";
import { Chip, type ChipProps } from "@/components/base/badges/chip";
import { ChartRangeControl } from "@/components/charts/chart-range-control";
import { useCountUp } from "@/hooks/use-count-up";
import { cx } from "@/utils/cx";

export type ComboChartDatum = {
  label: string;
  [key: string]: string | number;
};

export interface ComboChartSeries {
  key: string;
  label: string;
  color?: string;
  activeColor?: string;
  format?: (value: number) => string;
}

export interface ComboChartRange {
  id: string;
  label: string;
  data: ComboChartDatum[];
  headline?: number;
  delta?: number;
}

export interface ComboChartCardProps {
  title?: string;
  data: ComboChartDatum[];
  bar: ComboChartSeries;
  line: ComboChartSeries;
  headline?: number;
  delta?: number;
  /** A noninteractive period pill. */
  range?: string;
  /** Periods shown in the top-right dropdown. */
  ranges?: ComboChartRange[];
  defaultRange?: string;
  onRangeChange?: (id: string) => void;
  /** Adds the total and average tiles and gives the plot the preview's fixed height. */
  tiles?: boolean;
  className?: string;
}

type Tone = { color: string; activeColor: string };

const DEFAULT_TONES: Tone[] = [2, 6, 5, 3, 8, 7, 4, 1].map((tone) => ({
  color: `var(--color-chart-${tone})`,
  activeColor: `var(--color-chart-${tone}-active)`,
}));

const formatNumber = (value: number) => value.toLocaleString("en-US");
const formatCompact = (value: number) =>
  value >= 1000 ? `${Math.round(value / 100) / 10}K`.replace(".0K", "K") : String(Math.round(value));
const formatPercent = (value: number) => `${Math.round(value * 10) / 10}%`;

function useAnimatedNumber(value: number) {
  const precision = Number.isInteger(value)
    ? 1
    : Math.abs(value * 10 - Math.round(value * 10)) < 0.000001
      ? 10
      : 100;
  return useCountUp(Math.round(value * precision)) / precision;
}

function resolveTone(index: number, color?: string, activeColor?: string): Tone {
  if (color) {
    return {
      color,
      activeColor: activeColor ?? `color-mix(in srgb, ${color} 82%, black)`,
    };
  }
  return DEFAULT_TONES[index % DEFAULT_TONES.length];
}

function describeDelta(delta: number): { label: string; color: ChipProps["color"] } {
  const percent = Math.round(Math.abs(delta) * 1000) / 10;
  if (percent === 0) return { label: "0.0%", color: "neutral" };
  return {
    label: `${delta > 0 ? "+" : "-"}${percent}%`,
    color: delta > 0 ? "lime" : "rose",
  };
}

function ChartCard({ className, children }: { className?: string; children: ReactNode }) {
  return (
    <section
      className={cx(
        "flex h-[329px] w-full min-w-0 flex-col gap-4 rounded-2xl bg-background-secondary-default px-4 pt-4 pb-3",
        className,
      )}
    >
      {children}
    </section>
  );
}

function ChartHeader({
  label,
  value,
  format,
  delta,
  hovering,
  fadeKey,
  range,
  ranges,
  rangeId,
  onRangeChange,
}: {
  label: string;
  value: number;
  format: (value: number) => string;
  delta?: ReturnType<typeof describeDelta>;
  hovering: boolean;
  fadeKey: string;
  range?: string;
  ranges?: ComboChartRange[];
  rangeId?: string;
  onRangeChange: (id: string) => void;
}) {
  const animatedValue = useAnimatedNumber(value);

  return (
    <div className="flex w-full items-start justify-between gap-3">
      <div className="flex min-w-0 flex-1 flex-col gap-0.5">
        <p className="w-full truncate text-body-medium text-text-secondary">{label}</p>
        <div className="flex w-full items-center gap-2">
          <p
            key={fadeKey}
            className="animate-number-fade text-title-1-medium whitespace-nowrap text-text-primary tabular-nums"
          >
            {format(animatedValue)}
          </p>
          {delta && (
            <Chip variant="bold" color={delta.color} className={hovering ? "invisible" : undefined}>
              {delta.label}
            </Chip>
          )}
        </div>
      </div>
      {ranges && ranges.length > 0 ? (
        <ChartRangeControl ranges={ranges} value={rangeId} onValueChange={onRangeChange} />
      ) : range ? (
        <ChartRangeControl label={range} />
      ) : null}
    </div>
  );
}

function ChartStatTiles({
  items,
}: {
  items: Array<{ label: string; value: string; color: string; activeColor: string }>;
}) {
  return (
    <div className="-mx-2 -mb-1 grid grid-cols-2 gap-2 sm:grid-cols-6">
      {items.map((item, index) => (
        <div
          key={`${item.label}-${index}`}
          className="col-span-1 flex min-w-0 flex-col items-start gap-px rounded-2lg bg-background-inner-default px-2.5 py-2 sm:col-span-3"
        >
          <div className="flex min-w-0 max-w-full items-center gap-1.5">
            <span
              className="size-3 shrink-0 rounded-[4px] transition-colors duration-150 ease-out"
              style={{ backgroundColor: item.color }}
            />
            <span className="truncate text-body-regular text-text-secondary">{item.label}</span>
          </div>
          <span className="text-body-medium whitespace-nowrap text-text-primary tabular-nums">{item.value}</span>
        </div>
      ))}
    </div>
  );
}

function PulseDot({ cx: dotX, cy: dotY, color }: { cx?: number; cy?: number; color: string }) {
  if (dotX == null || dotY == null) return null;

  return (
    <g>
      <circle cx={dotX} cy={dotY} r={5} fill={color} opacity={0.3}>
        <animate attributeName="r" values="5;13" dur="1.4s" repeatCount="indefinite" />
        <animate attributeName="opacity" values="0.35;0" dur="1.4s" repeatCount="indefinite" />
      </circle>
      <circle
        cx={dotX}
        cy={dotY}
        r={5}
        fill={color}
        stroke="var(--color-background-secondary-default)"
        strokeWidth={3}
      />
    </g>
  );
}

const BAR_RADIUS: [number, number, number, number] = [8, 8, 8, 8];

export function ComboChartCard({
  title = "Sessions",
  data,
  bar,
  line,
  headline,
  delta,
  range,
  ranges,
  defaultRange,
  onRangeChange,
  tiles = false,
  className,
}: ComboChartCardProps) {
  const [activeIndex, setActiveIndex] = useState<number | null>(null);
  const availableRanges = ranges;
  const [selectedRangeId, setSelectedRangeId] = useState(defaultRange);
  const selectedRange =
    availableRanges?.find((item) => item.id === selectedRangeId) ?? availableRanges?.[0];

  const chartData = selectedRange?.data ?? data;
  const barSeries = bar;
  const lineSeries = line;
  const rangeHeadline = selectedRange?.headline ?? headline;
  const rangeDelta = selectedRange?.delta ?? delta;
  const barTone = resolveTone(0, barSeries.color, barSeries.activeColor);
  const lineTone = resolveTone(1, lineSeries.color, lineSeries.activeColor);
  const formatBar = barSeries.format ?? formatNumber;
  const formatLine = lineSeries.format ?? formatPercent;
  const valueAt = (key: string, index: number) => Number(chartData[index]?.[key] ?? 0);
  const barTotal = chartData.reduce((sum, row) => sum + Number(row[barSeries.key] ?? 0), 0);
  const lineAverage = chartData.length
    ? chartData.reduce((sum, row) => sum + Number(row[lineSeries.key] ?? 0), 0) / chartData.length
    : 0;
  const isHovering = activeIndex !== null && activeIndex < chartData.length;
  const currentLabel = isHovering ? String(chartData[activeIndex].label) : title;
  const currentHeadline = isHovering
    ? valueAt(barSeries.key, activeIndex)
    : (rangeHeadline ?? barTotal);
  const currentLineValue = isHovering ? valueAt(lineSeries.key, activeIndex) : lineAverage;

  const selectRange = (id: string) => {
    setActiveIndex(null);
    setSelectedRangeId(id);
    onRangeChange?.(id);
  };

  return (
    <ChartCard className={cx(tiles && "h-auto", className)}>
      <ChartHeader
        label={`${currentLabel} · ${lineSeries.label} ${formatLine(currentLineValue)}`}
        value={currentHeadline}
        format={formatBar}
        delta={rangeDelta === undefined ? undefined : describeDelta(rangeDelta)}
        hovering={isHovering}
        fadeKey={`${selectedRange?.id ?? ""}:${activeIndex}`}
        range={range}
        ranges={availableRanges}
        rangeId={selectedRange?.id}
        onRangeChange={selectRange}
      />

      <div className={cx("min-h-0 w-full flex-1", tiles && "h-[196px] flex-none")}>
        <ResponsiveContainer width="100%" height="100%">
          <ComposedChart
            data={chartData}
            margin={{ top: 4, right: 0, bottom: 0, left: 0 }}
            barCategoryGap="22%"
            onMouseMove={(state) => {
              const index = Number(state?.activeTooltipIndex);
              setActiveIndex(state?.isTooltipActive && Number.isInteger(index) ? index : null);
            }}
            onMouseLeave={() => setActiveIndex(null)}
          >
            <CartesianGrid
              vertical={false}
              stroke="var(--color-chart-track)"
              strokeDasharray="4 4"
            />
            <YAxis
              yAxisId="bars"
              width={44}
              tickCount={4}
              tickFormatter={formatCompact}
              tickLine={false}
              axisLine={false}
              tick={{ fontSize: 12, fill: "var(--color-text-tertiary)" }}
            />
            <YAxis
              yAxisId="line"
              orientation="right"
              width={40}
              tickCount={4}
              tickFormatter={formatLine}
              tickLine={false}
              axisLine={false}
              tick={{ fontSize: 12, fill: "var(--color-text-tertiary)" }}
            />
            <XAxis
              dataKey="label"
              tickLine={false}
              axisLine={false}
              tickMargin={12}
              interval="preserveStartEnd"
              tick={{ fontSize: 13, fill: "var(--color-text-tertiary)" }}
            />
            <Tooltip content={() => null} cursor={false} isAnimationActive={false} />
            <Bar
              yAxisId="bars"
              dataKey={barSeries.key}
              name={barSeries.label}
              radius={BAR_RADIUS}
              maxBarSize={34}
              activeBar={false}
              isAnimationActive
              animationDuration={450}
            >
              {chartData.map((row, index) => (
                <Cell
                  key={String(row.label)}
                  fill={index === activeIndex ? barTone.activeColor : barTone.color}
                  opacity={isHovering && index !== activeIndex ? 0.3 : 1}
                  className="transition-[fill,opacity] duration-200 ease-out"
                />
              ))}
            </Bar>
            <Line
              yAxisId="line"
              type="monotone"
              dataKey={lineSeries.key}
              stroke="var(--color-background-secondary-default)"
              strokeWidth={7}
              strokeLinecap="round"
              strokeLinejoin="round"
              dot={false}
              activeDot={false}
              legendType="none"
              isAnimationActive
              animationDuration={450}
            />
            <Line
              yAxisId="line"
              type="monotone"
              dataKey={lineSeries.key}
              name={lineSeries.label}
              stroke={lineTone.activeColor}
              strokeWidth={2}
              strokeLinecap="round"
              strokeLinejoin="round"
              dot={{
                r: 3,
                fill: lineTone.activeColor,
                stroke: "var(--color-background-secondary-default)",
                strokeWidth: 2,
              }}
              activeDot={<PulseDot color={lineTone.activeColor} />}
              isAnimationActive
              animationDuration={450}
            />
          </ComposedChart>
        </ResponsiveContainer>
      </div>

      {tiles && (
        <ChartStatTiles
          items={[
            {
              label: `${barSeries.label} · total`,
              value: formatBar(isHovering ? valueAt(barSeries.key, activeIndex) : barTotal),
              color: barTone.color,
              activeColor: barTone.activeColor,
            },
            {
              label: `${lineSeries.label} · ${isHovering ? "this month" : "average"}`,
              value: formatLine(currentLineValue),
              color: lineTone.color,
              activeColor: lineTone.activeColor,
            },
          ]}
        />
      )}
    </ChartCard>
  );
}

Props

Generated from the component's TypeScript types. Standard DOM and React Aria props are omitted.

ComboChartCard

PropTypeDefaultDescription
barrequiredComboChartSeries
datarequiredComboChartDatum[]
linerequiredComboChartSeries
classNamestring
defaultRangestring
deltanumber
headlinenumber
onRangeChange(id: string) => void
rangestringA noninteractive period pill.
rangesComboChartRange[]Periods shown in the top-right dropdown.
tilesbooleanfalseAdds the total and average tiles and gives the plot the preview's fixed height.
titlestringSessions