Skip to content

SankeyDiagram

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

개요

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

주요 특징

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

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;
}

Events

typescript
interface SankeyDiagramEmits {
    /** 노드 호버 시 발생 */
    'node-hover': [node: TopologyNode];
    /** 노드 클릭 시 발생 */
    'node-click': [node: TopologyNode];
    /** 링크 호버 시 발생 */
    'link-hover': [link: TopologyEdge];
    /** 링크 클릭 시 발생 */
    'link-click': [link: TopologyEdge];
    /** 호버 해제 시 발생 */
    'hover-out': [];
    /** 차트 렌더링 완료 시 발생 */
    'chart-ready': [];
}

기본 사용법

vue
<template>
    <SankeyDiagram
        :data="topologyData"
        :width="800"
        :height="600"
        :node-align="'justify'"
        :selected-metric="'timeSum'"
        :error-only-highlight="false"
        @node-click="handleNodeClick"
        @link-click="handleLinkClick"
    />
</template>

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

const topologyData = ref<TopologyData>(sampleTopologyData);

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

const handleLinkClick = (edge: TopologyEdge) => {
    console.log('Link clicked:', edge);
};
</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-apis';

// 차트 설정
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