Files
ewoooc/templates/monthly_summary_analysis.html
OoO 30a173cf69
All checks were successful
CD Pipeline / deploy (push) Successful in 58s
統一全站暖色視覺與市場情報骨架
2026-05-06 20:24:46 +08:00

1526 lines
70 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends 'ewoooc_base.html' %}
{% block title %}月份總表數據分析 - WOOO TECH{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="https://cdn.datatables.net/1.11.5/css/dataTables.bootstrap5.min.css">
<style>
.monthly-analysis-page {
display: flex;
flex-direction: column;
gap: 18px;
}
.monthly-analysis-hero {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 16px;
align-items: center;
padding: 22px;
border: 1px solid var(--momo-border-strong);
border-radius: 8px;
background:
radial-gradient(circle at 18px 18px, rgba(42, 37, 32, 0.12) 1px, transparent 1px),
linear-gradient(135deg, rgba(242, 178, 90, 0.22), rgba(255, 255, 255, 0.94) 46%, rgba(42, 37, 32, 0.06));
background-size: 18px 18px, auto;
box-shadow: var(--momo-shadow-soft);
}
.monthly-analysis-title {
display: flex;
align-items: center;
gap: 10px;
margin: 0;
color: var(--momo-text-strong);
font-family: var(--momo-font-display);
font-size: clamp(1.45rem, 2vw, 2.08rem);
font-weight: 800;
letter-spacing: 0;
}
.monthly-analysis-title i {
color: var(--momo-warm-caramel) !important;
}
.monthly-version-pill {
display: inline-flex;
align-items: center;
gap: 7px;
border: 1px solid rgba(42, 37, 32, 0.14);
border-radius: 999px;
background: rgba(255, 255, 255, 0.7);
color: var(--momo-text-muted);
font-family: var(--momo-font-mono);
font-size: 0.78rem;
font-weight: 800;
padding: 6px 10px;
}
.card {
border: 1px solid var(--momo-border-subtle) !important;
border-radius: 8px;
box-shadow: var(--momo-shadow-soft);
margin-bottom: 1.5rem;
background: rgba(255, 255, 255, 0.84);
}
.card-header {
background: rgba(250, 247, 240, 0.88) !important;
border-bottom: 1px solid var(--momo-border-subtle);
font-weight: 700;
padding: 1.25rem;
}
.filter-section {
background:
radial-gradient(circle at 14px 14px, rgba(42, 37, 32, 0.1) 1px, transparent 1px),
linear-gradient(135deg, rgba(42, 37, 32, 0.94), rgba(68, 54, 40, 0.9));
background-size: 16px 16px, auto;
border-radius: 8px;
padding: 20px;
color: white;
margin-bottom: 2rem;
box-shadow: var(--momo-shadow-medium);
}
.form-label {
font-weight: 600;
font-size: 0.85rem;
color: rgba(255, 255, 255, 0.9);
margin-bottom: 0.4rem;
}
/* 模擬業績分析頁面的 Dropdown 樣式 */
.custom-dropdown-btn {
background-color: white !important;
border: 1px solid #dee2e6 !important;
text-align: left;
position: relative;
padding: 0.75rem 1.25rem;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
color: #2c3e50;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.custom-dropdown-btn::after {
position: absolute;
right: 15px;
top: 50%;
transform: translateY(-50%);
}
.stat-card {
border-left: 4px solid var(--momo-warm-caramel) !important;
transition: transform 0.3s;
}
.stat-card:hover {
transform: translateY(-5px);
}
.kpi-value {
font-size: 2.2rem;
font-weight: 800;
color: var(--momo-text-strong);
font-family: var(--momo-font-mono);
}
.small {
font-size: 1rem !important;
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.8);
z-index: 9999;
display: none;
justify-content: center;
align-items: center;
backdrop-filter: blur(2px);
}
.monthly-analysis-page .table thead th,
.monthly-analysis-page .table-light th {
background: rgba(250, 247, 240, 0.96) !important;
color: var(--momo-text-muted);
font-weight: 800;
}
@media (max-width: 768px) {
.monthly-analysis-hero {
grid-template-columns: 1fr;
}
}
</style>
{% endblock %}
{% block ewooo_content %}
<div id="loadingOverlay" class="loading-overlay">
<div class="spinner-border text-primary" role="status"></div>
</div>
<div class="monthly-analysis-page">
{% include 'components/_analysis_report_tabs.html' %}
<section class="monthly-analysis-hero">
<div>
<h1 class="monthly-analysis-title"><i class="fas fa-chart-pie me-2 text-primary"></i>月份總表數據分析</h1>
<p class="text-muted mb-0 mt-2">以資料庫 `monthly_summary_analysis` 的月結資料檢視業績、毛利、YoY 與品牌/區域/價格帶結構。</p>
</div>
<div class="monthly-version-pill">
<i class="fas fa-database"></i>
<span>{{ system_version }}</span>
</div>
</section>
<!-- ═══════ 進階篩選器 (對標業績分析) ═══════ -->
<div class="filter-section">
<div class="row g-3">
<div class="col-md-2">
<label class="form-label"><i class="fas fa-calendar-alt me-1"></i> 年份</label>
<div class="dropdown">
<button class="btn btn-light custom-dropdown-btn dropdown-toggle" type="button" id="btnYear"
data-bs-toggle="dropdown">
選擇年份
</button>
<ul class="dropdown-menu w-100" id="listYear" style="max-height: 300px; overflow-y: auto;">
<li><a class="dropdown-item" href="#" onclick="selectFilter('year', '')">全部年份</a></li>
</ul>
</div>
</div>
<div class="col-md-2">
<label class="form-label"><i class="fas fa-calendar-day me-1"></i> 月份</label>
<div class="dropdown">
<button class="btn btn-light custom-dropdown-btn dropdown-toggle" type="button" id="btnMonth"
data-bs-toggle="dropdown">
選擇月份
</button>
<ul class="dropdown-menu w-100" id="listMonth" style="max-height: 300px; overflow-y: auto;">
<li><a class="dropdown-item" href="#" onclick="selectFilter('month', '')">全部月份</a></li>
</ul>
</div>
</div>
<div class="col-md-2">
<label class="form-label"><i class="fas fa-map-marker-alt me-1"></i> 區名稱</label>
<div class="dropdown">
<button class="btn btn-light custom-dropdown-btn dropdown-toggle" type="button" id="btnArea"
data-bs-toggle="dropdown">
所有區域
</button>
<ul class="dropdown-menu w-100" id="listArea" style="max-height: 300px; overflow-y: auto;">
<li><a class="dropdown-item" href="#" onclick="selectFilter('area', '')">所有區域</a></li>
</ul>
</div>
</div>
<div class="col-md-3">
<label class="form-label"><i class="fas fa-truck me-1"></i> 廠商名稱</label>
<div class="dropdown">
<button class="btn btn-light custom-dropdown-btn dropdown-toggle" type="button" id="btnVendor"
data-bs-toggle="dropdown">
所有及廠商
</button>
<ul class="dropdown-menu w-100" id="listVendor" style="max-height: 300px; overflow-y: auto;">
<li class="px-2 py-1"><input type="text" class="form-control form-control-sm"
placeholder="搜尋..." onkeyup="filterList(this)"></li>
<li><a class="dropdown-item" href="#" onclick="selectFilter('vendor', '')">所有廠商</a></li>
</ul>
</div>
</div>
<div class="col-md-2">
<label class="form-label"><i class="fas fa-exchange-alt me-1"></i> 借採轉</label>
<div class="dropdown">
<button class="btn btn-light custom-dropdown-btn dropdown-toggle" type="button" id="btnTrade"
data-bs-toggle="dropdown">
所有類別
</button>
<ul class="dropdown-menu w-100" id="listTrade">
<li><a class="dropdown-item" href="#" onclick="selectFilter('trade', '')">所有類別</a></li>
</ul>
</div>
</div>
<div class="col-md-1 d-flex align-items-end">
<button class="btn btn-warning w-100 fw-bold" onclick="fetchData()">
<i class="fas fa-sync-alt"></i> 刷新
</button>
</div>
</div>
</div>
<!-- ═══════ 商業洞察 (Top 3 Highlights) ═══════ -->
<div class="row g-3 mb-4" id="highlightsRow" style="display: none;">
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm overflow-hidden">
<div class="card-header bg-primary text-white py-2">
<h6 class="mb-0 small fw-bold"><i class="fas fa-trophy me-1"></i> 業績貢獻王 (Top 3 Brands)</h6>
</div>
<div class="card-body p-0">
<table class="table table-sm table-hover mb-0">
<tbody id="revHighlightsBody"></tbody>
</table>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm overflow-hidden">
<div class="card-header bg-success text-white py-2">
<h6 class="mb-0 small fw-bold"><i class="fas fa-coins me-1"></i> 獲利金雞母 (Top 3 Brands)</h6>
</div>
<div class="card-body p-0">
<table class="table table-sm table-hover mb-0">
<tbody id="profitHighlightsBody"></tbody>
</table>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm overflow-hidden">
<div class="card-header bg-warning text-dark py-2">
<h6 class="mb-0 small fw-bold"><i class="fas fa-fire me-1"></i> 人氣引流款 (Top 3 Brands)</h6>
</div>
<div class="card-body p-0">
<table class="table table-sm table-hover mb-0">
<tbody id="volHighlightsBody"></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- ═══════ 年度對比趨勢 (YoY Timeline) ═══════ -->
<div class="row mb-4">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-bold"><i class="fas fa-history me-2 text-primary"></i>年度 YoY 業績對比分析 (本期 vs
去年同期)</h6>
</div>
<div class="card-body">
<div id="yoyTrendChart" style="height: 400px;"></div>
<div class="table-responsive mt-3">
<table class="table table-bordered table-sm text-center mb-0 small"
style="border-color: #dee2e6;">
<tbody id="yoyTrendChartTableBody"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- ═══════ 廠商排行與類別分佈 ═══════ -->
<div class="row mb-4">
<div class="col-lg-12">
<div class="card border-0 shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-bold"><i class="fas fa-building me-2 text-primary"></i>Top 50 廠商獲利能力排行</h6>
</div>
<div class="card-body">
<div id="vendorRankingChart" style="height: 900px;"></div>
<div class="table-responsive mt-3">
<table class="table table-bordered table-sm text-center mb-0 small"
style="border-color: #dee2e6;">
<tbody id="vendorRankingChartTableBody"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-lg-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-header">
<h6 class="mb-0 fw-bold"><i class="fas fa-chart-pie me-2 text-primary"></i>全站類別業績分佈 (Top 12)
</h6>
</div>
<div class="card-body text-center">
<div id="divisionDistChart" style="height: 350px;"></div>
<div id="divisionDistChartTable" class="mt-2" style="max-height: 200px; overflow-y: auto;">
<table class="table table-bordered table-sm text-center mb-0 small"
style="border-color: #dee2e6;">
<tbody id="divisionDistChartTableBody"></tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-header">
<h6 class="mb-0 fw-bold"><i class="fas fa-tags me-2 text-primary"></i>價格帶業績貢獻比例</h6>
</div>
<div class="card-body">
<div id="priceRangeChart" style="height: 350px;"></div>
<div class="table-responsive mt-2">
<table class="table table-bordered table-sm text-center mb-0 small"
style="border-color: #dee2e6;">
<tbody id="priceRangeChartTableBody"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- 柱狀折線對比圖 (主圖) -->
<div class="row">
<div class="col-lg-12">
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-bold"><i class="fas fa-chart-line me-2 text-primary"></i>年度月份業績對比趨勢</h6>
</div>
<div class="card-body">
<div id="compareChart" style="height: 400px;"></div>
<div id="compareChartDataTable" class="mt-2">
<div class="table-responsive">
<table class="table table-bordered table-sm text-center mb-0 small"
style="border-color: #dee2e6;">
<tbody id="compareChartTableBody"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-lg-6">
<div class="card border-0 shadow-sm">
<div class="card-header">
<h6 class="mb-0 fw-bold"><i class="fas fa-th me-2 text-primary"></i>品牌策略 BCG 矩陣 (銷量 vs 毛利率)</h6>
</div>
<div class="card-body">
<div id="bcgMatrixChart" style="height: 400px;"></div>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card border-0 shadow-sm">
<div class="card-header">
<h6 class="mb-0 fw-bold"><i class="fas fa-braille me-2 text-primary"></i>價格 vs 銷售量分佈散佈圖</h6>
</div>
<div class="card-body">
<div id="priceVolumeScatterChart" style="height: 400px;"></div>
</div>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-lg-12">
<div class="card border-0 shadow-sm">
<div class="card-header">
<h6 class="mb-0 fw-bold"><i class="fas fa-th-large me-2 text-primary"></i>分類與月份淡旺季業績熱力圖</h6>
</div>
<div class="card-body">
<div id="seasonalityHeatmapChart" style="height: 400px;"></div>
</div>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-lg-12">
<div class="card border-0 shadow-sm">
<div class="card-header">
<h6 class="mb-0 fw-bold"><i class="fas fa-map me-2 text-primary"></i>區名稱全站銷售排行 (Area Ranking)
</h6>
</div>
<div class="card-body">
<div id="areaRankingChart" style="height: 400px;"></div>
<div class="table-responsive mt-3">
<table class="table table-bordered table-sm text-center mb-0 small"
style="border-color: #dee2e6;">
<tbody id="areaRankingChartTableBody"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- 專屬對比圖 (特定區域) -->
<div class="row">
<div class="col-lg-12">
<div class="card mb-4 border-info shadow-sm">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-bold text-info"><i class="fas fa-layer-group me-2"></i>開架保養 & 臉部清潔 (合併業績)
</h6>
</div>
<div class="card-body">
<div id="specialChart" style="height: 400px;"></div>
<div id="specialChartDataTable" class="mt-2">
<div class="table-responsive">
<table class="table table-bordered table-sm text-center mb-0 small"
style="border-color: #dee2e6;">
<tbody id="specialChartTableBody"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 身體保養專屬對比圖 -->
<div class="row">
<div class="col-lg-12">
<div class="card mb-4 border-success shadow-sm">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-bold text-success"><i class="fas fa-magic me-2"></i>身體保養 (年度業績分析)</h6>
</div>
<div class="card-body">
<div id="bodyCareChart" style="height: 400px;"></div>
<div id="bodyCareChartDataTable" class="mt-2">
<div class="table-responsive">
<table class="table table-bordered table-sm text-center mb-0 small"
style="border-color: #dee2e6;">
<tbody id="bodyCareChartTableBody"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 彩妝/指彩 & 精油擴香 專屬對比圖 -->
<div class="row">
<div class="col-lg-12">
<div class="card mb-4 border-warning shadow-sm">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-bold text-warning"><i class="fas fa-palette me-2"></i>彩妝/指彩 & 精油擴香 (合併業績)
</h6>
</div>
<div class="card-body">
<div id="makeupFragranceChart" style="height: 400px;"></div>
<div id="makeupFragranceChartDataTable" class="mt-2">
<div class="table-responsive">
<table class="table table-bordered table-sm text-center mb-0 small"
style="border-color: #dee2e6;">
<tbody id="makeupFragranceChartTableBody"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 私密保養 & 嬰幼洗沐 專屬對比圖 -->
<div class="row">
<div class="col-lg-12">
<div class="card mb-4 border-danger shadow-sm">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-bold text-danger"><i class="fas fa-heartbeat me-2"></i>私密保養 & 嬰幼洗沐 (合併業績)
</h6>
</div>
<div class="card-body">
<div id="privacyInfantChart" style="height: 400px;"></div>
<div id="privacyInfantChartDataTable" class="mt-2">
<div class="table-responsive">
<table class="table table-bordered table-sm text-center mb-0 small"
style="border-color: #dee2e6;">
<tbody id="privacyInfantChartTableBody"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 數據列表 -->
<div class="card shadow-sm mb-5">
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0 fw-bold"><i class="fas fa-list me-2"></i>資料明細</h5>
<a href="/system_settings" class="btn btn-sm btn-outline-primary"><i
class="fas fa-plus me-1"></i>匯入數據</a>
</div>
<div class="card-body">
<div class="table-responsive">
<table id="summaryTable" class="table table-hover align-middle" style="width:100%">
<thead class="bg-light text-secondary small">
<tr>
<th>年/月</th>
<th>處別</th>
<th>區名稱</th>
<th>PM</th>
<th>品牌</th>
<th>廠商</th>
<th>交易</th>
<th>銷售額(本月)</th>
<th>YoY</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.datatables.net/1.11.5/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.11.5/js/dataTables.bootstrap5.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<script>
let table;
let compareChart = echarts.init(document.getElementById('compareChart'));
let yoyTrendChart = echarts.init(document.getElementById('yoyTrendChart'));
let vendorRankingChart = echarts.init(document.getElementById('vendorRankingChart'));
let divisionDistChart = echarts.init(document.getElementById('divisionDistChart'));
let priceRangeChart = echarts.init(document.getElementById('priceRangeChart'));
let bcgMatrixChart = echarts.init(document.getElementById('bcgMatrixChart'));
let priceVolumeScatterChart = echarts.init(document.getElementById('priceVolumeScatterChart'));
let seasonalityHeatmapChart = echarts.init(document.getElementById('seasonalityHeatmapChart'));
let areaRankingChart = echarts.init(document.getElementById('areaRankingChart'));
let specialChart = echarts.init(document.getElementById('specialChart'));
let bodyCareChart = echarts.init(document.getElementById('bodyCareChart'));
let makeupFragranceChart = echarts.init(document.getElementById('makeupFragranceChart'));
let privacyInfantChart = echarts.init(document.getElementById('privacyInfantChart'));
let currentFilters = {
year: '', month: '', area: '', vendor: '', trade: ''
};
$(document).ready(function () {
table = $('#summaryTable').DataTable({
language: { url: "//cdn.datatables.net/plug-ins/1.11.5/i18n/zh-HANT.json" },
pageLength: 20,
order: [[0, 'desc'], [7, 'desc']],
columnDefs: [
{ targets: [7], className: 'text-end fw-bold' },
{ targets: 8, className: 'text-center' }
]
});
window.addEventListener('resize', () => {
compareChart.resize();
yoyTrendChart.resize();
vendorRankingChart.resize();
divisionDistChart.resize();
priceRangeChart.resize();
bcgMatrixChart.resize();
priceVolumeScatterChart.resize();
seasonalityHeatmapChart.resize();
areaRankingChart.resize();
specialChart.resize();
bodyCareChart.resize();
makeupFragranceChart.resize();
privacyInfantChart.resize();
});
fetchData();
});
function fetchData() {
$('#loadingOverlay').css('display', 'flex');
let url = `/api/monthly_summary_data?limit=2000`;
if (currentFilters.year) url += `&year=${currentFilters.year}`;
if (currentFilters.month) url += `&month=${currentFilters.month}`;
if (currentFilters.area) url += `&area_name=${encodeURIComponent(currentFilters.area)}`;
if (currentFilters.vendor) url += `&vendor=${encodeURIComponent(currentFilters.vendor)}`;
if (currentFilters.trade) url += `&trade_type=${encodeURIComponent(currentFilters.trade)}`;
fetch(url)
.then(res => res.json())
.then(data => {
if (data.status === 'success') {
console.log('API Data:', data);
updateUI(data);
updateFilters(data.filters);
}
$('#loadingOverlay').hide();
})
.catch(err => {
console.error(err);
$('#loadingOverlay').hide();
});
fetchSpecialChartData();
}
function fetchSpecialChartData() {
// 固定抓取 開架保養 與 臉部清潔
let url1 = `/api/monthly_summary_data?area_name=${encodeURIComponent('開架保養,臉部清潔')}&limit=1`;
fetch(url1)
.then(res => res.json())
.then(data => {
if (data.status === 'success') {
renderExcelChart(specialChart, 'specialChartTableBody', data.trend);
}
});
// 固定抓取 身體保養
let url2 = `/api/monthly_summary_data?area_name=${encodeURIComponent('身體保養')}&limit=1`;
fetch(url2)
.then(res => res.json())
.then(data => {
if (data.status === 'success') {
renderExcelChart(bodyCareChart, 'bodyCareChartTableBody', data.trend);
}
});
// 固定抓取 彩妝/指彩 與 精油擴香
let url3 = `/api/monthly_summary_data?area_name=${encodeURIComponent('彩妝/指彩,精油擴香')}&limit=1`;
fetch(url3)
.then(res => res.json())
.then(data => {
if (data.status === 'success') {
renderExcelChart(makeupFragranceChart, 'makeupFragranceChartTableBody', data.trend);
}
});
// 固定抓取 私密保養 與 嬰幼洗沐
let url4 = `/api/monthly_summary_data?area_name=${encodeURIComponent('私密保養,嬰幼洗沐')}&limit=1`;
fetch(url4)
.then(res => res.json())
.then(data => {
if (data.status === 'success') {
renderExcelChart(privacyInfantChart, 'privacyInfantChartTableBody', data.trend);
}
});
}
function selectFilter(type, value, recordText) {
currentFilters[type] = value;
const btnMap = {
year: 'btnYear', month: 'btnMonth', area: 'btnArea', vendor: 'btnVendor', trade: 'btnTrade'
};
const defaultMap = {
year: '選擇年份', month: '選擇月份', area: '所有區域', vendor: '所有廠商', trade: '所有類別'
};
$(`#${btnMap[type]}`).text(value || defaultMap[type]);
fetchData();
}
function updateUI(data) {
// KPI 更新
$('#totalRows').text(data.total_rows.toLocaleString());
$('#totalMonths').text(data.total_months);
const k = data.kpis;
$('#kpiSales').text('$' + k.sales.toLocaleString());
$('#kpiProfit').text('$' + k.profit.toLocaleString());
$('#kpiMargin').text(k.margin);
$('#kpiVol').text(k.vol.toLocaleString());
if (k.sales_yoa > 0) {
const yoy = ((k.sales - k.sales_yoa) / k.sales_yoa * 100).toFixed(1);
const cls = yoy >= 0 ? 'text-danger' : 'text-success';
$('#kpiSalesYoY').html(`<span class="${cls} fw-bold"><i class="fas fa-caret-up"></i> ${yoy}%</span> vs 去年同期`);
} else {
$('#kpiSalesYoY').text('-');
}
// 表格更新
table.clear();
data.rows.forEach(row => {
const sales = row.sales_amt_curr || 0;
const prevYear = row.sales_amt_yoa || 0;
let yoyHtml = '-';
if (prevYear > 0) {
const yoy = ((sales - prevYear) / prevYear * 100).toFixed(1);
const cls = yoy >= 0 ? 'text-danger' : 'text-success';
yoyHtml = `<span class="${cls} fw-bold">${yoy > 0 ? '+' : ''}${yoy}%</span>`;
}
table.row.add([
`${row.year}/${row.month}`,
row.division,
row.area_name || '-',
row.pm_name || '-',
`<span class="text-primary fw-600">${row.brand_name}</span>`,
`<span class="small text-muted">${row.vendor_name}</span>`,
`<span class="badge bg-light text-dark border">${row.trade_type || '-'}</span>`,
sales.toLocaleString(),
yoyHtml
]);
});
table.draw();
// 主圖表更新
renderExcelChart(compareChart, 'compareChartTableBody', data.trend);
// Phase 17: 更新新增的進階圖表
updateHighlights(data.highlights);
renderYoYTrendChart(yoyTrendChart, 'yoyTrendChartTableBody', data.yoy_trend);
renderVendorRankingChart(vendorRankingChart, 'vendorRankingChartTableBody', data.vendor_ranking);
renderDivisionDistChart(divisionDistChart, 'divisionDistChartTableBody', data.division_dist);
renderPriceRangeChart(priceRangeChart, 'priceRangeChartTableBody', data.price_contribution);
renderBCGMatrixChart(bcgMatrixChart, data.bcg_data);
renderScatterChart(priceVolumeScatterChart, data.bcg_data); // Reuse BCG data for demo scatter
renderSeasonalityHeatmap(seasonalityHeatmapChart, data.heatmap_data);
renderAreaRankingChart(areaRankingChart, 'areaRankingChartTableBody', data.area_ranking);
}
function updateFilters(f) {
const populate = (id, list, type) => {
const el = $(id);
if (el.children().length > 1) return; // 已載入則不重複載入
list.forEach(v => {
el.append(`<li><a class="dropdown-item" href="#" onclick="selectFilter('${type}', '${v}')">${v}</a></li>`);
});
};
populate('#listYear', f.years, 'year');
populate('#listMonth', f.months.map(String).sort((a, b) => a - b), 'month');
populate('#listArea', f.areas, 'area');
populate('#listTrade', f.trades, 'trade');
populate('#listVendor', f.vendors, 'vendor');
}
function renderExcelChart(chartInstance, tableBodyId, trend) {
// 解析趨勢數據:今/明兩年對比
const years = [...new Set(trend.map(t => t.date.split('/')[0]))].sort().reverse();
const currYear = years[0] || new Date().getFullYear().toString();
const prevYear = (parseInt(currYear) - 1).toString();
const months = Array.from({ length: 12 }, (_, i) => (i + 1).toString());
const currData = new Array(12).fill(0);
const prevData = new Array(12).fill(0);
const yoyData = new Array(12).fill(0);
trend.forEach(t => {
const [y, m] = t.date.split('/');
if (y === currYear) currData[parseInt(m) - 1] = t.sales;
if (y === prevYear) prevData[parseInt(m) - 1] = t.sales;
});
// 計算 YoY
for (let i = 0; i < 12; i++) {
if (prevData[i] > 0) {
yoyData[i] = parseFloat(((currData[i] - prevData[i]) / prevData[i] * 100).toFixed(1));
}
}
// 更新底標數據表格 (Excel Style)
let tableHtml = `
<tr>
<td style="width: 100px; background: #f8f9fa; font-weight: bold;">月份</td>
${months.map(m => `<td style="font-weight: bold; background: #f8f9fa; font-size: 1rem;">${m}</td>`).join('')}
</tr>
<tr>
<td class="text-start ps-2" style="font-size: 1rem;"><span style="display:inline-block; width:12px; height:12px; background:#6366f1; margin-right:5px;"></span>${currYear}</td>
${currData.map(v => `<td style="font-size: 1rem;">${v > 0 ? v.toLocaleString() : '-'}</td>`).join('')}
</tr>
<tr>
<td class="text-start ps-2" style="font-size: 1rem;"><span style="display:inline-block; width:12px; height:12px; background:#10b981; margin-right:5px;"></span>${prevYear}</td>
${prevData.map(v => `<td style="font-size: 1rem;">${v > 0 ? v.toLocaleString() : '-'}</td>`).join('')}
</tr>
<tr>
<td class="text-start ps-2" style="font-size: 1rem;"><i class="fas fa-minus" style="color:#94a3b8; margin-right:5px;"></i>YOY</td>
${yoyData.map((v, i) => `<td style="font-size: 1rem;">${(currData[i] > 0 || prevData[i] > 0) ? v + '%' : '-'}</td>`).join('')}
</tr>
`;
$(`#${tableBodyId}`).html(tableHtml);
const option = {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' }
},
legend: {
data: [`${currYear}`, `${prevYear}`, 'YOY'],
bottom: 0,
show: false
},
grid: { left: '80', right: '50', top: '50', bottom: '20' },
xAxis: {
type: 'category',
data: months,
axisLabel: { show: false },
axisTick: { show: true }
},
yAxis: [
{
type: 'value',
name: '業績',
axisLabel: { formatter: (v) => v.toLocaleString() },
splitLine: { lineStyle: { type: 'dashed' } }
},
{
type: 'value',
name: 'YoY',
axisLabel: { formatter: '{value}%' },
splitLine: { show: false }
}
],
series: [
{
name: `${currYear}`,
type: 'bar',
data: currData,
itemStyle: { color: '#6366f1' },
barMaxWidth: 20,
label: { show: true, position: 'top', fontSize: 16, fontWeight: 'bold', formatter: (p) => p.value ? (p.value / 10000).toFixed(0) + '萬' : '' }
},
{
name: `${prevYear}`,
type: 'bar',
data: prevData,
itemStyle: { color: '#10b981' },
barMaxWidth: 20,
label: { show: true, position: 'top', fontSize: 16, fontWeight: 'bold', formatter: (p) => p.value ? (p.value / 10000).toFixed(0) + '萬' : '' }
},
{
name: 'YOY',
type: 'line',
yAxisIndex: 1,
data: yoyData,
smooth: true,
itemStyle: { color: '#94a3b8' },
lineStyle: { width: 2 },
symbol: 'circle', symbolSize: 6,
label: {
show: true,
position: 'bottom',
fontSize: 16,
fontWeight: 'bold',
formatter: (p) => p.value ? p.value + '%' : ''
}
}
]
};
chartInstance.setOption(option);
}
function filterList(input) {
const filter = input.value.toLowerCase();
const list = input.closest('ul').querySelectorAll('li:not(:first-child)');
list.forEach(li => {
const text = li.textContent || li.innerText;
li.style.display = text.toLowerCase().indexOf(filter) > -1 ? "" : "none";
});
}
// Phase 17: 渲染函式實作
function updateHighlights(highlights) {
if (!highlights || (!highlights.rev_top.length && !highlights.profit_top.length)) {
$('#highlightsRow').hide();
return;
}
$('#highlightsRow').show();
const renderTable = (id, list, prefix = '') => {
const tbody = $(`#${id}`);
tbody.empty();
list.forEach((item, idx) => {
tbody.append(`
<tr>
<td class="ps-3"><span class="badge bg-secondary rounded-pill me-2">${idx + 1}</span>${item.name}</td>
<td class="text-end pe-3 fw-bold">${prefix}${item.value.toLocaleString()}</td>
</tr>
`);
});
};
renderTable('revHighlightsBody', highlights.rev_top, '$');
renderTable('profitHighlightsBody', highlights.profit_top, '$');
renderTable('volHighlightsBody', highlights.vol_top, '');
}
function renderYoYTrendChart(chart, tableId, data) {
// 邏輯類似 renderExcelChart 但專門處理後端傳來的 yoy_trend 格式
if (!data || !data.length) return;
const dates = data.map(d => d.date.split('/')[1] + '月');
const currData = data.map(d => d.curr);
const yoaData = data.map(d => d.yoa);
// 產生表格
const years = [...new Set(data.map(d => d.date.split('/')[0]))];
const currYearStr = years[0] || '本期';
const prevYearStr = '去年同期';
let tableHtml = `
<tr>
<td style="width: 100px; background: #f8f9fa; font-weight: bold;">月份</td>
${dates.map(m => `<td style="font-weight: bold; background: #f8f9fa;">${m}</td>`).join('')}
</tr>
<tr>
<td class="text-start ps-2"><span style="display:inline-block; width:12px; height:12px; background:#6366f1; margin-right:5px;"></span>${currYearStr}</td>
${currData.map(v => `<td>${v > 0 ? v.toLocaleString() : '-'}</td>`).join('')}
</tr>
<tr>
<td class="text-start ps-2"><span style="display:inline-block; width:12px; height:12px; background:#d1d5db; margin-right:5px;"></span>${prevYearStr}</td>
${yoaData.map(v => `<td>${v > 0 ? v.toLocaleString() : '-'}</td>`).join('')}
</tr>
`;
$(`#${tableId}`).html(tableHtml);
const option = {
tooltip: { trigger: 'axis' },
grid: { left: '60', right: '30', top: '40', bottom: '20' },
legend: { data: [currYearStr, prevYearStr], bottom: 0 },
xAxis: { type: 'category', data: dates },
yAxis: { type: 'value', axisLabel: { formatter: v => (v / 10000).toFixed(0) + '萬' } },
series: [
{
name: currYearStr, type: 'line', data: currData,
itemStyle: { color: '#6366f1' }, lineStyle: { width: 3 }, showSymbol: false, areaStyle: { opacity: 0.1 }
},
{
name: prevYearStr, type: 'line', data: yoaData,
itemStyle: { color: '#d1d5db' }, lineStyle: { type: 'dashed' }, showSymbol: false
}
]
};
chart.setOption(option);
}
function renderDivisionDistChart(chart, tableId, data) {
if (!data || !data.length) return;
// 計算各年度總業績
const total24 = data.reduce((sum, d) => sum + (d.sales_2024 || 0), 0);
const total25 = data.reduce((sum, d) => sum + (d.sales_2025 || 0), 0);
// 準備圓餅圖資料
const pieData2024 = data.map(d => ({
name: d.name,
value: d.sales_2024 || 0
})).filter(d => d.value > 0);
const pieData2025 = data.map(d => ({
name: d.name,
value: d.sales_2025 || 0
})).filter(d => d.value > 0);
const option = {
title: [
{ text: '2024 年', left: '23%', top: '5%', textAlign: 'center', textStyle: { fontSize: 16, fontWeight: 'bold', color: '#ea580c' } },
{ text: '2025 年', left: '73%', top: '5%', textAlign: 'center', textStyle: { fontSize: 16, fontWeight: 'bold', color: '#0284c7' } }
],
tooltip: {
trigger: 'item',
formatter: function (p) {
return `<strong>${p.name}</strong><br/>業績: ${(p.value / 10000).toFixed(0)}萬<br/>佔比: ${p.percent.toFixed(1)}%`;
}
},
legend: {
type: 'scroll',
orient: 'horizontal',
bottom: 0,
textStyle: { fontSize: 12, fontWeight: 'bold' }
},
series: [
{
name: '2024 年',
type: 'pie',
radius: ['30%', '60%'],
center: ['25%', '55%'],
data: pieData2024,
itemStyle: {
color: function (params) {
const colors = ['#f97316', '#ea580c', '#c2410c', '#fb923c', '#fdba74', '#fed7aa', '#dc2626', '#ef4444', '#f87171', '#fca5a5', '#b91c1c', '#991b1b'];
return colors[params.dataIndex % colors.length];
}
},
label: {
show: true,
fontSize: 11,
fontWeight: 'bold',
formatter: p => `${p.name}\n${(p.value / 10000).toFixed(0)}\n${p.percent.toFixed(1)}%`
},
emphasis: {
itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' }
}
},
{
name: '2025 年',
type: 'pie',
radius: ['30%', '60%'],
center: ['75%', '55%'],
data: pieData2025,
itemStyle: {
color: function (params) {
const colors = ['#0ea5e9', '#0284c7', '#0369a1', '#38bdf8', '#7dd3fc', '#bae6fd', '#14b8a6', '#0d9488', '#2dd4bf', '#5eead4', '#0f766e', '#115e59'];
return colors[params.dataIndex % colors.length];
}
},
label: {
show: true,
fontSize: 11,
fontWeight: 'bold',
formatter: p => `${p.name}\n${(p.value / 10000).toFixed(0)}\n${p.percent.toFixed(1)}%`
},
emphasis: {
itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' }
}
}
]
};
chart.setOption(option);
}
function renderPriceRangeChart(chart, tableId, data) {
if (!data || !data.length) return;
// 定義價格帶順序
const order = ['0-499', '500-999', '1,000-1,999', '2,000-4,999', '5,000-9,999', '10,000+'];
data.sort((a, b) => order.indexOf(a.range) - order.indexOf(b.range));
// 計算各年度總銷售額,用於計算佔比
const totalSales2024 = data.reduce((sum, item) => sum + (item.sales_2024 || 0), 0);
const totalSales2025 = data.reduce((sum, item) => sum + (item.sales_2025 || 0), 0);
const option = {
title: { text: '', left: 'center' },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: function (params) {
let res = params[0].name + '<br/>';
params.forEach(param => {
const val = param.value || 0;
const total = param.seriesName.includes('2024') ? totalSales2024 : totalSales2025;
const pct = total > 0 ? (val / total * 100).toFixed(1) + '%' : '0.0%';
res += `${param.marker} ${param.seriesName}: ${val.toLocaleString()} (${pct})<br/>`;
});
return res;
}
},
legend: { data: ['2024 銷售', '2025 銷售'], bottom: 0, textStyle: { fontSize: 13, fontWeight: 'bold' } },
grid: { left: '3%', right: '4%', bottom: '12%', top: '15%', containLabel: true },
xAxis: {
type: 'category',
data: data.map(d => d.range),
axisLabel: { interval: 0, fontSize: 13, fontWeight: 'bold' }
},
yAxis: {
type: 'value',
name: '銷售額',
nameTextStyle: { fontSize: 12, fontWeight: 'bold' },
axisLabel: { formatter: v => (v / 10000).toFixed(0) + '萬', fontSize: 12 }
},
series: [
{
name: '2024 銷售',
type: 'bar',
data: data.map(d => d.sales_2024 || 0),
itemStyle: { color: '#94a3b8' },
barWidth: 30,
label: {
show: true,
position: 'top',
fontSize: 12,
fontWeight: 'bold',
color: '#475569',
formatter: function (p) {
const val = p.value || 0;
const pct = totalSales2024 > 0 ? (val / totalSales2024 * 100).toFixed(1) + '%' : '';
const amt = (val / 10000).toFixed(0) + '萬';
return pct + '\n' + amt;
}
}
},
{
name: '2025 銷售',
type: 'bar',
data: data.map(d => d.sales_2025 || 0),
itemStyle: { color: '#8b5cf6' },
barWidth: 30,
label: {
show: true,
position: 'top',
fontSize: 12,
fontWeight: 'bold',
color: '#6d28d9',
formatter: function (p) {
const val = p.value || 0;
const pct = totalSales2025 > 0 ? (val / totalSales2025 * 100).toFixed(1) + '%' : '';
const amt = (val / 10000).toFixed(0) + '萬';
return pct + '\n' + amt;
}
}
}
]
};
chart.setOption(option);
// 移除表格 (清空容器)
$(`#${tableId}`).parent().html('');
}
function renderBCGMatrixChart(chart, data) {
// X: 毛利率 (Margin), Y: 銷售額 (Sales), Size: 銷量 (Vol)
if (!data || !data.length) return;
const seriesData = data.map(d => [
d.margin, // X
d.sales, // Y
d.qty, // Size
d.name // Name
]);
const option = {
tooltip: {
formatter: function (param) {
return `<div style="font-weight:bold">${param.value[3]}</div>
<div>毛利率: ${param.value[0]}%</div>
<div>銷售額: $${param.value[1].toLocaleString()}</div>
<div>銷量: ${param.value[2].toLocaleString()}</div>`;
}
},
grid: { left: '80', right: '50', top: '30', bottom: '30' },
xAxis: { name: '毛利率(%)', type: 'value', splitLine: { lineStyle: { type: 'dashed' } } },
yAxis: { name: '業績($)', type: 'value', splitLine: { lineStyle: { type: 'dashed' } } },
series: [
{
type: 'scatter',
data: seriesData,
symbolSize: function (data) {
// 簡單縮放:根號銷量 / 係數
return Math.sqrt(data[2]) * 1.5;
},
itemStyle: {
color: function (p) {
// 依據象限給色 (假設 30% 毛利, $50萬 業績為分界)
if (p.value[0] >= 30 && p.value[1] >= 500000) return '#10b981'; // Star
if (p.value[0] < 30 && p.value[1] >= 500000) return '#f59e0b'; // Cow
if (p.value[0] >= 30 && p.value[1] < 500000) return '#6366f1'; // Question
return '#ef4444'; // Dog
},
opacity: 0.7,
borderColor: '#fff',
borderWidth: 1
},
markLine: {
silent: true,
data: [
{ xAxis: 30, lineStyle: { color: '#999', type: 'solid' }, label: { formatter: '高獲利線 (30%)' } },
{ yAxis: 500000, lineStyle: { color: '#999', type: 'solid' }, label: { formatter: '高營收線 ($50萬)' } }
]
}
}
]
};
chart.setOption(option);
}
function renderScatterChart(chart, data) {
// Reusing BCG data for Price (calc) vs Volume
if (!data || !data.length) return;
const seriesData = data.map(d => {
const avgPrice = d.qty > 0 ? Math.round(d.sales / d.qty) : 0;
return [
avgPrice, // X: Price
d.qty, // Y: Volume
d.sales, // Size: Sales
d.name
];
});
const option = {
tooltip: {
formatter: function (param) {
return `<div style="font-weight:bold">${param.value[3]}</div>
<div>均價: $${param.value[0]}</div>
<div>銷量: ${param.value[1].toLocaleString()}</div>
<div>業績: $${param.value[2].toLocaleString()}</div>`;
}
},
grid: { left: '60', right: '50', top: '30', bottom: '30' },
xAxis: { name: '均價($)', type: 'value', splitLine: { show: false } },
yAxis: { name: '銷量', type: 'value', splitLine: { lineStyle: { type: 'dashed' } } },
series: [
{
type: 'scatter',
data: seriesData,
symbolSize: function (data) {
return Math.log(data[2]) * 3; // Log scale size based on sales
},
itemStyle: { color: '#ec4899', opacity: 0.6 }
}
]
};
chart.setOption(option);
}
function renderVendorRankingChart(chart, tableId, data) {
if (!data || !data.length) return;
// 取前 20 名做圖表
const chartData = data.slice().reverse(); // 反轉讓第一名在上面
const option = {
title: { text: '', left: 'center' },
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
legend: { data: ['2024 銷售額', '2025 銷售額'], bottom: 0, textStyle: { fontSize: 15, fontWeight: 'bold' } },
grid: { left: '160', right: '110', top: '20', bottom: '45' },
xAxis: {
type: 'value',
axisLabel: { formatter: v => (v / 10000).toFixed(0) + '萬', fontSize: 14, fontWeight: 'bold' }
},
yAxis: {
type: 'category',
data: chartData.map(d => d.name),
axisLabel: { width: 150, overflow: 'truncate', interval: 0, fontSize: 14, fontWeight: 'bold' }
},
series: [
{
name: '2024 銷售額', type: 'bar',
data: chartData.map(d => d.sales_2024 || 0),
itemStyle: { color: '#94a3b8' },
barWidth: 18,
barGap: '0%',
barCategoryGap: '75%',
label: {
show: true,
position: 'insideRight',
fontSize: 13,
fontWeight: 'bold',
color: '#fff',
textShadowColor: 'rgba(0,0,0,0.5)',
textShadowBlur: 2,
formatter: p => p.value > 0 ? (p.value / 10000).toFixed(0) + '萬' : ''
}
},
{
name: '2025 銷售額', type: 'bar',
data: chartData.map(d => d.sales_2025 || 0),
itemStyle: { color: '#3b82f6' },
barWidth: 18,
label: {
show: true,
position: 'right',
fontSize: 14,
fontWeight: 'bold',
color: '#1d4ed8',
formatter: p => p.value > 0 ? (p.value / 10000).toFixed(0) + '萬' : ''
}
}
]
};
chart.setOption(option);
// 表格渲染 (顯示 Top 20)
// 欄位順序排名、廠商名稱、總銷售額、銷售額YoY、2024銷售額、2025銷售額、總毛利額、毛利額YoY、2024毛利額、2025毛利額
let tableHtml = `
<thead class="table-light">
<tr>
<th class="align-middle">排名</th>
<th class="align-middle">廠商名稱</th>
<th class="align-middle text-end">總銷售額</th>
<th class="align-middle text-end">銷售額 YoY</th>
<th class="align-middle text-end">2024 銷售額</th>
<th class="align-middle text-end">2025 銷售額</th>
<th class="align-middle text-end">總毛利額</th>
<th class="align-middle text-end">毛利額 YoY</th>
<th class="align-middle text-end">2024 毛利額</th>
<th class="align-middle text-end">2025 毛利額</th>
</tr>
</thead>
<tbody>
`;
data.forEach((d, i) => {
const sales24 = d.sales_2024 || 0;
const sales25 = d.sales_2025 || 0;
const profit24 = d.profit_2024 || 0;
const profit25 = d.profit_2025 || 0;
const salesYoY = sales24 > 0 ? ((sales25 - sales24) / sales24 * 100).toFixed(1) + '%' : '-';
const profitYoY = profit24 > 0 ? ((profit25 - profit24) / profit24 * 100).toFixed(1) + '%' : '-';
const salesYoYClass = sales24 > 0 && (sales25 - sales24) >= 0 ? 'text-danger' : (sales24 > 0 ? 'text-success' : '');
const profitYoYClass = profit24 > 0 && (profit25 - profit24) >= 0 ? 'text-danger' : (profit24 > 0 ? 'text-success' : '');
tableHtml += `
<tr>
<td>${i + 1}</td>
<td class="text-start text-truncate" style="max-width: 200px;">${d.name}</td>
<td class="text-end fw-bold">${d.sales.toLocaleString()}</td>
<td class="text-end fw-bold ${salesYoYClass}">${salesYoY}</td>
<td class="text-end text-muted">${sales24.toLocaleString()}</td>
<td class="text-end text-primary fw-bold">${sales25.toLocaleString()}</td>
<td class="text-end fw-bold">${d.profit.toLocaleString()}</td>
<td class="text-end fw-bold ${profitYoYClass}">${profitYoY}</td>
<td class="text-end text-muted">${profit24.toLocaleString()}</td>
<td class="text-end text-success fw-bold">${profit25.toLocaleString()}</td>
</tr>
`;
});
tableHtml += `</tbody>`;
$(`#${tableId}`).parent().html(`<table class="table table-bordered table-hover table-sm text-center mb-0 small" style="white-space: nowrap;">${tableHtml}</table>`);
}
function renderSeasonalityHeatmap(chart, data) {
if (!data || !data.length) return;
// X軸: 月份 1-12
const months = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'];
// 取得所有區域名稱 (按銷售量排序)
const areaSales = {};
data.forEach(d => {
if (!areaSales[d.category]) areaSales[d.category] = 0;
areaSales[d.category] += d.sales;
});
const areas = Object.keys(areaSales).sort((a, b) => areaSales[b] - areaSales[a]);
// Y軸: 年度+區域 (2025 在上, 2024 在下)
const yAxisLabels = [];
[2025, 2024].forEach(year => {
areas.forEach(area => {
yAxisLabels.push(`${year} ${area}`);
});
});
// 構建熱力圖數據 [xIndex, yIndex, value]
const heatmapData = [];
data.forEach(d => {
const xIndex = d.month - 1;
const yLabel = `${d.year} ${d.category}`;
const yIndex = yAxisLabels.indexOf(yLabel);
if (xIndex >= 0 && xIndex < 12 && yIndex >= 0) {
heatmapData.push([xIndex, yIndex, d.sales]);
}
});
const maxSales = Math.max(...data.map(d => d.sales), 1);
const option = {
title: { text: '', left: 'center' },
tooltip: {
position: 'top',
formatter: p => {
const monthName = months[p.value[0]];
const categoryYear = yAxisLabels[p.value[1]];
const sales = p.value[2];
return `<strong>${categoryYear}</strong><br/>${monthName}<br/>業績: <strong>${(sales / 10000).toFixed(0)}萬</strong>`;
}
},
grid: { height: '75%', top: '5%', bottom: '18%', left: '15%', right: '3%' },
xAxis: {
type: 'category',
data: months,
splitArea: { show: true },
axisLabel: { fontSize: 14, fontWeight: 'bold' }
},
yAxis: {
type: 'category',
data: yAxisLabels,
splitArea: { show: true },
axisLabel: { fontSize: 14, fontWeight: 'bold' }
},
visualMap: {
min: 0,
max: maxSales,
calculable: true,
orient: 'horizontal',
left: 'center',
bottom: '0%',
textStyle: { fontSize: 12 },
inRange: { color: ['#fef3c7', '#fbbf24', '#f59e0b', '#d97706', '#b45309'] }
},
series: [{
type: 'heatmap',
data: heatmapData,
label: {
show: true,
fontSize: 12,
fontWeight: 'bold',
formatter: function (p) {
const val = p.value[2];
return (val / 10000).toFixed(0) + '萬';
}
},
emphasis: {
itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0, 0, 0, 0.5)' }
}
}]
};
chart.setOption(option);
}
function renderAreaRankingChart(chart, tableId, data) {
if (!data || !data.length) return;
// 按總業績排序
data.sort((a, b) => b.sales - a.sales);
const option = {
title: { text: '', left: 'center' },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: function (params) {
let res = params[0].name + '<br/>';
params.forEach(param => {
const val = param.value || 0;
res += `${param.marker} ${param.seriesName}: ${val.toLocaleString()}<br/>`;
});
return res;
}
},
legend: { data: ['2024 業績', '2025 業績'], bottom: 0, textStyle: { fontSize: 14 } },
grid: { left: '3%', right: '4%', bottom: '10%', containLabel: true },
xAxis: {
type: 'category',
data: data.map(d => d.name),
axisLabel: { interval: 0, fontSize: 14, fontWeight: 'bold' }
},
yAxis: {
type: 'value',
name: '業績',
axisLabel: { formatter: v => (v / 10000).toFixed(0) + '萬', fontSize: 13 }
},
series: [
{
name: '2024 業績',
type: 'bar',
data: data.map(d => d.sales_2024 || 0),
itemStyle: { color: '#94a3b8' },
label: {
show: true,
position: 'top',
fontSize: 14,
fontWeight: 'bold',
formatter: p => (p.value / 10000).toFixed(0) + '萬'
}
},
{
name: '2025 業績',
type: 'bar',
data: data.map(d => d.sales_2025 || 0),
itemStyle: { color: '#14b8a6' }, // 青色
label: {
show: true,
position: 'top',
fontSize: 14,
fontWeight: 'bold',
formatter: p => (p.value / 10000).toFixed(0) + '萬'
}
}
]
};
chart.setOption(option);
// 表格渲染
let tableHtml = `
<thead class="table-light"><tr><th>排名</th><th>區名稱</th><th>業績</th></tr></thead><tbody>
`;
data.forEach((d, i) => {
tableHtml += `<tr><td>${i + 1}</td><td class="text-start">${d.name}</td><td class="text-end fw-bold">$${d.sales.toLocaleString()}</td></tr>`;
});
tableHtml += `</tbody>`;
$(`#${tableId}`).parent().html(`<table class="table table-bordered table-hover table-sm text-center mb-0 small">${tableHtml}</table>`);
}
</script>
{% endblock %}