业务场景
客户作为一家大型制造企业,各地的门店,办事处和大区都有着许多的营销数据。客户希望通过BI的方式能够直观地将这些沉淀下来的数据进行可视化,了解各个门店(办事处,大区)的经营情况,体现出各自地区/门店的业务强势点在哪里,支持这些强势点之间能够互相横向比较。并且能够更方便地看出根据某项指标的数据,哪些门店/办事处/大区是排名靠前的,哪些是做得不理想的,来支持未来的决策规划以及考核,让这些数据真正体现出作用来。

前期需求调研之后,我们收集到了大区/办事处/门店分别的数据结构。其中大区/办事处画像指标为: 综合评分、竞争力、发展面积、增长潜力、市场容量、市场饱和度、执行力、招商能力、客户体验、直播能力。门店的画像指标则是:店效、平效、人效、专业能力、发展空间、竞对指数、商业氛围和终端6s。这些画像指标在数据之中都起到度量的作用。而维度数据则由分别的18个大区、108个办事处、上千个门店、日期(年月数据)构成。
拿到数据结构之后,我们来开始设计BI产品前端的具体设计。
原型设计
此处以大区部办事处的设计为例,办事处以及门店与大区部画像设计类似。
因为我们的度量指标比较多,而且需要横向对比,所以原型设计方面我们选择使用雷达图来进行数据可视化,雷达图可以支持我们绘制多边形来比较各个度量的数据值,并且视觉上能够一目了然了解到各个地区大区部的优势指标是什么。
首先根据选择大区情况,每页最多展示8个大区雷达图,避免视觉上过于冗杂。18个大区可以通过页面下方左右按钮浏览,也可以通过*放播**按钮轮播不同大区。

通过雷达图展示大区部的综合评分、直播能力、客户体验、招商能力、执行力、市场饱和度、市场容量、增长潜力、发展面积、竞争力等指标,按照选择指标排序,最高值排在第一位并显示橙色雷达图,雷达图下面显示大区名称。
鼠标悬停在雷达图上方,系统弹出提示框,提示框展示大区名称和指标值。上方设置筛选框,根据用户权限限制能够查看筛选的大区,指标筛选框为根据哪一项指标进行所有大区的综合排序,设置重置按钮进行重新选择。

支持某一个大区不同月份的指标对比,很直观看出某大区在不同月份指标的优劣:

总部权限登录可以看到18个大区画像。

大区部登录能看到本大区画像。

办事处则没有权限查看。
总体页面设计如下:

产品实现
根据仪表盘样式,这个样式需要支持颜色、点、提示信息、仪表盘背景等的自定义需求。这里使用echarts的api根据需求修改indicator中的各种格式。
样式中,需求为:1.雷达图背景颜色(灰白相见)2.雷达图提示信息(度量指标)3.雷达图图例颜色以及图里线,圈的颜色(蓝色,橙色,白色)4.每一个雷达图的下方需要有大区的对应字段。
分页以及自动轮播功能:由于大区有十二个,所以需要给用户分页功能,这个分页功能使用echarts组件中的jsapi,设置timeline的数据来实现分页以及自动轮播的功能,使用代码判断页数,并生成下方轮播键位。

大区数据需要渲染上去,一页中需要有八个组件,并且八个组件中绑定的数据需要从数据库中拉取后分别渲染到对应的组件上去。这里使用产品绑定的数据库从后端获取到数据,前端将数据当成一个jsobject来取数并进行渲染(第四段介绍)。这里的数据绑定之后,可以与产品的下拉筛选,排序等联动。其中排序使用参数列。

以及数据-字段中的高级排序功能,使其和参数列能够联动。

代码中,使用whileloop和forloop,一次性拉入多个组件需要的数据信息,绑定到echarts的options和series之中。位置信息使用如下算法每个雷达图能够固定在八分之一的分别位置上。雷达图中,根据数据的最大值来设置渲染的上限。


在分别的画像之下,不但要设置tooltip的提示信息(鼠标上移显示的信息),还需要设置对应的名称在画像下面。这里的名称也通过循环分别和各自的数据绑定来实现。

这里的第一名的大区需要使用if条件判断将背景改为黄色。

画像界面下方的两个雷达图组件也需要分别设置数据的上限。

最后通过脚本选项中赋予背景文本框css,实现阴影和圆角边框效果。

Echarts集成方法
下面介绍永洪9.4版本的echarts集成:
组件

在右侧的panel栏中选择图标组件中的自定义js组件。

在数据栏中,点击自定义js代码。

在JS一栏中使用javascript调取echartsapi进行开发。

数据绑定方面,自定义js组件支持拖曳方式将字段绑定至组件之中。

数据绑定之后,在组件的data一栏,显示为一个jsobject,第一个大括号内为绑定的字段,从第二个大括号开始为分别的字段值。
我们可以在js中写下console.log(options.data),并使用f12在控制台中查看代码中选定的数据。
代码
下面附画像页面中使用到的js自定义代码。
a.多雷达图(轮播)代码
//为保证正常绘图,代码格式与所选格式需保持一致;
//请输入JS代码或前往官网复制示例代码;
//复制完代码后,请先将代码中的数据列替换为绑定的数据列,绑定的数据列从上至下依次为column1、column2......,option部分请添加options.data以引用绑定列,chart绘制的DOMid会自动替换为变量$container
//为保证正常绘图,代码格式与所选格式需保持一致;
//请输入JS代码或前往官网复制示例代码;
//复制完代码后,请先将代码中的数据列替换为绑定的数据列,绑定的数据列从上至下依次为column1、column2......,option部分请添加options.data以引用绑定列,chart绘制的DOMid会自动替换为变量$container
//为保证正常绘图,代码格式与所选格式需保持一致;
//请输入JS代码或前往官网复制示例代码;
//复制完代码后,请先将代码中的数据列替换为绑定的数据列,绑定的数据列从上至下依次为column1、column2......,option部分请添加options.data以引用绑定列,chart绘制的DOMid会自动替换为变量$container
var i = 0 ;
var m = 0 ;
var radarloop =[] ;
var titlesloop =[] ;
while ( i < options.data.length ) {
if ( m == 8 ) {
m = 0
}
if ( m=== 0 ) {
radarloop.push ( [] ) ;
titlesloop.push ( [] ) ;
}
pos =[]
if (( parseInt ( m / 4 )) % 2 === 0 ) {
pos =[ 15 * ( m + 1 ) + 9 * m, 20 ]
}
else { pos =[ 15 * (( m- 4 ) + 1 ) + 9 * ( m- 4 ) , 65 ]}
titlesloop [ parseInt ( i / 8 ) ] .push ( {
textAlign : 'center' ,
text :options.data [ i ] .column1,
left :pos [ 0 ]+ '%' ,
top :pos [ 1 ]+ 20 + '%' ,
textStyle : {
fontSize : 20 ,
fontWeight : 'normal' ,
fontStyle : 'normal'
}
} )
radarloop [ parseInt ( i / 8 ) ] .push ( {
indicator : [{ text : '综合评分', max : 6000 } , { text : '竞争力', max : 6000 } , { text : '发展面积', max : 6000 } , { text : '增长潜力', max : 6000 } , { text : '市场容量', max : 6000 } , { text : '市场饱和度', max : 6000 } , { text : '执行力', max : 6000 } , { text : '招商能力', max : 6000 } , { text : '客户体验', max : 6000 } , { text : '直播能力', max : 6000 }] ,
center : [ pos [ 0 ]+ '%' ,pos [ 1 ]+ '%' ] ,
radius : 120 ,
startAngle : 90 ,
splitNumber : 5 ,
shape : '' ,
axisName : {
formatter : '{value}' ,
color : '#000' ,
fontSize : 10
} ,
splitArea : {
areaStyle : {
color : [ '#E6E6E6' , '#F5F5F5' , '#E6E6E6' , '#F5F5F5' ] ,
shadowColor : 'rgba(0,0, 0, 0)' ,
shadowBlur : 10
}
} ,
axisLine : {
lineStyle : {
color : '#DFDFDF'
}
} ,
name : {
textStyle : {
color : '#fff' ,
fontSize : 10
} ,
} ,
splitLine : {
lineStyle : {
color : '#DFDFDF'
}
}
} ) ;
m ++ ;
i ++ ;
}
var j = 0 ;
var k = 0 ;
var seriesloop =[]
pageloop =[] ;
while ( j < options.data.length ) {
valueloop =[ options.data [ j ] .column2,options.data [ j ] .column3,options.data [ j ] .column4,options.data [ j ] .column5,options.data [ j ] .column6,options.data [ j ] .column7,options.data [ j ] .column8,options.data [ j ] .column9,options.data [ j ] .column10,options.data [ j ] .column11 ]
if ( k == 8 ) {
k = 0
}
if ( k=== 0 ) {
seriesloop.push ( [] ) ;
pageloop.push ( parseInt ( j / 8 ) + 1 )
}
if ( k=== 0 && j=== 0 ) {
seriesloop [ parseInt ( j / 8 ) ] .push ( {
type : 'radar' ,
name :options.data [ j ] .column1,
tooltip : {
trigger : 'item'
} ,
radarIndex :k,
emphasis : {
lineStyle : {
width : 4 ,
}
} ,
data : [
{
value :valueloop,
//name:'Data A',
areaStyle : {
color : 'rgba(255,159, 0, 0.7)'
} ,
itemStyle : {
color : '#FFF' ,
borderColor : 'rgba(255,159, 0, 0.4)' ,
borderWidth : 0 . 5
} ,
lineStyle : {
color : 'rgba(255,159, 0, 0.7)'
}
} ,
] ,
symbol : 'circle' ,
} )
}
else {
seriesloop [ parseInt ( j / 8 ) ] .push ( {
type : 'radar' ,
name :options.data [ j ] .column1,
tooltip : {
trigger : 'item'
} ,
radarIndex :k,
emphasis : {
lineStyle : {
width : 4 ,
}
} ,
data : [
{
value :valueloop,
//name:'Data A',
areaStyle : {
color : '#7BACFA'
} ,
itemStyle : {
color : '#FFF' ,
borderColor : '#5ABAFD' ,
borderWidth : 0 . 5
} ,
lineStyle : {
color : '#2577FB'
}
} ,
] ,
symbol : 'circle' ,
} )
}
k ++ ;
j ++ ;
}
optionsloop =[] ;
for ( var a = 0 ;a < options.data.length;a ++ ) {
optionsloop.push ( { title :titlesloop [ a ] , radar :radarloop [ a ] , series :seriesloop [ a ]} ) ;
}
option ={
tooltip : { //本系列特定的tooltip设定。
show : true ,
formatter : "" ,
backgroundColor : "rgba(255,250,255,0.4)" , //提示框浮层的背景颜色。注意:series.tooltip仅在tooltip.trigger为'item'时有效。
borderColor : "#333" , //提示框浮层的边框颜色。...
borderWidth : 0 , //提示框浮层的边框宽。...
padding : 5 , //提示框浮层内边距,单位px,默认各方向内边距为5,接受数组分别设定上右下左边距。...
textStyle : { //提示框浮层的文本样式。...
//color ,fontStyle ,fontWeight ,fontFamily ,fontSize ,lineHeight,.......
} ,
} ,
timeline : {
data :pageloop,
label : {
formatter : function ( s ) { return "第"+ s + "页"; }
} ,
autoPlay : false ,
playInterval : 1500 ,
tooltip : { formatter : function ( s ) { return "第"+ s.value + "页"; }} ,
replaceMerge : [ 'title' , 'radar' , 'series' ]
} ,
options :optionsloop
} ;
b.单雷达图代码
//为保证正常绘图,代码格式与所选格式需保持一致;
//请输入JS代码或前往官网复制示例代码;
//复制完代码后,请先将代码中的数据列替换为绑定的数据列,绑定的数据列从上至下依次为column1、column2......,option部分请添加options.data以引用绑定列,chart绘制的DOMid会自动替换为变量$container
console. log ( options )
option ={
tooltip : {
show : true ,
formatter : "" ,
borderColor : "#333" ,
borderWidth : 0 ,
padding : 5 ,
textStyle : {} ,
} ,
radar : {
indicator : [{ text : '综合评分', max : 3000 } , { text : '竞争力', max : 3000 } , { text : '发展面积', max : 3000 } , { text : '增长潜力', max : 3000 } , { text : '市场容量', max : 3000 } , { text : '市场饱和度', max : 3000 } , { text : '执行力', max : 3000 } , { text : '招商能力', max : 3000 } , { text : '客户体验', max : 3000 } , { text : '直播能力', max : 3000 }] ,
center : [ '50%' , '50%' ] ,
radius : 200 ,
startAngle : 90 ,
splitNumber : 5 ,
shape : '' ,
axisName : {
formatter : '{value}' ,
color : '#000' ,
fontSize : 12
} ,
splitArea : {
areaStyle : {
color : [ '#E6E6E6' , '#F5F5F5' , '#E6E6E6' , '#F5F5F5' ] ,
shadowColor : 'rgba(0,0, 0, 0)' ,
shadowBlur : 10
}
} ,
axisLine : {
lineStyle : {
color : '#DFDFDF'
}
} ,
name : {
textStyle : {
color : '#fff' ,
fontSize : 10
} ,
} ,
splitLine : {
lineStyle : {
color : '#DFDFDF'
}
}
} ,
series : {
type : 'radar' ,
name :options.data [ 0 ] .column1,
tooltip : {
trigger : 'item'
} ,
emphasis : {
lineStyle : {
width : 4 ,
}
} ,
data : [
{
value : [ options.data [ 0 ] .column2,options.data [ 0 ] .column3,options.data [ 0 ] .column4,options.data [ 0 ] .column5,options.data [ 0 ] .column6,options.data [ 0 ] .column7,options.data [ 0 ] .column8,options.data [ 0 ] .column9,options.data [ 0 ] .column10,options.data [ 0 ] .column11 ] ,
areaStyle : {
color : '#7BACFA'
} ,
itemStyle : {
color : '#FFF' ,
borderColor : '#5ABAFD' ,
borderWidth : 0 . 5
} ,
lineStyle : {
color : '#2577FB'
}
} ,
] ,
symbol : 'circle' ,
}
} ;