BreakdownAnalysis
surface분석 Surface. 4탭(Trend, Distribution, Composition, Data) + 다중 차트 + DataTable 조합.
컴포넌트 의존 관계
깊이
100%
기본 사용
Event Breakdown
테스트 커버리지
2026년 2월 4일생성된 테스트 결과를 찾지 못했습니다.
BreakdownAnalysis 항목이 문서 메타에 연결되어 있지만 현재 생성 파일에는 없습니다.
테스트를 추가한 뒤 `bun run generate:test-results`를 실행하거나 `testDescribe` 매핑을 다시 확인하세요.
BreakdownAnalysis Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
title | string | "Breakdown" | 제목 |
lineData | ChartDataPoint[] | — | 추세 라인 차트 데이터 |
barData | ChartDataPoint[] | — | 분포 바 차트 데이터 |
pieData | PieChartDataPoint[] | — | 구성 파이 차트 데이터 |
tableData | BreakdownRow[] | — | 테이블 데이터 |
Surface 설치
CLI가 공식 배포 채널입니다. 필요한 Surface를 프로젝트로 복사한 뒤 직접 수정할 수 있습니다.
bash
npx @reopt-ai/opt-cli surface add breakdown-analysisConsumer 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.tsxbreakdown-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";