别再用PC思维做手机图表了
做Echarts适配手机的时候,很多人第一反应就是简单地把宽屏样式缩小,或者粗暴地缩放canvas。结果呢?字体糊成一片、触摸事件失效、内存直接爆掉。我见过不少项目在手机上跑Echarts,最后被迫换成H5静态图或者放弃图表功能。今天就把这些坑一个个拆开来,顺便给出一套靠谱的布局优化方案。
坑一:容器宽度计算错误
手机屏幕宽度变化太大,从320px的老旧安卓机到414px的iPhone Plus都有。很多人直接写死width: 100%,结果在部分机型上图表超出屏幕或者留白过多。
正确做法:用flex或者vw单位控制容器宽度,同时确保父元素没有意外的padding或border影响实际可用空间。
.chart-container {
width: 100%;
height: 200px;
padding: 0;
margin: 0;
box-sizing: border-box;
}
如果父元素有padding,记得用calc()或者调整内边距计算方式:
.chart-wrapper {
width: calc(100% - 20px);
margin: 10px;
}
坑二:字体大小不跟随屏幕
Echarts默认字体是12px,在手机上看起来可能太小。但如果你直接改成14px,在大屏手机上又会显得突兀。更坑的是,有些人用rem单位,却没有设置根元素的font-size基准值。
正确做法:使用em单位或者通过媒体查询针对不同屏幕调整字体。
option = {
textStyle: {
fontSize: '0.9em' // 相对于父元素字体大小
},
title: {
textStyle: {
fontSize: '1.1em'
}
},
legend: {
textStyle: {
fontSize: '0.85em'
}
}
};
或者用JS动态计算:
function getFontSize() {
const width = window.innerWidth;
if (width < 375) return 11;
if (width < 414) return 12;
return 13;
}
option.series.forEach(series => {
series.label = {
fontSize: getFontSize()
};
});
坑三:触摸事件被拦截
这是最常见的坑之一。Echarts的click、hover事件在手机上是靠触摸触发的,但很多手机浏览器有默认的触摸滚动行为,导致图表上的点击事件无法正确触发。
正确做法:在初始化Echarts实例时,明确禁用默认的触摸行为,或者用touchstart、touchend事件代理。
const chart = echarts.init(document.getElementById('chart'));
// 关键:禁用Echarts内部的触摸默认行为
chart.getZr().on('click', function (params) {
console.log('点击了', params);
});
// 如果父元素有滚动,需要阻止事件冒泡
document.getElementById('chart').addEventListener('touchmove', function(e) {
e.stopPropagation();
}, { passive: false });
另外,如果你用的是Vue或React框架,注意不要在外层容器上同时绑定滚动和点击事件,否则会有冲突。
坑四:高分屏模糊问题
现在的手机基本都是2倍或3倍屏,如果直接初始化Echarts,图表会显得模糊。因为canvas默认只渲染1倍分辨率的图像。
正确做法:初始化时传入正确的devicePixelRatio。
const chart = echarts.init(document.getElementById('chart'), null, {
devicePixelRatio: window.devicePixelRatio || 2,
width: document.getElementById('chart').offsetWidth,
height: 300
});
更优雅的方式是用一个自适应函数:
function initChart(domId, theme = null) {
const dom = document.getElementById(domId);
const width = dom.offsetWidth;
const height = dom.offsetHeight;
const dpr = window.devicePixelRatio || 1;
const chart = echarts.init(dom, theme, {
width: width,
height: height,
devicePixelRatio: dpr
});
// 保存dpr信息,后续resize时使用
chart.__dpr = dpr;
return chart;
}
坑五:图表尺寸在动态加载后错误
很多应用是异步加载数据的,图表容器可能在数据回来之前就已经渲染了。这时候调用setOption,Echarts会基于当前的容器大小来计算,但容器可能还在变化中。
正确做法:确保在容器尺寸稳定后再初始化或更新图表。
// 使用MutationObserver监听DOM变化
function observeChartContainer(dom) {
const observer = new MutationObserver(() => {
// 容器大小可能已变化
chart.resize();
});
observer.observe(dom, {
attributes: true,
childList: true,
subtree: true,
attributeFilter: ['style', 'class']
});
return observer;
}
// 或者在数据加载后手动resize
function loadDataAndRender() {
fetchData().then(data => {
chart.setOption({
series: [{ data: data }]
});
// 延迟一小段时间确保DOM已更新
setTimeout(() => {
chart.resize();
}, 100);
});
}
布局优化方案
1. 网格化布局
手机端空间有限,建议用CSS Grid或Flexbox做图表的网格布局。
<div class="chart-grid">
<div class="chart-item">
<div id="chart1"></div>
</div>
<div class="chart-item">
<div id="chart2"></div>
</div>
<div class="chart-item wide">
<div id="chart3"></div>
</div>
</div>
.chart-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
padding: 15px;
}
.chart-item {
height: 200px;
}
.chart-item.wide {
grid-column: span 2;
height: 250px;
}
这样在小屏手机上显示单列或双列,大屏上可以显示多列。
2. 自适应高度
图表高度不要写死,根据内容自适应。
function getChartHeight(option) {
// 根据系列数量和类型估算高度
const seriesCount = option.series ? option.series.length : 1;
const baseHeight = 200;
const additionalHeight = seriesCount * 30;
return baseHeight + additionalHeight;
}
const dom = document.getElementById('chart');
const height = getChartHeight(option);
dom.style.height = height + 'px';
const chart = echarts.init(dom);
chart.setOption(option);
3. 简化图表元素
手机端屏幕小,细节可以简化。比如:
- 隐藏部分数据标签
- 使用更粗的线条
- 减少网格线数量
- 简化图例
option = {
// 简化图例
legend: {
show: true,
icon: 'rect', // 使用简单图标
itemWidth: 15,
itemHeight: 10,
textStyle: {
fontSize: 11
}
},
// 简化网格
grid: {
left: '10%',
right: '5%',
top: '15%',
bottom: '15%',
containLabel: true
},
// 简化标签
label: {
show: false, // 默认不显示
position: 'top'
},
// 加粗线条
series: [{
lineStyle: {
width: 2
},
itemStyle: {
borderWidth: 2
}
}]
};
4. 懒加载策略
如果页面有很多图表,不要一次性全部初始化。
const chartInstances = {};
function lazyInitChart(domId) {
if (chartInstances[domId]) return chartInstances[domId];
const dom = document.getElementById(domId);
const chart = echarts.init(dom);
chartInstances[domId] = chart;
return chart;
}
// 使用IntersectionObserver实现懒加载
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const domId = entry.target.id;
lazyInitChart(domId);
observer.unobserve(entry.target);
}
});
}, {
rootMargin: '50px 0px',
threshold: 0.1
});
document.querySelectorAll('.chart-container').forEach(el => {
observer.observe(el);
});
5. 响应式断点处理
不要只用一个固定布局,根据屏幕宽度调整。
function handleResize() {
const width = window.innerWidth;
if (width < 375) {
// 小屏手机:单列,隐藏部分数据
option.grid.left = '15%';
option.grid.right = '10%';
option.legend.show = false;
} else if (width < 768) {
// 大屏手机:双列,正常显示
option.grid.left = '10%';
option.grid.right = '5%';
option.legend.show = true;
} else {
// 平板/桌面:多列
option.grid.left = '8%';
option.grid.right = '3%';
}
chart.setOption(option);
chart.resize();
}
window.addEventListener('resize', debounce(handleResize, 300));
实战案例:手机端数据看板
假设你要做一个手机端的数据看板,包含3个图表:趋势图、饼图、柱状图。
<div class="dashboard">
<div class="chart-card">
<h3>销售趋势</h3>
<div id="trendChart" class="chart-full"></div>
</div>
<div class="chart-row">
<div class="chart-card">
<h3>品类分布</h3>
<div id="pieChart" class="chart-half"></div>
</div>
<div class="chart-card">
<h3>区域销量</h3>
<div id="barChart" class="chart-half"></div>
</div>
</div>
</div>
.dashboard {
padding: 10px;
}
.chart-card {
background: #fff;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.chart-card h3 {
font-size: 14px;
margin: 0 0 10px 0;
color: #333;
}
.chart-full {
height: 220px;
}
.chart-half {
height: 180px;
}
.chart-row {
display: flex;
gap: 15px;
}
.chart-half {
flex: 1;
}
@media (max-width: 360px) {
.chart-row {
flex-direction: column;
}
.chart-half {
height: 200px;
}
}
// 趋势图配置
const trendOption = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
}
},
grid: {
left: '12%',
right: '8%',
top: '15%',
bottom: '15%'
},
xAxis: {
type: 'category',
data: ['1月', '2月', '3月', '4月', '5月', '6月'],
axisLabel: {
fontSize: 10,
rotate: 0
}
},
yAxis: {
type: 'value',
axisLabel: {
fontSize: 10,
formatter: '{value}'
}
},
series: [{
data: [820, 932, 901, 934, 1290, 1330],
type: 'line',
smooth: true,
lineStyle: {
width: 2
},
areaStyle: {
opacity: 0.3
}
}]
};
// 饼图配置
const pieOption = {
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
bottom: '5%',
left: 'center',
textStyle: {
fontSize: 10
}
},
series: [{
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 5,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: 12,
fontWeight: 'bold'
}
},
data: [
{ value: 1048, name: '电子产品' },
{ value: 735, name: '服装' },
{ value: 580, name: '食品' },
{ value: 484, name: '家居' },
{ value: 300, name: '其他' }
]
}]
};
// 柱状图配置
const barOption = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '12%',
right: '8%',
top: '10%',
bottom: '20%'
},
xAxis: {
type: 'category',
data: ['华北', '华东', '华南', '西南', '西北'],
axisLabel: {
fontSize: 9,
interval: 0,
rotate: 0
}
},
yAxis: {
type: 'value',
axisLabel: {
fontSize: 9
}
},
series: [{
data: [1200, 2100, 1800, 900, 600],
type: 'bar',
itemStyle: {
borderRadius: [3, 3, 0, 0]
}
}]
};
// 初始化图表
const trendChart = echarts.init(document.getElementById('trendChart'));
const pieChart = echarts.init(document.getElementById('pieChart'));
const barChart = echarts.init(document.getElementById('barChart'));
trendChart.setOption(trendOption);
pieChart.setOption(pieOption);
barChart.setOption(barOption);
// 响应式处理
window.addEventListener('resize', () => {
trendChart.resize();
pieChart.resize();
barChart.resize();
});
性能优化小贴士
手机端跑Echarts,性能很重要。这几个小技巧能让你的图表更流畅:
- 关闭不必要的动画:
animation: false或者缩短动画时间 - 使用轻量主题:不要用复杂的渐变和阴影
- 减少数据量:手机端不需要显示所有数据点,可以适当采样
- 及时销毁图表:页面切换时调用
chart.dispose()释放内存
// 关闭动画
option.animation = false;
// 或者缩短动画时间
option.animationDuration = 300;
option.animationDurationUpdate = 150;
// 数据采样
function sampleData(data, step) {
const sampled = [];
for (let i = 0; i < data.length; i += step) {
sampled.push(data[i]);
}
return sampled;
}
总结
手机端适配Echarts,核心就是三个词:简洁、自适应、性能。别把PC端的复杂效果搬到手机上,屏幕小,用户耐心也少。把必要的信息呈现清楚,触摸交互流畅,图表渲染不卡顿,这就够了。
记住,好的移动端图表不是把PC版缩小,而是重新设计。从布局到交互,从数据密度到视觉层次,都要为小屏幕重新考虑。这样用户才能在手机上获得良好的数据体验。
