資料科學 視覺化 圖表 程式設計 網頁開發

amCharts 互動式圖表與儀表板設計指南

amCharts 視覺化指南

1. amCharts 簡介

amCharts 是一套功能強大的 JavaScript 圖表庫,專為建立互動式、美觀的數據視覺化而設計。它提供了廣泛的圖表類型,從基本的柱狀圖和折線圖到複雜的地圖和儀表板。

主要特點

  • 豐富的圖表類型:提供超過 40 種不同的圖表類型
  • 高度可定制:幾乎所有元素都可以自訂
  • 互動性:內建豐富的互動功能,如縮放、拖動和懸停效果
  • 響應式設計:自動適應不同尺寸的螢幕
  • 跨瀏覽器兼容:支援所有主流瀏覽器
  • 簡單的 API:易於理解和使用的 API

amCharts 資料科學視覺化指南

課程目標

  • 了解 amCharts 的基本概念與優勢。
  • 掌握 amCharts 4 和 amCharts 5 的主要差異。
  • 學會使用 amCharts 創建常見的資料科學圖表。
  • 熟悉 amCharts 的核心功能與特性,例如資料處理、樣式客製化、互動性等。
  • 能夠將 amCharts 應用於實際的資料科學視覺化場景。
  • 掌握尋找和利用 amCharts 學習資源的方法。

教材大綱 第一單元:初識 amCharts

  • 第一節:amCharts 簡介:一個全面的資料視覺化函式庫
    • 什麼是 amCharts?互動性與多功能性
    • 簡史與理念
    • amCharts 4 與 amCharts 5 主要版本差異概述
  • 重點整理: 了解 amCharts 的定義、主要特點、發展歷程以及 amCharts 4 和 5 的關鍵區別。

第二單元:快速入門:創建你的第一個 amCharts 圖表

  • 第二節:開始使用您的第一個 amCharts 視覺化
    • 先決條件:JavaScript 和 SVG 支援
    • 安裝與設定
    • 創建一個簡單的圓餅圖:程式碼範例 (amCharts 4)
    • 創建一個簡單的圓餅圖:程式碼範例 (amCharts 5)
  • 重點整理: 學習如何在 HTML 中引入 amCharts,以及使用 JavaScript 創建一個基本的圓餅圖 (分別針對 amCharts 4 和 5)。

第三單元:探索 amCharts 的核心功能與特性

  • 第三節:探索 amCharts 的核心功能與特性
    • 多樣的圖表類型
    • 主要功能與特性 (以 amCharts 5 為主)
    • 資料處理與格式化
  • 重點整理: 認識 amCharts 提供的豐富圖表類型,以及其在效能、客製化、互動性、可存取性和資料處理方面的主要特性。

第四單元:進階應用:客製化與互動

  • 第四節:深入探討:進階概念與客製化
    • 反應式設計
    • 主題與樣式
    • 互動性與事件
    • 即時資料更新
    • 與框架的整合
  • 重點整理: 學習如何讓圖表具有反應式設計,如何使用主題和樣式進行客製化,以及如何添加互動功能和處理即時資料更新。

第五單元:實戰演練:amCharts 的實際應用與使用案例

  • 第五節:amCharts 的實際應用與使用案例
    • 商業儀表板與分析
    • 金融資料視覺化
    • 專案管理與時程
    • 銷售與行銷分析
    • 地理資料視覺化
    • 即時監控與應用
  • 重點整理: 了解 amCharts 在不同領域的實際應用案例,包括商業分析、金融、專案管理、銷售行銷和地理資料視覺化。

第六單元:進階學習資源與社群

  • 第六節:amCharts 的進一步學習與開發資源
    • 官方文件
    • 互動式範例
    • 教學與指南
    • 支援論壇
    • Pluralsight 課程
    • 最新更新與消息
  • 重點整理: 掌握尋找和利用 amCharts 官方文件、範例、教學、論壇以及其他學習資源的方法。

第七單元:amCharts 5 實戰範例

  • 第七節:amCharts 5 實戰範例
    • 基本配置與核心概念
    • 基本圖表類型
    • 資料處理與格式化
    • 自訂圖表樣式
    • 座標軸與刻度
    • 圖例與標籤
    • 互動性與動畫
    • 複合圖表
    • 地圖視覺化
    • 儀表板與多圖表整合
    • 金融儀表板
  • 重點整理: 通過實戰範例,深入了解 amCharts 5 的各種功能和應用。

補充說明

  • 在第七單元中,我已補充了儀表板與多圖表整合,以及金融儀表板的完整實作指南,包含程式碼和詳細註解。
  • 建議您在學習過程中,多參考 amCharts 的官方網站 (https://www.amcharts.com/),以獲取最新的資訊和資源。
  • 透過實際操作和練習,將能更深入地掌握 amCharts 的應用技巧。

2. 基礎配置與核心概念

amCharts 5 的核心概念包括:

  • Root:最頂層的元素,所有的圖表都在這之下
  • Container:用於放置圖表的容器元素
  • Chart:圖表本身,如 XY 圖表、餅圖等
  • Series:數據系列,如線條、柱狀等
  • Axis:座標軸,用於定義數據的刻度和範圍

安裝與引入

<!-- 引入 amCharts 核心庫 -->
<script src="https://cdn.amcharts.com/lib/5/index.js"></script>
<!-- 引入圖表類型 -->
<script src="https://cdn.amcharts.com/lib/5/xy.js"></script>
<!-- 引入主題 -->
<script src="https://cdn.amcharts.com/lib/5/themes/Animated.js"></script>

基本例子

// 從 JSON 字串載入資料
const json_data = `{
  "x_values": [1, 2, 3, 4, 5],
  "y_values": [5, 10, 15, 20, 25]
}`;

const data = JSON.parse(json_data);

// 創建 root 元素
const root = am5.Root.new("chartdiv");

// 設置主題
root.setThemes([am5themes_Animated.new(root)]);

// 創建 XY 圖表
const chart = root.container.children.push(
  am5xy.XYChart.new(root, {
    panX: true,
    panY: true,
    wheelX: "panX",
    wheelY: "zoomX"
  })
);

// 創建 X 軸
const xAxis = chart.xAxes.push(
  am5xy.ValueAxis.new(root, {
    renderer: am5xy.AxisRendererX.new(root, {}),
    min: 0
  })
);

// 創建 Y 軸
const yAxis = chart.yAxes.push(
  am5xy.ValueAxis.new(root, {
    renderer: am5xy.AxisRendererY.new(root, {})
  })
);

// 創建數據系列
const series = chart.series.push(
  am5xy.LineSeries.new(root, {
    name: "Series",
    xAxis: xAxis,
    yAxis: yAxis,
    valueYField: "y",
    valueXField: "x",
    tooltip: am5.Tooltip.new(root, {
      labelText: "{valueY}"
    })
  })
);

// 處理數據
const seriesData = [];
for (let i = 0; i < data.x_values.length; i++) {
  seriesData.push({
    x: data.x_values[i],
    y: data.y_values[i]
  });
}

// 設置數據
series.data.setAll(seriesData);

// 添加游標
chart.set("cursor", am5xy.XYCursor.new(root, {}));

// 添加滾動條
chart.set("scrollbarX", am5.Scrollbar.new(root, {
  orientation: "horizontal"
}));

3. 基本圖表類型

折線圖 (XYChart)

// 從 JSON 字串載入資料
const json_data = `{
  "data": [
    {"date": "2023-01-01", "value": 100},
    {"date": "2023-02-01", "value": 120},
    {"date": "2023-03-01", "value": 115},
    {"date": "2023-04-01", "value": 130},
    {"date": "2023-05-01", "value": 145}
  ]
}`;

const data = JSON.parse(json_data);

// 創建 root 元素
const root = am5.Root.new("chartdiv");
root.setThemes([am5themes_Animated.new(root)]);

// 創建圖表
const chart = root.container.children.push(
  am5xy.XYChart.new(root, {
    panX: true,
    panY: true,
    wheelX: "panX",
    wheelY: "zoomX"
  })
);

// 創建 X 軸
const xAxis = chart.xAxes.push(
  am5xy.DateAxis.new(root, {
    baseInterval: { timeUnit: "day", count: 1 },
    renderer: am5xy.AxisRendererX.new(root, {})
  })
);

// 創建 Y 軸
const yAxis = chart.yAxes.push(
  am5xy.ValueAxis.new(root, {
    renderer: am5xy.AxisRendererY.new(root, {})
  })
);

// 創建數據系列
const series = chart.series.push(
  am5xy.LineSeries.new(root, {
    name: "Series",
    xAxis: xAxis,
    yAxis: yAxis,
    valueYField: "value",
    valueXField: "date",
    tooltip: am5.Tooltip.new(root, {
      labelText: "{valueY}"
    })
  })
);

// 設置數據
series.data.setAll(data.data);

// 添加游標
chart.set("cursor", am5xy.XYCursor.new(root, {}));

// 添加圖例
chart.children.push(
  am5.Legend.new(root, {
    centerX: am5.p50,
    x: am5.p50
  })
);

柱狀圖

// 從 JSON 字串載入資料
const json_data = `{
  "data": [
    {"category": "A", "value": 10},
    {"category": "B", "value": 15},
    {"category": "C", "value": 8},
    {"category": "D", "value": 12}
  ]
}`;

const data = JSON.parse(json_data);

// 創建 root 元素
const root = am5.Root.new("chartdiv");
root.setThemes([am5themes_Animated.new(root)]);

// 創建圖表
const chart = root.container.children.push(
  am5xy.XYChart.new(root, {
    panX: false,
    panY: false,
    wheelX: "none",
    wheelY: "none"
  })
);

// 創建 X 軸
const xAxis = chart.xAxes.push(
  am5xy.CategoryAxis.new(root, {
    categoryField: "category",
    renderer: am5xy.AxisRendererX.new(root, {
      cellStartLocation: 0.1,
      cellEndLocation: 0.9
    })
  })
);

xAxis.data.setAll(data.data);

// 創建 Y 軸
const yAxis = chart.yAxes.push(
  am5xy.ValueAxis.new(root, {
    renderer: am5xy.AxisRendererY.new(root, {})
  })
);

// 創建數據系列
const series = chart.series.push(
  am5xy.ColumnSeries.new(root, {
    name: "Series",
    xAxis: xAxis,
    yAxis: yAxis,
    valueYField: "value",
    categoryXField: "category",
    tooltip: am5.Tooltip.new(root, {
      labelText: "{valueY}"
    })
  })
);

// 設置數據
series.data.setAll(data.data);

// 添加標籤
series.columns.template.adapters.add("fill", (fill, target) => {
  return chart.get("colors").getIndex(series.columns.indexOf(target));
});

// 添加圖例
chart.children.push(
  am5.Legend.new(root, {
    centerX: am5.p50,
    x: am5.p50
  })
);

餅圖

// 從 JSON 字串載入資料
const json_data = `{
  "data": [
    {"category": "類別 A", "value": 30},
    {"category": "類別 B", "value": 25},
    {"category": "類別 C", "value": 15},
    {"category": "類別 D", "value": 10},
    {"category": "類別 E", "value": 20}
  ]
}`;

const data = JSON.parse(json_data);

// 創建 root 元素
const root = am5.Root.new("chartdiv");
root.setThemes([am5themes_Animated.new(root)]);

// 創建圖表
const chart = root.container.children.push(
  am5percent.PieChart.new(root, {
    layout: root.verticalLayout,
    innerRadius: am5.percent(50) // 若要創建環形圖
  })
);

// 創建數據系列
const series = chart.series.push(
  am5percent.PieSeries.new(root, {
    valueField: "value",
    categoryField: "category",
    endAngle: 270
  })
);

// 設置數據
series.data.setAll(data.data);

// 添加圖例
const legend = chart.children.push(
  am5.Legend.new(root, {
    centerX: am5.percent(50),
    x: am5.percent(50),
    marginTop: 15,
    marginBottom: 15
  })
);

legend.data.setAll(series.dataItems);

// 添加標籤
series.labels.template.set("visible", false);
series.ticks.template.set("visible", false);

// 添加動畫
series.appear(1000, 100);

雷達圖

// 從 JSON 字串載入資料
const json_data = `{
  "data": [
    {"category": "銷售", "value": 80, "full": 100},
    {"category": "行銷", "value": 65, "full": 100},
    {"category": "開發", "value": 90, "full": 100},
    {"category": "客服", "value": 75, "full": 100},
    {"category": "人力資源", "value": 60, "full": 100}
  ]
}`;

const data = JSON.parse(json_data);

// 創建 root 元素
const root = am5.Root.new("chartdiv");
root.setThemes([am5themes_Animated.new(root)]);

// 創建圖表
const chart = root.container.children.push(
  am5radar.RadarChart.new(root, {
    panX: false,
    panY: false,
    wheelX: "panX",
    wheelY: "zoomX"
  })
);

// 創建雷達軸
const xAxis = chart.xAxes.push(
  am5xy.CategoryAxis.new(root, {
    renderer: am5radar.AxisRendererCircular.new(root, {
      minGridDistance: 30
    }),
    categoryField: "category"
  })
);

xAxis.data.setAll(data.data);

// 創建數值軸
const yAxis = chart.yAxes.push(
  am5xy.ValueAxis.new(root, {
    renderer: am5radar.AxisRendererRadial.new(root, {})
  })
);

// 創建數據系列
const series = chart.series.push(
  am5radar.RadarLineSeries.new(root, {
    name: "Series",
    xAxis: xAxis,
    yAxis: yAxis,
    valueYField: "value",
    categoryXField: "category",
    fill: am5.color(0x5555ff),
    stroke: am5.color(0x5555ff)
  })
);

series.strokes.template.set("strokeWidth", 2);
series.bullets.push(function() {
  return am5.Bullet.new(root, {
    sprite: am5.Circle.new(root, {
      radius: 5,
      fill: series.get("fill")
    })
  });
});

// 設置數據
series.data.setAll(data.data);

// 添加背景系列
const series2 = chart.series.push(
  am5radar.RadarLineSeries.new(root, {
    name: "Series",
    xAxis: xAxis,
    yAxis: yAxis,
    valueYField: "full",
    categoryXField: "category",
    stroke: am5.color(0xdddddd),
    fill: am5.color(0xdddddd)
  })
);

series2.data.setAll(data.data);

4. 資料處理與格式化

amCharts 提供了多種方式來處理和格式化數據,包括直接 JSON 字串、API 請求和代理伺服器。

數據加載模式對照表

加載方式 適用場景 範例 API 端點
直接 JSON 字串 靜態小數據 雷達圖範例中的 json_data
遠端 API 請求 動態更新數據 https://api.marketdata.com/nvda
代理伺服器 跨域請求解決方案 /json-proxy?url=外部API

從 API 獲取數據

// 初始化圖表
let chart = am5xy.XYChart.new(root, {
  panX: true,
  panY: true
});

// 加載遠程數據
chart.dataSource.url = "https://api.example.com/line-data";
chart.dataSource.parser = new am5xy.JSONParser();

// 創建坐標軸
let xAxis = chart.xAxes.push(am5xy.DateAxis.new(root, {}));
let yAxis = chart.yAxes.push(am5xy.ValueAxis.new(root, {}));

// 創建折線序列
let series = chart.series.push(am5xy.LineSeries.new(root, {
  name: "銷售額",
  xAxis: xAxis,
  yAxis: yAxis,
  valueYField: "value",
  valueXField: "date"
}));

處理跨域 (CORS) 問題

當遇到跨域限制時,可以使用代理伺服器來解決問題:

// 使用代理伺服器獲取數據
chart.dataSource.url = "/json-proxy?url=https://external-api.com/data";
chart.dataSource.parser = new am5xy.JSONParser();

5. 自訂圖表樣式

amCharts 提供了豐富的樣式自訂選項,讓您可以根據需求調整圖表的外觀。

主題設定

// 引入主題
root.setThemes([
  am5themes_Animated.new(root),
  am5themes_Dark.new(root) // 使用深色主題
]);

// 創建自訂主題
let myTheme = am5.Theme.new(root);
myTheme.rule("Grid").setAll({
  strokeWidth: 0.5,
  strokeOpacity: 0.5
});

// 應用自訂主題
root.setThemes([
  am5themes_Animated.new(root),
  myTheme
]);

顏色設定

// 設定自訂顏色
chart.get("colors").set("colors", [
  am5.color(0x5555FF),
  am5.color(0xFF5555),
  am5.color(0x55FF55),
  am5.color(0xFFFF55)
]);

// 為特定系列設定顏色
series.set("fill", am5.color(0x5555FF));
series.set("stroke", am5.color(0x5555FF));

// 根據數據值設定顏色
series.columns.template.adapters.add("fill", (fill, target) => {
  const value = target.dataItem.get("valueY");
  if (value < 10) {
    return am5.color(0xFF0000);
  } else if (value < 20) {
    return am5.color(0xFFFF00);
  } else {
    return am5.color(0x00FF00);
  }
});

6. 座標軸與刻度

座標軸是圖表中顯示數據刻度的重要元素,amCharts 提供了多種自訂選項。

座標軸類型

// 數值軸
const valueAxis = chart.yAxes.push(
  am5xy.ValueAxis.new(root, {
    min: 0, // 最小值
    max: 100, // 最大值
    strictMinMax: true, // 嚴格限制最大最小值
    renderer: am5xy.AxisRendererY.new(root, {})
  })
);

// 類別軸
const categoryAxis = chart.xAxes.push(
  am5xy.CategoryAxis.new(root, {
    categoryField: "category",
    renderer: am5xy.AxisRendererX.new(root, {})
  })
);

// 日期軸
const dateAxis = chart.xAxes.push(
  am5xy.DateAxis.new(root, {
    baseInterval: { timeUnit: "day", count: 1 }, // 基本間隔為1天
    renderer: am5xy.AxisRendererX.new(root, {})
  })
);

// 持續時間軸
const durationAxis = chart.yAxes.push(
  am5xy.DurationAxis.new(root, {
    baseUnit: "minute", // 基本單位為分鐘
    renderer: am5xy.AxisRendererY.new(root, {})
  })
);

刻度與網格線

// 設定 X 軸網格線
xAxis.get("renderer").grid.template.setAll({
  stroke: am5.color(0x000000),
  strokeOpacity: 0.1,
  strokeWidth: 1
});

// 設定 Y 軸網格線
yAxis.get("renderer").grid.template.setAll({
  stroke: am5.color(0x000000),
  strokeOpacity: 0.1,
  strokeWidth: 1
});

// 設定刻度樣式
xAxis.get("renderer").labels.template.setAll({
  fill: am5.color(0x666666),
  fontSize: 12
});

// 設定座標軸標題
xAxis.set("title", am5.Label.new(root, {
  text: "日期",
  fill: am5.color(0x000000),
  fontSize: 16,
  fontWeight: "bold"
}));

yAxis.set("title", am5.Label.new(root, {
  text: "銷售額",
  fill: am5.color(0x000000),
  fontSize: 16,
  fontWeight: "bold",
  rotation: -90
}));

7. 圖例與標籤

圖例和標籤可以幫助用戶更好地理解圖表中的數據。

圖例配置

// 添加圖例
const legend = chart.children.push(
  am5.Legend.new(root, {
    centerX: am5.p50, // 水平置中
    x: am5.p50, // 水平置中
    marginTop: 15,
    marginBottom: 15
  })
);

// 設定圖例數據
legend.data.setAll(chart.series.values);

// 自訂圖例樣式
legend.labels.template.setAll({
  fontSize: 14,
  fill: am5.color(0x666666)
});

legend.markers.template.setAll({
  width: 16,
  height: 16
});

// 互動設定:點擊圖例項目顯示/隱藏相應系列
legend.itemContainers.template.events.on("click", (e) => {
  const series = e.target.dataItem.dataContext;
  if (!series.isHidden()) {
    series.hide();
  } else {
    series.show();
  }
});

數據標籤

// 柱狀圖標籤
series.columns.template.setAll({
  tooltipText: "{categoryX}: {valueY}",
  tooltipY: 0,
  strokeOpacity: 0
});

// 為柱狀圖添加數據標籤
series.columns.template.adapters.add("fill", function(fill, target) {
  return chart.get("colors").getIndex(series.columns.indexOf(target));
});

// 折線圖標籤
series.bullets.push(function() {
  return am5.Bullet.new(root, {
    locationY: 0,
    sprite: am5.Label.new(root, {
      text: "{valueY}",
      fill: am5.color(0x000000),
      centerY: am5.p50,
      centerX: am5.p50,
      populateText: true
    })
  });
});

// 餅圖標籤
pieSeries.labels.template.setAll({
  fontSize: 12,
  text: "{category}: {value}",
  radius: 10
});

pieSeries.ticks.template.setAll({
  forceHidden: false,
  strokeOpacity: 0.5
});

8. 互動性與動畫

amCharts 提供了豐富的互動性和動畫選項,使圖表更加生動有趣。

游標與縮放

// 添加游標
const cursor = chart.set("cursor", am5xy.XYCursor.new(root, {
  behavior: "zoomX", // 僅水平方向縮放
  xAxis: xAxis,
  yAxis: yAxis
}));

// 游標樣式
cursor.lineX.set("visible", true);
cursor.lineX.set("stroke", am5.color(0x000000));
cursor.lineX.set("strokeDasharray", [2, 2]);

cursor.lineY.set("visible", true);
cursor.lineY.set("stroke", am5.color(0x000000));
cursor.lineY.set("strokeDasharray", [2, 2]);

// 添加縮放按鈕
chart.set("zoomOutButton", am5.Button.new(root, {
  x: am5.p100,
  y: 0,
  themeTags: ["zoom"],
  icon: am5.Graphics.new(root, {
    themeTags: ["icon"],
    stroke: am5.color(0x000000),
    strokeWidth: 2,
    draw: function(display) {
      display.moveTo(0, 0);
      display.lineTo(16, 0);
      display.moveTo(0, 8);
      display.lineTo(16, 8);
      display.moveTo(0, 16);
      display.lineTo(16, 16);
    }
  })
}));

動畫效果

// 設置圖表動畫
chart.appear(1000, 100);

// 設置系列動畫
series.appear(1000, 100);

// 設置柱狀圖動畫
series.columns.template.setAll({
  cornerRadiusTL: 5,
  cornerRadiusTR: 5,
  strokeOpacity: 0
});

// 自訂動畫設置
series.set("sequencedInterpolation", true); // 序列化動畫
series.set("sequencedDelay", 100); // 動畫延遲
series.set("easing", am5.ease.cubicOut); // 動畫過渡效果

// 設置滑鼠懸停效果
series.columns.template.states.create("hover", {
  scale: 1.1,
  fill: am5.color(0xFF0000)
});

9. 複合圖表

複合圖表結合了多種圖表類型,可以在一個視圖中展示不同維度的數據。

多軸圖表

// 創建主軸 (左側)
const yAxis = chart.yAxes.push(
  am5xy.ValueAxis.new(root, {
    renderer: am5xy.AxisRendererY.new(root, {})
  })
);

// 創建第二軸 (右側)
const yAxis2 = chart.yAxes.push(
  am5xy.ValueAxis.new(root, {
    renderer: am5xy.AxisRendererY.new(root, {
      opposite: true // 顯示在右側
    })
  })
);

// 創建折線圖系列 (使用左側軸)
const lineSeries = chart.series.push(
  am5xy.LineSeries.new(root, {
    name: "銷售額",
    xAxis: xAxis,
    yAxis: yAxis,
    valueYField: "sales",
    valueXField: "date",
    stroke: am5.color(0x5555FF),
    fill: am5.color(0x5555FF)
  })
);

// 創建柱狀圖系列 (使用右側軸)
const columnSeries = chart.series.push(
  am5xy.ColumnSeries.new(root, {
    name: "利潤",
    xAxis: xAxis,
    yAxis: yAxis2,
    valueYField: "profit",
    valueXField: "date",
    stroke: am5.color(0xFF5555),
    fill: am5.color(0xFF5555)
  })
);

// 設置軸標題
yAxis.set("title", am5.Label.new(root, {
  text: "銷售額",
  rotation: -90,
  y: am5.p50,
  centerX: am5.p50
}));

yAxis2.set("title", am5.Label.new(root, {
  text: "利潤",
  rotation: 90,
  y: am5.p50,
  centerX: am5.p50
}));

混合圖表

// JSON 數據
const json_data = `{
  "data": [
    {"date": "2025-01", "value1": 100, "value2": 120, "value3": 70},
    {"date": "2025-02", "value1": 110, "value2": 130, "value3": 85},
    {"date": "2025-03", "value1": 130, "value2": 100, "value3": 60},
    {"date": "2025-04", "value1": 105, "value2": 110, "value3": 90}
  ]
}`;

const data = JSON.parse(json_data);

// 創建根元素
const root = am5.Root.new("chartdiv");
root.setThemes([am5themes_Animated.new(root)]);

// 創建圖表
const chart = root.container.children.push(
  am5xy.XYChart.new(root, {
    panX: true,
    panY: true,
    wheelX: "panX",
    wheelY: "zoomX"
  })
);

// 創建 X 軸
const xAxis = chart.xAxes.push(
  am5xy.CategoryAxis.new(root, {
    categoryField: "date",
    renderer: am5xy.AxisRendererX.new(root, {}),
    tooltip: am5.Tooltip.new(root, {})
  })
);

xAxis.data.setAll(data.data);

// 創建 Y 軸
const yAxis = chart.yAxes.push(
  am5xy.ValueAxis.new(root, {
    renderer: am5xy.AxisRendererY.new(root, {})
  })
);

// 創建柱狀圖系列
const columnSeries = chart.series.push(
  am5xy.ColumnSeries.new(root, {
    name: "收入",
    xAxis: xAxis,
    yAxis: yAxis,
    valueYField: "value1",
    categoryXField: "date",
    tooltip: am5.Tooltip.new(root, {
      labelText: "{name}: {valueY}"
    })
  })
);

columnSeries.columns.template.setAll({
  width: am5.percent(80),
  tooltipY: 0
});

columnSeries.data.setAll(data.data);

// 創建折線圖系列
const lineSeries1 = chart.series.push(
  am5xy.LineSeries.new(root, {
    name: "利潤",
    xAxis: xAxis,
    yAxis: yAxis,
    valueYField: "value2",
    categoryXField: "date",
    stroke: am5.color(0xFF5555),
    fill: am5.color(0xFF5555),
    tooltip: am5.Tooltip.new(root, {
      labelText: "{name}: {valueY}"
    })
  })
);

lineSeries1.strokes.template.setAll({
  strokeWidth: 2
});

// 添加圓點
lineSeries1.bullets.push(function() {
  return am5.Bullet.new(root, {
    sprite: am5.Circle.new(root, {
      radius: 4,
      fill: lineSeries1.get("fill")
    })
  });
});

lineSeries1.data.setAll(data.data);

// 創建第二個折線圖系列
const lineSeries2 = chart.series.push(
  am5xy.LineSeries.new(root, {
    name: "成本",
    xAxis: xAxis,
    yAxis: yAxis,
    valueYField: "value3",
    categoryXField: "date",
    stroke: am5.color(0x55FF55),
    fill: am5.color(0x55FF55),
    tooltip: am5.Tooltip.new(root, {
      labelText: "{name}: {valueY}"
    })
  })
);

lineSeries2.strokes.template.setAll({
  strokeWidth: 2,
  strokeDasharray: [2, 2]
});

lineSeries2.bullets.push(function() {
  return am5.Bullet.new(root, {
    sprite: am5.Circle.new(root, {
      radius: 4,
      fill: lineSeries2.get("fill")
    })
  });
});

lineSeries2.data.setAll(data.data);

// 添加圖例
const legend = chart.children.push(
  am5.Legend.new(root, {
    centerX: am5.percent(50),
    x: am5.percent(50)
  })
);
legend.data.setAll(chart.series.values);

// 添加游標
chart.set("cursor", am5xy.XYCursor.new(root, {
  behavior: "zoomX"
}));

10. 地圖視覺化

amCharts 提供了強大的地圖視覺化功能,可以創建交互式的地理數據展示。

// 創建根元素
const root = am5.Root.new("chartdiv");
root.setThemes([am5themes_Animated.new(root)]);

// 創建地圖
const chart = root.container.children.push(
  am5map.MapChart.new(root, {
    panX: "rotateX",
    panY: "rotateY",
    projection: am5map.geoMercator()
  })
);

// 創建世界地圖多邊形系列
const polygonSeries = chart.series.push(
  am5map.MapPolygonSeries.new(root, {
    geoJSON: am5geodata_worldLow,
    exclude: ["AQ"] // 排除南極洲
  })
);

// 設置多邊形填充色
polygonSeries.mapPolygons.template.setAll({
  tooltipText: "{name}",
  toggleKey: "active",
  interactive: true,
  fill: am5.color(0xDDDDDD)
});

// 設置懸停狀態
polygonSeries.mapPolygons.template.states.create("hover", {
  fill: am5.color(0xAAAAAA)
});

// 設置活動狀態
polygonSeries.mapPolygons.template.states.create("active", {
  fill: am5.color(0x5555FF)
});

// 添加熱力數據系列
const heatSeries = chart.series.push(
  am5map.MapPolygonSeries.new(root, {
    geoJSON: am5geodata_worldLow,
    valueField: "value",
    calculateAggregates: true
  })
);

// 設置熱力圖顏色
heatSeries.mapPolygons.template.setAll({
  tooltipText: "{name}: {value}",
  interactive: true
});

// 設置熱力圖數據
heatSeries.data.setAll([
  { id: "TW", value: 100 },
  { id: "US", value: 75 },
  { id: "JP", value: 60 },
  { id: "DE", value: 50 },
  { id: "FR", value: 45 },
  { id: "UK", value: 40 }
]);

// 設置熱力圖顏色範圍
heatSeries.set("heatRules", [{
  target: heatSeries.mapPolygons.template,
  dataField: "value",
  min: am5.color(0xFFFFFF),
  max: am5.color(0xFF0000),
  key: "fill"
}]);

// 添加縮放控制
chart.set("zoomControl", am5map.ZoomControl.new(root, {}));

// 初始化地圖位置
chart.appear(1000, 100);

11. 儀表板與多圖表整合

整合多個圖表可創建功能強大的數據儀表板,以下示範垂直/水平混合布局即時數據聯動

// 創建根元素並啟用動畫主題
const root = am5.Root.new("dashboard");
root.setThemes([am5themes_Animated.new(root)]);

// 主容器設定 (垂直布局)
const container = root.container.children.push(
  am5.Container.new(root, {
    width: am5.percent(100),
    height: am5.percent(100),
    layout: root.verticalLayout, // 垂直排列子元素
    paddingTop: 10,
    paddingBottom: 10
  })
);

//=== 第一行:關鍵指標區 (水平布局) ===//
const topRow = container.children.push(
  am5.Container.new(root, {
    width: am5.percent(100),
    height: am5.percent(30),  // 佔儀表板30%高度
    layout: root.horizontalLayout, // 水平排列圖表
    paddingLeft: 15,
    paddingRight: 15
  })
);

// 元件1:即時業績圓餅圖
const pieChart = topRow.children.push(
  am5pie.PieChart.new(root, {
    width: am5.percent(50),   // 佔行寬50%
    height: am5.percent(100),
    radius: am5.percent(75),  // 控制餅圖大小
    innerRadius: am5.percent(40) // 創建環形效果
  })
);

// 圓餅數據系列
const pieSeries = pieChart.series.push(
  am5pie.PieSeries.new(root, {
    name: "即時業績分布",
    valueField: "value",
    categoryField: "category",
    alignLabels: true  // 自動調整標籤位置
  })
);

// 設定樣式與互動
pieSeries.slices.template.setAll({
  stroke: am5.color(0xffffff),
  strokeWidth: 2,
  templateField: "sliceSettings" // 允許個別設定樣式
});

pieSeries.labels.template.setAll({
  text: "{category}: [bold]{valuePercentTotal.formatNumber('0.0')}%[/]",
  fontSize: 12,
  fill: am5.color(0x555555)
});

// 載入範例數據
pieSeries.data.setAll([
  { category: "北美", value: 45, sliceSettings: { fill: am5.color(0x4CAF50) } },
  { category: "歐洲", value: 30, sliceSettings: { fill: am5.color(0x2196F3) } },
  { category: "亞太", value: 25, sliceSettings: { fill: am5.color(0xFF9800) } }
]);

// 元件2:即時效能儀表板
const gaugeChart = topRow.children.push(
  am5radar.RadarChart.new(root, {
    width: am5.percent(50),
    height: am5.percent(100),
    innerRadius: am5.percent(60) // 控制儀表板中心空白區域
  })
);

// 儀表板軸設定
const axisRenderer = am5radar.AxisRendererCircular.new(root, {
  innerRadius: -0.5,  // 指針旋轉中心點
  strokeOpacity: 0.8,
  strokeWidth: 2
});

const valueAxis = gaugeChart.xAxes.push(
  am5xy.ValueAxis.new(root, {
    min: 0,
    max: 100,
    strictMinMax: true,
    renderer: axisRenderer
  })
);

// 指針系列設定
const gaugeSeries = gaugeChart.series.push(
  am5radar.ClockHand.new(root, {
    animationDuration: 1000,  // 指針動畫時間(毫秒)
    pinRadius: 15,  // 指針樞紐半徑
    radius: am5.percent(80), // 指針長度
    value: 75  // 初始數值
  })
);

// 動態更新指針 (範例)
setInterval(() => {
  gaugeSeries.set("value", Math.random() * 100);
}, 3000);

//=== 第二行:趨勢分析區 ===//
const bottomRow = container.children.push(
  am5.Container.new(root, {
    width: am5.percent(100),
    height: am5.percent(70),  // 佔儀表板70%高度
    layout: root.verticalLayout,
    paddingTop: 10
  })
);

// 元件3:互動式折線圖
const lineChart = bottomRow.children.push(
  am5xy.XYChart.new(root, {
    width: am5.percent(100),
    height: am5.percent(100),
    panX: true,
    panY: true,
    wheelX: "panX",
    wheelY: "zoomX"
  })
);

// 時間軸設定
const dateAxis = lineChart.xAxes.push(
  am5xy.DateAxis.new(root, {
    baseInterval: { timeUnit: "day", count: 1 },
    renderer: am5xy.AxisRendererX.new(root, {
      minGridDistance: 50  // 最小刻度間距(像素)
    })
  })
);

// 數值軸設定
const valueAxisLine = lineChart.yAxes.push(
  am5xy.ValueAxis.new(root, {
    extraMin: 0.1,  // 防止數據貼底
    renderer: am5xy.AxisRendererY.new(root, {})
  })
);

// 折線系列設定
const lineSeries = lineChart.series.push(
  am5xy.LineSeries.new(root, {
    name: "即時趨勢",
    xAxis: dateAxis,
    yAxis: valueAxisLine,
    valueYField: "value",
    valueXField: "date",
    stroke: am5.color(0x673AB7),
    tooltip: am5.Tooltip.new(root, {
      labelText: "[{categoryX.formatDate('yyyy-MM-dd')}] 數值: {valueY}",
      pointerOrientation: "horizontal"
    })
  })
);

// 生成模擬時間序列數據
const generateLineData = () => {
  const data = [];
  let value = 50;
  const today = new Date();

  for (let i = -30; i <= 0; i++) {
    const date = new Date(today);
    date.setDate(date.getDate() + i);

    value += (Math.random() - 0.5) * 10;
    data.push({
      date: date.getTime(),
      value: Math.round(value)
    });
  }
  return data;
};

lineSeries.data.setAll(generateLineData());

// 添加十字線
lineChart.set("cursor", am5xy.XYCursor.new(root, {
  behavior: "zoomX",
  xAxis: dateAxis,
  yAxis: valueAxisLine
}));

// 添加滾動條
const scrollbar = lineChart.set("scrollbarX", am5xy.XYChartScrollbar.new(root, {
  orientation: "horizontal",
  height: 50
}));
scrollbar.series.push(lineSeries);

儀表板技術亮點

  1. 混合布局系統

    • 使用 verticalLayouthorizontalLayout 嵌套實現複雜布局
    • 百分比尺寸設定 (am5.percent()) 確保響應式顯示
  2. 數據聯動機制

// 點擊餅圖時更新折線圖數據
pieSeries.slices.template.events.on("click", (ev) => {
  const category = ev.target.dataItem.get("category");
  lineSeries.data.setAll(generateRegionData(category));
});
  1. 樣式模板化
// 統一設定工具提示樣式
am5.Tooltip.rootStyles.setAll({
  fontSize: "0.8em",
  fill: am5.color(0xFFFFFF),
  stroke: am5.color(0x000000)
});
  1. 性能優化
// 啟用數據處理加速
root.autoResize = true;
root.fps = 60; // 限制幀率節省資源

// 關閉非焦點圖表的動畫
lineChart.series.template.set("forceHidden", true);
root.events.on("focus", () => {
  lineChart.series.template.set("forceHidden", false);
});

儀表板擴充建議

  1. 添加控制元件
// 創建數據刷新按鈕
const refreshBtn = container.children.push(am5.Button.new(root, {
  labelText: "即時更新",
  x: am5.percent(95),
  y: am5.percent(98),
  background: am5.RoundedRectangle.new(root, {
    fill: am5.color(0x2196F3)
  })
}));

refreshBtn.events.on("click", () => {
  pieSeries.data.setAll(generateNewPieData());
});
  1. 跨圖表互動
// 同步顯示工具提示
lineChart.get("cursor").lineX.set("visible", true);
pieChart.set("tooltip", lineChart.get("tooltip"));
  1. 狀態保存
// 保存當前縮放狀態
let initialSelection;
lineChart.events.on("selected", (ev) => {
  initialSelection = ev.target.get("selection");
});

整合關鍵點說明

  1. 容器層級管理:使用 container.children.push() 嚴格控制元素疊加順序
  2. 資源回收機制:需在卸載時手動清理對象
// 卸載儀表板時
root.dispose();

完整實作教學

一、準備工作

  1. 建立 Replit 專案

    • 登入 Replit
    • 建立一個新的 HTML、CSS、JS 項目
  2. 安裝 amCharts 5

    <script src="https://cdn.amcharts.com/lib/5/index.js"></script>
    <script src="https://cdn.amcharts.com/lib/5/percent.js"></script>
    <script src="https://cdn.amcharts.com/lib/5/radar.js"></script>
    <script src="https://cdn.amcharts.com/lib/5/themes/Animated.js"></script>
    

二、HTML 結構

index.html 中設置儀表板容器:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>amCharts 儀表板</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      margin: 0;
      padding: 0;
    }
    #dashboard {
      width: 100%;
      height: 100vh;
      display: flex;
      flex-direction: column;
    }
    .row {
      display: flex;
      flex: 1;
    }
    .chart {
      flex: 1;
      margin: 10px;
    }
  </style>
</head>
<body>
  <div id="dashboard">
    <div class="row">
      <div id="pieChart" class="chart"></div>
      <div id="gaugeChart" class="chart"></div>
    </div>
    <div class="row">
      <div id="lineChart" class="chart"></div>
    </div>
  </div>
  <script src="script.js"></script>
</body>
</html>

三、JavaScript 實現 (儀表板邏輯)

1. 初始化 amCharts 核心
// 引入 amCharts 主題
am5.ready(function() {
  const root = am5.Root.new("dashboard");
  root.setThemes([am5themes_Animated.new(root)]);
});
2. 圓餅圖 (Pie Chart)
const pieChart = am5percent.PieChart.new(root, {
  layout: root.horizontalLayout
});

const pieSeries = pieChart.series.push(am5percent.PieSeries.new(root, {
  valueField: "value",
  categoryField: "category"
}));

pieSeries.data.setAll([
  { category: "北美", value: 45 },
]);

amCharts 金融儀表板實作指南

核心需求

  1. 整合股價K線圖 (Candlestick)交易量柱狀圖
  2. 添加技術指標面板 (RSI、MACD)
  3. 實現多圖表聯動互動
  4. 支援即時數據更新
  5. 使用正體中文註解

技術規格

index.html
├─ styles.css
└─ script.js

實作步驟

1. 基礎架構

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>即時金融儀表板</title>
    <style>
        /* 自適應容器 */
        #dashboard {
            width: 100vw;
            height: 100vh;
            font-family: "Microsoft JhengHei", sans-serif;
        }
    </style>
</head>
<body>
    <div id="dashboard"></div>

    <!-- amCharts 核心依賴 -->
    <script src="https://cdn.amcharts.com/lib/5/index.js"></script>
    <script src="https://cdn.amcharts.com/lib/5/stock.js"></script>
    <script src="https://cdn.amcharts.com/lib/5/themes/Animated.js"></script>
    <script src="script.js"></script>
</body>
</html>

2. 樣式優化

/* styles.css */
.am5-chart-div {
    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
    border-radius: 8px;
    margin: 10px;
}

/* 手機端響應式 */
@media (max-width: 600px) {
    #dashboard {
        flex-direction: column;
    }
}

3. 儀表板核心邏輯

// script.js
am5.ready(function() {
    //▇▇ 根容器設定 ▇▇
    const root = am5.Root.new("dashboard");
    root.setThemes([am5themes_Animated.new(root)]);

    //▇▇ 即時數據模擬器 ▇▇
    function generateMockData() {
        const data = [];
        let value = 500;
        const date = new Date();

        for (let i = -100; i <= 0; i++) {
            const newDate = new Date(date);
            newDate.setDate(date.getDate() + i);

            const open = value;
            const close = open + (Math.random() - 0.5) * 20;
            const high = Math.max(open, close) + Math.random() * 10;
            const low = Math.min(open, close) - Math.random() * 10;

            data.push({
                date: newDate.getTime(),
                open: open.toFixed(2),
                high: high.toFixed(2),
                low: low.toFixed(2),
                close: close.toFixed(2),
                volume: Math.round(Math.random() * 1e6)
            });

            value = close;
        }
        return data;
    }

    //▇▇ 主圖表組建 ▇▇
    const stockChart = am5stock.StockChart.new(root, {
        layout: root.verticalLayout
    });

    //➊ K線面板
    const mainPanel = stockChart.panels.push(am5stock.StockPanel.new(root, {
        height: 0.6,
        panX: true,
        panY: true
    }));

    //➋ 交易量面板
    const volumePanel = stockChart.panels.push(am5stock.StockPanel.new(root, {
        height: 0.2
    }));

    //▇▇ 即時更新機制 ▇▇
    setInterval(() => {
        const newData = generateMockData();
        stockChart.data.setAll([{
            category: "NVDA",
            data: newData
        }]);
    }, 5000); // 每5秒更新

    // 添加蜡烛序列
    let candleSeries = mainPanel.series.push(am5stock.CandlestickSeries.new(root, {
        name: "NVDA",
        xAxis: dateAxis,
        highField: "high",
        lowField: "low",
        openField: "open",
        closeField: "close"
    }));

    // 添加交易量柱狀圖
    let volumeSeries = volumePanel.series.push(am5stock.ColumnSeries.new(root, {
        name: "交易量",
        xAxis: dateAxis,
        valueYField: "volume"
    }));

    // 添加RSI指標
    const rsiPanel = stockChart.panels.push(am5stock.StockPanel.new(root, {
        height: 0.1
    }));
    const rsiSeries = rsiPanel.series.push(am5stock.LineSeries.new(root, {
        name: "RSI",
        valueYField: "rsi",
        valueXField: "date"
    }));

    // 添加MACD指標
    const macdPanel = stockChart.panels.push(am5stock.StockPanel.new(root, {
        height: 0.1
    }));
    const macdSeries = macdPanel.series.push(am5stock.LineSeries.new(root, {
        name: "MACD",
        valueYField: "macd",
        valueXField: "date"
    }));

    // 初始化日期軸
    const dateAxis = mainPanel.xAxes.push(am5xy.DateAxis.new(root, {
        baseInterval: { timeUnit: "day", count: 1 },
        renderer: am5xy.AxisRendererX.new(root, {})
    }));

    // 初始化數據
    const initialData = generateMockData();
    stockChart.data.setAll([{
        category: "NVDA",
        data: initialData
    }]);
});

4. 技術指標計算

// 在generateMockData()中添加RSI與MACD計算
function calculateRSI(data) {
    const rsiValues = [];
    for (let i = 0; i < data.length; i++) {
        if (i < 14) {
            rsiValues.push(null); // 頭14天無法計算RSI
        } else {
            const gains = [];
            const losses = [];
            for (let j = i - 14; j <= i; j++) {
                const gain = data[j].close - data[j - 1].close;
                if (gain > 0) {
                    gains.push(gain);
                } else {
                    losses.push(-gain);
                }
            }
            const avgGain = gains.reduce((a, b) => a + b, 0) / gains.length;
            const avgLoss = losses.reduce((a, b) => a + b, 0) / losses.length;
            const rsi = 100 - (100 / (1 + avgGain / avgLoss));
            rsiValues.push(rsi);
        }
    }
    return rsiValues;
}

function calculateMACD(data) {
    const macdValues = [];
    const ema12 = [];
    const ema26 = [];

    for (let i = 0; i < data.length; i++) {
        if (i < 12) {
            ema12.push(null);
        } else {
            const sum = data.slice(i - 12, i).reduce((a, b) => a + b.close, 0);
            ema12.push(sum / 12);
        }

        if (i < 26) {
            ema26.push(null);
        } else {
            const sum = data.slice(i - 26, i).reduce((a, b) => a + b.close, 0);
            ema26.push(sum / 26);
        }

        if (i >= 26) {
            const macd = ema12[i] - ema26[i];
            macdValues.push(macd);
        } else {
            macdValues.push(null);
        }
    }
    return macdValues;
}

// 更新generateMockData()以包含RSI與MACD
function generateMockData() {
    const data = [];
    let value = 500;
    const date = new Date();

    for (let i = -100; i <= 0; i++) {
        const newDate = new Date(date);
        newDate.setDate(date.getDate() + i);

        const open = value;
        const close = open + (Math.random() - 0.5) * 20;
        const high = Math.max(open, close) + Math.random() * 10;
        const low = Math.min(open, close) - Math.random() * 10;

        data.push({
            date: newDate.getTime(),
            open: open.toFixed(2),
            high: high.toFixed(2),
            low: low.toFixed(2),
            close: close.toFixed(2),
            volume: Math.round(Math.random() * 1e6)
        });

        value = close;
    }

    const rsiValues = calculateRSI(data);
    const macdValues = calculateMACD(data);

    for (let i = 0; i < data.length; i++) {
        data[i].rsi = rsiValues[i];
        data[i].macd = macdValues[i];
    }

    return data;
}

部署說明

部署指令

npm install http-server -g && http-server -p 3000

預期成果

  • ✅ 即時更新股價走勢圖
  • ✅ 雙Y軸交易量顯示
  • ✅ 移動平均線(MA5/MA20)疊加
  • ✅ RSI技術指標面板
  • ✅ 支援觸控縮放操作

除錯提示

如遇到 CORS 錯誤,添加代理中間件:

chart.dataSource.url = "/proxy?url=REAL_API_ENDPOINT";

進階功能擴充建議

  1. 增加主題切換按鈕(日間/夜間模式)
  2. 實作數據導出功能(PNG/CSV)
  3. 整合新聞事件標記(用於財報公布時點)
  4. 添加自訂指標計算器
  5. 建立歷史數據回測模組

參考資源