有很多时候,看到一些柱状图/折线图的数据,总是跟自己想要的有出入,又无能为力,只有把整张图截下来,再用ps进行修改,这种比较常见。
不过随着echarts的广泛应用,在web上手动生成自己想要的折线图,似乎不再是件难事。
实现步骤
首先,需要在json文件中引入echarts组件
"ec-canvas": "/components/ec-canvas/ec-canvas"
然后写好图形的骨架以及样式
<view class="container-area">
<!-- 折线图 -->
<view class='echart_wrap'>
<ec-canvas id="mychart" canvas-id="mychart-line" ec="{{ ec }}"></ec-canvas>
</view>
<!-- 输入框 -->
<view style="display:flex;align-items: center;justify-content: space-between;margin-left:0rpx;padding-left:64rpx;padding-right:64rpx;margin-top:-136rpx;">
<view class="input-area" wx:for="{{items}}" wx:key="name">
<view class="title">
<view class="title-name">{{item.name}}</view>
<view class="title-unit" style="margin-left: 0rpx;line-height:48rpx;color: #AAAAAA;font-size: 24rpx;">{{item.unit}}</view>
</view>
<view class="input-text">
<input class="vue-input" value="{{item.default}}" type="text" data-name='{{item.name}}' bindtap="toEntryData"/>
</view>
</view>
</view>
<!-- 确认按钮 -->
<view class="btn-row">
<view bindtap="add" class="btn-add">添加数据</view>
</view>
<!-- 动态表单 -->
<view class="active-form" wx:for="{{items2}}" wx:key="name">
<view style="display:flex;align-items: center;margin-left:0rpx;padding-left:42rpx;padding-right:0rpx;margin-top:15rpx;margin-bottom:0rpx;">
<view class="title">
<view class="title-name">{{item.name1}}</view>
<view class="title-unit" style="margin-left: 0rpx;line-height:48rpx;color: #AAAAAA;font-size: 24rpx;">{{item.unit1}}</view>
</view>
<view class="input-text">
<input class="vue-input" value="{{item.default1}}" type="text" data-id='{{item.id}}' data-name='{{item.name1}}' bindtap="toEntryValue"/>
</view>
<view class="title">
<view class="title-name">{{item.name2}}</view>
<view class="title-unit" style="margin-left: 0rpx;line-height:48rpx;color: #AAAAAA;font-size: 24rpx;">{{item.unit2}}</view>
</view>
<view class="input-text">
<input class="vue-input" value="{{item.default2}}" type="text" data-id='{{item.id}}' data-name='{{item.name2}}' bindtap="toEntryValue"/>
</view>
<view data-id='{{item.id}}' class="btn-delete" bindtap="remove">
-
</view>
</view>
</view>
</view>
.container-area{
background: #FFF;
position: relative;
z-index:999;
opacity: 1;
padding: 0;
}
.echart_wrap{
width:100%;
height:600rpx;
}
.input-area{
display:flex;
margin-left: 8rpx;
margin-right:19rpx;
margin-top:33rpx;
margin-bottom:30rpx;
}
.title{
height: 48rpx;
display: flex;
}
.title .title-name{
font-size: 28rpx;
margin-right: 10rpx;
font-weight: 600;
}
.title .title-unit{
font-size: 20rpx;
font-weight: 600;
color: #AAAAAA;
position: relative;
bottom: 0rpx;
line-height: 50rpx;
margin-left:2rpx;
}
.input-text{
margin-left:15rpx;
}
.vue-input{
width: 136rpx;
border: 2rpx solid #D9D9D9;
border-radius: 8px;
background: #F3F3F3;
height: 48rpx;
padding: 0 10rpx 0 4rpx;
font-weight: bold;
font-size: 30rpx;
line-height: 42rpx;
display: flex;
align-items: center;
text-align: right;
color: #000000;
}
.btn-row{
display: flex;
justify-content: center;
}
.btn-add{
height: 70rpx;
line-height: 70rpx;
padding-left: 30rpx;
padding-right: 30rpx;
color: #FFF;
background-color: #D0021B;
border-radius: 10rpx;
text-align: center;
}
.active-form{
margin-left:-11rpx;
}
.active-form .input-text{
margin-right:30rpx;
}
.btn-delete{
width: 64rpx;
height: 48rpx;
text-align: center;
font-weight: bold;
font-size: 30rpx;
line-height: 48rpx;
box-shadow: 0px 8rpx 10rpx rgba(0, 0, 0, 0.06);
background: #F3F3F3;
border-radius: 8rpx;
}
从设计之初,认识到折线图是由数据决定的,而数据,肯定不能写死,它是根据用户手动添加/修改/删除的一套逻辑决定最终的展示数据,这需要在代码设计的时候就要考虑到。
逻辑部分,首先需要引入echarts并配置折线图的初始数据
import * as echarts from '../../components/ec-canvas/echarts';
var Chart=null;
// 图表配置项
var option = {
title: {//标题
text: '速度变化折线图',
left: 'center',
top:15
},
renderAsImage: true, //支持渲染为图片模式
color: ["#FFC34F", "#FF6D60", "#44B2FB"],//图例图标颜色
legend: {
show: true,
itemGap: 25,//每个图例间的间隔
top: 30,
x: 30,//水平安放位置,离容器左侧的距离 'left'
z: 100,
textStyle: {
color: '#383838'
},
},
grid: {//网格
left: 10,
top:80,
containLabel: true,//grid 区域是否包含坐标轴的刻度标签
},
xAxis: { //横坐标
type: 'category',
name: '时间', //横坐标名称
nameTextStyle: { //在name值存在下,设置name的样式
color: 'red',
fontStyle: 'normal',
padding:-10
},
nameLocation: 'end',
splitLine: { //坐标轴在 grid 区域中的分隔线。
show: true,
lineStyle: {
type: 'dashed'
}
},
boundaryGap: false,
data: [],
axisLabel: {
textStyle: {
fontSize: 13,
color: '#5D5D5D'
}
}
},
yAxis: {//纵坐标
type: 'value',
position:'left',
name: '速度',//纵坐标名称
nameTextStyle:{//在name值存在下,设置name的样式
color:'#D0021B',
fontStyle:'normal',
},
splitNumber: 5,//坐标轴的分割段数
splitLine: {//坐标轴在 grid 区域中的分隔线。
show: true,
lineStyle: {
type: 'dashed'
}
},
axisLabel: {//坐标轴刻度标签
formatter: function (value) {
console.log(value)
var xLable = [];
xLable.push(value)
return xLable
},
textStyle: {
fontSize: 13,
color: '#5D5D5D',
}
},
min: 0,
max: 100,
},
series: [
{
name: '速度',
type: 'line',
smooth:true,
data:[],
symbol: 'none',
itemStyle: {
normal: {
lineStyle: {
color: '#D0021B'
}
}
}
},
],
}
然后在组件的data里面,初始化数据
data: {
ec: {
onInit: function (canvas, width, height) {
chart = echarts.init(canvas, null, {
width: width,
height: height
});
canvas.setChart(chart);
return chart;
},
lazyLoad: true // 延迟加载
},
items:{
type:Array,
value:[
{name:'速度',value:'speed',unit:'',default:0},
{name:'时间',value:'time',unit:'',default:0}
],
},
items2:{
type:Array,
value:[
],
},
default:0,
inputArr:[],
defaultInput:0,
params:{
speed:0,
time:0
},
abscissaArr:[],
ordinateArr:[],
},
其中,ec表示初始化折线图的chart,items和items2分别表示添加数据前后的表单数组,这表明初始的时候,已经有一个默认的坐标了,那就是(0,0),往后添加/修改删除的都是items2里面的数据,它是一个根据用户需要不断变化的数据,也决定图形的最终走向。
在方法里面,分别具备新增/修改/删除一个输入表单的点击事件,首先是新增
add(){
let params=this.data.params
let res=this.data.abscissaArr.some(item=>
item>params.time
)
if(res){
wx.showToast({
title: '输入时间过小!',
icon:'none'
})
return;
}
console.log(res)
if(!this.data.abscissaArr.includes(params.time)){
// 横坐标push
this.data.abscissaArr.push(params.time)
}
else{
wx.showToast({
title: '请勿输入重复时间',
icon:'none'
})
return;
}
//纵坐标push
this.data.ordinateArr.push(params.speed)
//动态更新表单
let dataObject={
id:'',name1:'速度',value1:'speed',unit1:'',default1:0,name2:'时
间',value2:'time',unit2:'',default2:0
}
dataObject.id=this.data.items2.length
dataObject.default1=params.speed
dataObject.default2=params.time
let aaa=dataObject
this.data.items2.push(aaa)
for(let i=0;i<this.data.items2.length;i++){
if(this.data.items2.length==1){
this.setData({[`items2[${0}]`]:aaa})
}
if(this.data.items2.length>1){
this.setData({[`items2[${this.data.items2.length-1}]`]:aaa})
}
}
//赋值给图表
option.xAxis.data=this.data.abscissaArr
option.series[0].data=this.data.ordinateArr
this.init_echarts()
},
再新增横纵坐标的时候,横坐标我索性直接做了一层限制,输入比当前数据小的,和当前一样的,直接pass掉,只有输入比当前数据大的,才将它push到横坐标所对应的数组里面,纵坐标则没有限制,直接根据输入的数据push,每新增一次,都会有一个动态表单,根据最新输入成功的数据(包括横纵坐标的id,name,value)对items2进行赋值,保证items2里面是最新的数据,同时,还要把最新的横纵坐标赋值给图表,这样更新echarts之后,才能生成最新的图形。
然后是修改
toEntryValue(e){
let title="修改"+e.currentTarget.dataset.name;
let that=this;
let type=e.currentTarget.dataset.name;
let id=e.currentTarget.dataset.id;
if(type=='时间'&&id==0){
wx.showToast({
title: '该条数据不可修改!',
icon:'none'
})
return;
}
let selectArray=this.data.items2.filter(function(item){
return item.id==id
})
let value=null
if(type=='速度'){
value=selectArray[0].default1;
}
if(type=='时间'){
value=selectArray[0].default2;
}
wx.showModal({
title:title,
editable:true,
content:value+'',
success:function(res){
if(res.confirm){
let content=res.content;
var v_regExpValidNum = new RegExp("^[-]?[0-9]*([.][0-9]{0,5})?$");
var isNumber = v_regExpValidNum.test(content);
if(!isNumber){
wx.showToast({
title: '请输入数字',
icon:'none'
})
return;
}
content=Number(content);
that.changeData(content,e.currentTarget.dataset.name,id)
}
}
})
},
// 改变动态表单并更新图表
changeData(value,type,id){
for(let i=0;i<this.data.items2.length;i++){
if(id==this.data.items2[i].id){
if(type=='速度'){
this.setData({[`items2[${i}].default1`]:value})
this.data.ordinateArr[id]=value
}
if(type=='时间'){
if(i!==0&&i!=this.data.items2.length-1&&value<this.data.items2[i+1].default2&&value>this.data.items2[i-1].default2){
this.setData({[`items2[${i}].default2`]:value})
this.data.abscissaArr[id]=value
}
else if(i==0&&this.data.items2.length==1){
this.setData({[`items2[${i}].default2`]:value})
this.data.abscissaArr[id]=value
}
else if(i==0&&this.data.items2.length>1&&value<this.data.items2[i+1].default2){
this.setData({[`items2[${i}].default2`]:value})
this.data.abscissaArr[id]=value
}
else if(this.data.items2.length>1&&i==this.data.items2.length-
1&&value>this.data.items2[i-1].default2){
this.setData({[`items2[${i}].default2`]:value})
this.data.abscissaArr[id]=value
}
else{
wx.showToast({
title: '输入时间应该在正确的区间!',
icon:'none'
})
return;
}
}
}
}
this.init_echarts()
},
修改数据的时候,由于每一个表单具有横纵坐标,首先需要判断修改的是横坐标还是纵坐标,根据获取到的type进行判断,同时我对items2第一组数据的横坐标进行了限制,不允许修改。然后根据每组数据的id判断修改的是第几组数据,获取这组数据的坐标值,当弹窗出现的时候会返显这个数据。
弹窗出现,输入横坐标/纵坐标的数据,点击确定会调用一个函数,该函数将输入的value,对应的type,已经对应的id作为参数传入,在这里我又做了一层数据修改的限制,先找到items对应要赋值的数据,然后如果是修改的纵坐标,就直接赋值(包括items2本身,以及折线图数据),如果是横坐标,我需要让其修改的数据依然比它前面的数据大,比它后面的数据小,以及考虑了两端的情况,这样才能成功赋值。
最后是删除
remove(e){
let index=e.currentTarget.dataset.id
if(index==0){
wx.showToast({
title: '该条数据不可移除!',
icon:'none'
})
return;
}
let newArray=this.data.items2.filter(item=>{
return item.id!=index
})
this.setData({
items2:newArray
})
this.data.abscissaArr.splice(index,1)
this.data.ordinateArr.splice(index,1)
this.init_echarts()
for(let i=0;i<this.data.items2.length;i++){
this.setData({[`items2[${i}].id`]:i})
}
}
一个items2有多组数据,具体移除哪一种数据,是根据它们唯一的id值来确定的,每一个表单都绑定了一个唯一的id值,除了第一组数据不能删除,其他数据通过id值筛选出最新的数组,将删除的数据排除在外,再用筛选的数组重新给items2赋值,保证items2数据始终是最新的,同时直接通过数组的splice方法,删除图表数据中指定位置的横纵坐标。记住:由于这里删除了一个id,原items2数组中即少了一条数据,如果不重新对id赋值,这样下一次操作会出现数据错乱的情况。所以,在这里需要根据从小到大的顺序,对原items2数组的每一个id值重新赋值。
如此这般,就实现了可以根据需要手动生成横纵坐标的折线图。
效果展示