PathAnalysis
surface경로 분석 Surface. SankeyChart + 6탭(Flow, Top Paths, Drop-off, Patterns, Entry/Exit, Dwell) + BarChart + DataTable 조합.
컴포넌트 의존 관계
깊이
100%
기본 사용
Path Analysis
테스트 커버리지
2026년 2월 4일생성된 테스트 결과를 찾지 못했습니다.
PathAnalysis 항목이 문서 메타에 연결되어 있지만 현재 생성 파일에는 없습니다.
테스트를 추가한 뒤 `bun run generate:test-results`를 실행하거나 `testDescribe` 매핑을 다시 확인하세요.
PathAnalysis Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
nodes* | SankeyNode[] | — | 산키 노드 배열 |
links* | SankeyLink[] | — | 산키 링크 배열 |
topPaths | PathRow[] | — | 상위 경로 테이블 데이터 |
dropoffData | PathRow[] | — | 이탈 테이블 데이터 |
loopData | PathRow[] | — | Loop detection 데이터 (Patterns 탭) |
urlPatternData | PathRow[] | — | URL 패턴 그룹핑 데이터 (Patterns 탭) |
entryData | PathRow[] | — | 진입 페이지 데이터 (Entry/Exit 탭) |
exitData | PathRow[] | — | 이탈 페이지 데이터 (Entry/Exit 탭) |
conversionPaths | PathRow[] | — | 전환 경로 데이터 (Entry/Exit 탭) |
dwellChartData | ChartDataPoint[] | — | 체류 시간 바 차트 데이터 (Dwell 탭) |
dwellTableData | PathRow[] | — | 체류 시간 테이블 데이터 (Dwell 탭) |
filters | FilterGroupDef[] | — | FilterBar 필터 정의 (URL 그룹핑, 세그먼트 등) |
width | number | 700 | 산키 차트 너비 |
height | number | 350 | 산키 차트 높이 |
depth | number | — | 경로 분석 깊이 (표시할 단계 수) |
onDepthChange | (depth: number) => void | — | 깊이 변경 콜백 |
startPage | string | — | 시작 페이지 필터 |
onStartPageChange | (page: string) => void | — | 시작 페이지 변경 콜백 |
endPage | string | — | 종료 페이지 필터 |
onEndPageChange | (page: string) => void | — | 종료 페이지 변경 콜백 |
pageOptions | PageOption[] | — | 시작/종료 페이지 셀렉터 옵션 ({ id, label }) |
Surface 설치
CLI가 공식 배포 채널입니다. 필요한 Surface를 프로젝트로 복사한 뒤 직접 수정할 수 있습니다.
bash
npx @reopt-ai/opt-cli surface add path-analysisConsumer target
복사된 파일은 components/surfaces 아래에 저장됩니다.
tsx
import { PathAnalysis } from "@/components/surfaces/path-analysis";Registry metadata
- 설명
- 경로 분석 Surface. SankeyChart + 6탭(Flow, Top Paths, Drop-off, Patterns, Entry/Exit, Dwell) + BarChart + DataTable 조합.
- 파일 수
- 1개
- Registry dependencies
- 없음
- Package dependencies
- 없음
- 태그
- charttablefilteranalytics
- Install notes
- 없음
포함 파일
path-analysis.tsxpath-analysis.tsx
Surface 소스 보기
path-analysis.tsx
"use client";
import * as React from "react";
import {
TabsRoot,
TabList,
Tab,
TabPanel,
DataTable,
FilterBar,
NumberInput,
SurfaceLayout,
SummaryRow,
TimeRangeSelector,
type ColumnDef,
type FilterGroupDef,
type DateRange,
type StatCardType,
} from "@reopt-ai/opt-ui";
import {
SankeyChart,
BarChart,
type SankeyNode,
type SankeyLink,
type ChartDataPoint,
type ChartSeriesDef,
} from "@reopt-ai/opt-charts";
interface PathRow {
id: string;
[k: string]: unknown;
}
/** Labels for `PathAnalysis`. */
export interface PathAnalysisLabels {
title?: string;
flow?: string;
topPaths?: string;
dropoff?: string;
patterns?: string;
entryExit?: string;
dwell?: string;
loopDetection?: string;
urlPatterns?: string;
entryPages?: string;
exitPages?: string;
conversionPaths?: string;
filtersLabel?: string;
controlsLabel?: string;
detailsLabel?: string;
emptyTitle?: string;
emptyDescription?: string;
/** Label for the depth control */
depthLabel?: string;
/** Label for the start page selector */
startPageLabel?: string;
/** Label for the end page selector */
endPageLabel?: string;
/** Placeholder for page selectors */
pageSelectorPlaceholder?: string;
}
const defaultLabels: Required<PathAnalysisLabels> = {
title: "Path Analysis",
flow: "Flow",
topPaths: "Top Paths",
dropoff: "Drop-off",
patterns: "Patterns",
entryExit: "Entry / Exit",
dwell: "Dwell Time",
loopDetection: "Loop Detection",
urlPatterns: "URL Patterns",
entryPages: "Entry Pages",
exitPages: "Exit Pages",
conversionPaths: "Conversion Paths",
filtersLabel: "Filters",
controlsLabel: "Path controls",
detailsLabel: "Path analysis details",
emptyTitle: "No path data",
emptyDescription: "There are no user paths to analyze yet.",
depthLabel: "Depth",
startPageLabel: "Start Page",
endPageLabel: "End Page",
pageSelectorPlaceholder: "All pages",
};
/** Option for page selectors */
export interface PageOption {
id: string;
label: string;
}
/** Props for `PathAnalysis`. */
export interface PathAnalysisProps {
nodes: SankeyNode[];
links: SankeyLink[];
topPaths?: PathRow[];
topPathColumns?: ColumnDef<PathRow>[];
dropoffData?: PathRow[];
dropoffColumns?: ColumnDef<PathRow>[];
/** Loop detection rows (Patterns tab) */
loopData?: PathRow[];
loopColumns?: ColumnDef<PathRow>[];
/** URL pattern grouping rows (Patterns tab) */
urlPatternData?: PathRow[];
urlPatternColumns?: ColumnDef<PathRow>[];
/** Entry page rows (Entry/Exit tab) */
entryData?: PathRow[];
entryColumns?: ColumnDef<PathRow>[];
/** Exit page rows (Entry/Exit tab) */
exitData?: PathRow[];
exitColumns?: ColumnDef<PathRow>[];
/** Conversion path rows (Entry/Exit tab) */
conversionPaths?: PathRow[];
conversionPathColumns?: ColumnDef<PathRow>[];
/** Dwell time bar chart data */
dwellChartData?: ChartDataPoint[];
dwellSeries?: ChartSeriesDef[];
/** Dwell time table rows */
dwellTableData?: PathRow[];
dwellTableColumns?: ColumnDef<PathRow>[];
/** Stats cards displayed above the tabs */
stats?: StatCardType[];
/** FilterBar definitions (e.g., URL grouping, segment) */
filters?: FilterGroupDef[];
onFilterChange?: (
filterId: string,
value: string | string[] | boolean,
) => void;
/** Path analysis depth (number of steps to show) */
depth?: number;
/** Callback when depth changes */
onDepthChange?: (depth: number) => void;
/** Start page filter for path analysis */
startPage?: string;
/** Callback when start page changes */
onStartPageChange?: (page: string) => void;
/** End page filter for path analysis */
endPage?: string;
/** Callback when end page changes */
onEndPageChange?: (page: string) => void;
/** Available page options for start/end page selectors */
pageOptions?: PageOption[];
width?: number;
height?: number;
header?: React.ReactNode;
actions?: React.ReactNode;
labels?: PathAnalysisLabels;
loading?: boolean;
timeRange?: DateRange;
onTimeRangeChange?: (range: DateRange) => void;
className?: string;
}
/** Renders the `PathAnalysis` component. */
export function PathAnalysis({
nodes,
links,
topPaths = [],
topPathColumns = [
{ id: "path", header: "Path", accessor: "path" as keyof PathRow },
{ id: "users", header: "Users", accessor: "users" as keyof PathRow },
{
id: "conversion",
header: "Conversion",
accessor: "conversion" as keyof PathRow,
},
],
dropoffData = [],
dropoffColumns = [
{ id: "step", header: "Step", accessor: "step" as keyof PathRow },
{ id: "dropoff", header: "Drop-off", accessor: "dropoff" as keyof PathRow },
{ id: "percentage", header: "%", accessor: "percentage" as keyof PathRow },
],
loopData,
loopColumns = [
{
id: "pattern",
header: "Loop Pattern",
accessor: "pattern" as keyof PathRow,
},
{ id: "count", header: "Occurrences", accessor: "count" as keyof PathRow },
{
id: "avgCycles",
header: "Avg Cycles",
accessor: "avgCycles" as keyof PathRow,
},
],
urlPatternData,
urlPatternColumns = [
{
id: "pattern",
header: "URL Pattern",
accessor: "pattern" as keyof PathRow,
},
{ id: "pages", header: "Pages", accessor: "pages" as keyof PathRow },
{
id: "sessions",
header: "Sessions",
accessor: "sessions" as keyof PathRow,
},
],
entryData,
entryColumns = [
{ id: "page", header: "Entry Page", accessor: "page" as keyof PathRow },
{
id: "sessions",
header: "Sessions",
accessor: "sessions" as keyof PathRow,
},
{ id: "percentage", header: "%", accessor: "percentage" as keyof PathRow },
],
exitData,
exitColumns = [
{ id: "page", header: "Exit Page", accessor: "page" as keyof PathRow },
{
id: "sessions",
header: "Sessions",
accessor: "sessions" as keyof PathRow,
},
{ id: "percentage", header: "%", accessor: "percentage" as keyof PathRow },
],
conversionPaths,
conversionPathColumns = [
{ id: "path", header: "Path", accessor: "path" as keyof PathRow },
{
id: "conversions",
header: "Conversions",
accessor: "conversions" as keyof PathRow,
},
{ id: "rate", header: "Rate", accessor: "rate" as keyof PathRow },
],
dwellChartData,
dwellSeries = [
{ dataKey: "duration", name: "Avg Duration", color: "#3b82f6" },
],
dwellTableData,
dwellTableColumns = [
{ id: "page", header: "Page", accessor: "page" as keyof PathRow },
{
id: "avgDwell",
header: "Avg Dwell",
accessor: "avgDwell" as keyof PathRow,
},
{
id: "medianDwell",
header: "Median",
accessor: "medianDwell" as keyof PathRow,
},
],
stats,
filters,
onFilterChange,
depth,
onDepthChange,
startPage,
onStartPageChange,
endPage,
onEndPageChange,
pageOptions,
width = 700,
height = 350,
header,
actions,
labels: customLabels,
loading = false,
timeRange,
onTimeRangeChange,
className,
}: PathAnalysisProps) {
const labels = { ...defaultLabels, ...customLabels };
const sectionId = React.useId();
const isEmpty = nodes.length === 0;
const hasPatterns =
(loopData && loopData.length > 0) ||
(urlPatternData && urlPatternData.length > 0);
const hasEntryExit =
(entryData && entryData.length > 0) || (exitData && exitData.length > 0);
const hasDwell =
(dwellChartData && dwellChartData.length > 0) ||
(dwellTableData && dwellTableData.length > 0);
const hasFilters = filters && filters.length > 0;
const hasControls =
onDepthChange != null ||
onStartPageChange != null ||
onEndPageChange != null;
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">
{labels.title}
</h2>
<TimeRangeSelector value={timeRange} onChange={onTimeRangeChange} />
</div>
)}
{stats && stats.length > 0 && <SummaryRow stats={stats} columns={4} />}
{hasFilters && (
<section aria-labelledby={`${sectionId}-filters`}>
<span id={`${sectionId}-filters`} className="sr-only">
{labels.filtersLabel}
</span>
<FilterBar filters={filters} onFilterChange={onFilterChange} />
</section>
)}
{/* Path analysis controls: depth, start page, end page */}
{hasControls && (
<section
aria-labelledby={`${sectionId}-controls`}
className="gap-group flex flex-wrap items-end"
>
<span id={`${sectionId}-controls`} className="sr-only">
{labels.controlsLabel}
</span>
{onDepthChange && (
<label className="flex flex-col gap-1">
<span className="text-text-secondary text-xs font-medium">
{labels.depthLabel}
</span>
<NumberInput
value={depth ?? 5}
onChange={onDepthChange}
min={1}
max={20}
step={1}
/>
</label>
)}
{onStartPageChange && pageOptions && (
<label className="flex flex-col gap-1">
<span className="text-text-secondary text-xs font-medium">
{labels.startPageLabel}
</span>
<select
value={startPage ?? ""}
onChange={(e) => onStartPageChange(e.target.value)}
className="border-border bg-surface text-text-primary rounded-md border px-2.5 py-1.5 text-sm"
>
<option value="">{labels.pageSelectorPlaceholder}</option>
{pageOptions.map((p) => (
<option key={p.id} value={p.id}>
{p.label}
</option>
))}
</select>
</label>
)}
{onEndPageChange && pageOptions && (
<label className="flex flex-col gap-1">
<span className="text-text-secondary text-xs font-medium">
{labels.endPageLabel}
</span>
<select
value={endPage ?? ""}
onChange={(e) => onEndPageChange(e.target.value)}
className="border-border bg-surface text-text-primary rounded-md border px-2.5 py-1.5 text-sm"
>
<option value="">{labels.pageSelectorPlaceholder}</option>
{pageOptions.map((p) => (
<option key={p.id} value={p.id}>
{p.label}
</option>
))}
</select>
</label>
)}
</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">
{labels.detailsLabel}
</span>
<TabsRoot defaultSelectedId="flow">
<TabList>
<Tab id="flow">{labels.flow}</Tab>
<Tab id="top-paths">{labels.topPaths}</Tab>
<Tab id="dropoff">{labels.dropoff}</Tab>
{hasPatterns && <Tab id="patterns">{labels.patterns}</Tab>}
{hasEntryExit && <Tab id="entry-exit">{labels.entryExit}</Tab>}
{hasDwell && <Tab id="dwell">{labels.dwell}</Tab>}
</TabList>
<TabPanel tabId="flow">
<div className="overflow-x-auto pt-4">
<SankeyChart
nodes={nodes}
links={links}
width={width}
height={height}
aria-label="User flow"
/>
</div>
</TabPanel>
<TabPanel tabId="top-paths">
<div className="pt-4">
<DataTable
columns={topPathColumns}
data={topPaths}
keyExtractor={(r) => r.id}
/>
</div>
</TabPanel>
<TabPanel tabId="dropoff">
<div className="pt-4">
<DataTable
columns={dropoffColumns}
data={dropoffData}
keyExtractor={(r) => r.id}
/>
</div>
</TabPanel>
{hasPatterns && (
<TabPanel tabId="patterns">
<div className="gap-section flex flex-col pt-4">
{loopData && loopData.length > 0 && (
<div>
<h4 className="text-text-secondary mb-2 text-sm font-medium">
{labels.loopDetection}
</h4>
<DataTable
columns={loopColumns}
data={loopData}
keyExtractor={(r) => r.id}
/>
</div>
)}
{urlPatternData && urlPatternData.length > 0 && (
<div>
<h4 className="text-text-secondary mb-2 text-sm font-medium">
{labels.urlPatterns}
</h4>
<DataTable
columns={urlPatternColumns}
data={urlPatternData}
keyExtractor={(r) => r.id}
/>
</div>
)}
</div>
</TabPanel>
)}
{hasEntryExit && (
<TabPanel tabId="entry-exit">
<div className="gap-section flex flex-col pt-4">
<div className="gap-section grid md:grid-cols-2">
{entryData && entryData.length > 0 && (
<div>
<h4 className="text-text-secondary mb-2 text-sm font-medium">
{labels.entryPages}
</h4>
<DataTable
columns={entryColumns}
data={entryData}
keyExtractor={(r) => r.id}
/>
</div>
)}
{exitData && exitData.length > 0 && (
<div>
<h4 className="text-text-secondary mb-2 text-sm font-medium">
{labels.exitPages}
</h4>
<DataTable
columns={exitColumns}
data={exitData}
keyExtractor={(r) => r.id}
/>
</div>
)}
</div>
{conversionPaths && conversionPaths.length > 0 && (
<div>
<h4 className="text-text-secondary mb-2 text-sm font-medium">
{labels.conversionPaths}
</h4>
<DataTable
columns={conversionPathColumns}
data={conversionPaths}
keyExtractor={(r) => r.id}
/>
</div>
)}
</div>
</TabPanel>
)}
{hasDwell && (
<TabPanel tabId="dwell">
<div className="gap-section flex flex-col pt-4">
{dwellChartData && dwellChartData.length > 0 && (
<BarChart
data={dwellChartData}
series={dwellSeries}
height={250}
aria-label="Dwell time distribution"
/>
)}
{dwellTableData && dwellTableData.length > 0 && (
<DataTable
columns={dwellTableColumns}
data={dwellTableData}
keyExtractor={(r) => r.id}
/>
)}
</div>
</TabPanel>
)}
</TabsRoot>
</section>
)}
</SurfaceLayout>
);
}
PathAnalysis.displayName = "PathAnalysis";