reopt designreopt design
DocsExploreToolsPricingBuilder
Start
Overview
Start
Next.js 설치
Private install
Core Concepts
아키텍처
Composition Patterns
Accessibility
Keyboard Patterns
Styling
Theme System
Advanced Patterns
Build & Operate
Skills
AI Integration
CLI (opt surface add)
Dependency Graph
Tools
Canvas Catalog
Theme Builder
Form Builder
Templates
Templates
Releases
Release Notes
Oopt-ui
reopt designreopt design

A design system for the AI era

  • Docs
  • Pricing
  • Releases
  • GitHub
  • Terms of Service
  • Privacy Policy

© 2026 reopt-ai. All rights reserved.

BreakdownAnalysis

surface

분석 Surface. 4탭(Trend, Distribution, Composition, Data) + 다중 차트 + DataTable 조합.

컴포넌트 의존 관계

깊이
▼ USES (8)BreakdownAnalysisline-chartbar-chartpie-chartfilter-bardata-tableloading-overlaytime-range-selectortabs
100%

기본 사용

Event Breakdown

Event Breakdown

테스트 커버리지

2026년 2월 4일

생성된 테스트 결과를 찾지 못했습니다.

BreakdownAnalysis 항목이 문서 메타에 연결되어 있지만 현재 생성 파일에는 없습니다.

테스트를 추가한 뒤 `bun run generate:test-results`를 실행하거나 `testDescribe` 매핑을 다시 확인하세요.

BreakdownAnalysis Props

Prop타입기본값설명
titlestring"Breakdown"제목
lineDataChartDataPoint[]—추세 라인 차트 데이터
barDataChartDataPoint[]—분포 바 차트 데이터
pieDataPieChartDataPoint[]—구성 파이 차트 데이터
tableDataBreakdownRow[]—테이블 데이터

Surface 설치

CLI가 공식 배포 채널입니다. 필요한 Surface를 프로젝트로 복사한 뒤 직접 수정할 수 있습니다.

bash
npx @reopt-ai/opt-cli surface add breakdown-analysis

Consumer target

복사된 파일은 components/surfaces 아래에 저장됩니다.

tsx
import { BreakdownAnalysis } from "@/components/surfaces/breakdown-analysis";

Registry metadata

설명
분석 Surface. 4탭(Trend, Distribution, Composition, Data) + 다중 차트 + DataTable 조합.
파일 수
1개
Registry dependencies
없음
Package dependencies
없음
태그
chartfiltertableanalytics
Install notes
없음

포함 파일

  • breakdown-analysis.tsx→breakdown-analysis.tsx
Surface 소스 보기
breakdown-analysis.tsx
"use client";

import * as React from "react";
import {
  TabsRoot,
  TabList,
  Tab,
  TabPanel,
  FilterBar,
  DataTable,
  Button,
  SurfaceLayout,
  TimeRangeSelector,
  SelectRoot,
  SelectLabel,
  SelectTrigger,
  SelectPopover,
  SelectItem,
  type ColumnDef,
  type FilterGroupDef,
  type DateRange,
} from "@reopt-ai/opt-ui";
import {
  LineChart,
  BarChart,
  PieChart,
  type ChartDataPoint,
  type ChartSeriesDef,
  type PieChartDataPoint,
} from "@reopt-ai/opt-charts";

interface BreakdownRow {
  id: string;
  [k: string]: unknown;
}

/** View modes supported by `BreakdownTimeSeries`. */
export type BreakdownTimeSeriesViewMode = "chart" | "table" | "split";

/** Labels for `BreakdownAnalysis`. */
export interface BreakdownAnalysisLabels {
  title?: string;
  trend?: string;
  distribution?: string;
  composition?: string;
  data?: string;
  filtersLabel?: string;
  noTrendData?: string;
  noDistributionData?: string;
  noCompositionData?: string;
  emptyTitle?: string;
  emptyDescription?: string;
  chartView?: string;
  tableView?: string;
  splitView?: string;
  eventLabel?: string;
  propertyLabel?: string;
}

const defaultLabels: Required<BreakdownAnalysisLabels> = {
  title: "Breakdown",
  trend: "Trend",
  distribution: "Distribution",
  composition: "Composition",
  data: "Data",
  filtersLabel: "Filters",
  noTrendData: "No trend data",
  noDistributionData: "No distribution data",
  noCompositionData: "No composition data",
  emptyTitle: "No breakdown data",
  emptyDescription: "There is no data available to break down.",
  chartView: "Chart",
  tableView: "Table",
  splitView: "Split",
  eventLabel: "Event",
  propertyLabel: "Property",
};

/** Props for `BreakdownAnalysis`. */
export interface BreakdownAnalysisProps {
  title?: string;
  lineData?: ChartDataPoint[];
  lineSeries?: ChartSeriesDef[];
  barData?: ChartDataPoint[];
  barSeries?: ChartSeriesDef[];
  pieData?: PieChartDataPoint[];
  tableData?: BreakdownRow[];
  tableColumns?: ColumnDef<BreakdownRow>[];
  /** Table data for TimeSeries tab (when timeSeriesViewMode is "table" or "split") */
  timeSeriesTableData?: BreakdownRow[];
  /** Columns for TimeSeries tab table */
  timeSeriesTableColumns?: ColumnDef<BreakdownRow>[];
  /** View mode for the Trend tab: "chart" (default), "table", or "split" */
  timeSeriesViewMode?: BreakdownTimeSeriesViewMode;
  /** Callback when the Trend tab view mode changes */
  onTimeSeriesViewModeChange?: (mode: BreakdownTimeSeriesViewMode) => void;
  /** Currently selected event for filter selector */
  selectedEvent?: string;
  /** Callback when event selection changes */
  onEventChange?: (event: string) => void;
  /** Event options for the filter selector */
  eventOptions?: { id: string; label: string }[];
  /** Currently selected breakdown property */
  selectedProperty?: string;
  /** Callback when property selection changes */
  onPropertyChange?: (property: string) => void;
  /** Property options for the filter selector */
  propertyOptions?: { id: string; label: string }[];
  /** FilterBar definitions (e.g., event, source toggle, metric) */
  filters?: FilterGroupDef[];
  onFilterChange?: (
    filterId: string,
    value: string | string[] | boolean,
  ) => void;
  /** Custom filter content rendered above the FilterBar (e.g., source toggle, property selector) */
  filterContent?: React.ReactNode;
  header?: React.ReactNode;
  actions?: React.ReactNode;
  labels?: BreakdownAnalysisLabels;
  loading?: boolean;
  timeRange?: DateRange;
  onTimeRangeChange?: (range: DateRange) => void;
  className?: string;
}

/** Renders the `BreakdownAnalysis` component. */
export function BreakdownAnalysis({
  title,
  lineData = [],
  lineSeries = [],
  barData = [],
  barSeries = [],
  pieData = [],
  tableData = [],
  tableColumns = [
    { id: "name", header: "Name", accessor: "name" as keyof BreakdownRow },
    { id: "count", header: "Count", accessor: "count" as keyof BreakdownRow },
    {
      id: "percentage",
      header: "%",
      accessor: "percentage" as keyof BreakdownRow,
    },
  ],
  timeSeriesTableData,
  timeSeriesTableColumns = [
    { id: "name", header: "Name", accessor: "name" as keyof BreakdownRow },
    { id: "count", header: "Count", accessor: "count" as keyof BreakdownRow },
  ],
  timeSeriesViewMode,
  onTimeSeriesViewModeChange,
  selectedEvent,
  onEventChange,
  eventOptions,
  selectedProperty,
  onPropertyChange,
  propertyOptions,
  filters,
  onFilterChange,
  filterContent,
  header,
  actions,
  labels: customLabels,
  loading = false,
  timeRange,
  onTimeRangeChange,
  className,
}: BreakdownAnalysisProps) {
  const labels = { ...defaultLabels, ...customLabels };
  const sectionId = React.useId();

  // Use explicit title prop if provided (backward compat), otherwise labels.title
  const displayTitle = title ?? labels.title;
  const hasFilters = filters && filters.length > 0;
  const hasEventSelector = eventOptions && eventOptions.length > 0;
  const hasPropertySelector = propertyOptions && propertyOptions.length > 0;

  const isEmpty =
    lineData.length === 0 &&
    barData.length === 0 &&
    pieData.length === 0 &&
    tableData.length === 0;

  const trendViewMode = timeSeriesViewMode ?? "chart";

  const viewModeButtons: {
    mode: BreakdownTimeSeriesViewMode;
    label: string;
  }[] = [
    { mode: "chart", label: labels.chartView },
    { mode: "table", label: labels.tableView },
    { mode: "split", label: labels.splitView },
  ];

  return (
    <SurfaceLayout loading={loading} className={className}>
      {header || actions ? (
        <div className="flex items-center justify-between">
          {header && <div>{header}</div>}
          <div className="gap-element flex items-center">
            <TimeRangeSelector value={timeRange} onChange={onTimeRangeChange} />
            {actions}
          </div>
        </div>
      ) : (
        <div className="flex items-center justify-between">
          <h2 className="text-text-primary text-lg font-semibold">
            {displayTitle}
          </h2>
          <TimeRangeSelector value={timeRange} onChange={onTimeRangeChange} />
        </div>
      )}

      {/* Event / Property selectors */}
      {(hasEventSelector || hasPropertySelector) && (
        <div className="gap-group flex items-end">
          {hasEventSelector && (
            <div className="min-w-[160px]">
              <SelectRoot
                value={selectedEvent ?? ""}
                setValue={(v) =>
                  onEventChange?.(typeof v === "string" ? v : (v[0] ?? ""))
                }
              >
                <SelectLabel className="text-text-secondary mb-1 block text-xs font-medium">
                  {labels.eventLabel}
                </SelectLabel>
                <SelectTrigger className="border-border bg-surface text-text-primary inline-flex h-8 w-full items-center justify-between rounded-md border px-3 text-sm">
                  {eventOptions?.find((o) => o.id === selectedEvent)?.label ??
                    selectedEvent ??
                    ""}
                </SelectTrigger>
                <SelectPopover className="bg-surface border-border z-50 rounded-md border shadow-lg">
                  {eventOptions?.map((opt) => (
                    <SelectItem
                      key={opt.id}
                      value={opt.id}
                      className="text-text-primary hover:bg-bg-subtle cursor-pointer px-3 py-1.5 text-sm"
                    >
                      {opt.label}
                    </SelectItem>
                  ))}
                </SelectPopover>
              </SelectRoot>
            </div>
          )}
          {hasPropertySelector && (
            <div className="min-w-[160px]">
              <SelectRoot
                value={selectedProperty ?? ""}
                setValue={(v) =>
                  onPropertyChange?.(typeof v === "string" ? v : (v[0] ?? ""))
                }
              >
                <SelectLabel className="text-text-secondary mb-1 block text-xs font-medium">
                  {labels.propertyLabel}
                </SelectLabel>
                <SelectTrigger className="border-border bg-surface text-text-primary inline-flex h-8 w-full items-center justify-between rounded-md border px-3 text-sm">
                  {propertyOptions?.find((o) => o.id === selectedProperty)
                    ?.label ??
                    selectedProperty ??
                    ""}
                </SelectTrigger>
                <SelectPopover className="bg-surface border-border z-50 rounded-md border shadow-lg">
                  {propertyOptions?.map((opt) => (
                    <SelectItem
                      key={opt.id}
                      value={opt.id}
                      className="text-text-primary hover:bg-bg-subtle cursor-pointer px-3 py-1.5 text-sm"
                    >
                      {opt.label}
                    </SelectItem>
                  ))}
                </SelectPopover>
              </SelectRoot>
            </div>
          )}
        </div>
      )}

      {filterContent}

      {hasFilters && (
        <section aria-labelledby={`${sectionId}-filters`}>
          <span id={`${sectionId}-filters`} className="sr-only">
            {labels.filtersLabel}
          </span>
          <FilterBar filters={filters} onFilterChange={onFilterChange} />
        </section>
      )}

      {isEmpty ? (
        <div
          role="status"
          className="flex flex-col items-center justify-center py-16 text-center"
        >
          <h3 className="text-text-primary text-lg font-medium">
            {labels.emptyTitle}
          </h3>
          <p className="text-text-tertiary mt-1 text-sm">
            {labels.emptyDescription}
          </p>
        </div>
      ) : (
        <section aria-labelledby={`${sectionId}-tabs`}>
          <span id={`${sectionId}-tabs`} className="sr-only">
            {displayTitle}
          </span>
          <TabsRoot defaultSelectedId="trend">
            <TabList>
              <Tab id="trend">{labels.trend}</Tab>
              <Tab id="bar">{labels.distribution}</Tab>
              <Tab id="pie">{labels.composition}</Tab>
              <Tab id="table">{labels.data}</Tab>
            </TabList>
            <TabPanel tabId="trend">
              <div className="pt-4">
                {/* View mode toggle buttons (only shown when timeSeriesViewMode is provided) */}
                {timeSeriesViewMode !== undefined && (
                  <div className="mb-4 flex gap-1">
                    {viewModeButtons.map((btn) => (
                      <Button
                        key={btn.mode}
                        variant={
                          trendViewMode === btn.mode ? "primary" : "ghost"
                        }
                        size="sm"
                        onClick={() => onTimeSeriesViewModeChange?.(btn.mode)}
                      >
                        {btn.label}
                      </Button>
                    ))}
                  </div>
                )}

                {(trendViewMode === "chart" || trendViewMode === "split") &&
                  (lineData.length > 0 && lineSeries.length > 0 ? (
                    <LineChart
                      data={lineData}
                      series={lineSeries}
                      aria-label="Trend"
                    />
                  ) : (
                    <p className="text-text-tertiary py-8 text-center text-sm">
                      {labels.noTrendData}
                    </p>
                  ))}

                {(trendViewMode === "table" || trendViewMode === "split") && (
                  <div className={trendViewMode === "split" ? "mt-4" : ""}>
                    <DataTable
                      columns={timeSeriesTableColumns}
                      data={timeSeriesTableData ?? []}
                      keyExtractor={(r) => r.id}
                    />
                  </div>
                )}
              </div>
            </TabPanel>
            <TabPanel tabId="bar">
              <div className="pt-4">
                {barData.length > 0 && barSeries.length > 0 ? (
                  <BarChart
                    data={barData}
                    series={barSeries}
                    aria-label="Distribution"
                  />
                ) : (
                  <p className="text-text-tertiary py-8 text-center text-sm">
                    {labels.noDistributionData}
                  </p>
                )}
              </div>
            </TabPanel>
            <TabPanel tabId="pie">
              <div className="pt-4">
                {pieData.length > 0 ? (
                  <PieChart data={pieData} aria-label="Composition" />
                ) : (
                  <p className="text-text-tertiary py-8 text-center text-sm">
                    {labels.noCompositionData}
                  </p>
                )}
              </div>
            </TabPanel>
            <TabPanel tabId="table">
              <div className="pt-4">
                <DataTable
                  columns={tableColumns}
                  data={tableData}
                  keyExtractor={(r) => r.id}
                />
              </div>
            </TabPanel>
          </TabsRoot>
        </section>
      )}
    </SurfaceLayout>
  );
}

BreakdownAnalysis.displayName = "BreakdownAnalysis";