Skip to content

SankeyDiagram ​

MSA (Microservices Architecture) 분석을 위한 계층형 플로우 다이어그램 컴포넌트입니다.

개요 ​

SankeyDiagram은 마이크로서비스 간의 호출량과 데이터 플로우를 시각적으로 표현하는 차트입니다. 노드는 서비스를, 링크는 서비스 간 호출 관계를 나타내며, 링크의 두께로 호출량을 직관적으로 파악할 수 있습니다.

주요 특징 ​

  • 플로우 시각화: 서비스 간 호출량을 링크 두께로 표현
  • 메트릭 선택: count 또는 timeSum 메트릭 선택 가능
  • 노드 정렬: 다양한 노드 정렬 방식 지원 (left, center, right, justify)
  • 에러 하이라이팅: 에러 발생 링크 별도 색상 표시
  • 인터랙티브 컨트롤: 노드 크기, 폰트 크기 실시간 조정
  • 그라데이션 링크: 소스와 타겟 노드 색상을 혼합한 링크 색상
  • 상세 데이터 연동: 노드/엣지 선택 시 상세 정보 표시 지원
  • 노드 필터링: 마지막 depth의 상위 노드 수 제한 기능
  • 라벨 제어: 노드 라벨 표시/숨김 옵션

Props Interface ​

typescript
interface SankeyDiagramProps {
    /** 토폴로지 데이터 */
    data: TopologyData;
    /** 차트 너비 (기본값: 800) */
    width?: number;
    /** 차트 높이 (기본값: 600) */
    height?: number;
    /** 노드 정렬 방식 (기본값: 'justify') */
    nodeAlign?: 'left' | 'center' | 'right' | 'justify';
    /** 노드 스케일 컨트롤 표시 여부 (기본값: true) */
    showScaleControls?: boolean;
    /** 에러 하이라이팅 활성화 (기본값: false) */
    errorOnlyHighlight?: boolean;
    /** 선택된 메트릭 (기본값: 'timeSum') */
    selectedMetric?: 'timeSum' | 'count';
    /** 노드 라벨 숨김 여부 (기본값: false) */
    hideNodeLabels?: boolean;
    /** 마지막 depth의 상위 클러스터 수 제한 (기본값: -1, -1이면 전체 표시) */
    maxClusterCount?: number;
    /** 현재 선택된 엣지 - 상세 정보 표시용 */
    selectedEdge?: TopologyEdge;
    /** 엣지 상세 데이터 - API 호출 후 가져온 데이터 */
    edgeDetailData?: EdgeDetailData;
    /** 현재 선택된 노드 - 상세 정보 표시용 */
    selectedNode?: TopologyNode;
    /** 노드 상세 데이터 - API 호출 후 가져온 데이터 */
    nodeDetailData?: Record<string, number[]>;
}

Events ​

typescript
interface SankeyDiagramEmits {
    /** 노드 호버 시 발생 */
    'node-hover': [node: TopologyNode];
    /** 노드 클릭 시 발생 */
    'node-click': [node: TopologyNode];
    /** 노드 선택 시 발생 (상세 데이터 로딩 포함) */
    'node-select': [node: TopologyNode];
    /** 링크 호버 시 발생 */
    'link-hover': [link: TopologyEdge];
    /** 링크 클릭 시 발생 */
    'link-click': [link: TopologyEdge];
    /** 엣지 선택 시 발생 (상세 데이터 로딩 포함) */
    'edge-select': [edge: TopologyEdge];
    /** 호버 해제 시 발생 */
    'hover-out': [];
    /** 차트 렌더링 완료 시 발생 */
    'chart-ready': [];
    /** 레이아웃 오류 발생 시 발생 */
    'layout-error': [error: Error];
}

기본 사용법 ​

vue
<template>
    <SankeyDiagram
        :data="topologyData"
        :width="800"
        :height="600"
        :node-align="'justify'"
        :selected-metric="'timeSum'"
        :error-only-highlight="false"
        :hide-node-labels="false"
        :top-last-depth-count="-1"
        :selected-edge="selectedEdge"
        :edge-detail-data="edgeDetailData"
        :selected-node="selectedNode"
        :node-detail-data="nodeDetailData"
        @node-click="handleNodeClick"
        @node-select="handleNodeSelect"
        @link-click="handleLinkClick"
        @edge-select="handleEdgeSelect"
        @chart-ready="handleChartReady"
        @layout-error="handleLayoutError"
    />
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { SankeyDiagram } from '@jennifersoft/apm-components';
import type {
    TopologyData,
    TopologyNode,
    TopologyEdge,
    EdgeDetailData,
} from '@jennifersoft/apm-core';

const topologyData = ref<TopologyData>(sampleTopologyData);
const selectedEdge = ref<TopologyEdge | null>(null);
const edgeDetailData = ref<EdgeDetailData | null>(null);
const selectedNode = ref<TopologyNode | null>(null);
const nodeDetailData = ref<Record<string, number[]> | null>(null);

const handleNodeClick = (node: TopologyNode) => {
    console.log('Node clicked:', node);
};

const handleNodeSelect = (node: TopologyNode) => {
    selectedNode.value = node;
    console.log('Node selected:', node);
    // API 호출로 노드 상세 데이터 조회
};

const handleLinkClick = (edge: TopologyEdge) => {
    console.log('Link clicked:', edge);
};

const handleEdgeSelect = (edge: TopologyEdge) => {
    selectedEdge.value = edge;
    console.log('Edge selected:', edge);
    // API 호출로 엣지 상세 데이터 조회
};

const handleChartReady = () => {
    console.log('Chart is ready');
};

const handleLayoutError = (error: Error) => {
    console.error('Layout error:', error);
};
</script>

라이브 데모 ​

SankeyDiagram 라이브 데모

💡 사용법: 컴포넌트가 로드되면 노드나 링크 클릭/호버, 메트릭 변경에 따른 링크 두께 조정, 스케일 컨트롤로 노드 크기와 폰트 조정 등의 기능을 사용할 수 있습니다.

API 통합 및 실시간 데이터 ​

SankeyDiagram도 TopologyChart와 동일한 방식으로 JENNIFER APM API를 통해 실시간 데이터를 가져올 수 있습니다:

vue
<template>
    <div class="sankey-container">
        <!-- 로딩 상태 -->
        <div v-if="isLoading" class="loading-indicator">
            <div class="loading-text">
                데이터 로딩 중... {{ Math.round(progress * 100) }}%
            </div>
            <div class="progress-bar">
                <div
                    class="progress-fill"
                    :style="{ width: `${progress * 100}%` }"
                ></div>
            </div>
        </div>

        <!-- 에러 상태 -->
        <div v-else-if="error" class="error-indicator">
            <div class="error-text">{{ error }}</div>
            <button @click="fetchTopologyData" class="retry-button">
                다시 시도
            </button>
        </div>

        <!-- Sankey 차트 표시 -->
        <SankeyDiagram
            v-else
            :data="topologyData"
            :width="chartWidth"
            :height="chartHeight"
            :node-align="nodeAlign"
            :selected-metric="selectedMetric"
            :error-only-highlight="errorOnlyHighlight"
            :hide-node-labels="hideNodeLabels"
            @node-click="onNodeClick"
            @link-click="onLinkClick"
            @chart-ready="onChartReady"
        />
    </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { SankeyDiagram } from '@jennifersoft/apm-components';
import type {
    TopologyData,
    TopologyNode,
    TopologyEdge,
} from '@jennifersoft/apm-core';

// 차트 설정
const nodeAlign = ref<'left' | 'center' | 'right' | 'justify'>('justify');
const selectedMetric = ref<'timeSum' | 'count'>('timeSum');
const errorOnlyHighlight = ref(false);
const hideNodeLabels = ref(false);

// API 및 상태 관리 (TopologyChart와 동일한 로직)
// ... fetchTopologyData, loading states, event handlers 등
</script>

동적 메트릭 전환 ​

메트릭을 동적으로 전환하여 다양한 관점에서 데이터 플로우를 분석할 수 있습니다:

vue
<template>
    <div class="sankey-with-metrics">
        <!-- 메트릭 선택 컨트롤 -->
        <div class="metric-controls">
            <h4>분석 메트릭 선택</h4>
            <div class="metric-options">
                <label>
                    <input
                        type="radio"
                        v-model="selectedMetric"
                        value="timeSum"
                    />
                    응답 시간 기반 (ms)
                </label>
                <label>
                    <input
                        type="radio"
                        v-model="selectedMetric"
                        value="count"
                    />
                    호출 횟수 기반
                </label>
            </div>
            <div class="metric-info">
                <p v-if="selectedMetric === 'timeSum'">
                    📊 총 응답 시간이 긴 연결일수록 두꺼운 링크로 표시됩니다.
                    성능 병목을 찾는데 유용합니다.
                </p>
                <p v-else>
                    📊 호출 횟수가 많은 연결일수록 두꺼운 링크로 표시됩니다.
                    사용량 패턴 분석에 유용합니다.
                </p>
            </div>
        </div>

        <SankeyDiagram
            :data="topologyData"
            :selected-metric="selectedMetric"
            @metric-changed="onMetricChanged"
        />

        <!-- 메트릭별 통계 정보 -->
        <div class="metric-stats">
            <h4>
                {{ selectedMetric === 'timeSum' ? '응답 시간' : '호출 횟수' }}
                통계
            </h4>
            <div class="stats-grid">
                <div class="stat-item">
                    <span class="stat-label"
                        >총
                        {{
                            selectedMetric === 'timeSum'
                                ? '응답 시간'
                                : '호출 횟수'
                        }}:</span
                    >
                    <span class="stat-value"
                        >{{ totalMetricValue.toLocaleString()
                        }}{{ selectedMetric === 'timeSum' ? 'ms' : '건' }}</span
                    >
                </div>
                <div class="stat-item">
                    <span class="stat-label"
                        >평균
                        {{
                            selectedMetric === 'timeSum'
                                ? '응답 시간'
                                : '호출 횟수'
                        }}:</span
                    >
                    <span class="stat-value"
                        >{{ averageMetricValue.toLocaleString()
                        }}{{ selectedMetric === 'timeSum' ? 'ms' : '건' }}</span
                    >
                </div>
                <div class="stat-item">
                    <span class="stat-label"
                        >최대
                        {{
                            selectedMetric === 'timeSum'
                                ? '응답 시간'
                                : '호출 횟수'
                        }}:</span
                    >
                    <span class="stat-value"
                        >{{ maxMetricValue.toLocaleString()
                        }}{{ selectedMetric === 'timeSum' ? 'ms' : '건' }}</span
                    >
                </div>
            </div>
        </div>
    </div>
</template>

<script setup lang="ts">
import { computed } from 'vue';

const totalMetricValue = computed(() => {
    const key = selectedMetric.value;
    return topologyData.value.edges.reduce(
        (sum, edge) => sum + (edge.statistic[key] || 0),
        0
    );
});

const averageMetricValue = computed(() => {
    const total = totalMetricValue.value;
    const count = topologyData.value.edges.length;
    return count > 0 ? Math.round(total / count) : 0;
});

const maxMetricValue = computed(() => {
    const key = selectedMetric.value;
    return Math.max(
        ...topologyData.value.edges.map((edge) => edge.statistic[key] || 0)
    );
});

const onMetricChanged = (metric: string) => {
    console.log(`메트릭 변경됨: ${metric}`);
    // 메트릭 변경 시 추가 로직 수행
};
</script>

이벤트 로깅 및 분석 ​

SankeyDiagram의 인터랙션을 추적하고 사용자 행동을 분석할 수 있습니다:

vue
<template>
    <div class="sankey-with-analytics">
        <SankeyDiagram
            :data="topologyData"
            @node-click="logNodeClick"
            @link-click="logLinkClick"
            @node-hover="logNodeHover"
            @link-hover="logLinkHover"
        />

        <!-- 상호작용 분석 패널 -->
        <div class="interaction-analytics">
            <h4>사용자 상호작용 분석</h4>
            <div class="analytics-grid">
                <div class="analytics-item">
                    <span class="analytics-label">총 클릭 수:</span>
                    <span class="analytics-value">{{ totalClicks }}</span>
                </div>
                <div class="analytics-item">
                    <span class="analytics-label">노드 클릭:</span>
                    <span class="analytics-value">{{ nodeClicks }}</span>
                </div>
                <div class="analytics-item">
                    <span class="analytics-label">링크 클릭:</span>
                    <span class="analytics-value">{{ linkClicks }}</span>
                </div>
                <div class="analytics-item">
                    <span class="analytics-label">가장 많이 클릭된 노드:</span>
                    <span class="analytics-value">{{ mostClickedNode }}</span>
                </div>
            </div>

            <!-- 클릭 히트맵 -->
            <div class="click-heatmap">
                <h5>노드별 클릭 빈도</h5>
                <div
                    v-for="(count, nodeId) in nodeClickCounts"
                    :key="nodeId"
                    class="heatmap-item"
                >
                    <span class="node-name">{{ getNodeName(nodeId) }}</span>
                    <div class="click-bar">
                        <div
                            class="click-fill"
                            :style="{
                                width: `${(count / maxNodeClicks) * 100}%`,
                            }"
                        ></div>
                        <span class="click-count">{{ count }}</span>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue';

// 분석 데이터
const clickHistory = ref<
    Array<{
        type: 'node' | 'link';
        target: string;
        timestamp: number;
    }>
>([]);

const nodeClickCounts = ref<Record<string, number>>({});
const linkClickCounts = ref<Record<string, number>>({});

// 통계 계산
const totalClicks = computed(() => clickHistory.value.length);
const nodeClicks = computed(
    () => clickHistory.value.filter((c) => c.type === 'node').length
);
const linkClicks = computed(
    () => clickHistory.value.filter((c) => c.type === 'link').length
);

const mostClickedNode = computed(() => {
    const maxCount = Math.max(...Object.values(nodeClickCounts.value));
    const nodeId = Object.keys(nodeClickCounts.value).find(
        (id) => nodeClickCounts.value[id] === maxCount
    );
    return nodeId ? getNodeName(nodeId) : '없음';
});

const maxNodeClicks = computed(() =>
    Math.max(...Object.values(nodeClickCounts.value), 1)
);

// 이벤트 핸들러
const logNodeClick = (node: TopologyNode) => {
    const nodeId = getNodeId(node);
    clickHistory.value.push({
        type: 'node',
        target: nodeId,
        timestamp: Date.now(),
    });
    nodeClickCounts.value[nodeId] = (nodeClickCounts.value[nodeId] || 0) + 1;
};

const logLinkClick = (edge: TopologyEdge) => {
    const linkId = getLinkId(edge);
    clickHistory.value.push({
        type: 'link',
        target: linkId,
        timestamp: Date.now(),
    });
    linkClickCounts.value[linkId] = (linkClickCounts.value[linkId] || 0) + 1;
};
</script>

데이터 구조 ​

SankeyDiagram은 TopologyChart와 동일한 데이터 구조를 사용합니다.

TopologyData ​

typescript
interface TopologyData {
    nodes: TopologyNode[];
    edges: TopologyEdge[];
}

TopologyEdge (중요) ​

Sankey 다이어그램에서 링크의 두께는 statistic 필드의 값에 따라 결정됩니다:

typescript
interface TopologyEdge {
    relation: {
        source: TopologyNode;
        target: TopologyNode;
    };
    statistic: {
        count: number; // 호출 횟수 (selectedMetric="count"일 때 사용)
        timeSum: number; // 총 응답 시간 (selectedMetric="timeSum"일 때 사용)
        failureCount: number; // 실패 횟수 (에러 하이라이팅에 사용)
    };
}

노드 정렬 방식 ​

justify (기본값) ​

노드들이 전체 너비에 고르게 분산되어 배치됩니다. 가장 균형 잡힌 레이아웃을 제공합니다.

left ​

모든 노드가 왼쪽 정렬되어 배치됩니다. 시작점이 명확한 플로우에 적합합니다.

center ​

모든 노드가 중앙 정렬되어 배치됩니다. 대칭적인 구조를 강조하고 싶을 때 사용합니다.

모든 노드가 오른쪽 정렬되어 배치됩니다. 최종 결과를 강조하고 싶을 때 사용합니다.

메트릭 선택 ​

timeSum (기본값) ​

각 엣지의 timeSum 값을 사용하여 링크 두께를 결정합니다. 총 응답 시간이 클수록 링크가 두껍게 표시됩니다.

count ​

각 엣지의 count 값을 사용하여 링크 두께를 결정합니다. 호출 횟수가 많을수록 링크가 두껍게 표시됩니다.

에러 하이라이팅 ​

errorOnlyHighlight 옵션을 활성화하면:

  • 에러 링크: failureCount > 0