茎叶图
上一篇
回归曲线图
下一篇
矩形树图
Loading...
茎叶图是一种用于显示数据分布的可视化图表,它既能保留原始数据信息,又能直观地展示数据的分布形状。茎叶图通过将数据分解为"茎"(高位数字)和"叶"(低位数字)的形式来组织和显示数据。
茎叶图有别于直方图,茎叶图保留了每个数据点的具体数值,而直方图只显示数据落在各区间内的频次。茎叶图特别适合小到中等规模的数据集,能够同时显示数据的分布形状、集中趋势和具体数值。
当需要比较两组数据的分布时,可以使用背靠背的双向茎叶图,这样能够清晰地对比两组数据的分布特征。
英文名:Stem-and-Leaf Plot, Stem-and-Leaf Diagram
图表类型 | 单向茎叶图 |
---|---|
适合的数据 | 列表:一组连续数值数据 |
功能 | 显示数据分布形状,保留原始数据信息 |
数据与图形的映射 | 数值的高位数字作为茎(垂直排列) 数值的低位数字作为叶(水平排列) 每一行代表相同茎值的所有数据 |
适合的数据条数 | 20-100 条数据,数据过多时建议使用其他可视化方式 |
图表类型 | 双向茎叶图 |
---|---|
适合的数据 | 列表:两组连续数值数据 |
功能 | 对比两组数据的分布形状和特征 |
数据与图形的映射 | 共用茎(中间垂直排列) 左侧组数据的叶向左排列 右侧组数据的叶向右排列 通过颜色区分不同的数据组 |
适合的数据条数 | 每组 20-50 条数据 |
组成元素:
单向茎叶图适合展示一组数据的分布情况,能够清晰地显示数据的集中趋势和分布形状。
例子:学生考试成绩分布
下图展示了一个班级学生的数学成绩分布:
学生成绩 |
---|
65, 67, 69, 71, 72, 73, 74, 75, 76, 78, 79, 81, 82, 83, 85, 87, 89, 92, 93, 95 |
import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',autoFit: true,});// 单组数据const rawData = [65, 67, 69, 71, 72, 73, 74, 75, 76, 78, 79, 81, 82, 83, 85, 87, 89, 92, 93, 95,];// 处理单向茎叶图数据function processSingleStemLeaf(data) {const stemMap = new Map();data.forEach((score) => {const stem = Math.floor(score / 10);const leaf = score % 10;if (!stemMap.has(stem)) {stemMap.set(stem, []);}stemMap.get(stem).push(leaf);});// 排序叶Array.from(stemMap.values()).forEach((leaves) => {leaves.sort((a, b) => a - b);});const stems = Array.from(stemMap.keys()).sort((a, b) => b - a); // 从大到小排列const chartData = [];stems.forEach((stem, index) => {const yPos = index;const leaves = stemMap.get(stem);// 添加茎chartData.push({x: 0.4,y: yPos,text: `${stem}`,type: 'stem',fill: '#333',fontSize: 18,fontWeight: 'bold',});// 添加叶leaves.forEach((leaf, i) => {chartData.push({x: 0.47 + i * 0.04,y: yPos,text: `${leaf}`,type: 'leaf',fill: '#1890ff',fontSize: 14,fontWeight: 'normal',});});});return { chartData, maxY: stems.length };}const { chartData, maxY } = processSingleStemLeaf(rawData);chart.options({type: 'view',data: chartData,children: [{type: 'text',encode: {x: 'x',y: 'y',text: 'text',fill: 'fill',fontSize: 'fontSize',fontWeight: 'fontWeight',},style: {textAlign: 'center',textBaseline: 'middle',},},],scale: {x: { domain: [0, 1], nice: false },y: { domain: [-0.5, maxY - 0.5], nice: false },},axis: false,});// 使用 lineX 方法添加分割线chart.lineX().data([0.45]).style({lineWidth: 1,stroke: '#333',strokeOpacity: 0.6,});chart.render();
说明:
双向茎叶图特别适合比较两组数据的分布差异,能够直观地展示两组数据的相对位置和分布特征。
例子:比较两个班级的考试成绩分布
下图展示了 A 班和 B 班学生考试成绩的对比分布:
数据组 | 成绩范围 |
---|---|
A 班 | 45-78 |
B 班 | 43-82 |
import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',autoFit: true,});const rawData = {left: [45, 47, 48, 52, 53, 55, 56, 57, 59, 62, 63, 64, 65, 66, 67, 68, 69, 71, 72,73, 74, 75, 76, 77, 78,],right: [43, 44, 46, 51, 54, 55, 58, 59, 61, 62, 63, 64, 65, 66, 67, 68, 72, 73, 74,75, 76, 77, 79, 82,],};// 处理双向茎叶图数据function processDualStemLeaf(data) {const stemMap = new Map();['left', 'right'].forEach((side) => {data[side].forEach((score) => {const stem = Math.floor(score / 10);const leaf = score % 10;if (!stemMap.has(stem)) {stemMap.set(stem, { left: [], right: [] });}stemMap.get(stem)[side].push(leaf);});});// 排序Array.from(stemMap.values()).forEach((group) => {group.left.sort((a, b) => b - a); // 左侧降序group.right.sort((a, b) => a - b); // 右侧升序});const stems = Array.from(stemMap.keys()).sort((a, b) => b - a); // 茎从大到小const chartData = [];stems.forEach((stem, index) => {const yPos = index;const { left, right } = stemMap.get(stem);// 添加茎chartData.push({x: 0.5,y: yPos,text: `${stem}`,type: 'stem',fill: '#333',fontSize: 18,fontWeight: 'bold',});// 添加左侧叶(A班)left.forEach((leaf, i) => {chartData.push({x: 0.45 - (i + 1) * 0.035,y: yPos,text: `${leaf}`,type: 'leaf-left',fill: '#1f77b4',fontSize: 14,fontWeight: 'normal',});});// 添加右侧叶(B班)right.forEach((leaf, i) => {chartData.push({x: 0.55 + i * 0.035,y: yPos,text: `${leaf}`,type: 'leaf-right',fill: '#ff7f0e',fontSize: 14,fontWeight: 'normal',});});});// 添加标题chartData.push({x: 0.35,y: stems.length,text: 'A班',type: 'title',fill: '#1f77b4',fontSize: 16,fontWeight: 'bold',});chartData.push({x: 0.65,y: stems.length,text: 'B班',type: 'title',fill: '#ff7f0e',fontSize: 16,fontWeight: 'bold',});return { chartData, maxY: stems.length + 1 };}const { chartData, maxY } = processDualStemLeaf(rawData);chart.options({type: 'view',data: chartData,children: [{type: 'text',encode: {x: 'x',y: 'y',text: 'text',fill: 'fill',fontSize: 'fontSize',fontWeight: 'fontWeight',},style: {textAlign: 'center',textBaseline: 'middle',},},{type: 'lineX',data: [{ x: 0.47 }, { x: 0.53 }],encode: {x: 'x',},style: {lineWidth: 2,stroke: '#000',strokeOpacity: 0.8,},},],scale: {x: { domain: [0, 1], nice: false },y: { domain: [-0.5, maxY - 0.5], nice: false },},axis: false,});chart.render();
说明:
当需要比较多个类别的数据分布时,可以使用分组茎叶图。
例子:不同年龄段的身高分布
下图展示了青少年、成年人、老年人三个年龄段的身高分布:
import { Chart } from '@antv/g2';const chart = new Chart({container: 'container',autoFit: true,});const ageGroupData = {青少年: [155, 158, 160, 162, 165, 167, 168, 170, 172, 175],成年人: [160, 163, 165, 168, 170, 172, 175, 177, 180, 182, 185],老年人: [150, 155, 158, 160, 162, 165, 167, 168, 170, 172],};// 处理分组茎叶图数据function processGroupedStemLeaf(data) {const groups = Object.keys(data);const colors = ['#1f77b4', '#ff7f0e', '#2ca02c'];const chartData = [];let currentY = 0;groups.forEach((group, groupIndex) => {const groupData = data[group];const stemMap = new Map();// 添加组标题chartData.push({x: 0.1,y: currentY,text: group,type: 'group-title',fill: colors[groupIndex],fontSize: 16,fontWeight: 'bold',});currentY += 0.5;// 处理数据groupData.forEach((height) => {const stem = Math.floor(height / 10);const leaf = height % 10;if (!stemMap.has(stem)) {stemMap.set(stem, []);}stemMap.get(stem).push(leaf);});// 排序叶Array.from(stemMap.values()).forEach((leaves) => {leaves.sort((a, b) => a - b);});const stems = Array.from(stemMap.keys()).sort((a, b) => a - b);stems.forEach((stem) => {const leaves = stemMap.get(stem);// 添加茎chartData.push({x: 0.2,y: currentY,text: `${stem}`,type: 'stem',fill: '#333',fontSize: 14,fontWeight: 'bold',});// 添加叶leaves.forEach((leaf, i) => {chartData.push({x: 0.27 + i * 0.03,y: currentY,text: `${leaf}`,type: 'leaf',fill: colors[groupIndex],fontSize: 12,fontWeight: 'normal',});});currentY += 1;});currentY += 0.5; // 组间间距});return { chartData, maxY: currentY };}const { chartData, maxY } = processGroupedStemLeaf(ageGroupData);chart.options({type: 'view',data: chartData,children: [{type: 'text',encode: {x: 'x',y: 'y',text: 'text',fill: 'fill',fontSize: 'fontSize',fontWeight: 'fontWeight',},style: {textAlign: 'left',textBaseline: 'middle',},},],scale: {x: { domain: [0, 1], nice: false },y: { domain: [0, maxY], nice: false },},axis: false,});// 使用 lineX 方法添加分割线chart.lineX().data([0.25]).style({lineWidth: 1,stroke: '#333',strokeOpacity: 0.6,});chart.render();
说明:
例子 1: 数据量过大的情况
当数据量超过 100 个时,茎叶图会变得过于拥挤,难以阅读。此时建议使用直方图或箱线图。
例子 2: 数据范围过大的情况
当数据跨度很大(比如从 1 到 10000)时,茎的数量会过多,导致图表冗长。建议先对数据进行适当的分组或变换。
例子 3: 需要精确数值比较的场景
虽然茎叶图保留了原始数据,但当需要进行精确的数值计算和比较时,表格形式可能更合适。