同步至個人站點:ECharts 萬字入門指南
本文將作為你的 ECharts 入門指南,帶你系統性地梳理 ECharts 的核心知識體系,並透過一個貼近實際業務的“高三期末考試成績分析”案例,手把手教你如何將數據轉化為富有洞察力的圖表。
前端,本質上就是把數據可視化的技術。如何將枯燥的數據,以一種更直觀、更易於理解的方式呈現給用戶,始終是一個重要的課題。
圖表,無疑是這個問題的最佳答案之一。
小到健康 App 中記錄一周睡眠變化的柱狀圖,大到金融應用的實時股票儀表板,圖表作為一種強大的視覺語言,其傳遞信息的效率遠超純粹的文字或表格。它能幫助我們快速發現數據中的模式、趨勢和異常點。
在眾多選擇中,Apache ECharts 無疑是當下最閃耀的產品之一。憑藉其豐富的圖表類型、強大的交互功能、靈活的配置項以及活躍的社區,成為了全球前端開發者的首選。
然而,和 GSAP 動畫庫 一樣,很多人對 ECharts 的第一印象是“配置項繁多”、“學習曲線陡峭”。但事實果真如此嗎?
當我們靜下心來,深入探索 ECharts 的世界,會發現其核心概念並不複雜。萬變不離其宗,所有的圖表配置,都圍繞著一些固定的核心模塊展開:容器與大小、樣式、數據集、坐標軸、視覺映射、圖例等等。
ECharts 官網提供了海量的示例,幾乎涵蓋了你能想到的所有圖表效果。甚至,它的配置項文檔本身就是可交互的——你可以一邊修改配置,一邊實時預覽效果。這極大地降低了學習門檻。
學習任何一門技術的最好方式,就是從一個“Hello World”開始。
我們將使用最簡單的方式:通過 CDN 引入。你只需要在 HTML 文件中加入一個 <script>
標籤即可。這種方式無需構建工具,非常適合快速原型開發和學習。
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/echarts.min.js"></script>
將下面的代碼完整複製到一個 HTML 文件中,然後用瀏覽器打開它,你就能看到你的第一個 ECharts 圖表了。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>ECharts 入門示例</title>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/echarts.min.js"></script>
</head>
<body>
<div id="main" style="width: 600px;height:400px;"></div>
<script type="text/javascript">
// 3. 基於準備好的dom,初始化echarts實例
var myChart = echarts.init(document.getElementById("main"));
// 4. 指定圖表的配置項和數據
var option = {
// 標題
title: {
text: "我的第一個 ECharts 圖表",
},
// 提示框
tooltip: {},
// X 軸
xAxis: {
data: ["襯衫", "羊毛衫", "雪紡衫", "褲子", "高跟鞋", "襪子"],
},
// Y 軸
yAxis: {},
// 系列(系列決定了圖表類型和數據)
series: [
{
name: "銷量",
type: "bar", // 'bar' 表示這是一個柱狀圖
data: [5, 20, 36, 10, 10, 20],
},
],
};
// 5. 使用剛指定的配置項和數據顯示圖表。
myChart.setOption(option);
</script>
</body>
</html>
這個簡單的例子,已經揭示了 ECharts 的核心工作流程:引入腳本 -> 準備容器 -> 初始化實例 -> 設置配置項。而所有 ECharts 圖表的實現,都蘊含在那個巨大的 option
對象中。
option
對象ECharts 的 option
對象是一個龐大而複雜的 JavaScript 對象,option
描述了圖表的一切:數據、樣式、交互、動畫等等。理解了它的核心構成,就等於掌握了 ECharts 的精髓。
讓我們像剝洋蔥一樣,一層一層地解析 option
的核心組件。
series
:系列與圖表類型series
是 ECharts 中最重要的配置項,沒有之一。它是一個數組,數組中的每一個對象都代表一個“系列”。
什麼是“系列”?
你可以把它理解為一組相關的數據,以及這組數據如何被可視化的規則。一個圖表中可以包含多個系列,每個系列會按照自己的規則(即 type
)繪製成圖。
比如,在上面的例子中:
series: [
{
name: "銷量",
type: "bar", // 圖表類型
data: [5, 20, 36, 10, 10, 20], // 系列數據
},
],
type: 'bar'
告訴 ECharts,這個系列要繪製成柱狀圖。data: [...]
提供了這組柱狀圖的具體數值。如果我想在同一個圖表中,再增加一條折線圖來表示“產量”,只需在 series
數組中再增加一個對象即可。
series: [
{
name: "銷量",
type: "bar",
data: [5, 20, 36, 10, 10, 20],
},
{
name: "產量",
type: "line", // 圖表類型為折線圖
data: [15, 25, 30, 18, 15, 30],
},
],
ECharts 支持的圖表類型非常豐富,常見的有:
line
: 折線圖bar
: 柱狀圖pie
: 餅圖scatter
: 散點圖radar
: 雷達圖map
: 地圖tree
: 樹圖graph
: 關係圖series
的強大之處在於,它不僅定義了圖表的“形”,更承載了圖表的“魂”——數據。
xAxis
& yAxis
:坐標軸對於絕大多數圖表(如折線圖、柱狀圖、散點圖),坐標軸是必不可少的。ECharts 通過 xAxis
(X 軸) 和 yAxis
(Y 軸) 來進行配置。它們通常成對出現,共同定義了一個直角坐標系 (Grid)。
坐標軸主要由軸線、刻度、刻度標籤、軸名稱等部分組成。
一個常見的 xAxis
配置如下:
xAxis: {
type: 'category', // 坐標軸類型
data: ['一月', '二月', '三月', '四月', '五月'], // 類目數據
name: '月份', // 軸名稱
axisLine: { show: true }, // 顯示軸線
axisTick: { show: true }, // 顯示刻度
axisLabel: {
color: '#333',
fontSize: 12
} // 刻度標籤樣式
},
坐標軸的 type
是一個關鍵屬性,它決定了坐標軸如何解析數據:
'value'
: 數值軸。適用於連續數據,會自動根據 series.data
的最大最小值來生成刻度。'category'
: 類目軸。適用於離散的類目數據,類目數據需要通過 xAxis.data
來指定。'time'
: 時間軸。適用於連續的時序數據,能自動格式化時間標籤。'log'
: 對數軸。適用於數據跨度非常大的情況。yAxis
的配置與 xAxis
類似。在一個圖表中,可以有多個 X 軸和 Y 軸,通過 xAxisIndex
和 yAxisIndex
來關聯 series
。
grid
:繪圖網格grid
組件定義了直角坐標系在圖表容器中的位置和大小。當你想調整圖表主體部分(不包括標題、圖例等)的時候,就需要配置它。
grid: {
left: '3%', // 網格區域離容器左側的距離
right: '4%',
bottom: '3%',
top: '10%',
containLabel: true // 防止標籤溢出
},
containLabel: true
是一個非常實用的配置,它會自動計算坐標軸標籤的寬度,並調整 grid
的位置,以確保標籤能夠完整顯示。
title
:標題一個圖表應該有一個明確的標題。title
組件用於配置主標題和副標題。
title: {
text: '網站月度訪問量', // 主標題
subtext: '數據來源:模擬數據', // 副標題
left: 'center', // 標題水平居中
textStyle: {
color: '#c23531',
fontSize: 20
}
},
tooltip
:提示框當鼠標懸浮到圖表的數據項上時,tooltip
組件可以顯示詳細的數據信息,這是圖表交互的重要一環。
tooltip: {
trigger: 'axis', // 觸發類型
axisPointer: { // 坐標軸指示器配置
type: 'shadow' // 默認為直線,可選為:'line' | 'shadow' | 'cross'
}
},
trigger
屬性決定了提示框的觸發方式:
'item'
: 數據項觸發。鼠標懸浮到單個數據項(如柱狀圖的某個柱子、餅圖的某個扇區)時觸發。'axis'
: 坐標軸觸發。鼠標懸浮到坐標軸的某個刻度上時,會同時顯示該刻度下所有系列的數據。常用於折線圖和柱狀圖。'none'
: 不觸發。你可以通過 formatter
函數來自定義提示框顯示的內容,支持 HTML 字串和回調函數,給予了極高的靈活度。
legend
:圖例當圖表包含多個系列時,legend
(圖例) 組件用於區分不同的系列。
legend: {
data: ['銷量', '產量'], // 圖例項的名稱,需要與 series 的 name 對應
top: 'bottom' // 圖例位置
},
legend
和 series
是通過 name
屬性關聯的。點擊圖例項,能控制對應系列的顯示和隱藏,這是 ECharts 內置的交互行為。
dataset
:數據集從 ECharts 4 開始,引入了 dataset
組件,這是一個非常重要的概念,它實現了數據與配置的分離。
在之前的例子中,我們的數據是直接寫在 series.data
裡的。當數據量較大,或者多個系列需要共用同一份數據時,這種方式就顯得很臃腫。
dataset
允許我們統一定義數據源,然後在 series
中通過 encode
來映射數據。
var option = {
dataset: {
// 提供一份數據。
source: [
["product", "2015", "2016", "2017"],
["Matcha Latte", 43.3, 85.8, 93.7],
["Milk Tea", 83.1, 73.4, 55.1],
["Cheese Cocoa", 86.4, 65.2, 82.5],
["Walnut Brownie", 72.4, 53.9, 39.1],
],
},
// 聲明一個 X 軸,類目軸(category)。默認情況下,類目軸對應到 dataset 第一列。
xAxis: { type: "category" },
// 聲明一個 Y 軸,數值軸。
yAxis: {},
// 聲明多個 series。
series: [{ type: "bar" }, { type: "bar" }, { type: "bar" }],
};
在這個例子中,dataset.source
定義了一個二維數組。ECharts 會默認將第一行/第一列作為維度(dimension)。series
中甚至不需要寫 data
,ECharts 會自動從 dataset
中取數據。第一列 product
映射到 xAxis
,後續的 '2015', '2016', '2017' 三列數據,依次映射到三個 series
。
使用 dataset
的好處是:
visualMap
)使用。data-transform
對數據進行篩選、聚合等預處理。強烈推薦在開發中優先使用 dataset
來管理數據。
理論知識總是枯燥的,讓我們通過一個綜合案例,將前面學到的知識點串聯起來。
背景:假設我們是某高中的數據分析師,拿到了一份高三年級本次期末考試的成績單。我們需要通過數據可視化的方式,從多個維度對這次考試進行分析,為教學工作提供數據支持。
數據維度:班級、學生姓名、各科成績(語文、數學、英語、物理、化學、生物)、總分。
分析目標:直觀對比各個班級的平均總分,了解班級間的整體學習水平差異。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>場景一:各班級平均總分對比</title>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/echarts.min.js"></script>
</head>
<body>
<div id="bar-chart" style="width: 100%; height: 500px;"></div>
<script type="text/javascript">
// --- 數據準備 ---
function generateMockData() {
const classes = [
"高三(1)班",
"高三(2)班",
"高三(3)班",
"高三(4)班",
"高三(5)班",
"高三(6)班",
];
const subjects = ["語文", "數學", "英語", "物理", "化學", "生物"];
const studentsPerClass = 50;
let data = [];
let studentId = 1;
classes.forEach((className) => {
for (let i = 0; i < studentsPerClass; i++) {
let student = { 班級: className, 姓名: `學生${studentId}` };
let totalScore = 0;
subjects.forEach((subject) => {
const score = Math.floor(Math.random() * 101) + 50;
student[subject] = score;
totalScore += score;
});
student["總分"] = totalScore;
data.push(student);
studentId++;
}
});
return data;
}
const examData = generateMockData();
function calculateClassAverage(data) {
const classScores = {};
data.forEach((student) => {
const className = student["班級"];
if (!classScores[className]) {
classScores[className] = { total: 0, count: 0 };
}
classScores[className].total += student["總分"];
classScores[className].count++;
});
const averageData = Object.keys(classScores).map((className) => ({
className: className,
averageScore: (
classScores[className].total / classScores[className].count
).toFixed(2),
}));
averageData.sort((a, b) => b.averageScore - a.averageScore);
return averageData;
}
const classAverageData = calculateClassAverage(examData);
// --- ECharts 配置 ---
var barChart = echarts.init(document.getElementById("bar-chart"));
var barChartOption = {
title: {
text: "高三各班級期末考試平均總分對比",
left: "center",
},
tooltip: { trigger: "axis", axisPointer: { type: "shadow" } },
grid: { left: "3%", right: "4%", bottom: "3%", containLabel: true },
xAxis: {
type: "category",
data: classAverageData.map((item) => item.className),
axisLabel: { interval: 0, rotate: 30 },
},
yAxis: { type: "value", name: "平均分" },
series: [
{
name: "平均總分",
type: "bar",
barWidth: "60%",
data: classAverageData.map((item) => item.averageScore),
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#83bff6" },
{ offset: 0.5, color: "#188df0" },
{ offset: 1, color: "#188df0" },
]),
},
emphasis: {
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#2378f7" },
{ offset: 0.7, color: "#2378f7" },
{ offset: 1, color: "#83bff6" },
]),
},
},
label: { show: true, position: "top" },
},
],
};
barChart.setOption(barChartOption);
</script>
</body>
</html>
透過這個柱狀圖,我們可以一目了然地看出哪個班級的平均成績最高,哪個最低。
分析目標:以高三(1)班為例,分析數學單科成績的分佈情況(如:優秀、良好、及格、不及格)。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>場景二:單科成績分佈</title>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/echarts.min.js"></script>
</head>
<body>
<div id="pie-chart" style="width: 100%; height: 500px;"></div>
<script type="text/javascript">
// --- 數據準備 (復用上面的 generateMockData) ---
function generateMockData() {
const classes = [
"高三(1)班",
"高三(2)班",
"高三(3)班",
"高三(4)班",
"高三(5)班",
"高三(6)班",
];
const subjects = ["語文", "數學", "英語", "物理", "化學", "生物"];
const studentsPerClass = 50;
let data = [];
let studentId = 1;
classes.forEach((className) => {
for (let i = 0; i < studentsPerClass; i++) {
let student = { 班級: className, 姓名: `學生${studentId}` };
let totalScore = 0;
subjects.forEach((subject) => {
const score = Math.floor(Math.random() * 101) + 50;
student[subject] = score;
totalScore += score;
});
student["總分"] = totalScore;
data.push(student);
studentId++;
}
});
return data;
}
const examData = generateMockData();
function analyzeSubjectDistribution(data, className, subject) {
const classData = data.filter(
(student) => student["班級"] === className
);
const distribution = {
"優秀 (120+)": 0,
"良好 (100-119)": 0,
"及格 (90-99)": 0,
"不及格 (<90)": 0,
};
classData.forEach((student) => {
const score = student[subject];
if (score >= 120) distribution["優秀 (120+)"]++;
else if (score >= 100) distribution["良好 (100-119)"]++;
else if (score >= 90) distribution["及格 (90-99)"]++;
else distribution["不及格 (<90)"]++;
});
return Object.keys(distribution).map((level) => ({
name: level,
value: distribution[level],
}));
}
const mathDistributionData = analyzeSubjectDistribution(
examData,
"高三(1)班",
"數學"
);
// --- ECharts 配置 ---
var pieChart = echarts.init(document.getElementById("pie-chart"));
var pieChartOption = {
title: {
text: "高三(1)班數學成績分佈",
subtext: "期末考試",
left: "center",
},
tooltip: {
trigger: "item",
formatter: "{a} <br/>{b} : {c}人 ({d}%)",
},
legend: {
orient: "vertical",
left: "left",
data: mathDistributionData.map((item) => item.name),
},
series: [
{
name: "成績分佈",
type: "pie",
radius: "50%",
center: ["50%", "60%"],
data: mathDistributionData,
// 可選:將 type: 'pie' 改為南丁格爾玫瑰圖,視覺效果更強
// roseType: 'area',
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: "rgba(0, 0, 0, 0.5)",
},
},
},
],
};
pieChart.setOption(pieChartOption);
</script>
</body>
</html>
餅圖清晰地展示了各個分數段的人數占比。如果將 series.roseType
設置為 'area'
,它會變成一個南丁格爾玫瑰圖,扇區的半徑會根據數據大小進行調整,視覺衝擊力更強。
分析目標:探究學生的物理成績和化學成績之間是否存在關聯性。比如,是不是物理好的學生,化學也普遍不錯?
::: details 完整代碼
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>場景三:理科綜合成績關聯性分析</title>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/echarts.min.js"></script>
</head>
<body>
<div id="scatter-chart" style="width: 100%; height: 500px;"></div>
<script type="text/javascript">
// --- 數據準備 (復用上面的 generateMockData) ---
function generateMockData() {
const classes = [
"高三(1)班",
"高三(2)班",
"高三(3)班",
"高三(4)班",
"高三(5)班",
"高三(6)班",
];
const subjects = ["語文", "數學", "英語", "物理", "化學", "生物"];
const studentsPerClass = 50;
let data = [];
let studentId = 1;
classes.forEach((className) => {
for (let i = 0; i < studentsPerClass; i++) {
let student = { 班級: className, 姓名: `學生${studentId}` };
let totalScore = 0;
subjects.forEach((subject) => {
const score = Math.floor(Math.random() * 101) + 50;
student[subject] = score;
totalScore += score;
});
student["總分"] = totalScore;
data.push(student);
studentId++;
}
});
return data;
}
const examData = generateMockData();
function getSubjectCorrelationData(data, subjectX, subjectY) {
// 返回格式: [物理成績, 化學成績, 學生姓名, 班級]
return data.map((student) => [
student[subjectX],
student[subjectY],
student["姓名"],
student["班級"],
]);
}
const correlationData = getSubjectCorrelationData(
examData,
"物理",
"化學"
);
// --- ECharts 配置 ---
var scatterChart = echarts.init(document.getElementById("scatter-chart"));
var scatterChartOption = {
title: {
text: "高三年級物理-化學成績關聯性分析",
left: "center",
},
grid: { left: "3%", right: "7%", bottom: "3%", containLabel: true },
xAxis: {
type: "value",
name: "物理成績",
splitLine: { lineStyle: { type: "dashed" } },
},
yAxis: {
type: "value",
name: "化學成績",
splitLine: { lineStyle: { type: "dashed" } },
},
tooltip: {
trigger: "item",
formatter: function (params) {
// params.data 是一個數組 [物理成績, 化學成績, 姓名, 班級]
return `${params.data[3]} - ${params.data[2]}<br/>物理: ${params.data[0]}<br/>化學: ${params.data[1]}`;
},
},
series: [
{
name: "學生",
type: "scatter",
symbolSize: 6,
data: correlationData,
itemStyle: { color: "rgba(25, 100, 150, 0.6)" },
},
],
};
scatterChart.setOption(scatterChartOption);
</script>
</body>
</html>
:::
透過觀察散點圖的分佈趨勢,我們可以做出初步判斷。如果點主要集中在從左下到右上的對角線區域,則說明物理和化學成績呈正相關關係。我們還可以輕易地發現那些“偏科”的異常點(比如物理很高但化學很低的學生)。
分析目標:選取總分排名前三的學生,用雷達圖對比他們的各科能力,分析他們的學科優勢與短板。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>場景四:尖子生各科能力模型</title>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/echarts.min.js"></script>
</head>
<body>
<div id="radar-chart" style="width: 100%; height: 500px;"></div>
<script type="text/javascript">
// --- 數據準備 (復用上面的 generateMockData) ---
function generateMockData() {
const classes = [
"高三(1)班",
"高三(2)班",
"高三(3)班",
"高三(4)班",
"高三(5)班",
"高三(6)班",
];
const subjects = ["語文", "數學", "英語", "物理", "化學", "生物"];
const studentsPerClass = 50;
let data = [];
let studentId = 1;
classes.forEach((className) => {
for (let i = 0; i < studentsPerClass; i++) {
let student = { 班級: className, 姓名: `學生${studentId}` };
let totalScore = 0;
subjects.forEach((subject) => {
const score = Math.floor(Math.random() * 101) + 50;
student[subject] = score;
totalScore += score;
});
student["總分"] = totalScore;
data.push(student);
studentId++;
}
});
return data;
}
const examData = generateMockData();
function getTopStudents(data, topCount) {
const sorted = data.sort((a, b) => b["總分"] - a["總分"]);
return sorted.slice(0, topCount);
}
const topStudents = getTopStudents(examData, 3);
const radarData = topStudents.map(student => ({
name: student["姓名"],
value: [
student["語文"],
student["數學"],
student["英語"],
student["物理"],
student["化學"],
student["生物"]
]
}));
// --- ECharts 配置 ---
var radarChart = echarts.init(document.getElementById("radar-chart"));
var radarChartOption = {
title: {
text: "尖子生各科能力模型",
left: "center",
},
legend: {
data: topStudents.map(student => student["姓名"]),
bottom: "0%",
},
radar: {
indicator: [
{ name: '語文', max: 100 },
{ name: '數學', max: 100 },
{ name: '英語', max: 100 },
{ name: '物理', max: 100 },
{ name: '化學', max: 100 },
{ name: '生物', max: 100 }
],
shape: 'circle',
splitNumber: 5,
name: {
textStyle: {
color: '#999',
},
},
splitLine: {
lineStyle: {
color: ['#999', '#999', '#999', '#999', '#999', '#999'],
}
},
splitArea: {
areaStyle: {
color: ['rgba(255, 255, 255, 0.1)', 'rgba(255, 255, 255, 0.5)'],
}
}
},
series: [{
type: 'radar',
data: radarData,
areaStyle: { normal: {} }
}]
};
radarChart.setOption(radarChartOption);
</script>
</body>
</html>
這樣,我們就可以清晰地看到尖子生在各科的表現,幫助他們明確自身的強項與弱項,以便於未來的學習規劃。