trafficmonitor init...2
This commit is contained in:
parent
7e528deb14
commit
c4a0c30d8f
@ -225,7 +225,7 @@ class TrafficController extends AdminController
|
|||||||
/**
|
/**
|
||||||
* AJAX 요청을 처리하고, 모델에서 집계된 데이터를 JSON 형식으로 반환합니다.
|
* AJAX 요청을 처리하고, 모델에서 집계된 데이터를 JSON 형식으로 반환합니다.
|
||||||
*/
|
*/
|
||||||
public function getAggregatedData($uid): ResponseInterface
|
public function getAggregatedData($uid): string|ResponseInterface
|
||||||
{
|
{
|
||||||
$entity = $this->service->getEntity($uid);
|
$entity = $this->service->getEntity($uid);
|
||||||
if (!$entity instanceof TrafficEntity) {
|
if (!$entity instanceof TrafficEntity) {
|
||||||
@ -247,11 +247,18 @@ class TrafficController extends AdminController
|
|||||||
$start_timestamp = $startDate . ' 00:00:00';
|
$start_timestamp = $startDate . ' 00:00:00';
|
||||||
$end_timestamp = $endDate . ' 23:59:59';
|
$end_timestamp = $endDate . ' 23:59:59';
|
||||||
$aggregateDatas = $collectorService->getAggregateDatas($entity, $start_timestamp, $end_timestamp);
|
$aggregateDatas = $collectorService->getAggregateDatas($entity, $start_timestamp, $end_timestamp);
|
||||||
|
$aggregateCalculates = $collectorService->get95PercentileBoth($entity, $start_timestamp, $end_timestamp);
|
||||||
|
|
||||||
|
// 디버깅 로그 추가
|
||||||
|
// log_message('debug', 'aggregateDatas: ' . print_r($aggregateDatas, true));
|
||||||
|
log_message('debug', 'aggregateCalculates: ' . print_r($aggregateCalculates, true));
|
||||||
|
|
||||||
// 데이터를 JSON 형식으로 반환
|
// 데이터를 JSON 형식으로 반환
|
||||||
return $this->response->setJSON([
|
return $this->response->setJSON([
|
||||||
'message' => sprintf("[%s~%s] %s 트랙픽차트 ", $startDate, $endDate, $entity->getCustomTitle()),
|
'message' => sprintf("[%s~%s] %s 트랙픽차트 ", $startDate, $endDate, $entity->getCustomTitle()),
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'data' => $aggregateDatas // 이 데이터는 이제 사용자가 제공한 필드 구조를 가집니다.
|
'data' => $aggregateDatas, // 이 데이터는 이제 사용자가 제공한 필드 구조를 가집니다.
|
||||||
|
'calculates' => $aggregateCalculates
|
||||||
]);
|
]);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
// 예외 처리
|
// 예외 처리
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Entities\CollectorEntity;
|
use App\Entities\CollectorEntity;
|
||||||
|
use App\Entities\TrafficEntity;
|
||||||
|
|
||||||
class CollectorModel extends CommonModel
|
class CollectorModel extends CommonModel
|
||||||
{
|
{
|
||||||
@ -34,4 +35,43 @@ class CollectorModel extends CommonModel
|
|||||||
->orderBy('created_at', 'DESC')
|
->orderBy('created_at', 'DESC')
|
||||||
->first(); // 첫 번째 레코드(=가장 최신)
|
->first(); // 첫 번째 레코드(=가장 최신)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DESC 기준 95% + 1번째 값을 가져오는 함수
|
||||||
|
*
|
||||||
|
* @param string $field in_kbits 또는 out_kbits
|
||||||
|
* @param string $startDate '2024-10-01'
|
||||||
|
* @param string $endDate '2024-10-30'
|
||||||
|
* @return mixed 해당 자리의 수치값
|
||||||
|
*/
|
||||||
|
public function get95PercentileValue(TrafficEntity $trafficEntity, string $field, string $startDate, string $endDate)
|
||||||
|
{
|
||||||
|
// 1) 해당 조건의 전체 개수 구하기
|
||||||
|
$total =
|
||||||
|
$this->where("trafficinfo_uid", $trafficEntity->getPK())
|
||||||
|
->where("created_at >=", $startDate)
|
||||||
|
->where("created_at <=", $endDate)
|
||||||
|
->countAllResults(true); // true해야 queryu reset 함
|
||||||
|
// false = 쿼리 빌더 초기화 X
|
||||||
|
if ($total === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// 2) index = floor(total * 0.95) + 1
|
||||||
|
$index = floor($total * 0.95) + 1;
|
||||||
|
// 3) 95% + 1 번째 값 조회
|
||||||
|
$builder = $this->builder();
|
||||||
|
$builder->select($field)
|
||||||
|
->where("trafficinfo_uid", $trafficEntity->getPK())
|
||||||
|
->where("created_at >=", $startDate)
|
||||||
|
->where("created_at <=", $endDate)
|
||||||
|
->orderBy($field, "DESC")
|
||||||
|
->limit(1, $index - 1); // LIMIT offset, 1
|
||||||
|
|
||||||
|
// 🔥 실행되기 전에 SQL 확인
|
||||||
|
$sql = $builder->getCompiledSelect(false); //반드시 false해야 queryu reset 않함
|
||||||
|
log_message('debug', '[95PERCENTILE QUERY] ' . $sql);
|
||||||
|
|
||||||
|
$row = $builder->get()->getRowArray();
|
||||||
|
return $row[$field] ?? null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -103,8 +103,16 @@ class CollectorService extends CommonService
|
|||||||
}
|
}
|
||||||
return $entities;
|
return $entities;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* in_kbits과 out_kbits 두 값을 한꺼번에 구하는 함수
|
||||||
|
*/
|
||||||
|
public function get95PercentileBoth(TrafficEntity $trafficEntity, string $startDate, string $endDate): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'in_kbits' => $this->model->get95PercentileValue($trafficEntity, 'in', $startDate, $endDate),
|
||||||
|
'out_kbits' => $this->model->get95PercentileValue($trafficEntity, 'out', $startDate, $endDate)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function getCalculatedData(TrafficEntity $trafficEntity): array
|
public function getCalculatedData(TrafficEntity $trafficEntity): array
|
||||||
{
|
{
|
||||||
|
|||||||
@ -10,53 +10,39 @@
|
|||||||
<!-- Chart.js CDN 로드 -->
|
<!-- Chart.js CDN 로드 -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||||
<style>
|
<style>
|
||||||
/* 커스텀 스타일 및 색상 변수 설정 */
|
|
||||||
:root {
|
:root {
|
||||||
--bs-primary: #0d6efd;
|
--bs-primary: #0d6efd;
|
||||||
/* 기본 Bootstrap Primary */
|
|
||||||
--bs-success: #198754;
|
--bs-success: #198754;
|
||||||
/* IN 트래픽 색상 (Chart.js) */
|
|
||||||
--bs-danger: #dc3545;
|
--bs-danger: #dc3545;
|
||||||
/* OUT 트래픽 색상 (Chart.js) */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-container {
|
.chart-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 400px;
|
height: 400px;
|
||||||
/* 차트 높이 고정 */
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="app" class="container-fluid">
|
<div id="app" class="container-fluid">
|
||||||
<!-- Chart Card -->
|
|
||||||
<div class="card shadow-lg dashboard-card">
|
<div class="card shadow-lg dashboard-card">
|
||||||
<div class="card-body p-4 p-md-5">
|
<div class="card-body p-4 p-md-5">
|
||||||
<!-- 동적으로 업데이트될 메인 제목 영역 -->
|
<h2 id="chartTitle" class="h5 card-title text-dark mb-1"><?= $viewDatas['entity']->getCustomTitle() ?> 트래픽 속도 추이 (IN/OUT Kbit/s)</h2>
|
||||||
<h2 id="chartTitle" class="h5 card-title text-dark mb-1"><?= $viewDatas['entity']->getCustomTitle() ?>트래픽 속도 추이 (IN/OUT Kbit/s)</h2>
|
<div class="row g-3 mb-3">
|
||||||
<!-- 동적으로 업데이트될 날짜 정보 영역 (시작일, 종료일) -->
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-5"><input type="date" id="startDate" class="form-control" value="<?= date('Y-m-d') ?>"></div>
|
<div class="col-5"><input type="date" id="startDate" class="form-control" value="<?= date('Y-m-d') ?>"></div>
|
||||||
<div class="col-5"><input type="date" id="endDate" class="form-control" value="<?= date('Y-m-d') ?>"></div>
|
<div class="col-5"><input type="date" id="endDate" class="form-control" value="<?= date('Y-m-d') ?>"></div>
|
||||||
<div class="col-2"><button onclick="fetchDataAndDrawChart()" id="viewChartBtn" class="btn btn-primary">차트 보기</button></div>
|
<div class="col-2"><button onclick="fetchDataAndDrawChart()" id="viewChartBtn" class="btn btn-primary">차트 보기</button></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 로딩 인디케이터 -->
|
<div id="loadingIndicator" class="text-center p-5" style="display: none;">
|
||||||
<div id="loadingIndicator" class="text-center p-5 hidden" style="display: none;">
|
|
||||||
<div class="spinner-border text-primary" role="status">
|
<div class="spinner-border text-primary" role="status">
|
||||||
<span class="visually-hidden">Loading...</span>
|
<span class="visually-hidden">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-3 text-primary fw-bold">데이터를 불러오는 중입니다... 잠시만 기다려 주세요.</p>
|
<p class="mt-3 text-primary fw-bold">데이터를 불러오는 중입니다... 잠시만 기다려 주세요.</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- 오류 메시지 영역 -->
|
|
||||||
<div id="errorMessage" class="alert alert-danger text-center hidden" role="alert" style="display: none;">
|
<div id="errorMessage" class="alert alert-danger text-center" role="alert" style="display: none;"></div>
|
||||||
데이터를 불러오는데 실패했습니다. (서버 오류)
|
<div id="noDataMessage" class="alert alert-warning text-center" role="alert" style="display: none;">선택하신 기간에는 조회된 트래픽 데이터가 없습니다. 기간을 다시 설정해주세요.</div>
|
||||||
</div>
|
|
||||||
<!-- 데이터 없음 메시지 영역 -->
|
|
||||||
<div id="noDataMessage" class="alert alert-warning text-center hidden" role="alert" style="display: none;">
|
|
||||||
선택하신 기간에는 조회된 트래픽 데이터가 없습니다. 기간을 다시 설정해주세요.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<canvas id="trafficChart"></canvas>
|
<canvas id="trafficChart"></canvas>
|
||||||
@ -64,87 +50,52 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Bootstrap 5 JS Bundle 로드 -->
|
|
||||||
|
<!-- Bootstrap JS Bundle -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// 전역 변수 설정
|
|
||||||
let trafficChart = null;
|
let trafficChart = null;
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
// DOM 로드 후 바로 차트를 불러옵니다.
|
|
||||||
fetchDataAndDrawChart();
|
fetchDataAndDrawChart();
|
||||||
});
|
});
|
||||||
|
|
||||||
// -----------------------------------------------------------
|
|
||||||
// 1. CI4 백엔드 엔드포인트를 호출하여 데이터 가져오기 (AJAX)
|
|
||||||
// -----------------------------------------------------------
|
|
||||||
|
|
||||||
async function fetchAggregatedData(startDate, endDate) {
|
async function fetchAggregatedData(startDate, endDate) {
|
||||||
const baseEndpoint = '<?= base_url("/admin/traffic/data/{$viewDatas['entity']->getPK()}") ?>';
|
const baseEndpoint = '<?= base_url("/admin/traffic/data/{$viewDatas['entity']->getPK()}") ?>';
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
startDate: startDate,
|
startDate,
|
||||||
endDate: endDate
|
endDate
|
||||||
});
|
});
|
||||||
|
|
||||||
const endpoint = `${baseEndpoint}?${params.toString()}`;
|
const endpoint = `${baseEndpoint}?${params.toString()}`;
|
||||||
|
|
||||||
// ** 디버깅: 요청 정보 로깅 **
|
|
||||||
// console.log("--- API 요청 시작 (Debugging) ---");
|
|
||||||
// console.log(`요청 기간: ${startDate} ~ ${endDate}`);
|
|
||||||
// console.log(`요청 URL: ${endpoint}`);
|
|
||||||
|
|
||||||
const maxRetries = 3;
|
const maxRetries = 3;
|
||||||
let delay = 1000;
|
let delay = 1000;
|
||||||
|
|
||||||
for (let i = 0; i < maxRetries; i++) {
|
for (let i = 0; i < maxRetries; i++) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(endpoint, {
|
const response = await fetch(endpoint, {
|
||||||
method: 'GET',
|
method: 'GET'
|
||||||
});
|
});
|
||||||
|
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
// ** 디버깅: 서버 응답 로깅 **
|
if (result.status === 'error') throw new Error(result.message || "서버 오류");
|
||||||
// console.log("서버 응답 JSON (Raw Result):", result);
|
|
||||||
// console.log("--- API 요청 종료 (Debugging) ---");
|
|
||||||
|
|
||||||
|
return result;
|
||||||
if (result.status === 'error') {
|
|
||||||
throw new Error(result.message || "서버에서 오류 상태를 반환했습니다.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 데이터가 비어있는 경우
|
|
||||||
if (!result.data || result.data.length === 0) {
|
|
||||||
return []; // 빈 배열 반환하여 데이터 없음 처리
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.data;
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Attempt ${i + 1} failed:`, error);
|
console.error(`Attempt ${i + 1} failed:`, error);
|
||||||
if (i === maxRetries - 1) {
|
if (i === maxRetries - 1) throw new Error("서버에서 데이터를 가져오는데 실패했습니다.");
|
||||||
throw new Error("서버에서 데이터를 가져오는데 실패했습니다. 콘솔을 확인하세요.");
|
|
||||||
}
|
|
||||||
await new Promise(resolve => setTimeout(resolve, delay));
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
delay *= 2;
|
delay *= 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// -----------------------------------------------------------
|
|
||||||
// 2. 차트 그리기 및 업데이트 로직
|
|
||||||
// -----------------------------------------------------------
|
|
||||||
|
|
||||||
async function fetchDataAndDrawChart() {
|
async function fetchDataAndDrawChart() {
|
||||||
const startDate = document.getElementById('startDate').value;
|
const startDate = document.getElementById('startDate').value;
|
||||||
const endDate = document.getElementById('endDate').value;
|
const endDate = document.getElementById('endDate').value;
|
||||||
|
|
||||||
const chartTitleElement = document.getElementById('chartTitle');
|
const chartTitleElement = document.getElementById('chartTitle');
|
||||||
const loadingIndicator = document.getElementById('loadingIndicator');
|
const loadingIndicator = document.getElementById('loadingIndicator');
|
||||||
const errorMessage = document.getElementById('errorMessage');
|
const errorMessage = document.getElementById('errorMessage');
|
||||||
@ -152,32 +103,36 @@
|
|||||||
const canvas = document.getElementById('trafficChart');
|
const canvas = document.getElementById('trafficChart');
|
||||||
const viewChartBtn = document.getElementById('viewChartBtn');
|
const viewChartBtn = document.getElementById('viewChartBtn');
|
||||||
|
|
||||||
// UI 초기화 및 로딩 표시
|
|
||||||
loadingIndicator.style.display = 'block';
|
loadingIndicator.style.display = 'block';
|
||||||
errorMessage.style.display = 'none';
|
errorMessage.style.display = 'none';
|
||||||
noDataMessage.style.display = 'none';
|
noDataMessage.style.display = 'none';
|
||||||
canvas.style.opacity = '0';
|
canvas.style.opacity = '0';
|
||||||
viewChartBtn.disabled = true;
|
viewChartBtn.disabled = true;
|
||||||
|
|
||||||
// 제목 및 기간 정보 업데이트
|
|
||||||
chartTitleElement.textContent = `트래픽 속도 추이 (IN/OUT Kbit/s)`;
|
|
||||||
try {
|
try {
|
||||||
const aggregatedData = await fetchAggregatedData(startDate, endDate);
|
const result = await fetchAggregatedData(startDate, endDate);
|
||||||
|
|
||||||
if (aggregatedData.length === 0) {
|
// calculates 안전 처리
|
||||||
// 데이터가 비어 있을 경우
|
const calculates = result.calculates || {};
|
||||||
|
const in95 = calculates.in_kbits ?? '-';
|
||||||
|
const out95 = calculates.out_kbits ?? '-';
|
||||||
|
|
||||||
|
// 차트 제목 업데이트
|
||||||
|
chartTitleElement.textContent = `트래픽 속도 추이 (IN: ${in95} Kbit/s, OUT: ${out95} Kbit/s)`;
|
||||||
|
|
||||||
|
const data = result.data || [];
|
||||||
|
if (data.length === 0) {
|
||||||
noDataMessage.style.display = 'block';
|
noDataMessage.style.display = 'block';
|
||||||
if (trafficChart) {
|
if (trafficChart) {
|
||||||
trafficChart.destroy(); // 기존 차트 파괴
|
trafficChart.destroy();
|
||||||
trafficChart = null;
|
trafficChart = null;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 데이터 추출 및 Number 변환 (데이터가 유효함을 가정)
|
const labels = data.map(d => d.created_at);
|
||||||
const labels = aggregatedData.map(d => d.created_at);
|
const inData = data.map(d => Number(d.in_kbits));
|
||||||
const inData = aggregatedData.map(d => Number(d.in_kbits));
|
const outData = data.map(d => Number(d.out_kbits));
|
||||||
const outData = aggregatedData.map(d => Number(d.out_kbits));
|
|
||||||
|
|
||||||
drawChart(labels, inData, outData);
|
drawChart(labels, inData, outData);
|
||||||
|
|
||||||
@ -185,101 +140,90 @@
|
|||||||
console.error("Chart data processing error:", error);
|
console.error("Chart data processing error:", error);
|
||||||
errorMessage.textContent = `데이터 로딩 실패: ${error.message}`;
|
errorMessage.textContent = `데이터 로딩 실패: ${error.message}`;
|
||||||
errorMessage.style.display = 'block';
|
errorMessage.style.display = 'block';
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
loadingIndicator.style.display = 'none';
|
loadingIndicator.style.display = 'none';
|
||||||
canvas.style.opacity = (trafficChart || noDataMessage.style.display === 'block') ? '1' : '0';
|
canvas.style.opacity = '1';
|
||||||
viewChartBtn.disabled = false;
|
viewChartBtn.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawChart(labels, inData, outData) {
|
function drawChart(labels, inData, outData) {
|
||||||
const ctx = document.getElementById('trafficChart').getContext('2d');
|
const ctx = document.getElementById('trafficChart').getContext('2d');
|
||||||
|
if (trafficChart) trafficChart.destroy();
|
||||||
|
|
||||||
// 기존 차트가 있으면 파괴
|
trafficChart = new Chart(ctx, {
|
||||||
if (trafficChart) {
|
type: 'line',
|
||||||
trafficChart.destroy();
|
data: {
|
||||||
}
|
labels,
|
||||||
|
datasets: [{
|
||||||
try {
|
label: 'IN Kbit/s',
|
||||||
trafficChart = new Chart(ctx, {
|
data: inData,
|
||||||
type: 'line',
|
borderColor: '#198754',
|
||||||
data: {
|
backgroundColor: 'rgba(25, 135, 84, 0.1)',
|
||||||
labels: labels,
|
fill: true,
|
||||||
datasets: [{
|
tension: 0.3,
|
||||||
label: 'IN Kbit/s',
|
pointRadius: 3,
|
||||||
data: inData,
|
pointHoverRadius: 6
|
||||||
borderColor: '#198754', // Bootstrap Success Color (Green)
|
|
||||||
backgroundColor: 'rgba(25, 135, 84, 0.1)',
|
|
||||||
fill: true,
|
|
||||||
tension: 0.3,
|
|
||||||
pointRadius: 3,
|
|
||||||
pointHoverRadius: 6,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'OUT Kbit/s',
|
|
||||||
data: outData,
|
|
||||||
borderColor: '#dc3545', // Bootstrap Danger Color (Red)
|
|
||||||
backgroundColor: 'rgba(220, 53, 69, 0.1)',
|
|
||||||
fill: true,
|
|
||||||
tension: 0.3,
|
|
||||||
pointRadius: 3,
|
|
||||||
pointHoverRadius: 6,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
scales: {
|
|
||||||
y: {
|
|
||||||
beginAtZero: true,
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Kbit/s', // Y축 레이블에 Kbit/s 명시
|
|
||||||
font: {
|
|
||||||
weight: 'bold'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
x: {
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: '기간',
|
|
||||||
font: {
|
|
||||||
weight: 'bold'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ticks: {
|
|
||||||
autoSkip: true,
|
|
||||||
maxTicksLimit: 20
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
plugins: {
|
{
|
||||||
legend: {
|
label: 'OUT Kbit/s',
|
||||||
position: 'top',
|
data: outData,
|
||||||
labels: {
|
borderColor: '#dc3545',
|
||||||
usePointStyle: true,
|
backgroundColor: 'rgba(220, 53, 69, 0.1)',
|
||||||
padding: 20
|
fill: true,
|
||||||
}
|
tension: 0.3,
|
||||||
},
|
pointRadius: 3,
|
||||||
tooltip: {
|
pointHoverRadius: 6
|
||||||
mode: 'index',
|
|
||||||
intersect: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
hover: {
|
|
||||||
mode: 'nearest',
|
|
||||||
intersect: true
|
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Kbit/s',
|
||||||
|
font: {
|
||||||
|
weight: 'bold'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: '기간',
|
||||||
|
font: {
|
||||||
|
weight: 'bold'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
autoSkip: true,
|
||||||
|
maxTicksLimit: 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: 'top',
|
||||||
|
labels: {
|
||||||
|
usePointStyle: true,
|
||||||
|
padding: 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
mode: 'nearest',
|
||||||
|
intersect: true
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
} catch (e) {
|
});
|
||||||
console.error("Chart.js Initialization Failed:", e);
|
|
||||||
document.getElementById('errorMessage').textContent = `차트 생성 실패: ${e.message}. 브라우저의 콘솔을 확인하세요.`;
|
|
||||||
document.getElementById('errorMessage').style.display = 'block';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user