652 lines
15 KiB
Vue
652 lines
15 KiB
Vue
![]() |
<template>
|
|||
|
<div class="graph-container">
|
|||
|
<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>
|
|||
|
</template>
|
|||
|
<script setup lang="ts">
|
|||
|
import { ref, onMounted, watch, computed, onUnmounted } from "vue";
|
|||
|
import * as echarts from "echarts";
|
|||
|
const props = defineProps<{ batchIndex: number }>();
|
|||
|
|
|||
|
// --- 기존 culture-graph.vue에서 필요한 상수/데이터 복사 ---
|
|||
|
const pastelColors = [
|
|||
|
"#A3D8F4",
|
|||
|
"#F7B7A3",
|
|||
|
"#B5EAD7",
|
|||
|
"#FFDAC1",
|
|||
|
"#C7CEEA",
|
|||
|
"#FFF1BA",
|
|||
|
"#FFB7B2",
|
|||
|
"#B4A7D6",
|
|||
|
"#AED9E0",
|
|||
|
"#FFC3A0",
|
|||
|
"#E2F0CB",
|
|||
|
"#FFB347",
|
|||
|
"#C1C8E4",
|
|||
|
"#FFFACD",
|
|||
|
"#FFD1DC",
|
|||
|
];
|
|||
|
const yAxisList = [
|
|||
|
{
|
|||
|
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",
|
|||
|
},
|
|||
|
];
|
|||
|
|
|||
|
const intervalValue = ref(60); // 1분
|
|||
|
const totalSeconds = 100 * 60 * 60;
|
|||
|
const pointsPerLine = computed(
|
|||
|
() => Math.floor(totalSeconds / intervalValue.value) + 1
|
|||
|
);
|
|||
|
const xLabels = computed(() =>
|
|||
|
Array.from({ length: pointsPerLine.value }, (_, i) => i * intervalValue.value)
|
|||
|
);
|
|||
|
|
|||
|
// --- batchIndex에 따라 곡선 패턴이 다르게 나오도록 smoothData 수정 ---
|
|||
|
function smoothData(
|
|||
|
min: number,
|
|||
|
max: number,
|
|||
|
xLabels: number[],
|
|||
|
phase = 0,
|
|||
|
_amp = 1,
|
|||
|
offset = 0,
|
|||
|
seriesIndex: number
|
|||
|
) {
|
|||
|
let _prevValue = 0.5;
|
|||
|
const values = [];
|
|||
|
// batchIndex를 곡선 패턴에 반영 (phase, ampVar, randomFactor 등)
|
|||
|
const batchPhase = phase + (props.batchIndex * Math.PI) / 5;
|
|||
|
const ampVar = 0.2 + ((seriesIndex + props.batchIndex) % 5) * 0.1;
|
|||
|
const randomFactor = 0.01 + 0.01 * ((seriesIndex + props.batchIndex) % 4);
|
|||
|
const trendType = (seriesIndex + props.batchIndex) % 4;
|
|||
|
const rangeMin = min + (max - min) * 0.1;
|
|||
|
const rangeMax = max - (max - min) * 0.1;
|
|||
|
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(batchPhase + t * Math.PI * ampVar * 8);
|
|||
|
} else if (trendType === 1) {
|
|||
|
base =
|
|||
|
0.8 - 0.6 * t + 0.13 * Math.cos(batchPhase + t * Math.PI * ampVar * 8);
|
|||
|
} else if (trendType === 2) {
|
|||
|
base = 0.5 + 0.22 * Math.sin(batchPhase + t * Math.PI * ampVar * 12);
|
|||
|
} else {
|
|||
|
base =
|
|||
|
0.5 + 0.08 * Math.sin(batchPhase + t * Math.PI * ampVar * 6) + 0.2 * t;
|
|||
|
}
|
|||
|
// 노이즈도 batchIndex에 따라 다르게
|
|||
|
const noise =
|
|||
|
(Math.sin(i + props.batchIndex * 13) * 0.5 + 0.5) *
|
|||
|
randomFactor *
|
|||
|
(Math.random() - 0.5);
|
|||
|
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]);
|
|||
|
}
|
|||
|
return values;
|
|||
|
}
|
|||
|
|
|||
|
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 visibleYAxisList = computed(() =>
|
|||
|
filteredYAxisList.value.slice(
|
|||
|
yAxisScrollIndex.value,
|
|||
|
yAxisScrollIndex.value + 2
|
|||
|
)
|
|||
|
);
|
|||
|
|
|||
|
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),
|
|||
|
},
|
|||
|
]);
|
|||
|
|
|||
|
const batchMarkLines = [
|
|||
|
// 배치1
|
|||
|
[
|
|||
|
{ xAxis: 6 * 3600, name: "1F" },
|
|||
|
{ xAxis: 10 * 3600, name: "2F" },
|
|||
|
{ xAxis: 12 * 3600, name: "Seed" },
|
|||
|
{ xAxis: 22 * 3600, name: "Main" },
|
|||
|
],
|
|||
|
// 배치2
|
|||
|
[
|
|||
|
{ xAxis: 12 * 3600, name: "1F" },
|
|||
|
{ xAxis: 18 * 3600, name: "2F" },
|
|||
|
{ xAxis: 16 * 3600, name: "Seed" },
|
|||
|
{ xAxis: 36 * 3600, name: "Main" },
|
|||
|
],
|
|||
|
// 배치3
|
|||
|
[
|
|||
|
{ xAxis: 15 * 3600, name: "1F" },
|
|||
|
{ xAxis: 30 * 3600, name: "2F" },
|
|||
|
{ xAxis: 45 * 3600, name: "Seed" },
|
|||
|
{ xAxis: 60 * 3600, name: "Main" },
|
|||
|
],
|
|||
|
// 배치4
|
|||
|
[
|
|||
|
{ xAxis: 8 * 3600, name: "1F" },
|
|||
|
{ xAxis: 16 * 3600, name: "2F" },
|
|||
|
{ xAxis: 24 * 3600, name: "Seed" },
|
|||
|
{ xAxis: 50 * 3600, name: "Main" },
|
|||
|
],
|
|||
|
];
|
|||
|
|
|||
|
// --- 차트 렌더링 로직 (간단화) ---
|
|||
|
const growthChartRef = ref<HTMLDivElement | null>(null);
|
|||
|
let chartInstance: echarts.ECharts | null = null;
|
|||
|
const renderChart = () => {
|
|||
|
if (!growthChartRef.value) return;
|
|||
|
if (!chartInstance) {
|
|||
|
chartInstance = echarts.init(growthChartRef.value);
|
|||
|
}
|
|||
|
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 markLine = {
|
|||
|
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: batchMarkLines[props.batchIndex] || [],
|
|||
|
};
|
|||
|
const option = {
|
|||
|
grid: { left: 120, right: 60, top: 40, bottom: 120, containLabel: true },
|
|||
|
xAxis: [
|
|||
|
{
|
|||
|
type: "value",
|
|||
|
splitNumber: 20,
|
|||
|
position: "bottom",
|
|||
|
axisLabel: {
|
|||
|
fontSize: 12,
|
|||
|
color: "#666",
|
|||
|
fontWeight: "normal",
|
|||
|
interval: 0,
|
|||
|
hideOverlap: true,
|
|||
|
formatter: function (value: number) {
|
|||
|
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: filteredSeriesList.value.map((s, idx) => ({
|
|||
|
...s,
|
|||
|
yAxisIndex: filteredYAxisList.value.findIndex(y => y.name === s.name),
|
|||
|
type: "line",
|
|||
|
symbol: "none",
|
|||
|
sampling: "lttb",
|
|||
|
lineStyle: { width: 0.5 },
|
|||
|
...(idx === 0 ? { markLine } : {}),
|
|||
|
})),
|
|||
|
legend: { show: false },
|
|||
|
animation: false,
|
|||
|
};
|
|||
|
chartInstance.setOption(option, true);
|
|||
|
};
|
|||
|
|
|||
|
onMounted(() => {
|
|||
|
renderChart();
|
|||
|
});
|
|||
|
|
|||
|
watch([checkedList, yAxisScrollIndex, () => props.batchIndex], () => {
|
|||
|
renderChart();
|
|||
|
});
|
|||
|
|
|||
|
onUnmounted(() => {
|
|||
|
if (chartInstance) {
|
|||
|
chartInstance.dispose();
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
// 줌 컨트롤 함수
|
|||
|
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);
|
|||
|
};
|
|||
|
</script>
|
|||
|
<style scoped>
|
|||
|
.graph-container {
|
|||
|
width: 100%;
|
|||
|
height: 1000px;
|
|||
|
}
|
|||
|
.main-graph-area {
|
|||
|
flex: 1;
|
|||
|
display: flex;
|
|||
|
flex-direction: column;
|
|||
|
overflow: hidden;
|
|||
|
}
|
|||
|
.echarts-container {
|
|||
|
flex: 1;
|
|||
|
position: relative;
|
|||
|
background: #fff;
|
|||
|
width: 100%;
|
|||
|
height: 100%;
|
|||
|
min-height: 1000px;
|
|||
|
}
|
|||
|
.echarts-graph {
|
|||
|
width: 100%;
|
|||
|
height: 100%;
|
|||
|
min-height: 1000px;
|
|||
|
}
|
|||
|
.zoom-controls {
|
|||
|
position: absolute;
|
|||
|
top: 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>
|