Open Flash Chart (OFC) 曾是 PHP 產生互動式圖表的熱門選擇,雖然 Flash 技術已淘汰,但其「PHP 產生 JSON、前端渲染」的架構思想仍具參考價值。本文回顧 OFC 整合方式,並提供現代化 JavaScript 圖表庫替代方案對照表。
傳統 OFC 架構:
┌─────────────┐ JSON ┌──────────────┐ SWF ┌─────────────┐
│ PHP 後端 │ ───────────▶ │ 瀏覽器 │ ───────────▶ │ Flash Player │
│ 產生資料 │ data-file │ 載入 SWF │ 渲染圖表 │ (已淘汰) │
└─────────────┘ └──────────────┘ └─────────────┘
│ │
│ 替代: get-data 回呼函數 │ 替代: JS 圖表庫
▼ ▼
┌─────────────┐ ┌──────────────┐
│ PHP API │ │ Chart.js │
│ 回傳 JSON │ │ ECharts │
└─────────────┘ └──────────────┘
<?php
// api/chart_data.php
header('Content-Type: application/json; charset=utf-8');
/**
* 通用圖表資料結構 (適用於 Chart.js, ECharts, ApexCharts 等)
*/
class ChartData {
public array $labels = [];
public array $datasets = [];
public ?string $title = null;
public function addDataset(string $label, array $data, array $options = []): self {
$this->datasets[] = array_merge([
'label' => $label,
'data' => $data,
'borderWidth' => 2,
'fill' => false,
], $options);
return $this;
}
public function setLabels(array $labels): self {
$this->labels = $labels;
return $this;
}
public function toJson(): string {
return json_encode([
'labels' => $this->labels,
'datasets' => $this->datasets,
'title' => $this->title,
], JSON_UNESCAPED_UNICODE | JSON_NUMERIC_CHECK);
}
}
// 範例:銷售趨勢圖
$chart = new ChartData();
$chart->title = '2024 年月銷售趨勢';
$chart->setLabels(['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月']);
$chart->addDataset('線上銷售', [120, 132, 101, 134, 90, 230, 210, 225, 180, 195, 210, 240], [
'borderColor' => 'rgb(54, 162, 235)',
'backgroundColor' => 'rgba(54, 162, 235, 0.1)',
'tension' => 0.3,
]);
$chart->addDataset('實體門市', [80, 95, 88, 110, 105, 150, 140, 160, 130, 145, 155, 170], [
'borderColor' => 'rgb(255, 99, 132)',
'backgroundColor' => 'rgba(255, 99, 132, 0.1)',
'tension' => 0.3,
]);
echo $chart->toJson();
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>銷售趨勢圖 - Chart.js</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<style>
.chart-container { max-width: 800px; margin: 2rem auto; }
</style>
</head>
<body>
<div class="chart-container">
<canvas id="salesChart"></canvas>
</div>
<script>
async function loadChart() {
try {
const response = await fetch('/api/chart_data.php');
const data = await response.json();
const ctx = document.getElementById('salesChart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: data.labels,
datasets: data.datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: data.title,
font: { size: 18 }
},
legend: { position: 'top' },
tooltip: { mode: 'index', intersect: false }
},
scales: {
y: { beginAtZero: true, title: { display: true, text: '銷售額 (萬元)' } },
x: { title: { display: true, text: '月份' } }
},
interaction: { mode: 'nearest', axis: 'x', intersect: false }
}
});
} catch (e) {
console.error('圖表載入失敗:', e);
}
}
loadChart();
</script>
</body>
</html>
<div id="echart-bar" style="width: 100%; height: 400px;"></div>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<script>
async function loadEChart() {
const res = await fetch('/api/chart_data.php');
const data = await res.json();
const chart = echarts.init(document.getElementById('echart-bar'));
chart.setOption({
title: { text: data.title, left: 'center' },
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
legend: { top: '5%', data: data.datasets.map(d => d.label) },
toolbox: {
show: true,
feature: {
saveAsImage: {},
dataView: { readOnly: false },
restore: {},
dataZoom: { yAxisIndex: 'none' }
}
},
dataZoom: [{ type: 'inside' }, { type: 'slider', bottom: 0 }],
xAxis: { type: 'category', data: data.labels },
yAxis: { type: 'value', name: '銷售額' },
series: data.datasets.map(ds => ({
name: ds.label,
type: 'bar',
data: ds.data,
itemStyle: { color: ds.backgroundColor || ds.borderColor },
emphasis: { focus: 'series' }
}))
});
// 響應式調整
window.addEventListener('resize', () => chart.resize());
}
loadEChart();
</script>
<?php
// api/realtime_data.php - Server-Sent Events (SSE)
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
$lastId = isset($_SERVER['HTTP_LAST_EVENT_ID']) ? (int)$_SERVER['HTTP_LAST_EVENT_ID'] : 0;
// 模擬即時資料來源
function getLatestMetrics(int $sinceId): array {
// 實際從 Redis/DB 讀取
return [
'id' => $sinceId + 1,
'timestamp' => date('H:i:s'),
'cpu' => rand(10, 90),
'memory' => rand(30, 85),
'requests' => rand(100, 5000),
];
}
while (true) {
$data = getLatestMetrics($lastId);
echo "id: {$data['id']}\n";
echo "data: " . json_encode($data) . "\n\n";
@ob_flush();
@flush();
$lastId = $data['id'];
sleep(2); // 2 秒推送一次
// 檢查客戶端連線
if (connection_aborted()) break;
}
<!-- 前端 SSE 接收 -->
<script>
const evtSource = new EventSource('/api/realtime_data.php');
const chart = /* Chart.js 實例 */;
evtSource.onmessage = function(e) {
const data = JSON.parse(e.data);
// 滾動更新:移除最舊、加入最新
if (chart.data.labels.length > 20) {
chart.data.labels.shift();
chart.data.datasets.forEach(ds => ds.data.shift());
}
chart.data.labels.push(data.timestamp);
chart.data.datasets[0].data.push(data.cpu);
chart.data.datasets[1].data.push(data.memory);
chart.update('none'); // 無動畫更新
};
evtSource.onerror = () => console.log('SSE 連線中斷,嘗試重連...');
</script>
| 需求 | Chart.js | ECharts | ApexCharts | Highcharts | uPlot |
|---|---|---|---|---|---|
| 授權 | MIT | Apache 2.0 | MIT | 商業付費 | MIT |
| 包大小 (gz) | ~60KB | ~150KB | ~80KB | ~100KB+ | ~15KB |
| 圖表類型 | 8 種基礎 | 20+ 種豐富 | 15+ 種 | 最完整 | 時間序列專精 |
| 大數據量 | ~5k 點 | ~100k 點 | ~50k 點 | ~100k+ | 百萬級 (Canvas/WebGL) |
| 互動功能 | 基本 | 極豐富 (dataZoom, brush) | 良好 | 最強 | 基礎 |
| TypeScript | 內建 | 內建 | 內建 | 內建 | 內建 |
| 框架整合 | Vue/React/Svelte 官方包 | Vue/React 官方包 | Vue/React 官方包 | 官方包 | 需自行封裝 |
| SSR 支援 | 佳 | 佳 (Node 渲染) | 佳 | 佳 | 需處理 |
| 推薦場景 | 通用儀表板、中小專案 | 企業級 BI、複雜視覺化 | SaaS 產品、漸層動畫 | 金融/科研/商業報表 | 即時監控、高頻資料 |
<?php
/**
* 圖表資料 API 基礎類別
* 統一輸出格式、快取、權限、錯誤處理
*/
abstract class ChartApiController {
protected int $cacheTtl = 300; // 5 分鐘快取
public function handleRequest(): void {
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: public, max-age=' . $this->cacheTtl);
header('Access-Control-Allow-Origin: *'); // 視需求調整
try {
// 權限檢查
if (!$this->authorize()) {
http_response_code(403);
echo json_encode(['error' => 'Forbidden']);
return;
}
// 參數驗證
$params = $this->validateParams($_GET);
// 快取鍵
$cacheKey = 'chart:' . $this->getChartType() . ':' . md5(json_encode($params));
$data = $this->getFromCache($cacheKey);
if ($data === null) {
$data = $this->buildChartData($params);
$this->saveToCache($cacheKey, $data, $this->cacheTtl);
}
echo $this->formatResponse($data);
} catch (Throwable $e) {
http_response_code(500);
error_log('Chart API Error: ' . $e->getMessage());
echo json_encode(['error' => 'Internal server error']);
}
}
abstract protected function getChartType(): string;
abstract protected function buildChartData(array $params): array;
abstract protected function validateParams(array $input): array;
protected function authorize(): bool {
return true; // 子類別覆寫
}
protected function getFromCache(string $key): ?string {
// Redis/Memcached/APCu 實作
return null;
}
protected function saveToCache(string $key, string $data, int $ttl): void {}
protected function formatResponse(array $data): string {
return json_encode($data, JSON_UNESCAPED_UNICODE | JSON_NUMERIC_CHECK);
}
}
// 具體實作
class SalesTrendChartController extends ChartApiController {
protected function getChartType(): string { return 'sales_trend'; }
protected function validateParams(array $input): array {
return [
'year' => $input['year'] ?? date('Y'),
'department' => $input['dept'] ?? 'all',
];
}
protected function buildChartData(array $params): array {
// 從 DB 查詢...
$labels = ['1月','2月','3月','4月','5月','6月'];
$online = [120, 132, 101, 134, 90, 230];
$offline = [80, 95, 88, 110, 105, 150];
return [
'labels' => $labels,
'datasets' => [
['label' => '線上', 'data' => $online, 'borderColor' => '#36a2eb'],
['label' => '實體', 'data' => $offline, 'borderColor' => '#ff6384'],
],
'title' => "{$params['year']} 年上半年銷售趨勢",
];
}
}
// 入口
$controller = new SalesTrendChartController();
$controller->handleRequest();
Open Flash Chart 的歷史使命已結束,但其後端產生資料規格、前端負責渲染的分離架構思想在現代圖表庫中完美傳承。選型建議:
PHP 端應專注於資料查詢、權限控制、快取策略、統一輸出格式,避免在後端處理圖表樣式配置,保持前後端職責清晰分離。