Vue +Element UI (饿了么UI) +Echarts 实现图表自适应配置
接到一个需求,让用户能够通过配置, 配置出自己想要的统计图, 填入数据分析需求:1.需要客户配置的数据项有哪些?2.客户配置数据的存储一.可配置的项主标题(带颜色),副标题(带颜色)图表种类(分为 坐标系,非坐标系图, 差别在于 有无 xAxis, yAxis 属性)图表背景色是否需要开启类目是否需要开启工具(左上角下载图片等工具)类目的位置图表的类型 ...
·
接到一个需求,让用户能够通过配置, 配置出自己想要的统计图, 填入数据
分析需求:
-
需要客户配置的数据项有哪些?
-
客户配置数据的存储
JSON转String 存数据库就行.....
一.可配置的项
- 主标题(带颜色),副标题(带颜色)
- 图表种类(分为 坐标系,非坐标系图, 差别在于 有无 xAxis, yAxis 属性)
- 图表背景色
- 是否需要开启类目
- 是否需要开启工具(左上角下载图片等工具)
- 类目的位置
- 图表的类型 暂时有 柱状图 bar ,折线图 line, 饼图 pie, 雷达图 radar
- 坐标系图 位置大小, 非坐标系 位置, 大小
- 饼图 是否开启南丁格尔图, 雷达图是否开启透明度
二.基本效果图
三.代码
<template>
<div class="i-container">
<div class="i-main" style="padding:0;">
<el-form ref="dataForm" :model="data" :rules="validRules" label-position="right" label-width="20%"
style="width: 100%;height:100%;overflow:hidden;">
<el-row style="height:100%;">
<el-col :span="12" style="height:100%;overflow:auto;padding:10px 20px;">
<div class="i-title max bold">基本信息</div>
<el-form-item label="类型" prop="type">
<el-select v-model="data.type">
<el-option v-for="item in dictData['CHART']" :key="item.value" :value="item.value" :label="item.label"></el-option>
</el-select>
</el-form-item>
<el-form-item label="组件图名称" prop="name">
<el-input v-model="data.name" placeholder="组件图名称" />
</el-form-item>
<!-- <el-form-item label="状态" prop="status">
<el-select v-model="data.status">
<el-option v-for="item in dictData['status']" :key="item.value" :value="item.value" :label="item.label"></el-option>
</el-select>
</el-form-item> -->
<div class="i-title max bold">配置信息</div>
<!-- <div v-for="(item,index) in opts" :key="index">
<el-form-item :label="item.label">
<el-input v-if="item.type=='input'" v-model="optionItem[item.code]" @change="setOptions(item.code)"/>
</el-form-item>
</div> -->
<el-form-item label="图表种类">
<el-select v-model="subConfig.tbzl" @change="initOptions(subConfig.tbzl)">
<el-option v-for="(item,index) in TBZL" :key="index" :value="item.value" :label="item.text"></el-option>
</el-select>
</el-form-item>
<!-- <el-form-item label="图表主标题">
<el-input v-model="options.title.text" style="width:50%"/>
<el-color-picker v-model="options.title.textStyle.color" show-alpha></el-color-picker>
</el-form-item>
<el-form-item label="图表副标题">
<el-input v-model="options.title.subtext" style="width:50%"/>
<el-color-picker v-model="options.title.subtextStyle.color" show-alpha></el-color-picker>
</el-form-item> -->
<el-form-item label="图表背景色">
<el-color-picker v-model="options.backgroundColor" show-alpha></el-color-picker>
</el-form-item>
<el-form-item label="开启类目">
<el-switch v-model="subConfig.legend" @change="initOptionsLegend()"></el-switch>
</el-form-item>
<el-form-item label="开启工具">
<el-switch v-model="subConfig.toolbox" @change="initOptionsToolbox()"></el-switch>
</el-form-item>
<el-form-item label="坐标系调整" v-if="subConfig.tbzl=='zbxt'">
<el-switch v-model="subConfig.posit" @change="initOptionsGrid()"></el-switch>
</el-form-item>
<div v-if="subConfig.legend">
<div class="i-title max bold">类目配置</div>
<el-row>
<el-col :span="6">
<el-form-item label="左">
<el-input v-model="options.legend.left" @change="initOptionsLegendPoist(options.legend.left,'left')" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="右">
<el-input v-model="options.legend.right" @change="initOptionsLegendPoist(options.legend.right,'right')" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="上">
<el-input v-model="options.legend.top" @change="initOptionsLegendPoist(options.legend.top,'top')"/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="下">
<el-input v-model="options.legend.bottom" @change="initOptionsLegendPoist(options.legend.top,'bottom')"/>
</el-form-item>
</el-col>
</el-row>
</div>
<div v-if="subConfig.posit">
<div class="i-title max bold">位置|大小</div>
<el-row>
<el-col :span="6">
<el-form-item label="左">
<el-input v-model="subConfig.grid.x" @change="initOptionsGrid(subConfig.grid)" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="右">
<el-input v-model="subConfig.grid.x2" @change="initOptionsGrid(subConfig.grid)" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="上">
<el-input v-model="subConfig.grid.y" @change="initOptionsGrid(subConfig.grid)" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="下">
<el-input v-model="subConfig.grid.y2" @change="initOptionsGrid(subConfig.grid)" />
</el-form-item>
</el-col>
</el-row>
<!-- <el-row>
<el-col :span="12">
<el-form-item label="宽度">
<el-input v-model="subConfig.grid.width" @change="initOptionsGrid(subConfig.grid)" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="高度">
<el-input v-model="subConfig.grid.height" @change="initOptionsGrid(subConfig.grid)" />
</el-form-item>
</el-col>
</el-row> -->
</div>
<el-table :data="options.series" border fit v-if="subConfig.tbzl">
<el-table-column label="配置项">
<template slot-scope="{row, $index}">
<el-form-item label="图表类型">
<el-select v-model="row.type" @change="initOptionSeriesData(row.type,$index)">
<el-option v-for="item in selectionTBLX" :key="item.value" :value="item.value" :label="item.name"></el-option>
</el-select>
</el-form-item>
<el-form-item label="类目名称" v-if="row.type!='radar'">
<el-input v-model="row.name" @change="initOptionSeriesName(row.type,row.name,$index)" placeholder="类目名称"/>
</el-form-item>
<el-form-item label="南丁格尔图" v-if="row.type=='pie'">
<el-switch v-model="row.roseType" @change="initOptionsRoseType()"></el-switch>
</el-form-item>
<!-- <el-form-item label="图表数据">
<el-input v-text="row.data" placeholder="图表数据"/>
</el-form-item> -->
<el-form-item label="图表位置" v-if="row.type=='pie'||row.type=='radar'">
<el-row>
<el-col :span="12">
<el-form-item label="X轴">
<el-input v-model="row.Xcenter" @change="initOptionsCenter(row.Xcenter,row.Ycenter,$index,row.type)"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="Y轴">
<el-input v-model="row.Ycenter" @change="initOptionsCenter(row.Xcenter,row.Ycenter,$index,row.type)"></el-input>
</el-form-item>
</el-col>
</el-row>
</el-form-item>
<el-form-item label="图表大小" v-if="row.type=='pie'">
<el-row>
<el-col :span="12">
<el-form-item label="内圈">
<el-input v-model="row.Xradius" @change="initOptionsRadius(row.Xradius,row.Yradius,$index,row.type)"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="外圈">
<el-input v-model="row.Yradius" @change="initOptionsRadius(row.Xradius,row.Yradius,$index,row.type)"></el-input>
</el-form-item>
</el-col>
</el-row>
</el-form-item>
<el-form-item label="透明度" v-if="row.type=='radar'">
<el-slider v-model="row.Topacity" @change="initOptionsOpacity(row.Topacity,$index,row.type)" style="width:50%"></el-slider>
</el-form-item>
</template>
</el-table-column>
<el-table-column label width="40">
<template slot="header">
<i class="s-btn-add el-icon-circle-plus" @click="addRow('series')"></i>
</template>
<template slot-scope="{row, $index}">
<i class="s-btn-del el-icon-remove" @click="removeRow('series',$index)"></i>
</template>
</el-table-column>
</el-table>
</el-col>
<el-col :span="12" style="padding:10px 20px;height:100%">
<el-tabs v-model="activeName" style="height:100%;" class="s-tabs">
<el-tab-pane label="图形实例" name="txsl">
<div class="s-optionDiv" ref="sOpt"></div>
<el-button type="primary" class="s-btn-top" icon="el-icon-refresh" @click="doFlush()">刷新</el-button>
</el-tab-pane>
<el-tab-pane label="源码" name="ym" >
<el-input :rows="30" type="textarea" v-model="optionCode" @change="ChangeOptions(optionCode)"></el-input>
<el-button @click="doJSON(optionCode)" class="s-btn-top" type="primary" icon="el-icon-tickets">格式化</el-button>
</el-tab-pane>
<el-tab-pane label="测试数据" name="cssj" >
<el-input :rows="30" type="textarea" v-model="optionData" @change="ChangeOptionsData(optionData)"></el-input>
<el-button @click="doJSON(optionData)" class="s-btn-top" type="primary" icon="el-icon-tickets">格式化</el-button>
</el-tab-pane>
</el-tabs>
</el-col>
</el-row>
</el-form>
</div>
<div class="i-footer border">
<el-button :loading="loading" type="primary" icon="el-icon-check" @click="update()">提交</el-button>
</div>
</div>
</template>
<script>
import echarts from 'echarts'
import { loadEntity, updateEntity, saveEntity,dictData,dictLoad } from "./data";
export default {
props: {
params: {
required: true,
default: {}
}
},
data() {
return {
activeName:"txsl",
loading: false,
dictData,
optionItem:{},
data: {},
chart:null,
// opts:[
// {label:"图表主标题",code:"title.text",type:"input",dict:""},
// {label:"主标题颜色",code:"title.textStyle.color",type:"input",dict:""},
// {label:"图表副标题",code:"title.subtext",type:"input",dict:""},
// {label:"副标题颜色",code:"title.subtextStyle.color",type:"input",dict:"",children:[
// ]},
// ],
TBZL: [
{
text: '坐标系图',
value:'zbxt',
TBLX: [
{ "name": "柱状图", "value": "bar", "ptype": "zbxt" },
{ "name": "折线图", "value": "line", "ptype": "zbxt" },
]
},
{
text: '非坐标系图',
value:'fzbxt',
TBLX: [
{ "name": "饼图", "value": "pie", "ptype": "fzbxt" },
{ "name": "雷达图", "value": "radar", "ptype": "fzbxt" },
]
},
],
options:{
legend:{},
series:[],
title:{
text:null,
textStyle:{},
subtext:null,
subtextStyle:{}
},
},
toption:{
},
optionCode:null,
optionData:null,
subConfig:{
legend:true,
grid:{},
},
validRules: {
name: [
{ max: 255, message: "长度不能超过255", trigger: "blur" }
]
,option: [
{ max: 2000, message: "长度不能超过2000", trigger: "blur" }
]
,data: [
{ max: 2000, message: "长度不能超过2000", trigger: "blur" }
]
,type: [
{ max: 255, message: "长度不能超过255", trigger: "blur" }
]
}
};
},
computed: {
selectionTBLX: {
get: function() {
let _this = this;
return this.TBZL.filter(function(item) {
return item.value == _this.subConfig.tbzl;
})[0].TBLX;
}
}
},
watch:{
options:{
immediate:false,
deep:true,
handler(){
const item = this.$refs.sOpt
this.chart = echarts.init(item);
this.chart.setOption(this.options,true);
this.optionCode =JSON.stringify(this.options);
this.optionData =JSON.stringify(this.options.series)
this.optionCode=this.doJSON(this.optionCode)
this.optionData=this.doJSON(this.optionData)
}
},
},
mounted() {
let _this = this;
this.dictLoad();
const id= this.params.id;
if (id) {
loadEntity({ id: id }).then(function(response) {
_this.data = response.data;
_this.options = JSON.parse(_this.data.options);
//如果数据类型是折线图或柱状图, 则显示坐标系图 同时取位置信息出来
if(_this.options.series[0].type=='line'||_this.options.series[0].type=='bar'){
_this.subConfig.tbzl="zbxt";
if(_this.options.grid!=null){
_this.subConfig.posit=true;
_this.subConfig.grid = _this.options.grid;
}
}else{
_this.subConfig.tbzl="fzbxt";
}
//有工具开启工具栏
if(_this.options.toolbox!=null){
_this.subConfig.toolbox=true;
}
});
}
},
methods: {
dictLoad,
setOptions(pcode){
const val = this.optionItem[pcode]
const codes = pcode.split(".")
let data = this.options;
const size = codes.length;
for(let i=0; i<size; i++){
const code = codes[i];
if((i+1)==size){
data[code] = val;
}else{
if(!data[code]){
this.$set(data,code,{})
}
data = data[code];
}
}
},
update() {
let _this = this;
this.$refs.dataForm.validate((valid)=>{
if(valid){
if (_this.data.id) {
_this.loading = true
_this.data.options = JSON.stringify(_this.options);
updateEntity(this.data).then(function () {
_this.loading = false
_this.Alert("提交成功");
_this.close();
});
} else {
_this.loading = true
_this.data.options = JSON.stringify(_this.options);
saveEntity(_this.data).then(function () {
_this.loading = false
_this.Alert("提交成功");
_this.close();
});
}
}
})
},
close() {
this.$emit("close", { type: "edit" });
},
addRow(key) {
if (!this.options[key]) {
this.$set(this.options, key, []);
}
this.options[key].push({});
},
removeRow(key, index) {
this.options[key].splice(index, 1);
},
doFlush(){
this.chart.setOption(this.options,true);
},
ChangeOptions(code){
this.options = JSON.parse(code);
},
ChangeOptionsData(code){
this.options.series = JSON.parse(code);
},
/**
* 格式化JSON
*/
doJSON(txt,sc){
sc = sc?sc:" ";
txt = txt.replace(/\-\-(.+)\n/ig,function(code){
return code.replace(/\[/ig,"#ZKL#").replace(/\]/ig,"#ZKR#").replace(/\{/ig,"#DKL#").replace(/\}/ig,"#DKR#").replace(/\,/ig,"#DH#");
})
txt = txt.replace(/\n\s*/ig,"");
txt = txt.replace(/\<\!\[CDATA\[(.*)\]\]\>/ig,"#CD#$1#DC#");
txt = txt.replace(/\"([^\"]*)\"/ig,function($0){
var txt = $0;
return txt.replace(/\[/ig,"#ZKL#").replace(/\]/ig,"#ZKR#").replace(/\{/ig,"#DKL#").replace(/\}/ig,"#DKR#").replace(/\,/ig,"#DH#");
});
txt = txt.replace(/(,|\[|\]|\{|\})/ig,"#SP#$1#SP#").replace(/(#SP#){2,}/ig,"#SP#").replace(/^#SP#/i,"");
var ta = txt.split("#SP#");
var space="";
for(var i=0;i<ta.length;i++){
var f = ta[i];
if(f=="{"||f=="["){
ta[i] = space+ta[i];
space+=sc;
}else if(f=="}"||f=="]"){
space = space.replace(sc,"");
ta[i] = space+ta[i];
}else{
ta[i] = space+ta[i];
}
}
var rstr = ta.join("\n").replace(/\,\s*/ig,",").replace(/ \,/ig,",").replace(/#ZKR#/g,"]").replace(/#ZKL#/g,"[").replace(/#DKL#/g,"{").replace(/#DKR#/g,"}").replace(/#DH#/g,",")
rstr = rstr.replace(/\:\n*\s*(\{|\[)/ig,":$1").replace(/#CD#/ig,"<![CDATA[").replace(/#DC#/ig,"]]>");
return rstr;
},
/**
* 切换图表类型,重置options
*/
initOptions(value){
this.options ={
legend:{},
series:[],
title:{
text:null,
textStyle:{},
subtext:null,
subtextStyle:{}
},
};
if(value=='zbxt'){
var tOptions = {
xAxis:{type:"category",data:['实例一','实例二','实例三','实例四','实例五','实例六','实例七']},
yAxis:{type:"value"},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
crossStyle: {
color: '#999'
}
}
},
}
this.options.xAxis = tOptions.xAxis;
this.options.yAxis = tOptions.yAxis;
this.options.tooltip = tOptions.tooltip;
}else if(value=="fzbxt"){
}
},
/**
* 生成测试数据
*/
initOptionSeriesData(type,index){
//如果是坐标系图, 直接随机生成一个数组
var array = [];
if(this.subConfig.tbzl=='zbxt'){
for(var i=1; i<8;i++){
array.push(Math.floor(Math.random()*100+1))
}
this.options.series[index].data = array;
}
//非坐标系图 判断 是 圆图 还是雷达图
else if(this.subConfig.tbzl="fzbxt"){
//如果是圆图, 则随机生成7个对象存到数组里面 对象结构为 {value:xx,name:xx}
//存储类目
var tmpLegend = [];
if(type=='pie'){
for(var i=1; i<8;i++){
var name = "实例"+i*(index+1)
tmpLegend.push(name)
array.push({value:Math.floor(Math.random()*100+1),name:name})
}
this.options.series[index].data = array;
var obj = {data:tmpLegend}
//如果是第一条数据 直接赋值对象,如果不是第一条, 直接合并对象的data数组,避免覆盖
if(this.options.legend.data!=null){
this.options.legend.data = tmpLegend.concat(this.options.legend.data);
}else{
this.options.legend = obj;
}
var tooltip={trigger: 'item',formatter: '{a} <br/>{b}: {c} ({d}%)'};
this.options.tooltip = tooltip;
}
//如果是雷达图 则对象结构为 {value:[],name:xx}
else if(type=='radar'){
var tmpObj = [];
var tmpindicator = [];
//模拟数据
for(var i=0;i<3;i++){
var name = "数据"+i;
for(var j=1; j<6; j++){
tmpObj.push(Math.floor(Math.random()*100+1))
}
tmpLegend.push(name);
array.push({value:tmpObj,name:name})
tmpObj = [];
}
//模拟项
for(var i=1;i<6;i++){
var name ="实例"+i*(index+1);
tmpindicator.push({name:name,max:100});
}
this.options.series[index].data = array;
var radar={indicator:tmpindicator}
var legend = {data:tmpLegend}
this.options.radar = radar;
this.options.tooltip={ trigger: 'item'};
this.options.legend = legend.concat(this.options.legend);
}
}
},
/**
* 针对坐标系 添加类目
*/
initOptionSeriesName(type,name,index){
if(type=='zbxt'){
this.options.legend.data[index]=name;
}
},
/**
* 类目开关处理
*/
initOptionsLegend(){
//默认开启类目,当点击取消之后 再点击开启类目,就把之前已经保存的类目重新赋值
if(this.subConfig.legend){
for(var i=0; i<this.options.series.length; i++){
var obj = this.options.series[i];
//坐标系图和非坐标系图的 数据结构不同,坐标系图 类目 直接 series[i].name 就行
if(this.subConfig.tbzl=='zbxt'){
this.options.legend.data.push(obj.name);
}
//非坐标系图类目 数据结构为 series[i].data[j].name;
else if(this.subConfig.tbzl=='fzbxt'){
var tmpObj = obj.data;
for(var j=0; j<tmpObj.length; j++){
this.options.legend.data.push(tmpObj[j].name);
}
}
}
}else{
//关闭类目,就把类目数据全部清空
this.options.legend.data=[];
}
},
/**
* 工具栏开关处理
*/
initOptionsToolbox(){
//默认关闭工具栏
var toolbox={
show: true,
feature: {
dataZoom: { yAxisIndex: 'none'},
dataView: {readOnly: false},
// magicType: {type: ['line', 'bar']},
restore: {},
saveAsImage: {}
}
}
// 开启工具栏 把定好的对象赋值上去就行,
// 关闭工具栏 把定好的对象赋空就行
if(this.subConfig.toolbox){
this.options.toolbox = toolbox;
}else{
this.options.toolbox =null;
}
},
/**是否开启南丁格尔图 */
initOptionsRoseType(type,index,bol){
if(type=='pie' && bol){
this.options.series[index].roseType='area';
}else{
delete this.options.series[index].roseType;
}
},
//坐标系图开启位置调整
initOptionsGrid(obj){
if(this.subConfig.tbzl=='zbxt'){
if(this.subConfig.posit==true){
this.options.grid = obj;
}else{
delete this.options.grid;
}
}
},
//非坐标系开启位置调整
initOptionsCenter(x,y,index,type){
var array =[x,y];
if(type=='pie'){
this.options.series[index].center = array;
}else if(type=='radar'){
this.options.radar.center = array;
}
},
//非坐标系开启大小调整
initOptionsRadius(x,y,index,type){
var array =[x,y];
if(type=='pie'){
this.options.series[index].radius = array;
}
// else if(type=='radar'){
// this.options.radar.radius = array;
// }
},
/***
* 雷达图调整区域透明度
* data :数值
* index:第几项数据
* type:图表类型
*/
initOptionsOpacity(data,index,type){
if(type=="radar"){
var areaStyle = {opacity:(data/100)}
this.options.series[index].areaStyle = areaStyle;
}
},
/**
* value:值
* type:类型
*/
initOptionsLegendPoist(value,type){
if(value==""){
if(type=='left'){
delete this.options.legend.left;
}else if(type=='right'){
delete this.options.legend.right;
}else if(type=="top"){
delete this.options.legend.top;
}else if(type=="bottom"){
delete this.options.legend.bottom;
}
}
}
}
};
</script>
<style lang="scss" scoped>
.s-optionDiv{
width: 100%;
height: 100%;
border: 1px solid #dddddd;
}
.s-tabs{
position: relative;
/deep/ .el-tabs__content{
position: absolute;
top:50px;
bottom:0;
left:0;
right:0;
overflow: inherit;
.el-tab-pane{
height: 100%;
}
}
.s-btn-top{
position: absolute;
top:-50px;
right:0;
}
}
</style>
四.总结
还存在的小bug
- 非options的直接属性, 借用subConfig 来实现的数据 渲染不同步, 需要点击刷新重新渲染,右侧的图才会渲染成功
- 类目位置调整, 应该是 有了 left有了值, right 就不能有值, 一样, top有值bottom 就不能有值,否则后者渲染失效
- 数据源码的源码刷新不及时
- 在选中饼图 调整位置的时候 会保存Xcenter ,Ycenter 俩个 无用参数
- 等
代码存在的缺陷
- 多次判断坐标种类 及 subConfig.tbzl 和 图表类型 options.type 是否可以统一处理,方便扩展
- 每次切换坐标系图和非坐标系图的时候, 重置 this.options 是否可以事项保存一个值, 而不是用重新赋值的方法
- 由于JS功力有限....多次借助其他对象来操作数据,造成Xcenter 等这样不需要的参数 参杂在option里面
- 提供的可选功能有限
- 等
可改进
- 在非坐标系图多项数据的时候, 例如 俩个饼图, 是否可以初始化 位置,大小, 进行默认排位置
- 需要提供, 主副标题及颜色的话, 加俩个属性就行了
- 最主要的是 数据不能及时同步和渲染.功力太差
- 等
更多推荐
已为社区贡献1条内容
所有评论(0)