前言
我們在前面的案例曾經教學過怎麼使用 Google Apps Script 來監看 YouBike 的各站點資訊。
在 《零基礎快速入門:GenAI 搭配 Google Apps Script 的工作自動化寶典》Chapter 4-6 自動監測 YouBike 站 示範了如何透過 API 把外部資料抓進我們的 Google Sheets。
在 《零基礎快速入門:GenAI 搭配 Google Apps Script 的工作自動化寶典》Chapter 4-7 自動監測 YouBike 站 Part 2 則示範了如何針對感興趣的站點每五分鐘記錄一次。
現在我們透過前幾個案例,
掌握了 Google Apps Script 的前端技巧,
在這一篇我們就要來試著在網頁上做一個 dashboard,
用精美的互動圖表來顯示出 YouBike 的資訊。
開發目標
我們的 dashboard 就先從簡單的目標開始:
- 顯示出隨時間變化的車輛數量折線圖
- 顯示出目前最新現況的可借/可還數量
我們就先做一個基礎,後續還需要什麼資訊,都可以再加上去。
反正既然食材有了 (API 抓來的資料),
要創作什麼料理就看你的創意了。
該怎麼向 ChatGPT 發問?
完整的真實對話紀錄可參見:https://chatgpt.com/share/6705360d-a588-800e-8b00-59d9d554bcc9
以下摘要我整個開發過程的幾個重點提問:
- 我現在有一個 Google Sheets,用來紀錄 YouBike 某個站點的資訊,資料格式如下:…
- 我現在想要在網頁上做一個 grafana 風格的 dashboard,請問你建議我可以用哪些圖表顯示哪些資訊呢?
- 好的,請幫我依照我的資料格式,用 Google Apps Script 寫一個網頁 dashboard
- 我發現這邊抓的資料太多了,能不能根據 infoTime 欄位,只抓過去七天的資料呢?
- 我的資料是同一個站點的不同時間的資訊。我想要的 dashboard 是可以顯示出過去七天的車位數變化的折線圖
- 請幫我加上「根據 infoTime 移除重複資料」的功能
- 幫我加一個 logger.log,印出資料筆數
- 為什麼我發現 seenTimes 在 add 之後也還是空的呢?
- 請把網頁上的其餘資訊拿掉,只留折線圖就好
- 為什麼折線圖的橫軸跟時間不成比例呢?
- 我可以在網頁上加一個日間區間的選項嗎?讓使用者可以自己決定要觀看哪一段 infoTime 的資料
- 可以在網頁上加上一個切換的選項,讓使用者決定要看「可借數」或是「可還數」嗎?
- 請在網頁上加一個區塊,顯示當前 infoTime 最新的一筆資料的可借數量以及
- infoTime。要採用美觀清楚的方式呈現,不要只是把資訊印出來。
- 請把時間區間預設為過去七天
- 注意:要顯示「當前」infoTime 最新的一筆資料的可借數量以及infoTime。不是使用者篩選日期中的最新一筆,而是距今最近的一筆。意即不管使用者篩選了哪一段時期,最新一筆的資訊都不會改變
- 請把顯示的時間都改成 GMT+8
網頁儀表板成果
輕輕鬆鬆就有一個可以調整時間區間的折線圖,
而且是互動式的。
將滑鼠移到資料點上,可動態顯示出該點的時間與數量。
程式碼長什麼樣子
Code.js (包含之前抓資料的程式碼,以及這次為了顯示儀表板新增的程式碼)
function fetchHeaders() {
var url = "https://tcgbusfs.blob.core.windows.net/dotapp/youbike/v2/youbike_immediate.json";
// 設置請求的選項,method 設置為 "get",muteHttpExceptions 設置為 true 以獲取完整的響應信息
var options = {
"method": "get",
"muteHttpExceptions": true // 這樣可以獲得完整的回應,包括標頭
};
var response = UrlFetchApp.fetch(url, options);
var allHeaders = response.getAllHeaders(); // 獲取所有的 HTTP 標頭
Logger.log(allHeaders); // 在 GAS 控制台中顯示所有標頭
Logger.log(response);
}
function writeYouBikeDataToSheet() {
var url = 'https://tcgbusfs.blob.core.windows.net/dotapp/youbike/v2/youbike_immediate.json?ran=123';
var sheetId = 'YOUR_SHEET_ID'; // 請替換為您的 Google Sheets ID
try {
// 設置請求的選項,method 設置為 "get",muteHttpExceptions 設置為 true 以獲取完整的響應信息
var options = {
"method": "get",
"muteHttpExceptions": true // 這樣可以獲得完整的回應,包括標頭
};
// 獲取數據
var response = UrlFetchApp.fetch(url, options);
Logger.log(response);
var data = JSON.parse(response.getContentText());
// 獲取所有工作表
var sheets = SpreadsheetApp.openById(sheetId).getSheets();
// 遍歷所有工作表
for (var i = 0; i < sheets.length; i++) {
// 獲取當前工作表
var sheet = sheets[i];
// 打印工作表名稱
Logger.log('Sheet Name: ' + sheet.getName());
// 查找特定站點的數據
var targetStation = data.find(function(station) {
return station.sna === sheet.getName();
});
Logger.log(targetStation);
// 如果找到站點數據,寫入 Google Sheets
if (targetStation) {
var sheet = SpreadsheetApp.openById(sheetId).getSheetByName(sheet.getName());
sheet.appendRow([
targetStation.sno,
targetStation.sna,
targetStation.sarea,
targetStation.mday,
targetStation.ar,
targetStation.sareaen,
targetStation.snaen,
targetStation.aren,
targetStation.act,
targetStation.srcUpdateTime,
targetStation.updateTime,
targetStation.infoTime,
targetStation.infoDate,
targetStation.total,
targetStation.available_rent_bikes,
targetStation.latitude,
targetStation.longitude,
targetStation.available_return_bikes
]);
} else {
Logger.log("No data found for the specified station.");
}
}
} catch (e) {
Logger.log('Error fetching or processing data: ' + e.message);
}
}
function doGet() {
// 打開 Google Sheets,請把 "YOUR_SHEET_ID" 替換為你的 Google Sheets ID
const sheet = SpreadsheetApp.openById("YOUR_SHEET_ID").getSheetByName("YouBike2.0_松仁路121巷口");
const data = sheet.getDataRange().getValues();
// 取得資料的標題
const headers = data[0];
const rows = data.slice(1);
// 將資料轉換為 JSON 格式
let jsonData = rows.map(row => {
let entry = {};
headers.forEach((header, index) => {
entry[header] = row[index];
});
return entry;
});
// 找到最新的一筆資料
const latestEntry = jsonData.reduce((latest, current) => {
return new Date(new Date(current.infoTime).getTime() + 8 * 60 * 60 * 1000) > new Date(new Date(latest.infoTime).getTime() + 8 * 60 * 60 * 1000) ? current : latest;
}, jsonData[0]);
// 建立 HTML 頁面
const template = HtmlService.createTemplateFromFile('Dashboard');
template.data = JSON.stringify(jsonData);
template.latestEntry = JSON.stringify(latestEntry);
return template.evaluate()
.setTitle('YouBike Dashboard')
.setWidth(1200)
.setHeight(800);
}
// 這段函數是為了載入其他 HTML 文件
function include(filename) {
return HtmlService.createHtmlOutputFromFile(filename).getContent();
}
Dashboard.html
<!DOCTYPE html>
<html>
<head>
<title>YouBike Dashboard</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/moment"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-moment"></script>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f0f0f0;
margin: 20px;
}
#chart-container {
width: 100%;
max-width: 800px;
margin: 20px auto;
}
#controls {
text-align: center;
margin-bottom: 20px;
}
#latest-info {
text-align: center;
background-color: #ffffff;
border-radius: 10px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
padding: 20px;
margin: 20px auto;
width: 80%;
max-width: 600px;
}
#latest-info h2 {
margin: 0;
color: #333;
}
#latest-info p {
font-size: 1.5em;
margin: 10px 0;
color: #2b8a3e;
}
</style>
</head>
<body>
<h1>YouBike Dashboard</h1>
<div id="controls">
<label for="startDate">開始日期:</label>
<input type="date" id="startDate">
<label for="endDate">結束日期:</label>
<input type="date" id="endDate">
<label for="dataType">資料類型:</label>
<select id="dataType">
<option value="available_rent_bikes">可租借車輛數</option>
<option value="available_return_bikes">可還車位數</option>
</select>
<button onclick="updateChart()">更新圖表</button>
</div>
<div id="latest-info">
<h2>最新資訊</h2>
<p id="latestInfoTime"></p>
<p id="latestAvailableRentBikes"></p>
</div>
<div id="chart-container">
<canvas id="availabilityChart"></canvas>
</div>
<script>
let chart;
let jsonData = JSON.parse('<?= data ?>');
let latestEntry = JSON.parse('<?= latestEntry ?>');
function updateChart() {
const startDateInput = document.getElementById('startDate');
const endDateInput = document.getElementById('endDate');
// 預設為過去七天
if (!startDateInput.value || !endDateInput.value) {
const today = new Date();
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(today.getDate() - 7);
startDateInput.value = sevenDaysAgo.toISOString().split('T')[0];
endDateInput.value = today.toISOString().split('T')[0];
}
const startDate = new Date(new Date(startDateInput.value).getTime() + 8 * 60 * 60 * 1000);
const endDate = new Date(new Date(endDateInput.value).getTime() + 8 * 60 * 60 * 1000);
const dataType = document.getElementById('dataType').value;
const filteredData = jsonData.filter(entry => {
const infoTime = new Date(entry.infoTime);
return infoTime >= startDate && infoTime <= endDate;
});
const labels = filteredData.map(entry => new Date(entry.infoTime));
const dataValues = filteredData.map(entry => entry[dataType]);
if (chart) {
chart.destroy();
}
const ctx = document.getElementById('availabilityChart').getContext('2d');
chart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: dataType === 'available_rent_bikes' ? '可租借車輛數' : '可還車位數',
data: dataValues,
borderColor: 'rgba(75, 192, 192, 1)',
fill: false
}
]
},
options: {
responsive: true,
scales: {
x: {
type: 'time',
time: {
unit: 'hour',
displayFormats: {
hour: 'YYYY-MM-DD HH:mm'
}
},
title: {
display: true,
text: '日期與時間'
}
},
y: {
title: {
display: true,
text: '數量'
}
}
}
}
});
// 顯示最新的資訊
document.getElementById('latestInfoTime').innerText = `更新時間: ${new Date(latestEntry.infoTime).toLocaleString('zh-TW', { timeZone: 'Asia/Taipei' })}`;
document.getElementById('latestAvailableRentBikes').innerText = `可租借車輛數: ${latestEntry.available_rent_bikes}`;
}
document.addEventListener('DOMContentLoaded', function() {
updateChart();
});
</script>
</body>
</html>
ChatGPT Canvas 心得
鐵人賽進行到一半,
ChatGPT 突然就有了新功能:ChatGPT 4.0 with Canvas
ChatGPT 4.0 with Canvas 是一種升級版本的 ChatGPT,
具有一個特別的「畫布」功能,
可以用來進行文檔創作和編輯。
它可以把創建的內容(如文章、代碼等)顯示在畫布區域中,
這樣用戶可以更方便地查看、編輯和與 AI 互動,
進行反覆修改和改進。
這個功能非常適合用於編寫長篇文字或代碼時,
使你可以直觀地看到整個文本的結構和變化,
而不必在單一對話框中滾動尋找內容。
你可以將這些創作結果進一步修改和保存,
以便在不同的階段反覆檢查和完善。
講起來很抽象,
但是在請 ChatGPT 寫 code 的時候真的非常方便!
我在開發本篇教學的程式充份感受到了!
目前是付費版 ChatGPT Plus 才可以用,
不知道未來會不會全面開放。
已知未解問題
不知為何,從 YouBike 抓來的資料有時候會卡在過去的某一個時間點,
而且可能一卡就是好幾個小時。
例如都已經10月8日了,打 API 抓到的資料卻還顯示為9/30。
用一模一樣的抓法反覆測試,又有機會成功抓到當下最新的資料。
但再用一模一樣的抓法再試一次,又有可能抓到舊資料。
不定期發生,我還抓不準問題的成因。
關於這個問題,我也根據這個頁面的資訊,
寫信請教機關聯絡人陳心恩 ([email protected])
不過寄了兩次都未獲回應。
也許打電話會比較有機會。