import panel as pn
import pandas as pd
import numpy as np
from bokeh.plotting import figure
from bokeh.models import HoverTool, ColumnDataSource, FactorRange
from plot_config_dialog import ParameterDialog, AxisParameterDialog
import threading

class GraphPanel:
    """그래프 표시 및 설정 담당 클래스"""
    
    def __init__(self):
        self.current_df = None
        
        # 선택 콜백
        self.on_selection_callback = None
        
        # 그래프 리스트 (각 그래프의 정보를 저장)
        self.graphs = []
        self.graph_id_counter = 0
        
        # 드래그앤드롭 선택 상태
        self._dnd_selected = None  # 첫 번째 클릭된 graph_info
        
        # 그래프 컨테이너 (가로 스크롤 가능)
        self.graphs_container = pn.Row()
        
        self._create_widgets()
    
    def set_selection_callback(self, callback):
        """선택 콜백 설정"""
        self.on_selection_callback = callback
    
    def update_tag_dropdowns(self):
        """모든 Tag 카드의 옵션 업데이트
        Accept All 버튼으로 새로운 Tag가 추가되었을 때 호출
        """
        if self.current_df is None:
            return
        
        for i, graph_info in enumerate(self.graphs):
            if graph_info['type'] == 'tag':
                if i == 0:
                    indices = None
                else:
                    indices = self.graphs[i - 1]['filtered_indices']
                self._update_tag_card(graph_info, self.current_df, indices=indices)
                print(f"[GraphPanel] Updated tag card {graph_info['id']} with new tags")
    
    def _on_add_tag(self, event):
        """Tag 카드 추가"""
        graph_id = self.graph_id_counter
        self.graph_id_counter += 1
        
        # 새 Tag 카드 생성
        tag_info = self._create_tag_card(graph_id)
        self.graphs.append(tag_info)
        
        # 데이터가 있으면 즉시 업데이트
        if self.current_df is not None:
            # 왼쪽에 다른 그래프가 있으면 그 그래프의 filtered_indices 사용
            if len(self.graphs) > 1:
                prev_graph = self.graphs[-2]  # 새로 추가하기 전의 마지막 그래프
                prev_indices = prev_graph['filtered_indices']
                self._update_tag_card(tag_info, self.current_df, indices=prev_indices)
            else:
                # 첫 번째면 전체 데이터
                self._update_tag_card(tag_info, self.current_df, indices=None)
        
        # 레이아웃 업데이트
        self._update_graphs_layout()
        
        # 레이아웃 업데이트 후 테이블 업데이트 (맨 오른쪽 그래프 = 방금 추가한 카드)
        # filtered_indices가 빈 리스트여도 테이블 업데이트 (빈 테이블 표시)
        if self.current_df is not None and self.on_selection_callback:
            print(f"[Add Tag] Updating table with {len(tag_info['filtered_indices'])} rows")
            self.on_selection_callback(tag_info['filtered_indices'])
    
    def _on_add_bar(self, event):
        """Bar 그래프 추가"""
        graph_id = self.graph_id_counter
        self.graph_id_counter += 1
        
        x_column = 'AED'
        
        # 새 Bar 그래프 생성
        graph_info = self._create_bar_graph(graph_id, x_column)
        self.graphs.append(graph_info)
        
        # 데이터가 있으면 즉시 업데이트
        if self.current_df is not None:
            # 왼쪽에 다른 그래프가 있으면 그 그래프의 filtered_indices 사용
            if len(self.graphs) > 1:
                prev_graph = self.graphs[-2]  # 새로 추가하기 전의 마지막 그래프
                prev_indices = prev_graph['filtered_indices']
                self._update_bar_graph(graph_info, self.current_df, indices=prev_indices)
            else:
                # 첫 번째 그래프면 전체 데이터
                self._update_bar_graph(graph_info, self.current_df, indices=None)
        
        # 레이아웃 업데이트
        self._update_graphs_layout()
        
        # 레이아웃 업데이트 후 테이블 업데이트 (맨 오른쪽 그래프 = 방금 추가한 그래프)
        if self.current_df is not None and self.on_selection_callback and graph_info['filtered_indices']:
            print(f"[Add Bar] Updating table with {len(graph_info['filtered_indices'])} rows")
            self.on_selection_callback(graph_info['filtered_indices'])
    
    def _on_add_scatter(self, event):
        """Scatter 그래프 추가"""
        graph_id = self.graph_id_counter
        self.graph_id_counter += 1
        
        x_column = 'AED'
        y_column = None
        
        # 새 Scatter 그래프 생성
        graph_info = self._create_scatter_graph(graph_id, x_column, y_column)
        self.graphs.append(graph_info)
        
        # 데이터가 있으면 즉시 업데이트
        if self.current_df is not None:
            # 왼쪽에 다른 그래프가 있으면 그 그래프의 filtered_indices 사용
            if len(self.graphs) > 1:
                prev_graph = self.graphs[-2]  # 새로 추가하기 전의 마지막 그래프
                prev_indices = prev_graph['filtered_indices']
                self._update_scatter_graph(graph_info, self.current_df, indices=prev_indices)
            else:
                # 첫 번째 그래프면 전체 데이터
                self._update_scatter_graph(graph_info, self.current_df, indices=None)
        
        # 레이아웃 업데이트
        self._update_graphs_layout()
        
        # 레이아웃 업데이트 후 테이블 업데이트 (맨 오른쪽 그래프 = 방금 추가한 그래프)
        if self.current_df is not None and self.on_selection_callback and graph_info['filtered_indices']:
            print(f"[Add Scatter] Updating table with {len(graph_info['filtered_indices'])} rows")
            self.on_selection_callback(graph_info['filtered_indices'])
    
    def _delete_graph(self, graph_info):
        """그래프 삭제
        - 삭제되는 카드 이후의 모든 카드들의 selection 복원
        """
        if graph_info not in self.graphs:
            return
        
        # 삭제할 그래프의 위치 찾기
        delete_index = self.graphs.index(graph_info)
        
        # 삭제되는 카드 이후의 모든 카드들의 selection 정보 저장
        saved_selections = []
        for i in range(delete_index + 1, len(self.graphs)):
            card = self.graphs[i]
            
            if card['type'] == 'bar':
                saved_selections.append({
                    'type': 'bar',
                    'indices': list(card['source'].selected.indices)
                })
            elif card['type'] == 'scatter':
                saved_selections.append({
                    'type': 'scatter',
                    'indices': list(card['source'].selected.indices)
                })
            elif card['type'] == 'tag':
                # Tag card는 드롭다운 선택이 자동으로 유지되므로 별도 저장 불필요
                saved_selections.append({
                    'type': 'tag',
                    'indices': []
                })
        
        # graphs 리스트에서 제거
        self.graphs.remove(graph_info)
        
        # 레이아웃 업데이트
        self._update_graphs_layout()
        
        # 데이터가 있는 경우 업데이트
        if self.current_df is not None and len(self.graphs) > 0:
            # 삭제된 위치부터 오른쪽 그래프들 업데이트
            start_index = delete_index
            
            for i in range(start_index, len(self.graphs)):
                graph = self.graphs[i]
                
                # 입력 데이터 결정
                if i == 0:
                    # 첫 번째 그래프면 전체 데이터
                    indices = None
                else:
                    # 왼쪽 그래프의 filtered_indices 사용
                    indices = self.graphs[i - 1]['filtered_indices']
                
                # 그래프 업데이트
                if graph['type'] == 'bar':
                    self._update_bar_graph(graph, self.current_df, indices=indices)
                elif graph['type'] == 'scatter':
                    self._update_scatter_graph(graph, self.current_df, indices=indices)
                elif graph['type'] == 'tag':
                    self._update_tag_card(graph, self.current_df, indices=indices)
                
                # Selection 복원 (saved_selections의 인덱스는 start_index 기준)
                selection_idx = i - start_index
                if selection_idx < len(saved_selections):
                    saved = saved_selections[selection_idx]
                    
                    if saved['type'] == 'bar' and len(saved['indices']) > 0:
                        # Bar graph의 bin selection 복원
                        graph['source'].selected.indices = saved['indices']
                    elif saved['type'] == 'scatter' and len(saved['indices']) > 0:
                        # Scatter graph의 point selection 복원
                        graph['source'].selected.indices = saved['indices']
        
        # 테이블 업데이트 (맨 오른쪽 그래프의 데이터)
        if len(self.graphs) > 0 and self.on_selection_callback:
            rightmost_graph = self.graphs[-1]
            if rightmost_graph['filtered_indices']:
                self.on_selection_callback(rightmost_graph['filtered_indices'])
        elif self.on_selection_callback:
            # 그래프가 없으면 전체 데이터 표시
            if self.current_df is not None:
                self.on_selection_callback(self.current_df.index.tolist())
    
    def _update_graphs_layout(self):
        self.graphs_container.clear()

        HEADER_HEIGHT = 42
        CONFIG_WIDTH = 100

        def create_header(drag_handle, config_widget, delete_button):

            items = [drag_handle, pn.layout.HSpacer()]

            if config_widget is not None:
                items.append(config_widget)

            items.append(delete_button)

            return pn.Row(
                *items,
                align="center",
                height=HEADER_HEIGHT,
                sizing_mode="stretch_width",
                margin=(5,5,0,5),
            )

        for graph_info in self.graphs:

            # ---------------- TAG FILTER ----------------
            if graph_info['type'] == 'tag':

                header = create_header(
                    graph_info['drag_handle'],
                    None,
                    graph_info['delete_button']
                )

                body = pn.Column(
                    graph_info['pane'],
                    graph_info['checkbox_group'],
                    sizing_mode='stretch_width',
                    styles={'padding': '5px 10px'}
                )

                tag_column = pn.Column(
                    header,
                    body,

                    width=650,
                    height=455,

                    styles={
                        'border': '1px solid #ddd',
                        'border-radius': '6px',
                        'background': '#ffffff'
                    },

                    margin=(5,5,5,5),
                )

            # ---------------- BAR ----------------
            elif graph_info['type'] == 'bar':

                header = create_header(
                    graph_info['drag_handle'],
                    graph_info['config_button'],
                    graph_info['delete_button']
                )

                tag_column = pn.Column(
                    header,
                    graph_info['pane'],
                    width=650,
                    height=455,
                    margin=(5,5,5,5),
                    styles={
                        'border': '1px solid #ddd',
                        'border-radius': '6px',
                    },
                )

            # ---------------- SCATTER ----------------
            else:

                header = create_header(
                    graph_info['drag_handle'],
                    graph_info['config_button'],
                    graph_info['delete_button']
                )

                tag_column = pn.Column(
                    header,
                    graph_info['pane'],
                    width=650,
                    height=455,
                    margin=(5,5,5,5),
                    styles={
                        'border': '1px solid #ddd',
                        'border-radius': '6px',
                    },
                )

            self.graphs_container.append(tag_column)


    
    def _create_widgets(self):
        """위젯 생성"""
        # Show All Data 버튼
        self.reset_filter_button = pn.widgets.Button(
            name='Show All Data',
            button_type='warning',
            width=120,
            height=35,
            styles={
                'font-size': '14px',
                'font-weight': 'bold'
            }
        )
        
        # 그래프 추가 버튼들
        self.add_tag_button = pn.widgets.Button(
            name='+Tag',
            button_type='primary',
            width=120,
            height=35,
            styles={
                'font-size': '16px',
                'font-weight': 'bold'
            }
        )
        
        self.add_bar_button = pn.widgets.Button(
            name='+Bar',
            button_type='primary',
            width=120,
            height=35,
            styles={
                'font-size': '16px',
                'font-weight': 'bold'
            }
        )
        
        self.add_scatter_button = pn.widgets.Button(
            name='+Scatter',
            button_type='primary',
            width=120,
            height=35,
            styles={
                'font-size': '16px',
                'font-weight': 'bold'
            }
        )

        self.delete_all_button = pn.widgets.Button(
            name="Delete All",
            button_type="danger",
            width=120,
            height=35
        )
        
        # 버튼 클릭 이벤트 연결
        self.reset_filter_button.on_click(self._on_reset_filter)
        self.add_tag_button.on_click(self._on_add_tag)
        self.add_bar_button.on_click(self._on_add_bar)
        self.add_scatter_button.on_click(self._on_add_scatter)
        self.delete_all_button.on_click(self._delete_all_graphs)

    
    def _on_reset_filter(self, event):
        """필터 초기화 - 모든 그래프와 테이블 리셋"""
        self.reset_all_filters()

    def _delete_all_graphs(self, event=None):
        # graphs 리스트 초기화
        self.graphs.clear()

        # 그래프 UI 영역 비우기
        self._update_graphs_layout()
        self.on_selection_callback(self.current_df.index.tolist())

    # ───────── Drag & Drop (Button 방식) ─────────

    def _create_drag_handle(self, label):
        """드래그 핸들 버튼 생성 - pn.widgets.Button.on_click은 Bokeh document lock 안에서 실행됨"""
        handle = pn.widgets.Button(
            name=f'≡ {label}',
            button_type='light',
            width=150,
            height=30,
            styles={
                'cursor': 'grab',
                'font-weight': '600',
                'font-size': '14px',
            }
        )
        handle.on_click(lambda event: self._on_handle_click(handle))
        return handle

    def _on_handle_click(self, clicked_handle):
        """드래그 핸들 클릭 처리 - 2클릭 방식으로 카드 이동"""
        # 클릭된 카드의 graph_info 찾기
        clicked_graph = None
        for g in self.graphs:
            if g.get('drag_handle') is clicked_handle:
                clicked_graph = g
                break
        if clicked_graph is None:
            return

        if self._dnd_selected is None:
            # 첫 번째 클릭 - 선택
            self._dnd_selected = clicked_graph
            clicked_handle.button_type = 'primary'  # 파란색 하이라이트
            print(f"[DnD] Selected: {clicked_graph['type']} (id={clicked_graph['id']})")
        else:
            first = self._dnd_selected
            self._dnd_selected = None

            # 모든 핸들 선택 해제
            for g in self.graphs:
                if 'drag_handle' in g:
                    g['drag_handle'].button_type = 'light'

            if first is clicked_graph:
                # 같은 카드 다시 클릭 → 선택 해제만
                print(f"[DnD] Deselected")
                return

            # 다른 카드 클릭 → 이동
            from_idx = self.graphs.index(first)
            to_idx = self.graphs.index(clicked_graph)
            print(f"[DnD] Reorder: {from_idx} → {to_idx}")

            # 리스트에서 이동
            graph = self.graphs.pop(from_idx)
            self.graphs.insert(to_idx, graph)

            # 레이아웃 재구성
            self._update_graphs_layout()

            # 필터링 체인 재계산
            if self.current_df is not None:
                start = min(from_idx, to_idx)
                for i in range(start, len(self.graphs)):
                    g = self.graphs[i]
                    inp = None if i == 0 else self.graphs[i - 1]['filtered_indices']
                    if g['type'] == 'bar':
                        self._update_bar_graph(g, self.current_df, indices=inp)
                    elif g['type'] == 'scatter':
                        self._update_scatter_graph(g, self.current_df, indices=inp)
                    elif g['type'] == 'tag':
                        self._update_tag_card(g, self.current_df, indices=inp)

                if len(self.graphs) > 0 and self.on_selection_callback:
                    rightmost = self.graphs[-1]
                    print(f"[DnD Reorder] Table update: {len(rightmost['filtered_indices'])} rows")
                    self.on_selection_callback(rightmost['filtered_indices'])

    
    def _create_tag_card(self, graph_id):
        """Tag 카드 생성 - 수평 바 차트 + 체크박스"""
        # Delete 버튼
        delete_button = pn.widgets.Button(
            name='✕',
            button_type='danger',
            width=40,
            height=30
        )
        
        # Bokeh 수평 바 차트
        tag_source = ColumnDataSource(data=dict(tags=[], counts=[]))
        tag_plot = figure(
            y_range=FactorRange(factors=[]),
            x_axis_label='Count',
            y_axis_label='Tag',
            width=580,
            height=350,
            tools='',
            toolbar_location=None,
        )
        tag_plot.hbar(
            y='tags', right='counts', height=0.6,
            source=tag_source, color='#1b6d85', alpha=0.8
        )
        tag_plot.xgrid.grid_line_color = '#eeeeee'
        tag_plot.ygrid.grid_line_color = None
        tag_pane = pn.pane.Bokeh(tag_plot, sizing_mode='fixed', width=580, height=350)
        
        # 체크박스 그룹
        checkbox_group = pn.widgets.CheckBoxGroup(
            options=[],
            value=[],
            inline=True
        )
        
        tag_info = {
            'type': 'tag',
            'id': graph_id,
            'drag_handle': self._create_drag_handle("Tag Filter"),
            'checkbox_group': checkbox_group,
            'plot': tag_plot,
            'pane': tag_pane,
            'source': tag_source,
            'delete_button': delete_button,
            'filtered_indices': None,
            'tag_names': [],
            '_updating': False,  # 프로그래밍 업데이트 중 콜백 억제 플래그
        }
        
        # Delete 버튼 콜백
        delete_button.on_click(lambda event: self._delete_graph(tag_info))
        
        # 체크박스 변경 콜백
        checkbox_group.param.watch(
            lambda event: self._on_tag_checkbox_change(tag_info),
            'value'
        )
        
        return tag_info
    
    def _on_tag_checkbox_change(self, tag_info):
        """체크박스 변경 시 필터링 업데이트 + 오른쪽 카드 전파"""
        if tag_info.get('_updating', False):
            return
        if self.current_df is None:
            return
        
        current_index = self.graphs.index(tag_info)
        
        # 입력 데이터 결정
        if current_index == 0:
            input_indices = None
        else:
            input_indices = self.graphs[current_index - 1]['filtered_indices']
        
        if input_indices is not None:
            if len(input_indices) == 0:
                filtered_df = pd.DataFrame()
            else:
                filtered_df = self.current_df.loc[input_indices]
        else:
            filtered_df = self.current_df
        
        # 체크된 태그로 필터링
        checked_tags = tag_info['checkbox_group'].value
        tag_info['filtered_indices'] = self._compute_tag_filtered_indices(filtered_df, checked_tags)
        
        print(f"[Tag Checkbox] Checked: {checked_tags}, filtered: {len(tag_info['filtered_indices'])} rows")
        
        # 오른쪽 그래프들 업데이트
        selected_indices = tag_info['filtered_indices']
        for i in range(current_index + 1, len(self.graphs)):
            next_graph = self.graphs[i]
            if next_graph['type'] == 'bar':
                self._update_bar_graph(next_graph, self.current_df, selected_indices)
            elif next_graph['type'] == 'scatter':
                self._update_scatter_graph(next_graph, self.current_df, selected_indices)
            elif next_graph['type'] == 'tag':
                self._update_tag_card(next_graph, self.current_df, selected_indices)
            selected_indices = next_graph['filtered_indices']
        
        # 테이블 업데이트
        if len(self.graphs) > 0 and self.on_selection_callback:
            rightmost_graph = self.graphs[-1]
            print(f"[Tag Checkbox] Table update: {len(rightmost_graph['filtered_indices'])} rows")
            self.on_selection_callback(rightmost_graph['filtered_indices'])
    
    def _compute_tag_filtered_indices(self, filtered_df, checked_tags):
        """체크된 태그 기반 필터링 (OR 조건)"""
        if len(checked_tags) == 0 or len(filtered_df) == 0 or 'Tag' not in filtered_df.columns:
            return []
        
        mask = pd.Series(False, index=filtered_df.index)
        for tag in checked_tags:
            if tag == '(No Tag)':
                mask |= filtered_df['Tag'].isna() | (filtered_df['Tag'].astype(str).str.strip() == '')
            else:
                mask |= (filtered_df['Tag'] == tag)
        
        return filtered_df[mask].index.tolist()
    
    def _create_bar_graph(self, graph_id, x_column):
        """Bar Graph 생성 - 독립적인 그래프 객체 반환"""
        # 설정 버튼
        config_button = pn.widgets.Button(
            name='⚙ X-Axis',
            button_type='light',
            width=100,
            height=30
        )
        
        # 삭제 버튼
        delete_button = pn.widgets.Button(
            name='✕',
            button_type='danger',
            width=40,
            height=30
        )
        
        # 데이터 소스
        bar_source = ColumnDataSource(data=dict(x=[], top=[], width=[], left=[], right=[]))
        
        # 그래프 생성
        bar_plot = figure(
            x_axis_label=f'{x_column}',
            y_axis_label='Count',
            width=600,
            height=400,
            tools='pan,wheel_zoom,box_zoom,box_select,lasso_select,reset,save'
        )
        
        bar_plot.vbar(x=[], top=[], width=0, color='navy', alpha=0.7)
        bar_pane = pn.pane.Bokeh(bar_plot, sizing_mode='fixed', width=600, height=400)
        
        # 그래프 정보 딕셔너리
        graph_info = {
            'id': graph_id,
            'type': 'bar',
            'drag_handle': self._create_drag_handle("Bar Plot"),
            'plot': bar_plot,
            'pane': bar_pane,
            'source': bar_source,
            'config_button': config_button,
            'delete_button': delete_button,
            'x_column': x_column,
            'filtered_indices': None  # 현재 표시 중인 데이터의 인덱스
        }
        
        # 설정 버튼 클릭 이벤트
        config_button.on_click(lambda event: self._show_bar_config_for_graph(graph_info))
        
        # 삭제 버튼 클릭 이벤트
        delete_button.on_click(lambda event: self._delete_graph(graph_info))
        
        return graph_info
    
    def _create_scatter_graph(self, graph_id, x_column, y_column):
        """Scatter Graph 생성 - 독립적인 그래프 객체 반환"""
        # 설정 버튼
        config_button = pn.widgets.Button(
            name='⚙ Axes',
            button_type='light',
            width=100,
            height=30
        )
        
        # 삭제 버튼
        delete_button = pn.widgets.Button(
            name='✕',
            button_type='danger',
            width=40,
            height=30
        )
        
        # 데이터 소스
        scatter_source = ColumnDataSource(data=dict(x=[], y=[], index=[]))
        
        # Y축 컬럼 설정 (일단 기본값)
        if y_column is None:
            y_column = 'Y'  # 기본값, 나중에 데이터로 업데이트됨
        
        # 그래프 생성
        scatter_plot = figure(
            x_axis_label=f'{x_column}',
            y_axis_label=f'{y_column}',
            width=600,
            height=400,
            tools='pan,wheel_zoom,box_zoom,box_select,lasso_select,reset,save'
        )
        
        scatter_plot.circle(x=[], y=[], size=5, color='blue', alpha=0.6)
        
        hover = HoverTool(tooltips=[
            ('X', '@x{0.2f}'),
            ('Y', '@y{0.3f}')
        ])
        scatter_plot.add_tools(hover)
        scatter_pane = pn.pane.Bokeh(scatter_plot, sizing_mode='fixed', width=600, height=400)
        
        # 그래프 정보 딕셔너리
        graph_info = {
            'id': graph_id,
            'type': 'scatter',
            'drag_handle': self._create_drag_handle("Scatter Plot"),
            'plot': scatter_plot,
            'pane': scatter_pane,
            'source': scatter_source,
            'config_button': config_button,
            'delete_button': delete_button,
            'x_column': x_column,
            'y_column': None,  # 데이터 로드 시 자동 선택
            'filtered_indices': None  # 현재 표시 중인 데이터의 인덱스
        }
        
        # 설정 버튼 클릭 이벤트
        config_button.on_click(lambda event: self._show_scatter_config_for_graph(graph_info))
        
        # 삭제 버튼 클릭 이벤트
        delete_button.on_click(lambda event: self._delete_graph(graph_info))
        
        return graph_info
    
    def _update_tag_card(self, tag_info, df, indices=None):
        """Tag 카드 업데이트 - 수평 바 차트 + 체크박스 갱신
        
        Args:
            tag_info: 태그 카드 정보
            df: 전체 데이터프레임
            indices: 표시할 데이터의 인덱스 리스트 (None이면 전체)
        """
        # 콜백 억제 플래그
        tag_info['_updating'] = True
        
        try:
            # 인덱스 기반 필터링
            if indices is not None:
                if len(indices) == 0:
                    filtered_df = pd.DataFrame()
                else:
                    filtered_df = df.loc[indices]
            else:
                filtered_df = df
            
            # Tag 컬럼 확인
            if 'Tag' not in df.columns:
                tag_info['source'].data = dict(tags=[], counts=[])
                tag_info['plot'].y_range.factors = []
                tag_info['checkbox_group'].options = []
                tag_info['checkbox_group'].value = []
                tag_info['filtered_indices'] = []
                tag_info['tag_names'] = []
                return
            
            # 전체 데이터에서 태그 고유값 추출
            all_tags = df['Tag'].unique().tolist()
            unique_tags = []
            has_empty = False
            for tag in all_tags:
                if pd.isna(tag) or str(tag).strip() == '':
                    has_empty = True
                else:
                    unique_tags.append(str(tag))
            unique_tags = sorted(unique_tags)
            if has_empty:
                unique_tags.insert(0, '(No Tag)')
            
            # filtered_df 기준 카운트 계산
            counts = []
            for tag in unique_tags:
                if len(filtered_df) == 0:
                    counts.append(0)
                elif tag == '(No Tag)':
                    cnt = filtered_df[
                        filtered_df['Tag'].isna() | (filtered_df['Tag'].astype(str).str.strip() == '')
                    ].shape[0]
                    counts.append(cnt)
                else:
                    cnt = filtered_df[filtered_df['Tag'] == tag].shape[0]
                    counts.append(cnt)
            
            # 바 차트 업데이트 (아래→위 순서로 표시)
            display_tags = list(unique_tags)  # 아래부터 위로
            display_counts = list(counts)
            
            tag_info['source'].data = dict(tags=display_tags, counts=display_counts)
            tag_info['plot'].y_range.factors = display_tags
            tag_info['tag_names'] = unique_tags
            
            # 체크박스 옵션 업데이트 (기존 선택 유지)
            prev_checked = tag_info['checkbox_group'].value
            tag_info['checkbox_group'].options = unique_tags
            
            # 기존 선택 중 여전히 유효한 것 유지, 없으면 전체 선택
            still_valid = [t for t in prev_checked if t in unique_tags]
            if still_valid:
                tag_info['checkbox_group'].value = still_valid
            else:
                # 처음이거나 이전 선택이 없으면 전체 선택
                tag_info['checkbox_group'].value = list(unique_tags)
            
            # filtered_indices 계산
            checked_tags = tag_info['checkbox_group'].value
            tag_info['filtered_indices'] = self._compute_tag_filtered_indices(filtered_df, checked_tags)
        
        finally:
            tag_info['_updating'] = False
    
    def _update_bar_graph(self, graph_info, df, indices=None):
        """특정 Bar Graph 업데이트
        
        Args:
            graph_info: 그래프 정보
            df: 전체 데이터프레임
            indices: 표시할 데이터의 인덱스 리스트 (None이면 전체)
        """
        column = graph_info['x_column']
        
        if column not in df.columns:
            return
        
        # 인덱스 기반 필터링
        if indices is not None:
            if len(indices) == 0:
                # 빈 리스트가 전달되면 빈 그래프 표시
                filtered_df = pd.DataFrame()
            else:
                filtered_df = df.loc[indices]
        else:
            filtered_df = df
        
        # filtered_indices 저장
        graph_info['filtered_indices'] = filtered_df.index.tolist()
        
        data = filtered_df[column].dropna() if column in filtered_df.columns else pd.Series()
        
        # 빈 그래프 생성
        bar_plot = figure(
            x_axis_label=f'{column}',
            y_axis_label='Count',
            width=600,
            height=400,
            tools='pan,wheel_zoom,box_zoom,box_select,lasso_select,reset,save'
        )
        
        if len(data) > 0:
            # 히스토그램 계산
            counts, edges = np.histogram(data, bins=30)
            
            # 막대의 중심 위치와 너비 계산
            centers = (edges[:-1] + edges[1:]) / 2
            widths = edges[1:] - edges[:-1]
            
            # 새로운 ColumnDataSource 생성
            bar_source = ColumnDataSource(data={
                'x': centers,
                'top': counts,
                'width': widths * 0.9,
                'left': edges[:-1],
                'right': edges[1:]
            })
            
            # 막대 그래프
            bar_plot.vbar(
                x='x', 
                top='top', 
                width='width',
                source=bar_source,
                color='navy', 
                alpha=0.7,
                legend_label='Count'
            )
            
            bar_plot.legend.location = "top_left"
            bar_plot.legend.click_policy = "hide"
            
            # Selection 콜백 등록
            bar_source.selected.on_change('indices', 
                lambda attr, old, new: self._on_bar_selection(graph_info, attr, old, new))
        else:
            # 빈 데이터일 때 빈 소스 생성
            bar_source = ColumnDataSource(data={
                'x': [],
                'top': [],
                'width': [],
                'left': [],
                'right': []
            })
        
        # 그래프 정보 업데이트
        graph_info['plot'] = bar_plot
        graph_info['source'] = bar_source
        graph_info['pane'].object = bar_plot
    
    def _on_bar_selection(self, graph_info, attr, old, new):
        """Bar plot에서 선택 시 호출 - 오른쪽 그래프들과 테이블 업데이트"""
        # old와 new가 같으면 실제 변경이 아니므로 무시
        if old == new:
            return
        
        print(f"[Bar Selection] Graph {graph_info['id']}, selected bars: {len(new)}, old: {len(old)}")
        
        if self.current_df is None:
            return
        
        column = graph_info['x_column']
        source = graph_info['source']
        
        # 선택된 막대들의 범위 수집
        selected_indices = []
        
        if len(new) > 0:
            # 선택이 있는 경우
            for i in new:
                left = source.data['left'][i]
                right = source.data['right'][i]
                
                # 현재 그래프의 filtered_indices 내에서만 검색
                # if graph_info['filtered_indices']:
                #     search_df = self.current_df.loc[graph_info['filtered_indices']]
                # else:
                #     search_df = self.current_df
                search_df = self.current_df
                
                # 해당 범위에 속하는 데이터의 인덱스 찾기
                mask = (search_df[column] >= left) & (search_df[column] < right)
                indices = search_df[mask].index.tolist()
                selected_indices.extend(indices)
            print(f"[Bar Selection] Selected {len(selected_indices)} data points")
            
            selected_indices = list(set(selected_indices))
            graph_info['filtered_indices'] = selected_indices
        else:
            # 선택이 없는 경우 - 현재 그래프의 전체 데이터
            selected_indices = graph_info['filtered_indices'] if graph_info['filtered_indices'] else self.current_df.index.tolist()
            print(f"[Bar Selection] No selection, using all data: {len(selected_indices)} points")

        graph_info['filtered_indices'] = selected_indices

        # 업데이트를 비동기로 실행
        def update_downstream():
            # 현재 그래프의 위치 찾기
            current_index = self.graphs.index(graph_info)
            
            # 오른쪽에 있는 모든 그래프 업데이트
            indices_to_propagate = selected_indices
            for i in range(current_index + 1, len(self.graphs)):
                next_graph = self.graphs[i]
                if next_graph['type'] == 'bar':
                    self._update_bar_graph(next_graph, self.current_df, indices_to_propagate)
                elif next_graph['type'] == 'scatter':
                    self._update_scatter_graph(next_graph, self.current_df, indices_to_propagate)
                elif next_graph['type'] == 'tag':
                    self._update_tag_card(next_graph, self.current_df, indices_to_propagate)
                
                # 다음 그래프의 선택된 인덱스를 현재 선택으로 업데이트
                indices_to_propagate = next_graph['filtered_indices']
            
            # 맨 오른쪽 그래프의 데이터로 테이블 업데이트 (빈 리스트여도 업데이트)
            if len(self.graphs) > 0:
                rightmost_graph = self.graphs[-1]
                print(f"[Bar Selection] Updating table with {len(rightmost_graph['filtered_indices'])} rows")
                if self.on_selection_callback:
                    self.on_selection_callback(rightmost_graph['filtered_indices'])
        
        # Panel의 이벤트 루프에서 실행
        try:
            pn.state.execute(update_downstream)
        except:
            # pn.state가 없으면 직접 실행
            update_downstream()
    
    def _update_scatter_graph(self, graph_info, df, indices=None):
        """특정 Scatter Graph 업데이트
        
        Args:
            graph_info: 그래프 정보
            df: 전체 데이터프레임
            indices: 표시할 데이터의 인덱스 리스트 (None이면 전체)
        """
        x_column = graph_info['x_column']
        y_column = graph_info['y_column']
        
        # X축 컬럼이 없으면 리턴
        if x_column not in df.columns:
            print(f"X column '{x_column}' not found in dataframe")
            return
        
        # Y축 컬럼 자동 선택 (우선순위: 기존 설정 > 미리 정의된 컬럼 > 첫 번째 숫자 컬럼)
        if y_column is None or y_column not in df.columns:
            # 우선 미리 정의된 컬럼 중에서 찾기
            for col in ['Ellipse Ratio', 'Box Ratio', 'Extent', 'Circularity']:
                if col in df.columns:
                    y_column = col
                    graph_info['y_column'] = col
                    break
            
            # 그래도 없으면 첫 번째 숫자 컬럼 사용
            if y_column is None or y_column not in df.columns:
                numeric_columns = [col for col in df.select_dtypes(include=[np.number]).columns 
                                 if col != 'Image' and col != x_column]
                if len(numeric_columns) > 0:
                    y_column = numeric_columns[0]
                    graph_info['y_column'] = y_column
                else:
                    print(f"No suitable Y column found for scatter plot")
                    return
        
        # 인덱스 기반 필터링
        if indices is not None:
            if len(indices) == 0:
                # 빈 리스트가 전달되면 빈 그래프 표시
                filtered_df = pd.DataFrame()
            else:
                filtered_df = df.loc[indices]
        else:
            filtered_df = df
        
        # filtered_indices 저장
        graph_info['filtered_indices'] = filtered_df.index.tolist()
        
        # 유효한 데이터만 필터링
        scatter_df = filtered_df[[x_column, y_column]].dropna() if len(filtered_df) > 0 and x_column in filtered_df.columns and y_column in filtered_df.columns else pd.DataFrame()
        
        print(f"Updating scatter plot: X='{x_column}', Y='{y_column}', data points={len(scatter_df)}")
        
        # 새 그래프 생성
        scatter_plot = figure(
            x_axis_label=f'{x_column}',
            y_axis_label=f'{y_column}',
            width=600,
            height=400,
            tools='pan,wheel_zoom,box_zoom,box_select,lasso_select,reset,save'
        )
        
        if len(scatter_df) > 0:
            # 새로운 ColumnDataSource 생성
            scatter_source = ColumnDataSource(data={
                'x': scatter_df[x_column].values,
                'y': scatter_df[y_column].values,
                'index': scatter_df.index.tolist()
            })
            
            # 산점도
            scatter_plot.circle(
                x='x', 
                y='y', 
                source=scatter_source,
                size=5, 
                color='blue', 
                alpha=0.6,
                legend_label='Particle'
            )
            
            # HoverTool 추가
            hover = HoverTool(tooltips=[
                ('X', '@x{0.2f}'),
                ('Y', '@y{0.3f}')
            ])
            scatter_plot.add_tools(hover)
            
            scatter_plot.legend.location = "top_right"
            
            # Selection 콜백 등록
            scatter_source.selected.on_change('indices',
                lambda attr, old, new: self._on_scatter_selection(graph_info, attr, old, new))
        else:
            # 빈 데이터일 때 빈 소스 생성
            scatter_source = ColumnDataSource(data={
                'x': [],
                'y': [],
                'index': []
            })
        
        # 그래프 정보 업데이트
        graph_info['plot'] = scatter_plot
        graph_info['source'] = scatter_source
        graph_info['pane'].object = scatter_plot
    
    def _on_scatter_selection(self, graph_info, attr, old, new):
        """Scatter plot에서 선택 시 호출 - 오른쪽 그래프들과 테이블 업데이트"""
        # old와 new가 같으면 실제 변경이 아니므로 무시
        if old == new:
            return
        
        print(f"[Scatter Selection] Graph {graph_info['id']}, selected points: {len(new)}, old: {len(old)}")

        if self.current_df is None:
            return
        
        source = graph_info['source']
        
        # 선택된 점들의 원본 인덱스 가져오기
        selected_indices = []
        
        if len(new) > 0:
            # 선택이 있는 경우
            selected_indices = [source.data['index'][i] for i in new]
            print(f"[Scatter Selection] Selected {len(selected_indices)} data points")
        else:
            # 선택이 없는 경우 - 현재 그래프의 전체 데이터
            selected_indices = graph_info['filtered_indices'] if graph_info['filtered_indices'] else self.current_df.index.tolist()
            print(f"[Scatter Selection] No selection, using all data: {len(selected_indices)} points")

        graph_info['filtered_indices'] = selected_indices
        
        # 업데이트를 비동기로 실행
        def update_downstream():
            # 현재 그래프의 위치 찾기
            current_index = self.graphs.index(graph_info)
            
            # 오른쪽에 있는 모든 그래프 업데이트
            indices_to_propagate = selected_indices
            for i in range(current_index + 1, len(self.graphs)):
                next_graph = self.graphs[i]
                if next_graph['type'] == 'bar':
                    self._update_bar_graph(next_graph, self.current_df, indices_to_propagate)
                elif next_graph['type'] == 'scatter':
                    self._update_scatter_graph(next_graph, self.current_df, indices_to_propagate)
                elif next_graph['type'] == 'tag':
                    self._update_tag_card(next_graph, self.current_df, indices_to_propagate)
                
                # 다음 그래프의 선택된 인덱스를 현재 선택으로 업데이트
                indices_to_propagate = next_graph['filtered_indices']
            
            # 맨 오른쪽 그래프의 데이터로 테이블 업데이트 (빈 리스트여도 업데이트)
            if len(self.graphs) > 0:
                rightmost_graph = self.graphs[-1]
                print(f"[Scatter Selection] Updating table with {len(rightmost_graph['filtered_indices'])} rows")
                if self.on_selection_callback:
                    self.on_selection_callback(rightmost_graph['filtered_indices'])
        
        # Panel의 이벤트 루프에서 실행
        try:
            pn.state.execute(update_downstream)
        except:
            # pn.state가 없으면 직접 실행
            update_downstream()
    
    def _show_bar_config_for_graph(self, graph_info):
        """특정 Bar Graph 설정 다이얼로그 표시"""
        if self.current_df is not None:
            # Image 컬럼 제외
            numeric_columns = [col for col in self.current_df.select_dtypes(include=[np.number]).columns.tolist() 
                             if col != 'Image']
            
            # 별도 스레드에서 다이얼로그 실행
            def show_dialog():
                dialog = ParameterDialog(
                    columns=numeric_columns,
                    current_column=graph_info['x_column'],
                    on_confirm=lambda col: self._on_bar_config_confirm(graph_info, col)
                )
                dialog.show()
            
            thread = threading.Thread(target=show_dialog, daemon=True)
            thread.start()
    
    def _show_scatter_config_for_graph(self, graph_info):
        """특정 Scatter Graph 설정 다이얼로그 표시"""
        if self.current_df is not None:
            # Image 컬럼 제외
            numeric_columns = [col for col in self.current_df.select_dtypes(include=[np.number]).columns.tolist()
                             if col != 'Image']
            
            # Y축 기본값 설정
            current_y = graph_info['y_column']
            if current_y is None or current_y not in numeric_columns:
                current_y = numeric_columns[0] if len(numeric_columns) > 0 else 'AED'
            
            # 별도 스레드에서 다이얼로그 실행
            def show_dialog():
                dialog = AxisParameterDialog(
                    columns=numeric_columns,
                    current_x=graph_info['x_column'],
                    current_y=current_y,
                    on_confirm=lambda x_col, y_col: self._on_scatter_config_confirm(graph_info, x_col, y_col)
                )
                dialog.show()
            
            thread = threading.Thread(target=show_dialog, daemon=True)
            thread.start()
    
    def _on_bar_config_confirm(self, graph_info, selected_column):
        """Bar Graph 설정 확인"""
        graph_info['x_column'] = selected_column
        
        if self.current_df is not None:
            # 현재 그래프의 위치 찾기
            current_index = self.graphs.index(graph_info)
            
            # 현재 그래프의 입력 데이터 (왼쪽 그래프의 filtered_indices)
            if current_index == 0:
                input_indices = None  # 전체 데이터
            else:
                input_indices = self.graphs[current_index - 1]['filtered_indices']
            
            # 현재 그래프 업데이트
            self._update_bar_graph(graph_info, self.current_df, indices=input_indices)
            
            # 오른쪽 그래프들도 업데이트
            selected_indices = graph_info['filtered_indices']
            for i in range(current_index + 1, len(self.graphs)):
                next_graph = self.graphs[i]
                if next_graph['type'] == 'bar':
                    self._update_bar_graph(next_graph, self.current_df, selected_indices)
                elif next_graph['type'] == 'scatter':
                    self._update_scatter_graph(next_graph, self.current_df, selected_indices)
                elif next_graph['type'] == 'tag':
                    self._update_tag_card(next_graph, self.current_df, selected_indices)
                selected_indices = next_graph['filtered_indices']
            
            # 테이블 업데이트 (빈 리스트여도 업데이트)
            if len(self.graphs) > 0 and self.on_selection_callback:
                rightmost_graph = self.graphs[-1]
                self.on_selection_callback(rightmost_graph['filtered_indices'])
    
    def _on_scatter_config_confirm(self, graph_info, x_col, y_col):
        """Scatter Graph 설정 확인"""
        graph_info['x_column'] = x_col
        graph_info['y_column'] = y_col
        
        if self.current_df is not None:
            # 현재 그래프의 위치 찾기
            current_index = self.graphs.index(graph_info)
            
            # 현재 그래프의 입력 데이터 (왼쪽 그래프의 filtered_indices)
            if current_index == 0:
                input_indices = None  # 전체 데이터
            else:
                input_indices = self.graphs[current_index - 1]['filtered_indices']
            
            # 현재 그래프 업데이트
            self._update_scatter_graph(graph_info, self.current_df, indices=input_indices)
            
            # 오른쪽 그래프들도 업데이트
            selected_indices = graph_info['filtered_indices']
            for i in range(current_index + 1, len(self.graphs)):
                next_graph = self.graphs[i]
                if next_graph['type'] == 'bar':
                    self._update_bar_graph(next_graph, self.current_df, selected_indices)
                elif next_graph['type'] == 'scatter':
                    self._update_scatter_graph(next_graph, self.current_df, selected_indices)
                elif next_graph['type'] == 'tag':
                    self._update_tag_card(next_graph, self.current_df, selected_indices)
                selected_indices = next_graph['filtered_indices']
            
            # 테이블 업데이트 (빈 리스트여도 업데이트)
            if len(self.graphs) > 0 and self.on_selection_callback:
                rightmost_graph = self.graphs[-1]
                self.on_selection_callback(rightmost_graph['filtered_indices'])
    
    def update_data(self, df: pd.DataFrame):
        """데이터 업데이트 - 모든 그래프 업데이트"""
        self.current_df = df
        
        if len(self.graphs) == 0:
            # 그래프가 없으면 전체 데이터를 테이블에 표시
            print(f"[GraphPanel] No graphs, showing all data: {len(df)} rows")
            if self.on_selection_callback:
                self.on_selection_callback(df.index.tolist())
            return
        
        # 모든 그래프 업데이트
        for i, graph_info in enumerate(self.graphs):
            if i == 0:
                # 첫 번째 그래프는 전체 데이터
                if graph_info['type'] == 'bar':
                    self._update_bar_graph(graph_info, df, indices=None)
                elif graph_info['type'] == 'scatter':
                    self._update_scatter_graph(graph_info, df, indices=None)
                elif graph_info['type'] == 'tag':
                    self._update_tag_card(graph_info, df, indices=None)
            else:
                # 나머지 그래프는 왼쪽 그래프의 filtered_indices 사용
                prev_graph = self.graphs[i - 1]
                prev_indices = prev_graph['filtered_indices']
                
                if graph_info['type'] == 'bar':
                    self._update_bar_graph(graph_info, df, indices=prev_indices)
                elif graph_info['type'] == 'scatter':
                    self._update_scatter_graph(graph_info, df, indices=prev_indices)
                elif graph_info['type'] == 'tag':
                    self._update_tag_card(graph_info, df, indices=prev_indices)
        
        # 맨 오른쪽 그래프의 데이터로 테이블 업데이트 (빈 리스트여도 업데이트)
        if self.on_selection_callback:
            rightmost_graph = self.graphs[-1]
            print(f"[GraphPanel] Updating table with rightmost graph data: {len(rightmost_graph['filtered_indices'])} rows")
            self.on_selection_callback(rightmost_graph['filtered_indices'])
        else:
            print(f"[GraphPanel] Warning: no selection callback registered")
    
    def reset_all_filters(self):
        """모든 그래프의 필터를 초기화하고 전체 데이터 표시"""
        if self.current_df is None:
            return
        
        # 모든 그래프를 다시 계산 (선택 없이)
        for i, graph_info in enumerate(self.graphs):
            if i == 0:
                # 첫 번째 그래프는 전체 데이터
                if graph_info['type'] == 'bar':
                    self._update_bar_graph(graph_info, self.current_df, indices=None)
                elif graph_info['type'] == 'scatter':
                    self._update_scatter_graph(graph_info, self.current_df, indices=None)
                elif graph_info['type'] == 'tag':
                    self._update_tag_card(graph_info, self.current_df, indices=None)
            else:
                # 나머지 그래프는 왼쪽 그래프의 filtered_indices 사용
                prev_graph = self.graphs[i - 1]
                prev_indices = prev_graph['filtered_indices']
                
                if graph_info['type'] == 'bar':
                    self._update_bar_graph(graph_info, self.current_df, indices=prev_indices)
                elif graph_info['type'] == 'scatter':
                    self._update_scatter_graph(graph_info, self.current_df, indices=prev_indices)
                elif graph_info['type'] == 'tag':
                    self._update_tag_card(graph_info, self.current_df, indices=prev_indices)
        
        # 맨 오른쪽 그래프의 데이터로 테이블 업데이트 (또는 전체 데이터, 빈 리스트여도 업데이트)
        if len(self.graphs) > 0 and self.on_selection_callback:
            rightmost_graph = self.graphs[-1]
            self.on_selection_callback(rightmost_graph['filtered_indices'])
        elif self.on_selection_callback and self.current_df is not None:
            # 그래프가 없으면 전체 데이터
            self.on_selection_callback(self.current_df.index.tolist())
    
    def get_graph_images(self):
        """모든 그래프를 matplotlib로 다시 그려서 PIL Image로 반환
        
        Bokeh export는 selenium/webdriver가 필요하므로,
        ColumnDataSource의 데이터를 matplotlib로 렌더링하여 PIL Image 생성.
        
        Returns:
            [{'name': str, 'image': PIL.Image}, ...] 또는 빈 리스트
        """
        import matplotlib
        matplotlib.use('Agg')  # GUI 없이 렌더링
        import matplotlib.pyplot as plt
        from PIL import Image
        from io import BytesIO
        
        images = []
        
        for i, graph_info in enumerate(self.graphs, start=1):
            try:
                if graph_info['type'] == 'bar':
                    source = graph_info.get('source')
                    if source is None:
                        continue
                    
                    x_col = graph_info.get('x_column', 'X')
                    x_data = list(source.data.get('x', []))
                    top_data = list(source.data.get('top', []))
                    width_data = list(source.data.get('width', []))
                    
                    if len(x_data) == 0:
                        continue
                    
                    fig, ax = plt.subplots(figsize=(8, 5))
                    ax.bar(x_data, top_data, width=width_data, color='navy', alpha=0.7, edgecolor='white', linewidth=0.5)
                    ax.set_xlabel(x_col, fontsize=11)
                    ax.set_ylabel('Count', fontsize=11)
                    ax.set_title(f'Bar Plot - {x_col}', fontsize=13, fontweight='bold')
                    ax.grid(axis='y', alpha=0.3)
                    fig.tight_layout()
                    
                    buf = BytesIO()
                    fig.savefig(buf, format='png', dpi=150, bbox_inches='tight')
                    plt.close(fig)
                    buf.seek(0)
                    pil_img = Image.open(buf).copy()
                    buf.close()
                    
                    name = f"graph_{i}_bar_{x_col}"
                    images.append({'name': name, 'image': pil_img})
                    print(f"[GraphPanel] Exported bar graph: {name}")
                
                elif graph_info['type'] == 'scatter':
                    source = graph_info.get('source')
                    if source is None:
                        continue
                    
                    x_col = graph_info.get('x_column', 'X')
                    y_col = graph_info.get('y_column', 'Y')
                    x_data = list(source.data.get('x', []))
                    y_data = list(source.data.get('y', []))
                    
                    if len(x_data) == 0:
                        continue
                    
                    fig, ax = plt.subplots(figsize=(8, 5))
                    ax.scatter(x_data, y_data, s=10, c='blue', alpha=0.6, edgecolors='none')
                    ax.set_xlabel(x_col, fontsize=11)
                    ax.set_ylabel(y_col, fontsize=11)
                    ax.set_title(f'Scatter Plot - {x_col} vs {y_col}', fontsize=13, fontweight='bold')
                    ax.grid(alpha=0.3)
                    fig.tight_layout()
                    
                    buf = BytesIO()
                    fig.savefig(buf, format='png', dpi=150, bbox_inches='tight')
                    plt.close(fig)
                    buf.seek(0)
                    pil_img = Image.open(buf).copy()
                    buf.close()
                    
                    name = f"graph_{i}_scatter_{x_col}_vs_{y_col}"
                    images.append({'name': name, 'image': pil_img})
                    print(f"[GraphPanel] Exported scatter graph: {name}")
                    
            except Exception as e:
                print(f"[GraphPanel] Error exporting graph {i}: {e}")
                import traceback
                traceback.print_exc()
        
        return images
    
    def get_layout(self):
        """그래프 레이아웃 반환"""
        # Card를 사용하여 스크롤 가능한 영역 생성
        graphs_card = pn.Card(
            self.graphs_container,
            title='',
            header_background='white',
            header_color='black',
            collapsed=False,
            collapsible=False,
            hide_header=True,
            sizing_mode='stretch_width',
            height=480,  # 높이 증가 (450 -> 480)
            scroll=True,
            styles={
                'overflow-x': 'auto',
                'overflow-y': 'hidden'
            },
            margin=(0, 0, 10, 0)  # 아래 여백 줄임
        )
        
        return pn.Column(
            # 버튼들
            pn.Row(
                self.reset_filter_button,
                self.add_tag_button,
                self.add_bar_button,
                self.add_scatter_button,
                self.delete_all_button,
                sizing_mode='stretch_width',
                margin=(0, 0, 5, 0)  # 버튼 행 아래 여백 줄임
            ),
            # 스크롤 가능한 그래프 영역
            graphs_card,
            sizing_mode='stretch_width',
            margin=(0, 0, 0, 0)  # 전체 여백 제거
        )