44 < meta charset ="UTF-8 ">
55 < meta name ="viewport " content ="width=device-width, initial-scale=1.0 ">
66 < title > 大興里空氣異味監測與分析報告</ title >
7- <!-- 載入 Tailwind CSS -->
87 < script src ="https://cdn.tailwindcss.com "> </ script >
9- <!-- 載入 Chart.js (用於圖表繪製) -->
108 < script src ="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js "> </ script >
11- <!-- 載入 PapaParse (用於快速解析 CSV) -->
129 < script src ="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.4.1/papaparse.min.js "> </ script >
1310 < style >
1411 /* 使用 Inter 字體 */
2320< body class ="bg-gray-50 min-h-screen p-4 sm:p-8 ">
2421
2522 < div class ="max-w-7xl mx-auto ">
26- <!-- 標題與更新時間 -->
2723 < header class ="text-center mb-8 ">
2824 < h1 class ="text-4xl font-extrabold text-blue-700 tracking-tight mb-2 ">
2925 大興里空氣異味監測與分析報告
@@ -33,30 +29,25 @@ <h1 class="text-4xl font-extrabold text-blue-700 tracking-tight mb-2">
3329 </ p >
3430 </ header >
3531
36- <!-- 錯誤訊息顯示區 (新增) -->
3732 < div id ="error-message " class ="hidden bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4 " role ="alert ">
3833 < strong class ="font-bold "> 資料載入錯誤:</ strong >
3934 < span id ="error-text " class ="block sm:inline "> </ span >
4035 </ div >
4136
42- <!-- 全域篩選器區 -->
4337 < section class ="bg-white p-6 rounded-xl shadow-lg mb-8 transition-shadow duration-300 hover:shadow-xl ">
4438 < h2 class ="text-xl font-semibold text-gray-700 mb-4 border-b pb-2 ">
4539 篩選條件
4640 </ h2 >
4741 < div class ="grid grid-cols-1 md:grid-cols-3 gap-6 ">
48- <!-- 地點篩選器 -->
4942 < div >
5043 < label for ="location-filter " class ="block text-sm font-medium text-gray-700 mb-1 ">
5144 發生位置
5245 </ label >
5346 < select id ="location-filter " class ="w-full p-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 transition duration-150 ">
5447 < option value ="all "> 所有地點</ option >
55- <!-- 選項將由 JS 動態載入 -->
56- </ select >
48+ </ select >
5749 </ div >
5850
59- <!-- 濃度篩選器 -->
6051 < div >
6152 < label for ="concentration-filter " class ="block text-sm font-medium text-gray-700 mb-1 ">
6253 氣味濃度
@@ -69,7 +60,6 @@ <h2 class="text-xl font-semibold text-gray-700 mb-4 border-b pb-2">
6960 </ select >
7061 </ div >
7162
72- <!-- 重設按鈕 -->
7363 < div class ="flex items-end ">
7464 < button id ="reset-button " class ="w-full bg-indigo-500 text-white p-2 rounded-lg font-medium hover:bg-indigo-600 transition duration-200 shadow-md ">
7565 重設篩選
@@ -78,67 +68,63 @@ <h2 class="text-xl font-semibold text-gray-700 mb-4 border-b pb-2">
7868 </ div >
7969 </ section >
8070
81- <!-- 關鍵數據指標 (KPIs) -->
8271 < section class ="grid grid-cols-2 lg:grid-cols-4 gap-6 mb-8 ">
83- <!-- 總回報筆數 -->
8472 < div class ="bg-white p-5 rounded-xl shadow-md border-b-4 border-blue-500 ">
8573 < p class ="text-sm font-medium text-gray-500 "> 總回報筆數</ p >
8674 < p id ="kpi-total " class ="text-3xl font-bold text-gray-900 mt-1 "> 0</ p >
8775 </ div >
88- <!-- 發生異味天數 -->
8976 < div class ="bg-white p-5 rounded-xl shadow-md border-b-4 border-green-500 ">
9077 < p class ="text-sm font-medium text-gray-500 "> 發生異味天數</ p >
9178 < p id ="kpi-days " class ="text-3xl font-bold text-gray-900 mt-1 "> 0</ p >
9279 </ div >
93- <!-- 最高濃度回報比例 -->
9480 < div class ="bg-white p-5 rounded-xl shadow-md border-b-4 border-red-500 ">
9581 < p class ="text-sm font-medium text-gray-500 "> 重度回報比例</ p >
9682 < p id ="kpi-heavy-ratio " class ="text-3xl font-bold text-gray-900 mt-1 "> 0%</ p >
9783 </ div >
98- <!-- 最常發生時段 -->
9984 < div class ="bg-white p-5 rounded-xl shadow-md border-b-4 border-yellow-500 ">
10085 < p class ="text-sm font-medium text-gray-500 "> 最常發生時段</ p >
10186 < p id ="kpi-peak-time " class ="text-2xl font-bold text-gray-900 mt-2 "> --</ p >
10287 </ div >
10388 </ section >
10489
105- <!-- 圖表區 -->
10690 < section class ="grid grid-cols-1 lg:grid-cols-2 gap-8 ">
107- <!-- 1. 污染類型佔比 (圓餅圖) -->
10891 < div class ="bg-white p-6 rounded-xl shadow-md transition-shadow duration-300 hover:shadow-xl ">
10992 < h3 class ="text-lg font-semibold text-gray-700 mb-4 "> 污染類型佔比</ h3 >
11093 < div class ="chart-container ">
11194 < canvas id ="typeChart "> </ canvas >
11295 </ div >
11396 </ div >
11497
115- <!-- 2. 地點與濃度交叉分析 (長條圖) -->
11698 < div class ="bg-white p-6 rounded-xl shadow-md transition-shadow duration-300 hover:shadow-xl ">
11799 < h3 class ="text-lg font-semibold text-gray-700 mb-4 "> 地點與濃度交叉分析</ h3 >
118100 < div class ="chart-container ">
119101 < canvas id ="locationConcentrationChart "> </ canvas >
120102 </ div >
121103 </ div >
122104
123- <!-- 3. 異味發生熱點時段 (柱狀圖) -->
124105 < div class ="lg:col-span-2 bg-white p-6 rounded-xl shadow-md transition-shadow duration-300 hover:shadow-xl ">
125106 < h3 class ="text-lg font-semibold text-gray-700 mb-4 "> 異味發生熱點時段 (12小時制)</ h3 >
126107 < div class ="chart-container h-96 ">
127108 < canvas id ="timeChart "> </ canvas >
128109 </ div >
129110 </ div >
130111
131- <!-- 4. 近七日回報紀錄趨勢 (折線圖) - NEW -->
132112 < div class ="lg:col-span-2 bg-white p-6 rounded-xl shadow-md transition-shadow duration-300 hover:shadow-xl ">
133113 < h3 class ="text-lg font-semibold text-gray-700 mb-4 "> 近七日回報紀錄趨勢</ h3 >
134114 < div class ="chart-container h-96 ">
135115 < canvas id ="weeklyTrendChart "> </ canvas >
136116 </ div >
137117 </ div >
118+
119+ < div class ="lg:col-span-2 bg-white p-6 rounded-xl shadow-md transition-shadow duration-300 hover:shadow-xl ">
120+ < h3 class ="text-lg font-semibold text-gray-700 mb-4 "> 近一個月回報紀錄趨勢</ h3 >
121+ < div class ="chart-container h-96 ">
122+ < canvas id ="monthlyTrendChart "> </ canvas >
123+ </ div >
124+ </ div >
138125 </ section >
139126 </ div >
140127
141- <!-- 載入中/錯誤訊息 -->
142128 < div id ="loading-overlay " class ="fixed inset-0 bg-gray-900 bg-opacity-75 flex justify-center items-center z-50 ">
143129 < p class ="text-white text-xl font-semibold ">
144130 < svg class ="animate-spin -ml-1 mr-3 h-8 w-8 text-white inline-block " xmlns ="http://www.w3.org/2000/svg " fill ="none " viewBox ="0 0 24 24 ">
@@ -224,7 +210,7 @@ <h3 class="text-lg font-semibold text-gray-700 mb-4">近七日回報紀錄趨勢
224210 return dateString ; // 解析失敗,返回原始字串
225211 } ;
226212
227- // 【新修正】 時間轉換函數:將 12 小時制 (如 3:00 PM) 或 24 小時制 (如 14:00) 轉換為 0-23 的小時整數
213+ // 時間轉換函數:將 12 小時制 (如 3:00 PM) 或 24 小時制 (如 14:00) 轉換為 0-23 的小時整數
228214 const convertTo24Hour = ( timeString ) => {
229215 if ( ! timeString ) return NaN ;
230216
@@ -310,7 +296,7 @@ <h3 class="text-lg font-semibold text-gray-700 mb-4">近七日回報紀錄趨勢
310296 const concentration = row [ concentrationKey ] ? String ( row [ concentrationKey ] ) . trim ( ) : null ;
311297 const type = row [ typeKey ] ? String ( row [ typeKey ] ) . trim ( ) : null ;
312298
313- // 【修正】 在此處預先轉換為 24 小時制的小時數
299+ // 在此處預先轉換為 24 小時制的小時數
314300 const hour24 = convertTo24Hour ( time ) ;
315301
316302 return { date, time, location, concentration, type, hour24 } ;
@@ -395,7 +381,7 @@ <h3 class="text-lg font-semibold text-gray-700 mb-4">近七日回報紀錄趨勢
395381
396382 if ( totalCount > 0 ) {
397383 const hourCounts = data . reduce ( ( acc , row ) => {
398- // 【修正】 直接使用預先計算好的 24 小時制小時數
384+ // 直接使用預先計算好的 24 小時制小時數
399385 const hour = row . hour24 ;
400386
401387 if ( ! isNaN ( hour ) ) {
@@ -438,7 +424,6 @@ <h3 class="text-lg font-semibold text-gray-700 mb-4">近七日回報紀錄趨勢
438424 } ;
439425
440426 const renderTypeChart = ( data ) => {
441- // 修正 reduce 函數的箭頭函數語法 (已修正)
442427 const typeCounts = data . reduce ( ( acc , row ) => {
443428 const type = row . type || '未知異味' ;
444429 acc [ type ] = ( acc [ type ] || 0 ) + 1 ;
@@ -471,7 +456,7 @@ <h3 class="text-lg font-semibold text-gray-700 mb-4">近七日回報紀錄趨勢
471456 const renderTimeChart = ( data ) => {
472457 const hourCounts = Array ( 24 ) . fill ( 0 ) ; // 0-23 小時
473458
474- // 【修正】 優化 12 小時制標籤生成邏輯,確保 0 點是 12 AM,12 點是 12 PM (來自前一次修正)
459+ // 優化 12 小時制標籤生成邏輯
475460 const hours = Array . from ( { length : 24 } , ( _ , i ) => {
476461 const hour = i ;
477462 let hour12 ;
@@ -494,7 +479,7 @@ <h3 class="text-lg font-semibold text-gray-700 mb-4">近七日回報紀錄趨勢
494479 } ) ;
495480
496481 data . forEach ( row => {
497- // 【修正】 直接使用預先計算好的 24 小時制小時數
482+ // 直接使用預先計算好的 24 小時制小時數
498483 const hour = row . hour24 ;
499484
500485 if ( ! isNaN ( hour ) && hour >= 0 && hour <= 23 ) {
@@ -600,7 +585,6 @@ <h3 class="text-lg font-semibold text-gray-700 mb-4">近七日回報紀錄趨勢
600585
601586 // 2. 計算過去 7 天的回報筆數
602587 const counts = dateKeys . map ( key => {
603- // 這裡 row.date 已經是 normalized (YYYY/MM/DD) 格式
604588 return data . filter ( row => row . date === key ) . length ;
605589 } ) ;
606590
@@ -644,6 +628,74 @@ <h3 class="text-lg font-semibold text-gray-700 mb-4">近七日回報紀錄趨勢
644628 } ) ;
645629 } ;
646630
631+ // 近一個月 (30天) 回報紀錄趨勢 (折線圖)
632+ const renderMonthlyTrendChart = ( data ) => {
633+ const today = new Date ( ) ;
634+ today . setHours ( 0 , 0 , 0 , 0 ) ;
635+
636+ let dateLabels = [ ] ; // 標籤 (月/日)
637+ let dateKeys = [ ] ; // 資料比對鍵 (年/月/日)
638+
639+ // 產生過去 30 天的日期列表 (從 29 天前到今天)
640+ for ( let i = 29 ; i >= 0 ; i -- ) {
641+ const d = new Date ( today ) ;
642+ d . setDate ( today . getDate ( ) - i ) ;
643+
644+ const label = `${ ( d . getMonth ( ) + 1 ) . toString ( ) . padStart ( 2 , '0' ) } /${ d . getDate ( ) . toString ( ) . padStart ( 2 , '0' ) } ` ;
645+ dateLabels . push ( label ) ;
646+
647+ dateKeys . push ( formatDateKey ( d ) ) ;
648+ }
649+
650+ // 計算過去 30 天的回報筆數
651+ const counts = dateKeys . map ( key => {
652+ return data . filter ( row => row . date === key ) . length ;
653+ } ) ;
654+
655+ // 繪製折線圖
656+ initializeChart ( 'monthlyTrendChart' , 'line' , {
657+ data : {
658+ labels : dateLabels ,
659+ datasets : [ {
660+ label : '回報筆數' ,
661+ data : counts ,
662+ backgroundColor : 'rgba(16, 185, 129, 0.5)' , // 綠色透明背景
663+ borderColor : '#10b981' ,
664+ borderWidth : 3 ,
665+ tension : 0.3 ,
666+ fill : true ,
667+ pointRadius : 3 ,
668+ pointHoverRadius : 5 ,
669+ } ]
670+ } ,
671+ options : {
672+ responsive : true ,
673+ maintainAspectRatio : false ,
674+ scales : {
675+ y : {
676+ beginAtZero : true ,
677+ title : { display : true , text : '回報筆數' } ,
678+ ticks : { precision : 0 }
679+ } ,
680+ x : {
681+ title : { display : true , text : '日期' } ,
682+ // 調整刻度以避免 30 個標籤過於擁擠,大約每 5 天顯示一次
683+ ticks : {
684+ autoSkip : true ,
685+ maxRotation : 0 ,
686+ minRotation : 0 ,
687+ maxTicksLimit : 7
688+ }
689+ }
690+ } ,
691+ plugins : {
692+ legend : { position : 'top' } ,
693+ title : { display : false }
694+ }
695+ }
696+ } ) ;
697+ } ;
698+
647699
648700 // --- 主渲染函數 ---
649701 const renderDashboard = ( data ) => {
@@ -652,6 +704,7 @@ <h3 class="text-lg font-semibold text-gray-700 mb-4">近七日回報紀錄趨勢
652704 renderTimeChart ( data ) ;
653705 renderLocationConcentrationChart ( data ) ;
654706 renderWeeklyTrendChart ( data ) ;
707+ renderMonthlyTrendChart ( data ) ;
655708 } ;
656709
657710 // --- 初始化與事件監聽 ---
0 commit comments