In [1]:
%%html
<style>
.jp-Cell.jp-CodeCell .jp-Cell-inputWrapper {
display: none;
}
</style>
台灣產業及社福移工人數 - 按國籍區分¶
圓餅圖的不足¶
- 比較困難:難以精確比較不同國籍移工數量,尤其比例接近時視覺區分不明。
- 無法呈現趨勢:僅顯示特定月份數據,無法反映長期變化。
- 小比例國籍被忽視:小國籍區域過小,可能被忽略,影響資訊完整性。
以下將呈現不同圖表方式,以更清晰呈現台灣產業及社福移工人數按國籍區分的資訊。
In [2]:
%%html
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@100..900&display=swap');
body {
font-family: "Noto Sans TC", Calibri, Arial, sans-serif;
font-size: 16px;
width: 100%;
}
#stacked-area-chart {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
font-size: 1em;
}
#stacked-area-chart svg {
width: 90%;
}
.stacked-area-chart-tooltip {
pointer-events: none;
font-size: 0.75em;
background: rgba(255, 255, 255, 0.7);
border: 1px solid #ccc;
color: #333;
padding: 5px;
border-radius: 3px;
}
</style>
<!-- Stacked Area Chart (START) -->
<div id="stacked-area-chart">
<svg></svg>
</div>
<script type="module">
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
// // Example JSON data
// let dataJson = [
// { "l": "印尼", "x": "2021-10", "y": 240628 },
// { "l": "馬來西亞", "x": "2021-10", "y": 7 },
// { "l": "菲律賓", "x": "2021-10", "y": 144074 },
// { "l": "泰國", "x": "2021-10", "y": 57316 },
// { "l": "越南", "x": "2021-10", "y": 238491 },
// { "l": "蒙古", "x": "2021-10", "y": 0 },
// { "l": "印尼", "x": "2021-11", "y": 238787 },
// { "l": "馬來西亞", "x": "2021-11", "y": 7 },
// { "l": "菲律賓", "x": "2021-11", "y": 143181 },
// { "l": "泰國", "x": "2021-11", "y": 57185 },
// { "l": "越南", "x": "2021-11", "y": 236511 },
// { "l": "蒙古", "x": "2021-11", "y": 0 },
// { "l": "印尼", "x": "2021-12", "y": 237168 },
// { "l": "馬來西亞", "x": "2021-12", unz"y": 7 },
// { "l": "菲律賓", "x": "2021-12", "y": 141808 },
// { "l": "泰國", "x": "2021-12", "y": 56954 },
// { "l": "越南", "x": "2021-12", "y": 234054 },
// { "l": "蒙古", "x": "2021-12", "y": 0 }
// ];
const DATA_JSON_URL = "https://raw.githubusercontent.com" +
"/AsherJingkongChen/d3-worker-plot-tw/refs/heads/main/data.json";
let dataJson = JSON.parse(await (await fetch(DATA_JSON_URL)).text());
// Parse the date and organize data
const parseDate = d3.timeParse("%Y-%m");
const data = Array.from(
d3.group(dataJson, d => parseDate(d.x)),
([key, values]) => {
const obj = { date: key };
values.forEach(d => {
obj[d.l] = d.y >= 100 ? d.y : 0; // Remove outliers by setting y < 100 to 0
});
return obj;
}
).sort((a, b) => d3.ascending(a.date, b.date));
// Sort keys (labels) by average y value and remove keys with average y < 100
const keys = Array.from(new Set(dataJson.map(d => d.l)))
.filter(key => {
const avg = d3.mean(dataJson.filter(d => d.l === key), d => d.y);
return avg >= 100;
})
.sort((a, b) => {
const avgA = d3.mean(dataJson.filter(d => d.l === a), d => d.y);
const avgB = d3.mean(dataJson.filter(d => d.l === b), d => d.y);
return d3.descending(avgA, avgB);
});
// Set up the dimensions and margins
const margin = { top: 50, right: 150, bottom: 50, left: 70 };
const width = 930 - margin.left - margin.right;
const height = 520 - margin.top - margin.bottom;
// Create SVG container
const svg = d3.select("#stacked-area-chart")
.select("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom + 50)
.attr("viewBox", `0 0 ${width + margin.left + margin.right} ${height + margin.top + margin.bottom + 50}`)
.attr("style", "max-width: 100%; height: auto;")
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// Set up color scale with increased lightness
const color = d3.scaleOrdinal()
.domain(keys)
.range(d3.schemeTableau10.map((color) => {
color = d3.color(color);
return color.brighter(0.4);
}));
// Set up stack generator
const stack = d3.stack()
.keys(keys)
.order(d3.stackOrderNone)
.offset(d3.stackOffsetNone);
const series = stack(data);
// Set up scales
const x = d3.scaleTime()
.domain(d3.extent(data, d => d.date))
.range([0, width]);
const y = d3.scaleLinear()
.domain([0, d3.max(series, layer => d3.max(layer, d => d[1]))])
.nice()
.range([height, 0]);
// Create axes
const xAxis = d3.axisBottom(x).tickValues(x.ticks(12)).tickFormat(d3.timeFormat("%Y-%m"));
const yAxis = d3.axisLeft(y).tickValues(y.ticks(6));
// Add Y grid lines
svg.append("g")
.call(yAxis)
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick line")
.clone()
.attr("x2", width)
.attr("stroke-opacity", 0.1))
.append("text")
.attr("x", -margin.left)
.attr("y", margin.top / 2)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.style("font-size", "1.5em")
.style("font-weight", "bold")
.text("人數");
// Add X axis
svg.append("g")
.attr("transform", `translate(0,${height})`)
.call(xAxis)
.selectAll("text")
.attr("transform", "rotate(-53)")
.style("text-anchor", "end")
// Add X axis label
svg.append("text")
.attr("x", width)
.attr("y", height + margin.bottom * 1.5) // Position below the X-axis
.attr("fill", "currentColor")
.attr("text-anchor", "end")
.style("font-size", "1em")
.style("font-weight", "bold")
.text("時間 (年 - 月)");
// Define the area generator
const area = d3.area()
.x(d => x(d.data.date))
.y0(d => y(d[0]))
.y1(d => y(d[1]))
.curve(d3.curveMonotoneX);
// Add the areas
const areas = svg.selectAll(".layer")
.data(series)
.join("path")
.attr("class", "layer")
.attr("fill", d => color(d.key))
.attr("d", area)
.on("mousemove", function (event, d) {
const [mouseX, mouseY] = d3.pointer(event);
const x0 = x.invert(mouseX);
const bisectDate = d3.bisector(d => d.date).left;
const i = bisectDate(data, x0, 1);
const d0 = data[i - 1];
const d1 = data[i];
const dClosest = x0 - d0.date > d1.date - x0 ? d1 : d0;
const value = dClosest[d.key];
tooltip.style("display", "block")
.style("left", `${event.pageX + 10}px`)
.style("top", `${event.pageY - 28}px`)
.html(`<strong>${d.key}</strong><br>${d3.timeFormat("%Y-%m")(dClosest.date)}:${value.toLocaleString()}人`);
})
.on("mouseout", function () {
tooltip.style("display", "none");
});
// Add tooltip
const tooltip = d3.select("body").append("div")
.attr("class", "stacked-area-chart-tooltip")
.style("display", "none")
.style("position", "absolute");
// Add labels directly on the lines (top of each stack)
svg.selectAll(".label")
.data(series)
.join("text")
.attr("class", "label")
.datum(d => ({ key: d.key, value: d[d.length - 1][1] }))
.attr("x", width + 5)
.attr("y", d => y(d.value) + 5)
.attr("fill", d => color(d.key))
.attr("text-anchor", "start")
.text(d => d.key);
// Add title
svg.append("text")
.attr("class", "chart-title")
.attr("x", width / 2)
.attr("y", 0)
.attr("fill", "currentColor")
.attr("text-anchor", "middle")
.style("font-size", "200%")
.style("font-weight", "bolder")
.text("台灣歷年移工人數 - 按國籍區分 - 以堆疊區域圖呈現");
</script>
<!-- Stacked Area Chart (END) -->
堆疊區域圖¶
特點:
- 時間軸 (X軸):固定12個月,確保可比較性。
- 人數 (Y軸):顯示每月總移工數,便於觀察總體變化。
- 分層顏色:使用分類色彩映射,僅顯示人數超過100的主要國籍,按面積大小排序堆疊。
優勢:
- 清晰趨勢:呈現各國籍移工數隨時間的變化,便於分析增減趨勢。
- 便捷比較:通過顏色區域面積大小直觀比較國籍比例。
- 聚焦主要國籍:避免小比例國籍干擾,提升視覺清晰度。
- 動態分析:快速定位增長主因,提供具體分析依據。
設計考量:
- 完整性:包含圖名、軸名稱、圖例及資料來源,確保資訊全面可靠。
- 分析性:滿足長期趨勢分析與國籍分布比較需求,提供豐富數據視角。
- 視覺清晰:忽略小於100人的國籍,強調主要數據,提升圖表清晰度。
- 總結:採用堆疊區域圖取代圓餅圖,解決了比較困難、趨勢呈現不足及小比例國籍表達不足等問題,提升了數據可讀性與分析性,幫助使用者更全面理解台灣移工人數變化與國籍分布,支持相關決策。
In [3]:
%%html
<style>
#line-chart #info-block {
background-color: #f8f9fa;
padding: 20px;
margin-bottom: 10px;
border-bottom: 2px solid #ddd;
}
#line-chart #info-block h2 {
margin: 0;
font-size: 24px;
color: #333;
}
#line-chart svg {
display: inline-block;
margin: 0;
}
#line-chart .axis-label {
font-size: 12px;
font-weight: bold;
}
#year-select {
margin: 20px;
padding: 5px;
}
</style>
<div id="tooltip"
style="position: absolute; visibility: hidden; background: white; border: 1px solid #ccc; padding:7px; border-radius: 3px; font-size: 12px;">
</div>
<h2>台灣移工年度統計圖</h2>
<div>
<label for="year-select">選擇年份:</label>
<select id="year-select"></select>
</div>
<div id="line-chart"></div>
<script type="module">
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
const DATA_JSON_URL = "https://raw.githubusercontent.com" +
"/AsherJingkongChen/d3-worker-plot-tw/refs/heads/main/data.json";
let dataJson = JSON.parse(await (await fetch(DATA_JSON_URL)).text());
const margin = { top: 50, right: 60, bottom: 100, left: 100 };
const width = 1200 - margin.left - margin.right;
const height = 900 - margin.top - margin.bottom;
const svg = d3
.select("#line-chart")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
const x = d3.scaleBand().range([0, width]).padding(0.1);
const y = d3.scaleLinear().range([height, 0]);
dataJson.forEach((data) => {
data.year = data.x.split("-")[0];
data.month = data.x.split("-")[1];
data.value = data.y;
data.country = data.l;
});
const countries = d3.group(dataJson, (data) => data.country);
const color = d3.scaleOrdinal()
.domain(Array.from(countries.keys()))
.range(d3.schemeCategory10);
const years = Array.from(new Set(dataJson.map((d) => d.year)));
const yearSelect = d3.select("#year-select");
yearSelect
.selectAll("option")
.data(years)
.enter()
.append("option")
.attr("value", (data) => data)
.text((data) => data);
// Function to update chart based on selected year
function updateChart(year) {
const filteredData = dataJson.filter((d) => d.year === year);
svg.selectAll("*").remove(); // Clear previous chart elements
const months = Array.from(
new Set(filteredData.map((d) => d.month))
).sort((a, b) => parseInt(a) - parseInt(b));
x.domain(months);
y.domain([0, d3.max(filteredData, (d) => d.value)]);
// Draw axes
svg
.append("g")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(x));
svg.append("g").call(d3.axisLeft(y));
// Draw axis labels
svg.append("text")
.attr("x", width)
.attr("y", height + (margin.bottom / 2))
.attr("fill", "currentColor")
.attr("text-anchor", "end")
.style("font-weight", "bold")
.text("月份");
svg.append("text")
.attr("x", -(margin.left / 2))
.attr("y", 0)
.attr("fill", "currentColor")
.attr("text-anchor", "end")
.style("font-weight", "bold")
.text("人數");
const line = d3
.line()
.x((d) => x(d.month) + x.bandwidth() / 2)
.y((d) => y(d.value));
const filteredCountries = d3.group(filteredData, (d) => d.country);
filteredCountries.forEach((countryData, countryName) => {
countryData.sort((a, b) => a.month - b.month);
svg
.append("path")
.data([countryData])
.attr("class", "line")
.attr("d", line)
.attr("fill", "none")
.attr("stroke", color(countryName));
svg
.selectAll(`.dot-${countryName}`)
.data(countryData)
.enter()
.append("circle")
.attr("class", `dot-${countryName}`)
.attr("cx", (d) => x(d.month) + x.bandwidth() / 2)
.attr("cy", (d) => y(d.value))
.attr("r", 4)
.attr("fill", color(countryName))
.on("mouseover", function (event, d) {
d3.select("#tooltip")
.style("visibility", "visible")
.html(
`<strong>國家:</strong> ${d.country}<br><strong>月份:</strong> ${d.month}<br><strong>人數:</strong> ${d.value}`
);
})
.on("mousemove", function (event) {
d3.select("#tooltip")
.style("top", `${event.pageY + 10}px`)
.style("left", `${event.pageX + 10}px`);
})
.on("mouseout", function () {
d3.select("#tooltip").style("visibility", "hidden");
});
});
// Draw legend
const legend = svg.append("g").attr("transform", "translate(0, -20)");
const legendItems = legend
.selectAll(".legend-item")
.data(Array.from(filteredCountries.keys()))
.enter()
.append("g")
.attr("class", "legend-item")
.attr("transform", (d, i) => `translate(${i * 100}, 0)`);
legendItems
.append("rect")
.attr("width", 15)
.attr("height", 15)
.attr("fill", (d) => color(d));
legendItems
.append("text")
.attr("x", 20)
.attr("y", 12)
.text((d) => d);
}
// Initialize with a default year
updateChart("2024");
// Event listener for year selection change
d3.select("#year-select").on("change", function () {
const selectedYear = this.value;
updateChart(selectedYear);
});
</script>
台灣移工年度統計圖
折線圖¶
特點:
- 時間軸 (X軸):根據當年統計月份變化。
- 人數 (Y軸):顯示每月各國移工數,便於觀察變化。
優勢:
- 清晰趨勢:呈現各國籍移工數隨時間的變化,便於分析增減趨勢。
- 便捷比較:通過折線圖直觀比較國籍比例。
- 聚焦主要國籍:避免小比例國籍干擾,提升視覺清晰度。
設計考量:
- 完整性:包含圖名、軸名稱、圖例及資料來源,確保資訊全面可靠。
- 分析性:可提供短期趨勢分析與國籍分布比較需求
- 視覺清晰:小於100人的國籍會落在下方,可相對強調主要數據,提升圖表清晰度。
- 總結:採用折線圖取代圓餅圖,解決了比較困難、趨勢呈現不足及小比例國籍表達不足等問題,提升了數據可讀性與分析性,幫助使用者更全面理解台灣移工人數變化與國籍分布,支持相關決策。
In [ ]: