1649 lines
		
	
	
		
			42 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
		
		
			
		
	
	
			1649 lines
		
	
	
		
			42 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| 
								 | 
							
								<template>
							 | 
						|||
| 
								 | 
							
								  <div>
							 | 
						|||
| 
								 | 
							
								    <PageDescription>
							 | 
						|||
| 
								 | 
							
								      <h1>배양 그래프</h1>
							 | 
						|||
| 
								 | 
							
								      <div class="box">
							 | 
						|||
| 
								 | 
							
								        <h2>1. 그래프 구성 및 기능</h2>
							 | 
						|||
| 
								 | 
							
								        <ul>
							 | 
						|||
| 
								 | 
							
								          <li>기본 형태의 <strong>배양 그래프</strong>입니다.</li>
							 | 
						|||
| 
								 | 
							
								          <li>
							 | 
						|||
| 
								 | 
							
								            <code>Real Data</code> 영역의 <strong>체크박스</strong>를 통해
							 | 
						|||
| 
								 | 
							
								            시리즈(데이터 라인)의 출력 여부를 선택할 수 있습니다.
							 | 
						|||
| 
								 | 
							
								          </li>
							 | 
						|||
| 
								 | 
							
								          <li>
							 | 
						|||
| 
								 | 
							
								            <strong>Y축에는 최대 2개의 Real Data 시리즈</strong>가 동시에
							 | 
						|||
| 
								 | 
							
								            출력됩니다.
							 | 
						|||
| 
								 | 
							
								          </li>
							 | 
						|||
| 
								 | 
							
								          <li><strong>가로 스크롤</strong>로 시간축을 이동할 수 있습니다.</li>
							 | 
						|||
| 
								 | 
							
								          <li>
							 | 
						|||
| 
								 | 
							
								            그래프 내에서 <strong>마우스 우클릭 시 컨텍스트 메뉴</strong>가
							 | 
						|||
| 
								 | 
							
								            나타납니다.
							 | 
						|||
| 
								 | 
							
								          </li>
							 | 
						|||
| 
								 | 
							
								          <li>
							 | 
						|||
| 
								 | 
							
								            그래프 내에서 <strong>마우스 휠 액션으로 확대/축소</strong>가
							 | 
						|||
| 
								 | 
							
								            됩니다.
							 | 
						|||
| 
								 | 
							
								          </li>
							 | 
						|||
| 
								 | 
							
								          <li>
							 | 
						|||
| 
								 | 
							
								            그래프 우측 상단에서 <strong>확대/축소 메뉴</strong>가 있습니다.
							 | 
						|||
| 
								 | 
							
								          </li>
							 | 
						|||
| 
								 | 
							
								          <li>
							 | 
						|||
| 
								 | 
							
								            <strong>세로선(Line Marker)</strong>을 통해 특정 시점의
							 | 
						|||
| 
								 | 
							
								            <strong>피드 정보</strong>를 확인할 수 있습니다.
							 | 
						|||
| 
								 | 
							
								          </li>
							 | 
						|||
| 
								 | 
							
								        </ul>
							 | 
						|||
| 
								 | 
							
								      </div>
							 | 
						|||
| 
								 | 
							
								      <div class="box">
							 | 
						|||
| 
								 | 
							
								        <h2>2. 출력되는 데이터 개수</h2>
							 | 
						|||
| 
								 | 
							
								        <h3>Interval 1분 (100시간, 시리즈 30개)</h3>
							 | 
						|||
| 
								 | 
							
								        <ul>
							 | 
						|||
| 
								 | 
							
								          <li>
							 | 
						|||
| 
								 | 
							
								            총 시리즈 30개 × 6,000개 = <span class="highlight">180,000개</span>
							 | 
						|||
| 
								 | 
							
								          </li>
							 | 
						|||
| 
								 | 
							
								        </ul>
							 | 
						|||
| 
								 | 
							
								        <h3>Interval 5초 (100시간, 시리즈 30개)</h3>
							 | 
						|||
| 
								 | 
							
								        <ul>
							 | 
						|||
| 
								 | 
							
								          <li>
							 | 
						|||
| 
								 | 
							
								            총 시리즈 30개 × 72,000개 =
							 | 
						|||
| 
								 | 
							
								            <span class="highlight">2,160,000개</span>
							 | 
						|||
| 
								 | 
							
								          </li>
							 | 
						|||
| 
								 | 
							
								        </ul>
							 | 
						|||
| 
								 | 
							
								      </div>
							 | 
						|||
| 
								 | 
							
								    </PageDescription>
							 | 
						|||
| 
								 | 
							
								    <div
							 | 
						|||
| 
								 | 
							
								      class="experiment-graph-page"
							 | 
						|||
| 
								 | 
							
								      @contextmenu="onContextMenu"
							 | 
						|||
| 
								 | 
							
								      @click="onClickAnywhere"
							 | 
						|||
| 
								 | 
							
								    >
							 | 
						|||
| 
								 | 
							
								      <div class="info-bar">
							 | 
						|||
| 
								 | 
							
								        <div class="info-row">
							 | 
						|||
| 
								 | 
							
								          <span class="info-label">Experiment :</span>
							 | 
						|||
| 
								 | 
							
								          <span class="info-value">Sample001</span>
							 | 
						|||
| 
								 | 
							
								          <span class="info-divider">|</span>
							 | 
						|||
| 
								 | 
							
								          <span class="info-label">Strain :</span>
							 | 
						|||
| 
								 | 
							
								          <span class="info-value">균주명(Test)</span>
							 | 
						|||
| 
								 | 
							
								          <span class="info-divider">|</span>
							 | 
						|||
| 
								 | 
							
								          <span class="info-label">Time :</span>
							 | 
						|||
| 
								 | 
							
								          <span class="info-value"
							 | 
						|||
| 
								 | 
							
								            >2025.07.01 - 00:00 ~ 2025.07.03 - 00:00</span
							 | 
						|||
| 
								 | 
							
								          >
							 | 
						|||
| 
								 | 
							
								        </div>
							 | 
						|||
| 
								 | 
							
								      </div>
							 | 
						|||
| 
								 | 
							
								      <div class="realdata-checkbox-bar">
							 | 
						|||
| 
								 | 
							
								        <span class="realdata-label">Real Data</span>
							 | 
						|||
| 
								 | 
							
								        <span style="font-size: 12px; color: #666; margin-left: 10px">
							 | 
						|||
| 
								 | 
							
								          ({{ checkedList.length }}/{{ yAxisList.length }})
							 | 
						|||
| 
								 | 
							
								        </span>
							 | 
						|||
| 
								 | 
							
								        <label
							 | 
						|||
| 
								 | 
							
								          v-for="col in yAxisList"
							 | 
						|||
| 
								 | 
							
								          :key="col.name"
							 | 
						|||
| 
								 | 
							
								          class="realdata-checkbox-item"
							 | 
						|||
| 
								 | 
							
								        >
							 | 
						|||
| 
								 | 
							
								          <input
							 | 
						|||
| 
								 | 
							
								            :checked="checkedList.includes(col.name)"
							 | 
						|||
| 
								 | 
							
								            type="checkbox"
							 | 
						|||
| 
								 | 
							
								            :value="col.name"
							 | 
						|||
| 
								 | 
							
								            @change="handleCheckboxChange(col.name, $event)"
							 | 
						|||
| 
								 | 
							
								          />
							 | 
						|||
| 
								 | 
							
								          <span
							 | 
						|||
| 
								 | 
							
								            :style="{
							 | 
						|||
| 
								 | 
							
								              color: col.color,
							 | 
						|||
| 
								 | 
							
								              fontWeight: checkedList.includes(col.name) ? 'bold' : 'normal',
							 | 
						|||
| 
								 | 
							
								            }"
							 | 
						|||
| 
								 | 
							
								            >{{ col.name }}</span
							 | 
						|||
| 
								 | 
							
								          >
							 | 
						|||
| 
								 | 
							
								        </label>
							 | 
						|||
| 
								 | 
							
								      </div>
							 | 
						|||
| 
								 | 
							
								      <!-- interval 입력 행 라디오 버튼으로 변경 -->
							 | 
						|||
| 
								 | 
							
								      <div
							 | 
						|||
| 
								 | 
							
								        class="interval-bar"
							 | 
						|||
| 
								 | 
							
								        style="
							 | 
						|||
| 
								 | 
							
								          padding: 8px 32px 8px 32px;
							 | 
						|||
| 
								 | 
							
								          background: #fff;
							 | 
						|||
| 
								 | 
							
								          border-bottom: 1px solid #e0e0e0;
							 | 
						|||
| 
								 | 
							
								          display: flex;
							 | 
						|||
| 
								 | 
							
								          align-items: center;
							 | 
						|||
| 
								 | 
							
								          gap: 10px;
							 | 
						|||
| 
								 | 
							
								        "
							 | 
						|||
| 
								 | 
							
								      >
							 | 
						|||
| 
								 | 
							
								        <span style="font-weight: bold; color: #444">Interval</span>
							 | 
						|||
| 
								 | 
							
								        <label style="margin-right: 10px">
							 | 
						|||
| 
								 | 
							
								          <input v-model.number="intervalValue" type="radio" :value="5" /> 5초
							 | 
						|||
| 
								 | 
							
								        </label>
							 | 
						|||
| 
								 | 
							
								        <label>
							 | 
						|||
| 
								 | 
							
								          <input v-model.number="intervalValue" type="radio" :value="60" /> 1분
							 | 
						|||
| 
								 | 
							
								        </label>
							 | 
						|||
| 
								 | 
							
								      </div>
							 | 
						|||
| 
								 | 
							
								      <div class="main-graph-area">
							 | 
						|||
| 
								 | 
							
								        <div class="echarts-container">
							 | 
						|||
| 
								 | 
							
								          <div class="zoom-controls">
							 | 
						|||
| 
								 | 
							
								            <button class="zoom-btn" title="확대" @click="zoomIn">
							 | 
						|||
| 
								 | 
							
								              <span>+</span>
							 | 
						|||
| 
								 | 
							
								            </button>
							 | 
						|||
| 
								 | 
							
								            <button class="zoom-btn" title="축소" @click="zoomOut">
							 | 
						|||
| 
								 | 
							
								              <span>−</span>
							 | 
						|||
| 
								 | 
							
								            </button>
							 | 
						|||
| 
								 | 
							
								            <button class="zoom-btn reset" title="초기화" @click="resetZoom">
							 | 
						|||
| 
								 | 
							
								              <span>⟲</span>
							 | 
						|||
| 
								 | 
							
								            </button>
							 | 
						|||
| 
								 | 
							
								          </div>
							 | 
						|||
| 
								 | 
							
								          <div ref="growthChartRef" class="echarts-graph"></div>
							 | 
						|||
| 
								 | 
							
								          <div class="yaxis-scroll-bar-overlay">
							 | 
						|||
| 
								 | 
							
								            <input
							 | 
						|||
| 
								 | 
							
								              v-model.number="yAxisScrollIndex"
							 | 
						|||
| 
								 | 
							
								              type="range"
							 | 
						|||
| 
								 | 
							
								              min="0"
							 | 
						|||
| 
								 | 
							
								              :max="Math.max(0, filteredYAxisList.length - 2)"
							 | 
						|||
| 
								 | 
							
								              :disabled="filteredYAxisList.length <= 2"
							 | 
						|||
| 
								 | 
							
								            />
							 | 
						|||
| 
								 | 
							
								          </div>
							 | 
						|||
| 
								 | 
							
								        </div>
							 | 
						|||
| 
								 | 
							
								      </div>
							 | 
						|||
| 
								 | 
							
								      <CustomContextMenu :visible="menuVisible" :x="menuX" :y="menuY" />
							 | 
						|||
| 
								 | 
							
								    </div>
							 | 
						|||
| 
								 | 
							
								  </div>
							 | 
						|||
| 
								 | 
							
								</template>
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								<script setup lang="ts">
							 | 
						|||
| 
								 | 
							
								import { ref, onMounted, watch, computed, onUnmounted } from "vue";
							 | 
						|||
| 
								 | 
							
								import * as echarts from "echarts";
							 | 
						|||
| 
								 | 
							
								import CustomContextMenu from "~/components/CustomContextMenu.vue";
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								// 타입 인터페이스 정의
							 | 
						|||
| 
								 | 
							
								interface YAxis {
							 | 
						|||
| 
								 | 
							
								  name: string;
							 | 
						|||
| 
								 | 
							
								  unit: string;
							 | 
						|||
| 
								 | 
							
								  color: string;
							 | 
						|||
| 
								 | 
							
								  min: number;
							 | 
						|||
| 
								 | 
							
								  max: number;
							 | 
						|||
| 
								 | 
							
								  ticks: number[];
							 | 
						|||
| 
								 | 
							
								  fontWeight: string;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								interface RealTimeData {
							 | 
						|||
| 
								 | 
							
								  [key: string]: [number, number][];
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								const growthChartRef = ref<HTMLDivElement | null>(null);
							 | 
						|||
| 
								 | 
							
								let chartInstance: echarts.ECharts | null = null;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								const pastelColors = [
							 | 
						|||
| 
								 | 
							
								  "#A3D8F4", // 파스텔 블루
							 | 
						|||
| 
								 | 
							
								  "#F7B7A3", // 파스텔 오렌지
							 | 
						|||
| 
								 | 
							
								  "#B5EAD7", // 파스텔 민트
							 | 
						|||
| 
								 | 
							
								  "#FFDAC1", // 파스텔 살구
							 | 
						|||
| 
								 | 
							
								  "#C7CEEA", // 파스텔 퍼플
							 | 
						|||
| 
								 | 
							
								  "#FFF1BA", // 파스텔 옐로우
							 | 
						|||
| 
								 | 
							
								  "#FFB7B2", // 파스텔 핑크
							 | 
						|||
| 
								 | 
							
								  "#B4A7D6", // 파스텔 연보라
							 | 
						|||
| 
								 | 
							
								  "#AED9E0", // 추가 파스텔 블루
							 | 
						|||
| 
								 | 
							
								  "#FFC3A0", // 추가 파스텔 오렌지
							 | 
						|||
| 
								 | 
							
								  "#E2F0CB", // 추가 파스텔 민트
							 | 
						|||
| 
								 | 
							
								  "#FFB347", // 추가 파스텔 살구
							 | 
						|||
| 
								 | 
							
								  "#C1C8E4", // 추가 파스텔 퍼플
							 | 
						|||
| 
								 | 
							
								  "#FFFACD", // 추가 파스텔 옐로우
							 | 
						|||
| 
								 | 
							
								  "#FFD1DC", // 추가 파스텔 핑크
							 | 
						|||
| 
								 | 
							
								];
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								const yAxisList: YAxis[] = [
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "ORP",
							 | 
						|||
| 
								 | 
							
								    unit: "",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[0],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 1000,
							 | 
						|||
| 
								 | 
							
								    ticks: [1000, 800, 600, 400, 200, 0],
							 | 
						|||
| 
								 | 
							
								    fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Air flow",
							 | 
						|||
| 
								 | 
							
								    unit: "(L/min)",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[1],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 30,
							 | 
						|||
| 
								 | 
							
								    ticks: [30, 25, 20, 15, 10, 5, 0],
							 | 
						|||
| 
								 | 
							
								    fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "DO",
							 | 
						|||
| 
								 | 
							
								    unit: "",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[2],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 200,
							 | 
						|||
| 
								 | 
							
								    ticks: [200, 150, 100, 50, 0],
							 | 
						|||
| 
								 | 
							
								    fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Feed TK1",
							 | 
						|||
| 
								 | 
							
								    unit: "(L)",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[3],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 10,
							 | 
						|||
| 
								 | 
							
								    ticks: [10, 8, 6, 4, 2, 0],
							 | 
						|||
| 
								 | 
							
								    fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Feed TK2",
							 | 
						|||
| 
								 | 
							
								    unit: "(L)",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[4],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 10,
							 | 
						|||
| 
								 | 
							
								    ticks: [10, 8, 6, 4, 2, 0],
							 | 
						|||
| 
								 | 
							
								    fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "pH",
							 | 
						|||
| 
								 | 
							
								    unit: "",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[5],
							 | 
						|||
| 
								 | 
							
								    min: 6.0,
							 | 
						|||
| 
								 | 
							
								    max: 8.0,
							 | 
						|||
| 
								 | 
							
								    ticks: [8.0, 7.5, 7.0, 6.5, 6.0],
							 | 
						|||
| 
								 | 
							
								    fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Pressure",
							 | 
						|||
| 
								 | 
							
								    unit: "(bar)",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[6],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 2,
							 | 
						|||
| 
								 | 
							
								    ticks: [2, 1.5, 1, 0.5, 0],
							 | 
						|||
| 
								 | 
							
								    fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "RPM",
							 | 
						|||
| 
								 | 
							
								    unit: "",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[7],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 3000,
							 | 
						|||
| 
								 | 
							
								    ticks: [3000, 2400, 1800, 1200, 600, 0],
							 | 
						|||
| 
								 | 
							
								    fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "CO2",
							 | 
						|||
| 
								 | 
							
								    unit: "(%)",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[8],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 10,
							 | 
						|||
| 
								 | 
							
								    ticks: [10, 8, 6, 4, 2, 0],
							 | 
						|||
| 
								 | 
							
								    fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "JAR Vol",
							 | 
						|||
| 
								 | 
							
								    unit: "(L)",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[9],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 20,
							 | 
						|||
| 
								 | 
							
								    ticks: [20, 16, 12, 8, 4, 0],
							 | 
						|||
| 
								 | 
							
								    fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "WEIGHT",
							 | 
						|||
| 
								 | 
							
								    unit: "(kg)",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[10],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 100,
							 | 
						|||
| 
								 | 
							
								    ticks: [100, 80, 60, 40, 20, 0],
							 | 
						|||
| 
								 | 
							
								    fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "O2",
							 | 
						|||
| 
								 | 
							
								    unit: "(%)",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[11],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 100,
							 | 
						|||
| 
								 | 
							
								    ticks: [100, 80, 60, 40, 20, 0],
							 | 
						|||
| 
								 | 
							
								    fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "NV",
							 | 
						|||
| 
								 | 
							
								    unit: "",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[12],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 10,
							 | 
						|||
| 
								 | 
							
								    ticks: [10, 8, 6, 4, 2, 0],
							 | 
						|||
| 
								 | 
							
								    fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "NIR",
							 | 
						|||
| 
								 | 
							
								    unit: "",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[13],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 10,
							 | 
						|||
| 
								 | 
							
								    ticks: [10, 8, 6, 4, 2, 0],
							 | 
						|||
| 
								 | 
							
								    fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Temperature",
							 | 
						|||
| 
								 | 
							
								    unit: "(℃)",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[14],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 50,
							 | 
						|||
| 
								 | 
							
								    ticks: [50, 40, 30, 20, 10, 0],
							 | 
						|||
| 
								 | 
							
								    fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Humidity",
							 | 
						|||
| 
								 | 
							
								    unit: "(%)",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[0],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 100,
							 | 
						|||
| 
								 | 
							
								    ticks: [100, 80, 60, 40, 20, 0],
							 | 
						|||
| 
								 | 
							
								    fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Flow Rate",
							 | 
						|||
| 
								 | 
							
								    unit: "(L/min)",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[1],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 60,
							 | 
						|||
| 
								 | 
							
								    ticks: [60, 50, 40, 30, 20, 10, 0],
							 | 
						|||
| 
								 | 
							
								    fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Conductivity",
							 | 
						|||
| 
								 | 
							
								    unit: "(μS/cm)",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[2],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 600,
							 | 
						|||
| 
								 | 
							
								    ticks: [600, 500, 400, 300, 200, 100, 0],
							 | 
						|||
| 
								 | 
							
								    fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Turbidity",
							 | 
						|||
| 
								 | 
							
								    unit: "(NTU)",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[3],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 12,
							 | 
						|||
| 
								 | 
							
								    ticks: [12, 10, 8, 6, 4, 2, 0],
							 | 
						|||
| 
								 | 
							
								    fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Dissolved Solids",
							 | 
						|||
| 
								 | 
							
								    unit: "(mg/L)",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[4],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 250,
							 | 
						|||
| 
								 | 
							
								    ticks: [250, 200, 150, 100, 50, 0],
							 | 
						|||
| 
								 | 
							
								    fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Alkalinity",
							 | 
						|||
| 
								 | 
							
								    unit: "(mg/L)",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[5],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 120,
							 | 
						|||
| 
								 | 
							
								    ticks: [120, 100, 80, 60, 40, 20, 0],
							 | 
						|||
| 
								 | 
							
								    fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Hardness",
							 | 
						|||
| 
								 | 
							
								    unit: "(mg/L)",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[6],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 100,
							 | 
						|||
| 
								 | 
							
								    ticks: [100, 80, 60, 40, 20, 0],
							 | 
						|||
| 
								 | 
							
								    fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Chlorine",
							 | 
						|||
| 
								 | 
							
								    unit: "(mg/L)",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[7],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 6,
							 | 
						|||
| 
								 | 
							
								    ticks: [6, 5, 4, 3, 2, 1, 0],
							 | 
						|||
| 
								 | 
							
								    fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Nitrate",
							 | 
						|||
| 
								 | 
							
								    unit: "(mg/L)",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[8],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 25,
							 | 
						|||
| 
								 | 
							
								    ticks: [25, 20, 15, 10, 5, 0],
							 | 
						|||
| 
								 | 
							
								    fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Phosphate",
							 | 
						|||
| 
								 | 
							
								    unit: "(mg/L)",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[9],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 12,
							 | 
						|||
| 
								 | 
							
								    ticks: [12, 10, 8, 6, 4, 2, 0],
							 | 
						|||
| 
								 | 
							
								    fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Sulfate",
							 | 
						|||
| 
								 | 
							
								    unit: "(mg/L)",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[10],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 60,
							 | 
						|||
| 
								 | 
							
								    ticks: [60, 50, 40, 30, 20, 10, 0],
							 | 
						|||
| 
								 | 
							
								    fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Ammonia",
							 | 
						|||
| 
								 | 
							
								    unit: "(mg/L)",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[11],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 18,
							 | 
						|||
| 
								 | 
							
								    ticks: [18, 15, 12, 9, 6, 3, 0],
							 | 
						|||
| 
								 | 
							
								    fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Nitrite",
							 | 
						|||
| 
								 | 
							
								    unit: "(mg/L)",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[12],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 6,
							 | 
						|||
| 
								 | 
							
								    ticks: [6, 5, 4, 3, 2, 1, 0],
							 | 
						|||
| 
								 | 
							
								    fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "BOD",
							 | 
						|||
| 
								 | 
							
								    unit: "(mg/L)",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[13],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 35,
							 | 
						|||
| 
								 | 
							
								    ticks: [35, 30, 25, 20, 15, 10, 5, 0],
							 | 
						|||
| 
								 | 
							
								    fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "COD",
							 | 
						|||
| 
								 | 
							
								    unit: "(mg/L)",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[14],
							 | 
						|||
| 
								 | 
							
								    min: 0,
							 | 
						|||
| 
								 | 
							
								    max: 120,
							 | 
						|||
| 
								 | 
							
								    ticks: [120, 100, 80, 60, 40, 20, 0],
							 | 
						|||
| 
								 | 
							
								    fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								];
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								// intervalValue를 라디오 버튼으로 선택 (5초, 1분)
							 | 
						|||
| 
								 | 
							
								const intervalValue = ref(60); // 기본값 1분
							 | 
						|||
| 
								 | 
							
								const totalSeconds = 100 * 60 * 60; // 100시간 = 360000초
							 | 
						|||
| 
								 | 
							
								const pointsPerLine = computed(
							 | 
						|||
| 
								 | 
							
								  () => Math.floor(totalSeconds / intervalValue.value) + 1
							 | 
						|||
| 
								 | 
							
								);
							 | 
						|||
| 
								 | 
							
								const xLabels = computed(() =>
							 | 
						|||
| 
								 | 
							
								  Array.from(
							 | 
						|||
| 
								 | 
							
								    { length: pointsPerLine.value },
							 | 
						|||
| 
								 | 
							
								    (_, i) => i * intervalValue.value // 초 단위
							 | 
						|||
| 
								 | 
							
								  )
							 | 
						|||
| 
								 | 
							
								);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								// 전체 데이터에서 4개의 마커 포인트를 선택
							 | 
						|||
| 
								 | 
							
								const globalMarkerPoints: Array<{ seriesIndex: number; timeIndex: number }> =
							 | 
						|||
| 
								 | 
							
								  [];
							 | 
						|||
| 
								 | 
							
								while (globalMarkerPoints.length < 4) {
							 | 
						|||
| 
								 | 
							
								  const seriesIndex = Math.floor(Math.random() * 30); // 0~29 시리즈
							 | 
						|||
| 
								 | 
							
								  const timeIndex = Math.floor(Math.random() * 100); // 0~99 시간
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  // 중복 체크
							 | 
						|||
| 
								 | 
							
								  const exists = globalMarkerPoints.some(
							 | 
						|||
| 
								 | 
							
								    p => p.seriesIndex === seriesIndex && p.timeIndex === timeIndex
							 | 
						|||
| 
								 | 
							
								  );
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  if (!exists) {
							 | 
						|||
| 
								 | 
							
								    globalMarkerPoints.push({ seriesIndex, timeIndex });
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								// 부드러운 곡선형 + 구간별 변화 가데이터 생성 함수 (xLabels를 파라미터로 받도록 변경)
							 | 
						|||
| 
								 | 
							
								function smoothData(
							 | 
						|||
| 
								 | 
							
								  min: number,
							 | 
						|||
| 
								 | 
							
								  max: number,
							 | 
						|||
| 
								 | 
							
								  xLabels: number[],
							 | 
						|||
| 
								 | 
							
								  phase = 0,
							 | 
						|||
| 
								 | 
							
								  _amp = 1, // amp는 사용하지 않으므로 _amp로 변경
							 | 
						|||
| 
								 | 
							
								  offset = 0,
							 | 
						|||
| 
								 | 
							
								  seriesIndex: number
							 | 
						|||
| 
								 | 
							
								) {
							 | 
						|||
| 
								 | 
							
								  let _prevValue = 0.5;
							 | 
						|||
| 
								 | 
							
								  const values = [];
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  // 데이터 범위 축소: min+10% ~ max-10%
							 | 
						|||
| 
								 | 
							
								  const rangeMin = min + (max - min) * 0.1;
							 | 
						|||
| 
								 | 
							
								  const rangeMax = max - (max - min) * 0.1;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  // 시리즈별 패턴 다양화: 증가/감소/진동/트렌드 섞기
							 | 
						|||
| 
								 | 
							
								  // 패턴 결정 (시리즈 인덱스에 따라)
							 | 
						|||
| 
								 | 
							
								  const trendType = seriesIndex % 4; // 0:증가, 1:감소, 2:진동, 3:랜덤
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  // 진동폭(ampVar, randomFactor)은 원복 (더 낮게)
							 | 
						|||
| 
								 | 
							
								  const ampVar = 0.2 + (seriesIndex % 5) * 0.1; // 0.2~0.6
							 | 
						|||
| 
								 | 
							
								  const randomFactor = 0.01 + 0.01 * (seriesIndex % 4); // 0.01~0.04
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  for (let i = 0; i < xLabels.length; i++) {
							 | 
						|||
| 
								 | 
							
								    const t = i / (xLabels.length - 1);
							 | 
						|||
| 
								 | 
							
								    let base;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    // 오르내림(파동 주기/계수)을 키움 (파동이 더 자주, 더 크게)
							 | 
						|||
| 
								 | 
							
								    if (trendType === 0) {
							 | 
						|||
| 
								 | 
							
								      base = 0.2 + 0.6 * t + 0.13 * Math.sin(phase + t * Math.PI * ampVar * 8);
							 | 
						|||
| 
								 | 
							
								    } else if (trendType === 1) {
							 | 
						|||
| 
								 | 
							
								      base = 0.8 - 0.6 * t + 0.13 * Math.cos(phase + t * Math.PI * ampVar * 8);
							 | 
						|||
| 
								 | 
							
								    } else if (trendType === 2) {
							 | 
						|||
| 
								 | 
							
								      base = 0.5 + 0.22 * Math.sin(phase + t * Math.PI * ampVar * 12);
							 | 
						|||
| 
								 | 
							
								    } else {
							 | 
						|||
| 
								 | 
							
								      base = 0.5 + 0.08 * Math.sin(phase + t * Math.PI * ampVar * 6) + 0.2 * t;
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    // 노이즈는 낮게
							 | 
						|||
| 
								 | 
							
								    const noise = (Math.random() - 0.5) * randomFactor;
							 | 
						|||
| 
								 | 
							
								    base += noise;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    base = Math.max(0, Math.min(1, base));
							 | 
						|||
| 
								 | 
							
								    const value = +(rangeMin + (rangeMax - rangeMin) * base + offset).toFixed(
							 | 
						|||
| 
								 | 
							
								      2
							 | 
						|||
| 
								 | 
							
								    );
							 | 
						|||
| 
								 | 
							
								    _prevValue = base;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    values.push([xLabels[i], value]);
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  console.log(values);
							 | 
						|||
| 
								 | 
							
								  return values;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								// seriesList를 intervalValue, xLabels에 따라 동적으로 생성
							 | 
						|||
| 
								 | 
							
								const seriesList = computed(() => [
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "ORP",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[0],
							 | 
						|||
| 
								 | 
							
								    yAxisIndex: 0,
							 | 
						|||
| 
								 | 
							
								    data: smoothData(400, 900, xLabels.value, 0, 1, 0, 0),
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Air flow",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[1],
							 | 
						|||
| 
								 | 
							
								    yAxisIndex: 1,
							 | 
						|||
| 
								 | 
							
								    data: smoothData(5, 25, xLabels.value, Math.PI / 2, 1, 0, 1),
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "DO",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[2],
							 | 
						|||
| 
								 | 
							
								    yAxisIndex: 2,
							 | 
						|||
| 
								 | 
							
								    data: smoothData(80, 180, xLabels.value, Math.PI / 3, 1, 0, 2),
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Feed TK1",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[3],
							 | 
						|||
| 
								 | 
							
								    yAxisIndex: 3,
							 | 
						|||
| 
								 | 
							
								    data: smoothData(2, 8, xLabels.value, Math.PI / 4, 1, 0, 3),
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Feed TK2",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[4],
							 | 
						|||
| 
								 | 
							
								    yAxisIndex: 4,
							 | 
						|||
| 
								 | 
							
								    data: smoothData(2, 8, xLabels.value, Math.PI / 5, 1, 0, 4),
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "pH",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[5],
							 | 
						|||
| 
								 | 
							
								    yAxisIndex: 5,
							 | 
						|||
| 
								 | 
							
								    data: smoothData(6.7, 7.6, xLabels.value, Math.PI / 6, 1, 0, 5),
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Pressure",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[6],
							 | 
						|||
| 
								 | 
							
								    yAxisIndex: 6,
							 | 
						|||
| 
								 | 
							
								    data: smoothData(0.5, 1.5, xLabels.value, Math.PI / 7, 1, 0, 6),
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "RPM",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[7],
							 | 
						|||
| 
								 | 
							
								    yAxisIndex: 7,
							 | 
						|||
| 
								 | 
							
								    data: smoothData(1000, 2500, xLabels.value, Math.PI / 8, 1, 0, 7),
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "CO2",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[8],
							 | 
						|||
| 
								 | 
							
								    yAxisIndex: 8,
							 | 
						|||
| 
								 | 
							
								    data: smoothData(2, 8, xLabels.value, Math.PI / 9, 1, 0, 8),
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "JAR Vol",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[9],
							 | 
						|||
| 
								 | 
							
								    yAxisIndex: 9,
							 | 
						|||
| 
								 | 
							
								    data: smoothData(5, 18, xLabels.value, Math.PI / 10, 1, 0, 9),
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "WEIGHT",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[10],
							 | 
						|||
| 
								 | 
							
								    yAxisIndex: 10,
							 | 
						|||
| 
								 | 
							
								    data: smoothData(20, 90, xLabels.value, Math.PI / 11, 1, 0, 10),
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "O2",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[11],
							 | 
						|||
| 
								 | 
							
								    yAxisIndex: 11,
							 | 
						|||
| 
								 | 
							
								    data: smoothData(20, 90, xLabels.value, Math.PI / 12, 1, 0, 11),
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "NV",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[12],
							 | 
						|||
| 
								 | 
							
								    yAxisIndex: 12,
							 | 
						|||
| 
								 | 
							
								    data: smoothData(2, 8, xLabels.value, Math.PI / 13, 1, 0, 12),
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "NIR",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[13],
							 | 
						|||
| 
								 | 
							
								    yAxisIndex: 13,
							 | 
						|||
| 
								 | 
							
								    data: smoothData(2, 8, xLabels.value, Math.PI / 14, 1, 0, 13),
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Temperature",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[14],
							 | 
						|||
| 
								 | 
							
								    yAxisIndex: 14,
							 | 
						|||
| 
								 | 
							
								    data: smoothData(30, 38, xLabels.value, Math.PI / 15, 1, 0, 14),
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Humidity",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[0],
							 | 
						|||
| 
								 | 
							
								    yAxisIndex: 15,
							 | 
						|||
| 
								 | 
							
								    data: smoothData(40, 80, xLabels.value, Math.PI / 16, 1, 0, 15),
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Flow Rate",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[1],
							 | 
						|||
| 
								 | 
							
								    yAxisIndex: 16,
							 | 
						|||
| 
								 | 
							
								    data: smoothData(10, 50, xLabels.value, Math.PI / 17, 1, 0, 16),
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Conductivity",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[2],
							 | 
						|||
| 
								 | 
							
								    yAxisIndex: 17,
							 | 
						|||
| 
								 | 
							
								    data: smoothData(100, 500, xLabels.value, Math.PI / 18, 1, 0, 17),
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Turbidity",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[3],
							 | 
						|||
| 
								 | 
							
								    yAxisIndex: 18,
							 | 
						|||
| 
								 | 
							
								    data: smoothData(0, 10, xLabels.value, Math.PI / 19, 1, 0, 18),
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Dissolved Solids",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[4],
							 | 
						|||
| 
								 | 
							
								    yAxisIndex: 19,
							 | 
						|||
| 
								 | 
							
								    data: smoothData(50, 200, xLabels.value, Math.PI / 20, 1, 0, 19),
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Alkalinity",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[5],
							 | 
						|||
| 
								 | 
							
								    yAxisIndex: 20,
							 | 
						|||
| 
								 | 
							
								    data: smoothData(20, 100, xLabels.value, Math.PI / 21, 1, 0, 20),
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Hardness",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[6],
							 | 
						|||
| 
								 | 
							
								    yAxisIndex: 21,
							 | 
						|||
| 
								 | 
							
								    data: smoothData(10, 80, xLabels.value, Math.PI / 22, 1, 0, 21),
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Chlorine",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[7],
							 | 
						|||
| 
								 | 
							
								    yAxisIndex: 22,
							 | 
						|||
| 
								 | 
							
								    data: smoothData(0, 5, xLabels.value, Math.PI / 23, 1, 0, 22),
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Nitrate",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[8],
							 | 
						|||
| 
								 | 
							
								    yAxisIndex: 23,
							 | 
						|||
| 
								 | 
							
								    data: smoothData(0, 20, xLabels.value, Math.PI / 24, 1, 0, 23),
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Phosphate",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[9],
							 | 
						|||
| 
								 | 
							
								    yAxisIndex: 24,
							 | 
						|||
| 
								 | 
							
								    data: smoothData(0, 10, xLabels.value, Math.PI / 25, 1, 0, 24),
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Sulfate",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[10],
							 | 
						|||
| 
								 | 
							
								    yAxisIndex: 25,
							 | 
						|||
| 
								 | 
							
								    data: smoothData(0, 50, xLabels.value, Math.PI / 26, 1, 0, 25),
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Ammonia",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[11],
							 | 
						|||
| 
								 | 
							
								    yAxisIndex: 26,
							 | 
						|||
| 
								 | 
							
								    data: smoothData(0, 15, xLabels.value, Math.PI / 27, 1, 0, 26),
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "Nitrite",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[12],
							 | 
						|||
| 
								 | 
							
								    yAxisIndex: 27,
							 | 
						|||
| 
								 | 
							
								    data: smoothData(0, 5, xLabels.value, Math.PI / 28, 1, 0, 27),
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "BOD",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[13],
							 | 
						|||
| 
								 | 
							
								    yAxisIndex: 28,
							 | 
						|||
| 
								 | 
							
								    data: smoothData(0, 30, xLabels.value, Math.PI / 29, 1, 0, 28),
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  {
							 | 
						|||
| 
								 | 
							
								    name: "COD",
							 | 
						|||
| 
								 | 
							
								    color: pastelColors[14],
							 | 
						|||
| 
								 | 
							
								    yAxisIndex: 29,
							 | 
						|||
| 
								 | 
							
								    data: smoothData(0, 100, xLabels.value, Math.PI / 30, 1, 0, 29),
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								]);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								const checkedList = ref(yAxisList.map(y => y.name)); // 기본 전체 체크
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								const filteredYAxisList = computed(() =>
							 | 
						|||
| 
								 | 
							
								  yAxisList.filter(y => checkedList.value.includes(y.name))
							 | 
						|||
| 
								 | 
							
								);
							 | 
						|||
| 
								 | 
							
								const filteredSeriesList = computed(() =>
							 | 
						|||
| 
								 | 
							
								  seriesList.value.filter(s => checkedList.value.includes(s.name))
							 | 
						|||
| 
								 | 
							
								);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								const yAxisScrollIndex = ref(0);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								// 실시간 데이터 관련 상태 추가
							 | 
						|||
| 
								 | 
							
								const isRealTimeMode = ref(false);
							 | 
						|||
| 
								 | 
							
								const realTimeData = ref<RealTimeData>({});
							 | 
						|||
| 
								 | 
							
								const realTimeTimer = ref<NodeJS.Timeout | null>(null);
							 | 
						|||
| 
								 | 
							
								const currentRealTimeIndex = ref(0);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								// 실시간 데이터 시작 시간 (48시간 이후)
							 | 
						|||
| 
								 | 
							
								const realTimeStartTime = 48;
							 | 
						|||
| 
								 | 
							
								const realTimeDuration = 72; // 72개 포인트 (24초 × 3개/초)
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								const visibleYAxisList = computed(() =>
							 | 
						|||
| 
								 | 
							
								  filteredYAxisList.value.slice(
							 | 
						|||
| 
								 | 
							
								    yAxisScrollIndex.value,
							 | 
						|||
| 
								 | 
							
								    yAxisScrollIndex.value + 2
							 | 
						|||
| 
								 | 
							
								  )
							 | 
						|||
| 
								 | 
							
								);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								const seriesWithRealTimeData = computed(() => {
							 | 
						|||
| 
								 | 
							
								  return filteredSeriesList.value.map(series => {
							 | 
						|||
| 
								 | 
							
								    const combinedData = [...series.data];
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    // 실시간 모드일 때 실시간 데이터 추가
							 | 
						|||
| 
								 | 
							
								    if (isRealTimeMode.value && realTimeData.value[series.name]) {
							 | 
						|||
| 
								 | 
							
								      combinedData.push(...realTimeData.value[series.name]);
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    return {
							 | 
						|||
| 
								 | 
							
								      ...series,
							 | 
						|||
| 
								 | 
							
								      data: combinedData,
							 | 
						|||
| 
								 | 
							
								      type: "line",
							 | 
						|||
| 
								 | 
							
								    };
							 | 
						|||
| 
								 | 
							
								  });
							 | 
						|||
| 
								 | 
							
								});
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								const mappedSeriesList = computed(() => {
							 | 
						|||
| 
								 | 
							
								  // 시간(시) → 초 단위 변환 함수
							 | 
						|||
| 
								 | 
							
								  const hourToSeconds = (h: number) => h * 3600;
							 | 
						|||
| 
								 | 
							
								  return seriesWithRealTimeData.value
							 | 
						|||
| 
								 | 
							
								    .map((s, idx) => {
							 | 
						|||
| 
								 | 
							
								      const globalIdx = filteredYAxisList.value.findIndex(
							 | 
						|||
| 
								 | 
							
								        y => y.name === s.name
							 | 
						|||
| 
								 | 
							
								      );
							 | 
						|||
| 
								 | 
							
								      if (globalIdx === -1) return null;
							 | 
						|||
| 
								 | 
							
								      // 첫 번째 시리즈에만 markLine 추가
							 | 
						|||
| 
								 | 
							
								      const markLine =
							 | 
						|||
| 
								 | 
							
								        idx === 0
							 | 
						|||
| 
								 | 
							
								          ? {
							 | 
						|||
| 
								 | 
							
								              symbol: "none",
							 | 
						|||
| 
								 | 
							
								              label: {
							 | 
						|||
| 
								 | 
							
								                show: true,
							 | 
						|||
| 
								 | 
							
								                position: "insideEndTop",
							 | 
						|||
| 
								 | 
							
								                fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								                fontSize: 13,
							 | 
						|||
| 
								 | 
							
								                color: "#222",
							 | 
						|||
| 
								 | 
							
								                formatter: function (params: { data?: { name?: string } }) {
							 | 
						|||
| 
								 | 
							
								                  return params.data && params.data.name
							 | 
						|||
| 
								 | 
							
								                    ? params.data.name
							 | 
						|||
| 
								 | 
							
								                    : "";
							 | 
						|||
| 
								 | 
							
								                },
							 | 
						|||
| 
								 | 
							
								              },
							 | 
						|||
| 
								 | 
							
								              lineStyle: { color: "#888", width: 2, type: "solid" },
							 | 
						|||
| 
								 | 
							
								              data: [
							 | 
						|||
| 
								 | 
							
								                { xAxis: hourToSeconds(6), name: "1F" },
							 | 
						|||
| 
								 | 
							
								                { xAxis: hourToSeconds(10), name: "2F" },
							 | 
						|||
| 
								 | 
							
								                { xAxis: hourToSeconds(12), name: "Seed" },
							 | 
						|||
| 
								 | 
							
								                { xAxis: hourToSeconds(22), name: "Main" },
							 | 
						|||
| 
								 | 
							
								              ],
							 | 
						|||
| 
								 | 
							
								            }
							 | 
						|||
| 
								 | 
							
								          : undefined;
							 | 
						|||
| 
								 | 
							
								      return {
							 | 
						|||
| 
								 | 
							
								        ...s,
							 | 
						|||
| 
								 | 
							
								        yAxisIndex: globalIdx,
							 | 
						|||
| 
								 | 
							
								        type: "line",
							 | 
						|||
| 
								 | 
							
								        symbol: "none",
							 | 
						|||
| 
								 | 
							
								        sampling: "lttb",
							 | 
						|||
| 
								 | 
							
								        lineStyle: { width: 0.5 },
							 | 
						|||
| 
								 | 
							
								        ...(isLargeMode
							 | 
						|||
| 
								 | 
							
								          ? {
							 | 
						|||
| 
								 | 
							
								              large: true,
							 | 
						|||
| 
								 | 
							
								              progressive: 500,
							 | 
						|||
| 
								 | 
							
								              progressiveThreshold: 10000,
							 | 
						|||
| 
								 | 
							
								            }
							 | 
						|||
| 
								 | 
							
								          : {}),
							 | 
						|||
| 
								 | 
							
								        ...(markLine ? { markLine } : {}),
							 | 
						|||
| 
								 | 
							
								      };
							 | 
						|||
| 
								 | 
							
								    })
							 | 
						|||
| 
								 | 
							
								    .filter(Boolean);
							 | 
						|||
| 
								 | 
							
								});
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								const handleCheckboxChange = (itemName: string, event: Event) => {
							 | 
						|||
| 
								 | 
							
								  const target = event.target as HTMLInputElement;
							 | 
						|||
| 
								 | 
							
								  const isChecked = target.checked;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  if (isChecked) {
							 | 
						|||
| 
								 | 
							
								    if (!checkedList.value.includes(itemName)) {
							 | 
						|||
| 
								 | 
							
								      checkedList.value = [...checkedList.value, itemName];
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								  } else {
							 | 
						|||
| 
								 | 
							
								    if (checkedList.value.length === 1) {
							 | 
						|||
| 
								 | 
							
								      alert("최소 1개의 데이터를 선택해주세요");
							 | 
						|||
| 
								 | 
							
								      target.checked = true;
							 | 
						|||
| 
								 | 
							
								      return;
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								    checkedList.value = checkedList.value.filter(name => name !== itemName);
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								};
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								const isLargeMode = pointsPerLine.value * yAxisList.length >= 10000;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								function updateGraphicLabels() {
							 | 
						|||
| 
								 | 
							
								  if (!chartInstance) return;
							 | 
						|||
| 
								 | 
							
								  const x1 = chartInstance.convertToPixel({ xAxisIndex: 0 }, 6);
							 | 
						|||
| 
								 | 
							
								  const x2 = chartInstance.convertToPixel({ xAxisIndex: 0 }, 10);
							 | 
						|||
| 
								 | 
							
								  const x3 = chartInstance.convertToPixel({ xAxisIndex: 0 }, 12);
							 | 
						|||
| 
								 | 
							
								  const x4 = chartInstance.convertToPixel({ xAxisIndex: 0 }, 22);
							 | 
						|||
| 
								 | 
							
								  chartInstance.setOption({
							 | 
						|||
| 
								 | 
							
								    graphic: [
							 | 
						|||
| 
								 | 
							
								      { id: "label-1f", left: x1 - 9, top: 10 },
							 | 
						|||
| 
								 | 
							
								      { id: "label-2f", left: x2 - 9, top: 10 },
							 | 
						|||
| 
								 | 
							
								      { id: "label-seed", left: x3 - 10, top: 10 },
							 | 
						|||
| 
								 | 
							
								      { id: "label-main", left: x4 - 10, top: 10 },
							 | 
						|||
| 
								 | 
							
								      {
							 | 
						|||
| 
								 | 
							
								        id: "line-1f",
							 | 
						|||
| 
								 | 
							
								        type: "line",
							 | 
						|||
| 
								 | 
							
								        shape: { x1: x1, y1: 0, x2: x1, y2: "100%" },
							 | 
						|||
| 
								 | 
							
								        style: { stroke: "#888", lineWidth: 1 },
							 | 
						|||
| 
								 | 
							
								      },
							 | 
						|||
| 
								 | 
							
								      {
							 | 
						|||
| 
								 | 
							
								        id: "line-2f",
							 | 
						|||
| 
								 | 
							
								        type: "line",
							 | 
						|||
| 
								 | 
							
								        shape: { x1: x2, y1: 0, x2: x2, y2: "100%" },
							 | 
						|||
| 
								 | 
							
								        style: { stroke: "#888", lineWidth: 1 },
							 | 
						|||
| 
								 | 
							
								      },
							 | 
						|||
| 
								 | 
							
								      {
							 | 
						|||
| 
								 | 
							
								        id: "line-seed",
							 | 
						|||
| 
								 | 
							
								        type: "line",
							 | 
						|||
| 
								 | 
							
								        shape: { x1: x3, y1: 0, x2: x3, y2: "100%" },
							 | 
						|||
| 
								 | 
							
								        style: { stroke: "#888", lineWidth: 1 },
							 | 
						|||
| 
								 | 
							
								      },
							 | 
						|||
| 
								 | 
							
								      {
							 | 
						|||
| 
								 | 
							
								        id: "line-main",
							 | 
						|||
| 
								 | 
							
								        type: "line",
							 | 
						|||
| 
								 | 
							
								        shape: { x1: x4, y1: 0, x2: x4, y2: "100%" },
							 | 
						|||
| 
								 | 
							
								        style: { stroke: "#888", lineWidth: 1 },
							 | 
						|||
| 
								 | 
							
								      },
							 | 
						|||
| 
								 | 
							
								    ],
							 | 
						|||
| 
								 | 
							
								  });
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								const renderChart = () => {
							 | 
						|||
| 
								 | 
							
								  if (!growthChartRef.value) return;
							 | 
						|||
| 
								 | 
							
								  if (!chartInstance) {
							 | 
						|||
| 
								 | 
							
								    chartInstance = echarts.init(growthChartRef.value);
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  // yAxis는 visibleYAxisList 2개만, 나머지는 show: false
							 | 
						|||
| 
								 | 
							
								  const yAxis = filteredYAxisList.value.map((y, i) => {
							 | 
						|||
| 
								 | 
							
								    const visibleIdx = visibleYAxisList.value.findIndex(
							 | 
						|||
| 
								 | 
							
								      vy => vy.name === y.name
							 | 
						|||
| 
								 | 
							
								    );
							 | 
						|||
| 
								 | 
							
								    return {
							 | 
						|||
| 
								 | 
							
								      type: "value",
							 | 
						|||
| 
								 | 
							
								      min: y.min,
							 | 
						|||
| 
								 | 
							
								      max: y.max,
							 | 
						|||
| 
								 | 
							
								      show: visibleIdx !== -1,
							 | 
						|||
| 
								 | 
							
								      position: "left",
							 | 
						|||
| 
								 | 
							
								      offset: visibleIdx === 1 ? 80 : 0,
							 | 
						|||
| 
								 | 
							
								      z: 10 + i,
							 | 
						|||
| 
								 | 
							
								      name: visibleIdx !== -1 ? y.name + (y.unit ? ` ${y.unit}` : "") : "",
							 | 
						|||
| 
								 | 
							
								      nameLocation: "middle",
							 | 
						|||
| 
								 | 
							
								      nameGap: 50,
							 | 
						|||
| 
								 | 
							
								      nameTextStyle: {
							 | 
						|||
| 
								 | 
							
								        color: y.color,
							 | 
						|||
| 
								 | 
							
								        fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								        fontSize: 11,
							 | 
						|||
| 
								 | 
							
								        writing: "tb-rl",
							 | 
						|||
| 
								 | 
							
								      },
							 | 
						|||
| 
								 | 
							
								      axisLabel: {
							 | 
						|||
| 
								 | 
							
								        show: visibleIdx !== -1,
							 | 
						|||
| 
								 | 
							
								        fontSize: 12,
							 | 
						|||
| 
								 | 
							
								        color: y.color,
							 | 
						|||
| 
								 | 
							
								        fontWeight: "bold",
							 | 
						|||
| 
								 | 
							
								      },
							 | 
						|||
| 
								 | 
							
								      splitLine: {
							 | 
						|||
| 
								 | 
							
								        show: visibleIdx === 1 || visibleIdx === 0,
							 | 
						|||
| 
								 | 
							
								        lineStyle: { color: "#f0f0f0", width: 1 },
							 | 
						|||
| 
								 | 
							
								      },
							 | 
						|||
| 
								 | 
							
								      axisLine: {
							 | 
						|||
| 
								 | 
							
								        show: visibleIdx !== -1,
							 | 
						|||
| 
								 | 
							
								        lineStyle: { color: y.color, width: 2 },
							 | 
						|||
| 
								 | 
							
								      },
							 | 
						|||
| 
								 | 
							
								      axisTick: {
							 | 
						|||
| 
								 | 
							
								        show: visibleIdx !== -1,
							 | 
						|||
| 
								 | 
							
								        lineStyle: { color: y.color },
							 | 
						|||
| 
								 | 
							
								      },
							 | 
						|||
| 
								 | 
							
								    };
							 | 
						|||
| 
								 | 
							
								  });
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  const option = {
							 | 
						|||
| 
								 | 
							
								    grid: {
							 | 
						|||
| 
								 | 
							
								      left: 120,
							 | 
						|||
| 
								 | 
							
								      right: 60,
							 | 
						|||
| 
								 | 
							
								      top: 40,
							 | 
						|||
| 
								 | 
							
								      bottom: 120,
							 | 
						|||
| 
								 | 
							
								      containLabel: true,
							 | 
						|||
| 
								 | 
							
								    },
							 | 
						|||
| 
								 | 
							
								    // brush: {
							 | 
						|||
| 
								 | 
							
								    //   xAxisIndex: "all",
							 | 
						|||
| 
								 | 
							
								    //   brushLink: "all",
							 | 
						|||
| 
								 | 
							
								    //   outOfBrush: { colorAlpha: 0.1 },
							 | 
						|||
| 
								 | 
							
								    //   brushType: "rect",
							 | 
						|||
| 
								 | 
							
								    //   throttleType: "debounce",
							 | 
						|||
| 
								 | 
							
								    //   throttleDelay: 300,
							 | 
						|||
| 
								 | 
							
								    // },
							 | 
						|||
| 
								 | 
							
								    toolbox: {
							 | 
						|||
| 
								 | 
							
								      feature: {
							 | 
						|||
| 
								 | 
							
								        dataZoom: {
							 | 
						|||
| 
								 | 
							
								          yAxisIndex: "none",
							 | 
						|||
| 
								 | 
							
								        },
							 | 
						|||
| 
								 | 
							
								      },
							 | 
						|||
| 
								 | 
							
								    },
							 | 
						|||
| 
								 | 
							
								    xAxis: [
							 | 
						|||
| 
								 | 
							
								      {
							 | 
						|||
| 
								 | 
							
								        type: "value",
							 | 
						|||
| 
								 | 
							
								        // min: 0, // <= 이 부분 주석 처리 또는 삭제
							 | 
						|||
| 
								 | 
							
								        // max: 100, // <= 이 부분 주석 처리 또는 삭제
							 | 
						|||
| 
								 | 
							
								        splitNumber: 20, // 20개 정도 라벨
							 | 
						|||
| 
								 | 
							
								        position: "bottom",
							 | 
						|||
| 
								 | 
							
								        axisLabel: {
							 | 
						|||
| 
								 | 
							
								          fontSize: 12,
							 | 
						|||
| 
								 | 
							
								          color: "#666",
							 | 
						|||
| 
								 | 
							
								          fontWeight: "normal",
							 | 
						|||
| 
								 | 
							
								          interval: 0, // 모든 라벨 표시 (1분 단위)
							 | 
						|||
| 
								 | 
							
								          hideOverlap: true, // 겹치는 라벨 자동 숨김
							 | 
						|||
| 
								 | 
							
								          formatter: function (value: number) {
							 | 
						|||
| 
								 | 
							
								            // value는 초 단위
							 | 
						|||
| 
								 | 
							
								            const totalSeconds = Math.round(value);
							 | 
						|||
| 
								 | 
							
								            const hours = Math.floor(totalSeconds / 3600);
							 | 
						|||
| 
								 | 
							
								            const minutes = Math.floor((totalSeconds % 3600) / 60);
							 | 
						|||
| 
								 | 
							
								            const seconds = totalSeconds % 60;
							 | 
						|||
| 
								 | 
							
								            return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
							 | 
						|||
| 
								 | 
							
								          },
							 | 
						|||
| 
								 | 
							
								        },
							 | 
						|||
| 
								 | 
							
								        splitLine: {
							 | 
						|||
| 
								 | 
							
								          show: true,
							 | 
						|||
| 
								 | 
							
								          lineStyle: { color: "#e0e0e0", width: 1 },
							 | 
						|||
| 
								 | 
							
								        },
							 | 
						|||
| 
								 | 
							
								        axisLine: {
							 | 
						|||
| 
								 | 
							
								          show: true,
							 | 
						|||
| 
								 | 
							
								          lineStyle: { color: "#ccc", width: 1 },
							 | 
						|||
| 
								 | 
							
								        },
							 | 
						|||
| 
								 | 
							
								        axisTick: {
							 | 
						|||
| 
								 | 
							
								          show: true,
							 | 
						|||
| 
								 | 
							
								          lineStyle: { color: "#ccc" },
							 | 
						|||
| 
								 | 
							
								        },
							 | 
						|||
| 
								 | 
							
								      },
							 | 
						|||
| 
								 | 
							
								    ],
							 | 
						|||
| 
								 | 
							
								    yAxis,
							 | 
						|||
| 
								 | 
							
								    series: mappedSeriesList.value,
							 | 
						|||
| 
								 | 
							
								    legend: { show: false },
							 | 
						|||
| 
								 | 
							
								    animation: false,
							 | 
						|||
| 
								 | 
							
								    dataZoom: [
							 | 
						|||
| 
								 | 
							
								      {
							 | 
						|||
| 
								 | 
							
								        type: "inside",
							 | 
						|||
| 
								 | 
							
								        xAxisIndex: 0,
							 | 
						|||
| 
								 | 
							
								        start: 0,
							 | 
						|||
| 
								 | 
							
								        end: 100,
							 | 
						|||
| 
								 | 
							
								        minValueSpan: 600, // 최소 10분까지만 확대
							 | 
						|||
| 
								 | 
							
								        zoomOnMouseWheel: true,
							 | 
						|||
| 
								 | 
							
								        moveOnMouseMove: true,
							 | 
						|||
| 
								 | 
							
								        moveOnMouseWheel: false,
							 | 
						|||
| 
								 | 
							
								        preventDefaultMouseMove: true,
							 | 
						|||
| 
								 | 
							
								      },
							 | 
						|||
| 
								 | 
							
								      {
							 | 
						|||
| 
								 | 
							
								        type: "slider",
							 | 
						|||
| 
								 | 
							
								        xAxisIndex: 0,
							 | 
						|||
| 
								 | 
							
								        start: 0,
							 | 
						|||
| 
								 | 
							
								        end: 100,
							 | 
						|||
| 
								 | 
							
								        minValueSpan: 600, // 최소 10분까지만 확대
							 | 
						|||
| 
								 | 
							
								        bottom: 10,
							 | 
						|||
| 
								 | 
							
								        height: 20,
							 | 
						|||
| 
								 | 
							
								        borderColor: "transparent",
							 | 
						|||
| 
								 | 
							
								        backgroundColor: "#f5f5f5",
							 | 
						|||
| 
								 | 
							
								        fillerColor: "rgba(25,118,210,0.2)",
							 | 
						|||
| 
								 | 
							
								        handleStyle: {
							 | 
						|||
| 
								 | 
							
								          color: "#1976d2",
							 | 
						|||
| 
								 | 
							
								        },
							 | 
						|||
| 
								 | 
							
								        textStyle: {
							 | 
						|||
| 
								 | 
							
								          color: "#666",
							 | 
						|||
| 
								 | 
							
								        },
							 | 
						|||
| 
								 | 
							
								      },
							 | 
						|||
| 
								 | 
							
								    ],
							 | 
						|||
| 
								 | 
							
								    tooltip: {
							 | 
						|||
| 
								 | 
							
								      trigger: "axis",
							 | 
						|||
| 
								 | 
							
								      position: function (pt: number[]) {
							 | 
						|||
| 
								 | 
							
								        return [pt[0], "10%"];
							 | 
						|||
| 
								 | 
							
								      },
							 | 
						|||
| 
								 | 
							
								      axisPointer: {
							 | 
						|||
| 
								 | 
							
								        type: "line",
							 | 
						|||
| 
								 | 
							
								        animation: false,
							 | 
						|||
| 
								 | 
							
								        lineStyle: {
							 | 
						|||
| 
								 | 
							
								          color: "#5470c6",
							 | 
						|||
| 
								 | 
							
								          width: 1,
							 | 
						|||
| 
								 | 
							
								          type: "dashed",
							 | 
						|||
| 
								 | 
							
								        },
							 | 
						|||
| 
								 | 
							
								      },
							 | 
						|||
| 
								 | 
							
								      backgroundColor: "rgba(255, 255, 255, 0.95)",
							 | 
						|||
| 
								 | 
							
								      borderColor: "#ccc",
							 | 
						|||
| 
								 | 
							
								      borderWidth: 1,
							 | 
						|||
| 
								 | 
							
								      textStyle: {
							 | 
						|||
| 
								 | 
							
								        color: "#333",
							 | 
						|||
| 
								 | 
							
								        fontSize: 13,
							 | 
						|||
| 
								 | 
							
								        fontWeight: "normal",
							 | 
						|||
| 
								 | 
							
								      },
							 | 
						|||
| 
								 | 
							
								      extraCssText:
							 | 
						|||
| 
								 | 
							
								        "box-shadow: 0 2px 8px rgba(0,0,0,0.15); border-radius: 4px;",
							 | 
						|||
| 
								 | 
							
								      formatter: function (
							 | 
						|||
| 
								 | 
							
								        params: Array<{
							 | 
						|||
| 
								 | 
							
								          seriesName: string;
							 | 
						|||
| 
								 | 
							
								          color: string;
							 | 
						|||
| 
								 | 
							
								          value: [number, number];
							 | 
						|||
| 
								 | 
							
								        }>
							 | 
						|||
| 
								 | 
							
								      ) {
							 | 
						|||
| 
								 | 
							
								        if (!Array.isArray(params)) params = [params];
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								        const baseDate = new Date("2025-07-01T00:00:00");
							 | 
						|||
| 
								 | 
							
								        const hour = Array.isArray(params[0].value) ? params[0].value[0] : 0;
							 | 
						|||
| 
								 | 
							
								        const date = new Date(baseDate.getTime());
							 | 
						|||
| 
								 | 
							
								        date.setHours(baseDate.getHours() + hour);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								        const yyyy = date.getFullYear();
							 | 
						|||
| 
								 | 
							
								        const mm = (date.getMonth() + 1).toString().padStart(2, "0");
							 | 
						|||
| 
								 | 
							
								        const dd = date.getDate().toString().padStart(2, "0");
							 | 
						|||
| 
								 | 
							
								        const HH = date.getHours().toString().padStart(2, "0");
							 | 
						|||
| 
								 | 
							
								        const dateStr = `${yyyy}.${mm}.${dd} - ${HH}:00`;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								        let tooltipContent = `<div style="font-weight: bold; color: #5470c6; margin-bottom: 8px; border-bottom: 1px solid #eee; padding-bottom: 4px;">${dateStr}</div>`;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								        params.forEach(param => {
							 | 
						|||
| 
								 | 
							
								          let unit = "";
							 | 
						|||
| 
								 | 
							
								          if (param.seriesName && yAxisList) {
							 | 
						|||
| 
								 | 
							
								            const y = yAxisList.find(y => y.name === param.seriesName);
							 | 
						|||
| 
								 | 
							
								            if (y && y.unit) unit = " " + y.unit;
							 | 
						|||
| 
								 | 
							
								          }
							 | 
						|||
| 
								 | 
							
								          const value = Array.isArray(param.value) ? param.value[1] : "";
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								          tooltipContent += `<div style="display: flex; justify-content: space-between; align-items: center; margin: 4px 0;">
							 | 
						|||
| 
								 | 
							
								            <span style="display: flex; align-items: center;">
							 | 
						|||
| 
								 | 
							
								              <span style="display: inline-block; width: 12px; height: 12px; background-color: ${param.color}; margin-right: 8px; border-radius: 2px;"></span>
							 | 
						|||
| 
								 | 
							
								              <span style="font-weight: 500;">${param.seriesName}${unit}</span>
							 | 
						|||
| 
								 | 
							
								            </span>
							 | 
						|||
| 
								 | 
							
								            <span style="font-weight: bold; color: #333; margin-left: 16px;">${value}</span>
							 | 
						|||
| 
								 | 
							
								          </div>`;
							 | 
						|||
| 
								 | 
							
								        });
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								        return tooltipContent;
							 | 
						|||
| 
								 | 
							
								      },
							 | 
						|||
| 
								 | 
							
								    },
							 | 
						|||
| 
								 | 
							
								    ...(isLargeMode
							 | 
						|||
| 
								 | 
							
								      ? {}
							 | 
						|||
| 
								 | 
							
								      : {
							 | 
						|||
| 
								 | 
							
								          graphic: [
							 | 
						|||
| 
								 | 
							
								            // updateGraphicLabels에서 동적으로 위치가 바뀌는 라벨/라인 정의 (초기값)
							 | 
						|||
| 
								 | 
							
								            {
							 | 
						|||
| 
								 | 
							
								              id: "label-1f",
							 | 
						|||
| 
								 | 
							
								              type: "text",
							 | 
						|||
| 
								 | 
							
								              z: 100,
							 | 
						|||
| 
								 | 
							
								              left: 0,
							 | 
						|||
| 
								 | 
							
								              top: 0,
							 | 
						|||
| 
								 | 
							
								              style: {
							 | 
						|||
| 
								 | 
							
								                text: "1F",
							 | 
						|||
| 
								 | 
							
								                font: "bold 15px sans-serif",
							 | 
						|||
| 
								 | 
							
								                fill: "#222",
							 | 
						|||
| 
								 | 
							
								                textAlign: "center",
							 | 
						|||
| 
								 | 
							
								              },
							 | 
						|||
| 
								 | 
							
								            },
							 | 
						|||
| 
								 | 
							
								            {
							 | 
						|||
| 
								 | 
							
								              id: "label-2f",
							 | 
						|||
| 
								 | 
							
								              type: "text",
							 | 
						|||
| 
								 | 
							
								              z: 100,
							 | 
						|||
| 
								 | 
							
								              left: 0,
							 | 
						|||
| 
								 | 
							
								              top: 0,
							 | 
						|||
| 
								 | 
							
								              style: {
							 | 
						|||
| 
								 | 
							
								                text: "2F",
							 | 
						|||
| 
								 | 
							
								                font: "bold 15px sans-serif",
							 | 
						|||
| 
								 | 
							
								                fill: "#222",
							 | 
						|||
| 
								 | 
							
								                textAlign: "center",
							 | 
						|||
| 
								 | 
							
								              },
							 | 
						|||
| 
								 | 
							
								            },
							 | 
						|||
| 
								 | 
							
								            {
							 | 
						|||
| 
								 | 
							
								              id: "label-seed",
							 | 
						|||
| 
								 | 
							
								              type: "text",
							 | 
						|||
| 
								 | 
							
								              z: 100,
							 | 
						|||
| 
								 | 
							
								              left: 0,
							 | 
						|||
| 
								 | 
							
								              top: 0,
							 | 
						|||
| 
								 | 
							
								              style: {
							 | 
						|||
| 
								 | 
							
								                text: "Seed",
							 | 
						|||
| 
								 | 
							
								                font: "bold 15px sans-serif",
							 | 
						|||
| 
								 | 
							
								                fill: "#222",
							 | 
						|||
| 
								 | 
							
								                textAlign: "center",
							 | 
						|||
| 
								 | 
							
								              },
							 | 
						|||
| 
								 | 
							
								            },
							 | 
						|||
| 
								 | 
							
								            {
							 | 
						|||
| 
								 | 
							
								              id: "label-main",
							 | 
						|||
| 
								 | 
							
								              type: "text",
							 | 
						|||
| 
								 | 
							
								              z: 100,
							 | 
						|||
| 
								 | 
							
								              left: 0,
							 | 
						|||
| 
								 | 
							
								              top: 0,
							 | 
						|||
| 
								 | 
							
								              style: {
							 | 
						|||
| 
								 | 
							
								                text: "Main",
							 | 
						|||
| 
								 | 
							
								                font: "bold 15px sans-serif",
							 | 
						|||
| 
								 | 
							
								                fill: "#222",
							 | 
						|||
| 
								 | 
							
								                textAlign: "center",
							 | 
						|||
| 
								 | 
							
								              },
							 | 
						|||
| 
								 | 
							
								            },
							 | 
						|||
| 
								 | 
							
								            {
							 | 
						|||
| 
								 | 
							
								              id: "line-1f",
							 | 
						|||
| 
								 | 
							
								              type: "line",
							 | 
						|||
| 
								 | 
							
								              shape: { x1: 0, y1: 0, x2: 0, y2: "100%" },
							 | 
						|||
| 
								 | 
							
								              style: { stroke: "#888", lineWidth: 1 },
							 | 
						|||
| 
								 | 
							
								            },
							 | 
						|||
| 
								 | 
							
								            {
							 | 
						|||
| 
								 | 
							
								              id: "line-2f",
							 | 
						|||
| 
								 | 
							
								              type: "line",
							 | 
						|||
| 
								 | 
							
								              shape: { x1: 0, y1: 0, x2: 0, y2: "100%" },
							 | 
						|||
| 
								 | 
							
								              style: { stroke: "#888", lineWidth: 1 },
							 | 
						|||
| 
								 | 
							
								            },
							 | 
						|||
| 
								 | 
							
								            {
							 | 
						|||
| 
								 | 
							
								              id: "line-seed",
							 | 
						|||
| 
								 | 
							
								              type: "line",
							 | 
						|||
| 
								 | 
							
								              shape: { x1: 0, y1: 0, x2: 0, y2: "100%" },
							 | 
						|||
| 
								 | 
							
								              style: { stroke: "#888", lineWidth: 1 },
							 | 
						|||
| 
								 | 
							
								            },
							 | 
						|||
| 
								 | 
							
								            {
							 | 
						|||
| 
								 | 
							
								              id: "line-main",
							 | 
						|||
| 
								 | 
							
								              type: "line",
							 | 
						|||
| 
								 | 
							
								              shape: { x1: 0, y1: 0, x2: 0, y2: "100%" },
							 | 
						|||
| 
								 | 
							
								              style: { stroke: "#888", lineWidth: 1 },
							 | 
						|||
| 
								 | 
							
								            },
							 | 
						|||
| 
								 | 
							
								          ],
							 | 
						|||
| 
								 | 
							
								        }),
							 | 
						|||
| 
								 | 
							
								  };
							 | 
						|||
| 
								 | 
							
								  chartInstance.setOption(option, true);
							 | 
						|||
| 
								 | 
							
								  if (!isLargeMode) updateGraphicLabels();
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  // brushEnd 이벤트로 드래그 영역 줌인
							 | 
						|||
| 
								 | 
							
								  if (chartInstance) {
							 | 
						|||
| 
								 | 
							
								    chartInstance.off("brushEnd");
							 | 
						|||
| 
								 | 
							
								    chartInstance.on("brushEnd", function (params) {
							 | 
						|||
| 
								 | 
							
								      const p = params as { areas?: { coordRange?: [number, number] }[] };
							 | 
						|||
| 
								 | 
							
								      if (p.areas && p.areas.length > 0) {
							 | 
						|||
| 
								 | 
							
								        const area = p.areas[0];
							 | 
						|||
| 
								 | 
							
								        if (area.coordRange && chartInstance) {
							 | 
						|||
| 
								 | 
							
								          chartInstance.dispatchAction({
							 | 
						|||
| 
								 | 
							
								            type: "dataZoom",
							 | 
						|||
| 
								 | 
							
								            startValue: area.coordRange[0],
							 | 
						|||
| 
								 | 
							
								            endValue: area.coordRange[1],
							 | 
						|||
| 
								 | 
							
								          });
							 | 
						|||
| 
								 | 
							
								        }
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								    });
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								};
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								watch(
							 | 
						|||
| 
								 | 
							
								  [checkedList, yAxisScrollIndex],
							 | 
						|||
| 
								 | 
							
								  ([newChecked, _newScroll], [oldChecked, _oldScroll]) => {
							 | 
						|||
| 
								 | 
							
								    const newCheckedArray = [...newChecked];
							 | 
						|||
| 
								 | 
							
								    const oldCheckedArray = oldChecked ? [...oldChecked] : [];
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    // 체크박스 변경이 있었는지 확인 (슬라이더 이동만으로는 실행하지 않음)
							 | 
						|||
| 
								 | 
							
								    const checkboxChanged = newChecked !== oldChecked;
							 | 
						|||
| 
								 | 
							
								    if (!checkboxChanged) {
							 | 
						|||
| 
								 | 
							
								      renderChart();
							 | 
						|||
| 
								 | 
							
								      return;
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    // 새로 체크된 y축이 있으면, 그 y축이 보이도록 슬라이더 이동
							 | 
						|||
| 
								 | 
							
								    if (
							 | 
						|||
| 
								 | 
							
								      oldCheckedArray.length > 0 &&
							 | 
						|||
| 
								 | 
							
								      newCheckedArray.length > oldCheckedArray.length
							 | 
						|||
| 
								 | 
							
								    ) {
							 | 
						|||
| 
								 | 
							
								      const added = newCheckedArray.find(x => !oldCheckedArray.includes(x));
							 | 
						|||
| 
								 | 
							
								      const idx = filteredYAxisList.value.findIndex(y => y.name === added);
							 | 
						|||
| 
								 | 
							
								      if (idx !== -1) {
							 | 
						|||
| 
								 | 
							
								        // 필터된 리스트가 2개 이하면 슬라이더를 0으로 고정
							 | 
						|||
| 
								 | 
							
								        if (filteredYAxisList.value.length <= 2) {
							 | 
						|||
| 
								 | 
							
								          yAxisScrollIndex.value = 0;
							 | 
						|||
| 
								 | 
							
								        } else {
							 | 
						|||
| 
								 | 
							
								          // 새로 추가된 항목이 현재 보이는 범위에 있는지 확인
							 | 
						|||
| 
								 | 
							
								          const currentStart = yAxisScrollIndex.value;
							 | 
						|||
| 
								 | 
							
								          const currentEnd = currentStart + 1;
							 | 
						|||
| 
								 | 
							
								          // 현재 범위에 없으면 슬라이더 이동
							 | 
						|||
| 
								 | 
							
								          if (idx < currentStart || idx > currentEnd) {
							 | 
						|||
| 
								 | 
							
								            const newScrollIndex = Math.max(
							 | 
						|||
| 
								 | 
							
								              0,
							 | 
						|||
| 
								 | 
							
								              Math.min(idx, filteredYAxisList.value.length - 2)
							 | 
						|||
| 
								 | 
							
								            );
							 | 
						|||
| 
								 | 
							
								            yAxisScrollIndex.value = newScrollIndex;
							 | 
						|||
| 
								 | 
							
								          }
							 | 
						|||
| 
								 | 
							
								        }
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								    // 체크박스 개수가 2개 이하면 슬라이더를 0으로 고정 (추가되지 않은 경우)
							 | 
						|||
| 
								 | 
							
								    else if (filteredYAxisList.value.length <= 2) {
							 | 
						|||
| 
								 | 
							
								      yAxisScrollIndex.value = 0;
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								    // 체크박스 해제 시 슬라이더를 가장 왼쪽으로 이동
							 | 
						|||
| 
								 | 
							
								    else if (
							 | 
						|||
| 
								 | 
							
								      oldCheckedArray.length > 0 &&
							 | 
						|||
| 
								 | 
							
								      newCheckedArray.length < oldCheckedArray.length
							 | 
						|||
| 
								 | 
							
								    ) {
							 | 
						|||
| 
								 | 
							
								      yAxisScrollIndex.value = 0;
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    renderChart();
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								// 실시간 데이터 변경 시 차트 업데이트
							 | 
						|||
| 
								 | 
							
								watch(
							 | 
						|||
| 
								 | 
							
								  realTimeData,
							 | 
						|||
| 
								 | 
							
								  () => {
							 | 
						|||
| 
								 | 
							
								    if (isRealTimeMode.value) {
							 | 
						|||
| 
								 | 
							
								      renderChart();
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								  },
							 | 
						|||
| 
								 | 
							
								  { deep: true }
							 | 
						|||
| 
								 | 
							
								);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								const menuVisible = ref(false);
							 | 
						|||
| 
								 | 
							
								const menuX = ref(0);
							 | 
						|||
| 
								 | 
							
								const menuY = ref(0);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								const currentZoomLevel = ref(1);
							 | 
						|||
| 
								 | 
							
								const zoomStep = 0.2;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								const zoomIn = () => {
							 | 
						|||
| 
								 | 
							
								  if (chartInstance) {
							 | 
						|||
| 
								 | 
							
								    currentZoomLevel.value = Math.min(currentZoomLevel.value + zoomStep, 3);
							 | 
						|||
| 
								 | 
							
								    applyZoom();
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								};
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								const zoomOut = () => {
							 | 
						|||
| 
								 | 
							
								  if (chartInstance) {
							 | 
						|||
| 
								 | 
							
								    currentZoomLevel.value = Math.max(currentZoomLevel.value - zoomStep, 0.5);
							 | 
						|||
| 
								 | 
							
								    applyZoom();
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								};
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								const resetZoom = () => {
							 | 
						|||
| 
								 | 
							
								  if (chartInstance) {
							 | 
						|||
| 
								 | 
							
								    currentZoomLevel.value = 1;
							 | 
						|||
| 
								 | 
							
								    applyZoom();
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								};
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								const applyZoom = () => {
							 | 
						|||
| 
								 | 
							
								  if (!chartInstance) return;
							 | 
						|||
| 
								 | 
							
								  const option = chartInstance.getOption();
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  const xDataZoom = {
							 | 
						|||
| 
								 | 
							
								    type: "inside",
							 | 
						|||
| 
								 | 
							
								    xAxisIndex: 0,
							 | 
						|||
| 
								 | 
							
								    start: 0,
							 | 
						|||
| 
								 | 
							
								    end: 100 / currentZoomLevel.value,
							 | 
						|||
| 
								 | 
							
								    zoomOnMouseWheel: true,
							 | 
						|||
| 
								 | 
							
								    moveOnMouseMove: true,
							 | 
						|||
| 
								 | 
							
								    moveOnMouseWheel: false,
							 | 
						|||
| 
								 | 
							
								    preventDefaultMouseMove: true,
							 | 
						|||
| 
								 | 
							
								  };
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  const yDataZoom = visibleYAxisList.value.map((_, index) => ({
							 | 
						|||
| 
								 | 
							
								    type: "inside",
							 | 
						|||
| 
								 | 
							
								    yAxisIndex: index,
							 | 
						|||
| 
								 | 
							
								    start: 0,
							 | 
						|||
| 
								 | 
							
								    end: 100 / currentZoomLevel.value,
							 | 
						|||
| 
								 | 
							
								    zoomOnMouseWheel: true,
							 | 
						|||
| 
								 | 
							
								    moveOnMouseWheel: false,
							 | 
						|||
| 
								 | 
							
								    preventDefaultMouseMove: true,
							 | 
						|||
| 
								 | 
							
								  }));
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  option.dataZoom = [xDataZoom, ...yDataZoom];
							 | 
						|||
| 
								 | 
							
								  chartInstance.setOption(option, false);
							 | 
						|||
| 
								 | 
							
								};
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								function onContextMenu(e: MouseEvent) {
							 | 
						|||
| 
								 | 
							
								  e.preventDefault();
							 | 
						|||
| 
								 | 
							
								  menuX.value = e.clientX;
							 | 
						|||
| 
								 | 
							
								  menuY.value = e.clientY;
							 | 
						|||
| 
								 | 
							
								  menuVisible.value = true;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								function onClickAnywhere() {
							 | 
						|||
| 
								 | 
							
								  menuVisible.value = false;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								onMounted(() => {
							 | 
						|||
| 
								 | 
							
								  renderChart();
							 | 
						|||
| 
								 | 
							
								  if (chartInstance && !isLargeMode) {
							 | 
						|||
| 
								 | 
							
								    chartInstance.on("finished", updateGraphicLabels);
							 | 
						|||
| 
								 | 
							
								    chartInstance.on("dataZoom", updateGraphicLabels);
							 | 
						|||
| 
								 | 
							
								    window.addEventListener("resize", updateGraphicLabels);
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  // 실시간 모드 자동 시작 비활성화
							 | 
						|||
| 
								 | 
							
								  // setTimeout(() => {
							 | 
						|||
| 
								 | 
							
								  //   startRealTimeMode();
							 | 
						|||
| 
								 | 
							
								  // }, 1000); // 1초 후 시작
							 | 
						|||
| 
								 | 
							
								});
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								onUnmounted(() => {
							 | 
						|||
| 
								 | 
							
								  stopRealTimeMode();
							 | 
						|||
| 
								 | 
							
								  if (chartInstance) {
							 | 
						|||
| 
								 | 
							
								    chartInstance.dispose();
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								  if (!isLargeMode) window.removeEventListener("resize", updateGraphicLabels);
							 | 
						|||
| 
								 | 
							
								});
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								// 각 시리즈별 평균 기울기 계산 함수
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								const calculateAverageSlope = (seriesData: [number, number][]) => {
							 | 
						|||
| 
								 | 
							
								  if (seriesData.length < 2) return 0;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  let totalSlope = 0;
							 | 
						|||
| 
								 | 
							
								  let count = 0;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  for (let i = 1; i < seriesData.length; i++) {
							 | 
						|||
| 
								 | 
							
								    const current = seriesData[i][1];
							 | 
						|||
| 
								 | 
							
								    const previous = seriesData[i - 1][1];
							 | 
						|||
| 
								 | 
							
								    totalSlope += current - previous;
							 | 
						|||
| 
								 | 
							
								    count++;
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  return count > 0 ? totalSlope / count : 0;
							 | 
						|||
| 
								 | 
							
								};
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								// 실시간 데이터 생성 함수
							 | 
						|||
| 
								 | 
							
								const generateRealTimeData = () => {
							 | 
						|||
| 
								 | 
							
								  filteredSeriesList.value.forEach(series => {
							 | 
						|||
| 
								 | 
							
								    let previousValue: number;
							 | 
						|||
| 
								 | 
							
								    if (
							 | 
						|||
| 
								 | 
							
								      realTimeData.value[series.name] &&
							 | 
						|||
| 
								 | 
							
								      (realTimeData.value[series.name] as [number, number][]).length > 0
							 | 
						|||
| 
								 | 
							
								    ) {
							 | 
						|||
| 
								 | 
							
								      const lastRealTimePoint = (
							 | 
						|||
| 
								 | 
							
								        realTimeData.value[series.name] as [number, number][]
							 | 
						|||
| 
								 | 
							
								      )[(realTimeData.value[series.name] as [number, number][]).length - 1];
							 | 
						|||
| 
								 | 
							
								      previousValue = lastRealTimePoint[1];
							 | 
						|||
| 
								 | 
							
								    } else {
							 | 
						|||
| 
								 | 
							
								      const lastDataPoint = series.data[series.data.length - 1] as [
							 | 
						|||
| 
								 | 
							
								        number,
							 | 
						|||
| 
								 | 
							
								        number,
							 | 
						|||
| 
								 | 
							
								      ];
							 | 
						|||
| 
								 | 
							
								      previousValue = lastDataPoint[1];
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    const averageSlope = calculateAverageSlope(
							 | 
						|||
| 
								 | 
							
								      series.data as [number, number][]
							 | 
						|||
| 
								 | 
							
								    );
							 | 
						|||
| 
								 | 
							
								    const predictedValue = previousValue + averageSlope * 0.5;
							 | 
						|||
| 
								 | 
							
								    const random = Math.random();
							 | 
						|||
| 
								 | 
							
								    let newValue: number;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    if (random < 0.7) {
							 | 
						|||
| 
								 | 
							
								      newValue = predictedValue;
							 | 
						|||
| 
								 | 
							
								    } else if (random < 0.9) {
							 | 
						|||
| 
								 | 
							
								      newValue = previousValue;
							 | 
						|||
| 
								 | 
							
								    } else {
							 | 
						|||
| 
								 | 
							
								      const variation = (Math.random() - 0.5) * 0.04;
							 | 
						|||
| 
								 | 
							
								      newValue = Math.max(0, previousValue * (1 + variation));
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    const newPoint: [number, number] = [
							 | 
						|||
| 
								 | 
							
								      realTimeStartTime + currentRealTimeIndex.value * 0.333,
							 | 
						|||
| 
								 | 
							
								      newValue,
							 | 
						|||
| 
								 | 
							
								    ];
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    if (realTimeData.value[series.name]) {
							 | 
						|||
| 
								 | 
							
								      (realTimeData.value[series.name] as [number, number][]).push(newPoint);
							 | 
						|||
| 
								 | 
							
								    } else {
							 | 
						|||
| 
								 | 
							
								      realTimeData.value[series.name] = [newPoint];
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								  });
							 | 
						|||
| 
								 | 
							
								};
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								// 실시간 모드 시작 (사용하지 않음)
							 | 
						|||
| 
								 | 
							
								const _startRealTimeMode = () => {
							 | 
						|||
| 
								 | 
							
								  if (isRealTimeMode.value) return;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  isRealTimeMode.value = true;
							 | 
						|||
| 
								 | 
							
								  currentRealTimeIndex.value = 0;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  // 실시간 데이터 초기화
							 | 
						|||
| 
								 | 
							
								  realTimeData.value = {};
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  // 1초마다 새로운 데이터 생성
							 | 
						|||
| 
								 | 
							
								  realTimeTimer.value = setInterval(() => {
							 | 
						|||
| 
								 | 
							
								    currentRealTimeIndex.value++;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    if (currentRealTimeIndex.value >= realTimeDuration) {
							 | 
						|||
| 
								 | 
							
								      // 72초 완료 후 실시간 모드 종료 (24초 × 3배 세밀도)
							 | 
						|||
| 
								 | 
							
								      stopRealTimeMode();
							 | 
						|||
| 
								 | 
							
								      return;
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    generateRealTimeData();
							 | 
						|||
| 
								 | 
							
								  }, 1000); // 1초마다 실행
							 | 
						|||
| 
								 | 
							
								};
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								watch(intervalValue, () => {
							 | 
						|||
| 
								 | 
							
								  renderChart();
							 | 
						|||
| 
								 | 
							
								});
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								// 실시간 모드 중지
							 | 
						|||
| 
								 | 
							
								const stopRealTimeMode = () => {
							 | 
						|||
| 
								 | 
							
								  if (!isRealTimeMode.value) return;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  isRealTimeMode.value = false;
							 | 
						|||
| 
								 | 
							
								  if (realTimeTimer.value) {
							 | 
						|||
| 
								 | 
							
								    clearInterval(realTimeTimer.value);
							 | 
						|||
| 
								 | 
							
								    realTimeTimer.value = null;
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								  currentRealTimeIndex.value = 0;
							 | 
						|||
| 
								 | 
							
								  realTimeData.value = {};
							 | 
						|||
| 
								 | 
							
								};
							 | 
						|||
| 
								 | 
							
								</script>
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								<style scoped>
							 | 
						|||
| 
								 | 
							
								.experiment-graph-page {
							 | 
						|||
| 
								 | 
							
								  width: 95vw;
							 | 
						|||
| 
								 | 
							
								  height: 76vh;
							 | 
						|||
| 
								 | 
							
								  background: #fff;
							 | 
						|||
| 
								 | 
							
								  display: flex;
							 | 
						|||
| 
								 | 
							
								  flex-direction: column;
							 | 
						|||
| 
								 | 
							
								  overflow: hidden;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								.info-bar {
							 | 
						|||
| 
								 | 
							
								  width: 100%;
							 | 
						|||
| 
								 | 
							
								  background: #fff;
							 | 
						|||
| 
								 | 
							
								  border-bottom: 1.5px solid #e0e0e0;
							 | 
						|||
| 
								 | 
							
								  padding: 18px 32px 10px 32px;
							 | 
						|||
| 
								 | 
							
								  font-size: 1.08rem;
							 | 
						|||
| 
								 | 
							
								  font-family: "Pretendard", "Noto Sans KR", sans-serif;
							 | 
						|||
| 
								 | 
							
								  font-weight: 500;
							 | 
						|||
| 
								 | 
							
								  letter-spacing: 0.01em;
							 | 
						|||
| 
								 | 
							
								  box-sizing: border-box;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								.info-row {
							 | 
						|||
| 
								 | 
							
								  display: flex;
							 | 
						|||
| 
								 | 
							
								  align-items: center;
							 | 
						|||
| 
								 | 
							
								  gap: 16px;
							 | 
						|||
| 
								 | 
							
								  position: relative;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								.info-label {
							 | 
						|||
| 
								 | 
							
								  color: #222;
							 | 
						|||
| 
								 | 
							
								  font-weight: 600;
							 | 
						|||
| 
								 | 
							
								  margin-right: 2px;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								.info-value {
							 | 
						|||
| 
								 | 
							
								  color: #1976d2;
							 | 
						|||
| 
								 | 
							
								  font-weight: 500;
							 | 
						|||
| 
								 | 
							
								  margin-right: 8px;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								.info-divider {
							 | 
						|||
| 
								 | 
							
								  color: #aaa;
							 | 
						|||
| 
								 | 
							
								  margin: 0 10px;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								.realdata-checkbox-bar {
							 | 
						|||
| 
								 | 
							
								  display: flex;
							 | 
						|||
| 
								 | 
							
								  align-items: center;
							 | 
						|||
| 
								 | 
							
								  gap: 8px;
							 | 
						|||
| 
								 | 
							
								  padding: 12px 32px 8px 32px;
							 | 
						|||
| 
								 | 
							
								  font-size: 1.08rem;
							 | 
						|||
| 
								 | 
							
								  border-bottom: 1px solid #e0e0e0;
							 | 
						|||
| 
								 | 
							
								  margin-bottom: 0px;
							 | 
						|||
| 
								 | 
							
								  background: #fff;
							 | 
						|||
| 
								 | 
							
								  flex-shrink: 0;
							 | 
						|||
| 
								 | 
							
								  overflow-x: auto;
							 | 
						|||
| 
								 | 
							
								  white-space: nowrap;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								.realdata-label {
							 | 
						|||
| 
								 | 
							
								  font-weight: bold;
							 | 
						|||
| 
								 | 
							
								  margin-right: 12px;
							 | 
						|||
| 
								 | 
							
								  color: #444;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								.realdata-checkbox-item {
							 | 
						|||
| 
								 | 
							
								  margin-right: 10px;
							 | 
						|||
| 
								 | 
							
								  font-size: 1.01rem;
							 | 
						|||
| 
								 | 
							
								  display: inline-flex;
							 | 
						|||
| 
								 | 
							
								  align-items: center;
							 | 
						|||
| 
								 | 
							
								  gap: 2px;
							 | 
						|||
| 
								 | 
							
								  user-select: none;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								.realdata-checkbox-item input[type="checkbox"] {
							 | 
						|||
| 
								 | 
							
								  accent-color: #7ecbff;
							 | 
						|||
| 
								 | 
							
								  width: 16px;
							 | 
						|||
| 
								 | 
							
								  height: 16px;
							 | 
						|||
| 
								 | 
							
								  margin-right: 2px;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								.main-graph-area {
							 | 
						|||
| 
								 | 
							
								  flex: 1;
							 | 
						|||
| 
								 | 
							
								  display: flex;
							 | 
						|||
| 
								 | 
							
								  flex-direction: column;
							 | 
						|||
| 
								 | 
							
								  overflow: hidden;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								.echarts-container {
							 | 
						|||
| 
								 | 
							
								  flex: 1;
							 | 
						|||
| 
								 | 
							
								  position: relative;
							 | 
						|||
| 
								 | 
							
								  background: #fff;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								.echarts-graph {
							 | 
						|||
| 
								 | 
							
								  width: 100%;
							 | 
						|||
| 
								 | 
							
								  height: 100%;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								.zoom-controls {
							 | 
						|||
| 
								 | 
							
								  position: absolute;
							 | 
						|||
| 
								 | 
							
								  top: 60px; /* 기존 10px에서 60px로 변경 */
							 | 
						|||
| 
								 | 
							
								  right: 10px;
							 | 
						|||
| 
								 | 
							
								  display: flex;
							 | 
						|||
| 
								 | 
							
								  flex-direction: column;
							 | 
						|||
| 
								 | 
							
								  gap: 4px;
							 | 
						|||
| 
								 | 
							
								  z-index: 1000;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								.zoom-btn {
							 | 
						|||
| 
								 | 
							
								  width: 32px;
							 | 
						|||
| 
								 | 
							
								  height: 32px;
							 | 
						|||
| 
								 | 
							
								  border: 1px solid #ddd;
							 | 
						|||
| 
								 | 
							
								  background: #fff;
							 | 
						|||
| 
								 | 
							
								  border-radius: 4px;
							 | 
						|||
| 
								 | 
							
								  cursor: pointer;
							 | 
						|||
| 
								 | 
							
								  display: flex;
							 | 
						|||
| 
								 | 
							
								  align-items: center;
							 | 
						|||
| 
								 | 
							
								  justify-content: center;
							 | 
						|||
| 
								 | 
							
								  font-size: 16px;
							 | 
						|||
| 
								 | 
							
								  font-weight: bold;
							 | 
						|||
| 
								 | 
							
								  color: #666;
							 | 
						|||
| 
								 | 
							
								  transition: all 0.2s ease;
							 | 
						|||
| 
								 | 
							
								  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								.zoom-btn:hover {
							 | 
						|||
| 
								 | 
							
								  background: #f5f5f5;
							 | 
						|||
| 
								 | 
							
								  border-color: #bbb;
							 | 
						|||
| 
								 | 
							
								  color: #333;
							 | 
						|||
| 
								 | 
							
								  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								.zoom-btn.reset {
							 | 
						|||
| 
								 | 
							
								  font-size: 14px;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								.zoom-btn:active {
							 | 
						|||
| 
								 | 
							
								  transform: translateY(1px);
							 | 
						|||
| 
								 | 
							
								  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								.yaxis-scroll-bar-overlay {
							 | 
						|||
| 
								 | 
							
								  position: absolute;
							 | 
						|||
| 
								 | 
							
								  left: 60px;
							 | 
						|||
| 
								 | 
							
								  bottom: 70px;
							 | 
						|||
| 
								 | 
							
								  width: 120px;
							 | 
						|||
| 
								 | 
							
								  height: 24px;
							 | 
						|||
| 
								 | 
							
								  display: flex;
							 | 
						|||
| 
								 | 
							
								  align-items: flex-start;
							 | 
						|||
| 
								 | 
							
								  justify-content: center;
							 | 
						|||
| 
								 | 
							
								  transform: translateY(50%);
							 | 
						|||
| 
								 | 
							
								  background: none;
							 | 
						|||
| 
								 | 
							
								  border-radius: 12px 12px 0 0;
							 | 
						|||
| 
								 | 
							
								  box-shadow: none;
							 | 
						|||
| 
								 | 
							
								  z-index: 30;
							 | 
						|||
| 
								 | 
							
								  padding: 0 8px;
							 | 
						|||
| 
								 | 
							
								  pointer-events: auto;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								.yaxis-scroll-bar-overlay input[type="range"] {
							 | 
						|||
| 
								 | 
							
								  width: 100%;
							 | 
						|||
| 
								 | 
							
								  height: 8px;
							 | 
						|||
| 
								 | 
							
								  accent-color: #bbb;
							 | 
						|||
| 
								 | 
							
								  background: rgba(180, 180, 180, 0.1);
							 | 
						|||
| 
								 | 
							
								  border-radius: 8px;
							 | 
						|||
| 
								 | 
							
								  opacity: 0.4;
							 | 
						|||
| 
								 | 
							
								  transition: box-shadow 0.2s;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								.yaxis-scroll-bar-overlay input[type="range"]::-webkit-slider-thumb {
							 | 
						|||
| 
								 | 
							
								  background: #eee;
							 | 
						|||
| 
								 | 
							
								  border: 1.5px solid #ccc;
							 | 
						|||
| 
								 | 
							
								  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
							 | 
						|||
| 
								 | 
							
								  opacity: 0.5;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								.yaxis-scroll-bar-overlay input[type="range"]:hover {
							 | 
						|||
| 
								 | 
							
								  box-shadow: 0 0 0 2px #bbb2;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								</style>
							 |