ExtJs4实战流量统计系统----系统核心是图表,曲线数量不固定怎么办?(二)
能最直观的展示流量数据的,当属各种图表了,神马折线图、柱状图、饼图......
这也是我选择ExtJs来重构这个项目的原因,因为ExtJs有一套图表控件,虽然功能较FusionCharts略弱~,
但ExtJs还有其他诸如Grid、Tree、Form等一系列用于开发一个完整系统的控件,这对于一个界面要靠自己的设计盲来说......不说你们也知道。
------------------------------我是分隔线-----------------------------------
刚开始动手,就发现Ext的折线图中,一条曲线就是一个字段,由该字段的多个值绘成一条曲线。
比如说:今天的流量数据,从0点到23点,那么要实现PV一条曲线,UV一条曲线,那再简单不过了。
创建个Chart,设置好对应的字段名就OK了,这像这样子↓。
------------------------------我是分隔线-----------------------------------
但除此类图表外,我还需要的是:只显示PV数据,但要的是一天一条曲线,比如昨天一条,今天一条,都是PV数据,都是0点到23点的数据,好对比嘛,
这就麻烦了,使劲研究API后,还是得抓后脑勺......
问了下度娘和google,倒是查了好几个,点进去一看,其实也就一两个,其他都是复制粘贴,解决办法也一样:那就是在服务器端处理好数据,格式化为Ext能解析的样式。
比如这样的数据:
目的是:X轴是小时(VTime),Y轴自然是PV了,然后23号一条张,24号一条线,当然,也可能要继续增加:25号一条,26号一条......
这样Ext原生就无法支持了,以Ext对数据格式的要求:"2013-02-23"和"2013-02-24"得是字段名,字段值是PV,主键是VTime。
如果在服务端处理的话,"2013-02-23"这样的东西,显然不能做为对象的属性。
那要返回json给Ext就只能拼字符串了,着实麻烦,直接放弃。
------------------------------我是分隔线-----------------------------------
那就只能用脚本来重新转换数据格式了。
原本我是希望能创建一个新的Reader,就像JsonReader一样,接受到数据后,通过此Reader转换数据,多方便;无奈以我目前的功能,实在无法参透Ext的源码。
没办法了,只能先从服务端获取原生数据,然后再逐条格式化,最后重新组合。
new一个新的store,设置好fields,将组合好的新格式数据,填充到store里,创建Chart,将store绑定到Chart。
OK,虽然笨拙了点,但好歹是实现了。
下边就是代码了,因为是就着需求来写这功能,原本看着还可以配置下参数实现通用。
但随着需求的变化,比如,需要从返回的日期字段里,分离出日,后来又要分离出月,这下好了,着急实现效果,顾不上通用不通用了,直接改之。
------------------------------我是分隔线-----------------------------------
首先,这个没疑问,Ajax获取数据。格式如上边的数据库截图。
然后便是转换数据格式了,思路大概是这样:
通过三个配置属性:
属性1:需要做为X轴值的字段名(如小时);
属性2:需要做为曲线数据的字段(如PV);
属性3:再就是需要做为一条曲线的字段(如日期)。
转换过程:循环所有返回的原生数据,逐条分析。
- 根据配置,找到属性3的值,把它放进一个数组(这个数组用来存属性3的值,也就是所有曲线,数组有多少条数据,就会有多少条曲线,不重复是肯定的),再new object(),属性3的值就是此对象的一个属性,属性的值,是以属性2为名的字段值,当然,X轴怎么能忘了呢,获取属性1为我的字段值,也做为此对象的一个属性值,属性名嘛可以通过配置也可以固定死。
- 这样,当第一条数据走完上述步骤后,新的数据组合里,已经有一条记录了,假设这第新记录是这样的:“VHour:5,2013-02-23:15555”,接下来再循环其他原生数据时,就得做判断了,比如如果新组合里,已经有小时=5的数据了,而当前循环到的数据,也是小时=5的数据,但日期是另一天,那么就不需要new object(),直接给已经存在的数据新增一个属性就是了,就是这样:“VHour:5,2013-02-23:15555,2013-02-24:3214”。
- 如此循环处理完所有原生数据,那么上边的截图数据,就会变成这样的格式,当然,新数据是json格式的。
VHour 2013-02-23 2013-02-24 0 30311 27480 1 19363 17506 2 12401 11292 3 9064 9449 4 7680 8631 5 12818 9248
代码在这(这是我根据自己的需求实现的,看不懂的可以留言):
1 /// <reference path="../../Scripts/ext4.2/ext-all.js" /> 2 Ext.define('Yiqi.Show.Chart.MultiDiyLine', { 3 extend: 'Ext.panel.Panel', 4 //requires: ['Yiqi.Chart.DiyJson'], 5 6 BottomTitle: '小时', 7 BaseFieldName: 'VTime', //--基本字段,以此字段为基础,将所有该字段值一样的数据,进行处理。 8 TurnFieldName: 'VDate', //--需要将值转为新的字段名的字段 9 NewTurnFieldFormat: '', 10 IsSplitTurnFieldDate: false, //--将转换字段,拆分为month及day 11 LineFormat: 'Y-m', 12 ReturnDateFormat: 'MS',//--MVC返回的时间值,需要这样的格式来解析 13 xFormat: 'd', 14 ValueToField: 'PV', //--作为新字段值的原字段。 15 ConstFields: [], //-- 16 root: 'listdata', 17 18 lineFields: [], 19 20 firstLoad: true, 21 22 url: '/CatagoryStatics/GetHourDataByDateBetween', 23 catagoryId: 6, 24 dtBegin: '', 25 dtEnd: '', 26 27 initComponent: function () { 28 var me = this; 29 this.loadMask = new Ext.LoadMask(this, { msg: "加载中...", removeMask: true }), 30 Ext.apply(this, { 31 layout: 'fit' 32 }); 33 34 this.callParent(arguments); 35 }, 36 afterRender: function () { 37 this.loadData(); 38 this.callParent(arguments); 39 }, 40 buildTbarItems: function () { 41 this.beginDateField = Ext.create('Ext.form.field.Date', { 42 name: 'dtbegin', 43 format: 'Y-m-d', 44 fieldLabel: '起止日期', 45 labelWidth: 60, 46 labelAlign: 'right', 47 width: 170, 48 value: Ext.Date.add(new Date(), Ext.Date.DAY, -1) 49 }); 50 this.endDateField = Ext.create('Ext.form.field.Date', { 51 name: 'datefield', 52 format: 'Y-m-d', 53 value: new Date() 54 }); 55 }, 56 loadData: function (dateBegin, dateEnd) { 57 var me = this; 58 me.loadMask.show(); 59 if (dateBegin) { 60 me.dtBegin = dateBegin; 61 } 62 if (dateEnd) { 63 me.dtEnd = dateEnd; 64 } 65 //--先获取数据 66 Ext.Ajax.request({ 67 url: me.url, 68 params: { 69 catagoryId: me.catagoryId, 70 dtBegin: me.dtBegin, 71 dtEnd: me.dtEnd 72 }, 73 success: function (response) { 74 if (response.responseText == "unauth") { 75 Yiqi.Common.Tools.IsLogin(response); 76 } 77 else { 78 //--解析并创建图表 79 var allData = Ext.decode(response.responseText); 80 me.buildAll(allData); 81 me.loadMask.hide(); 82 } 83 }, 84 failure: function (response) { 85 Yiqi.Common.Tools.DealAjaxError(response); 86 } 87 }); 88 }, 89 getParamsValue: function () { 90 this.params.dtBegin = this.beginDateField.getValue(); 91 this.params.dtEnd = this.endDateField.getValue(); 92 this.params.catagoryId = this.catagoryId; 93 }, 94 buildAll: function (allData) { 95 if (!this.firstLoad) { 96 this.removeAll(true); 97 } 98 this.firstLoad = false; 99 this.buildStore(allData); 100 var objFields = Ext.Array.remove(this.lineFields, this.BaseFieldName); 101 objFields = Ext.Array.sort(objFields); 102 this.buildChart(objFields); 103 this.add(this.lineChart); 104 }, 105 buildStore: function (orgData) { 106 //--重新转换数据格式 107 var newData = this.readRecords(orgData); 108 this.dataStore = Ext.create('Ext.data.JsonStore', { 109 autoDestroy: true, 110 fields: this.lineFields, 111 data: newData 112 }); 113 }, 114 buildChart: function (fields) { 115 this.buildChartSeries(fields); 116 this.lineChart = Ext.create('Ext.chart.Chart', { 117 style: 'background:#fff', 118 animate: true, 119 //shadow: false, 120 theme: 'Category2', 121 legend: { 122 position: 'right' 123 }, 124 axes: [{ 125 type: 'Numeric', 126 position: 'left', 127 fields: fields, 128 title: 'PV', 129 grid: true 130 }, { 131 type: 'Category', 132 position: 'bottom', 133 fields: [this.BaseFieldName], 134 title: this.BottomTitle 135 }], 136 series: this.series, 137 store: this.dataStore 138 }); 139 140 }, 141 buildAxes: function (fields) { 142 this.axes = [{ 143 type: 'Numeric', 144 position: 'left', 145 fields: fields, 146 grid: true 147 }, { 148 type: 'Category', 149 position: 'bottom', 150 fields: [this.BaseFieldName], 151 title: this.BottomTitle 152 }] 153 }, 154 buildChartSeries: function (fields) { 155 //--循环创建 156 var me = this; 157 var objSeries = []; 158 Ext.Array.each(fields, function (obj, idx, all) { 159 objSeries[idx] = { 160 type: 'line', 161 smooth: true, 162 highlight: { 163 size: 2, 164 radius: 2 165 }, 166 style: { 167 'stroke-width': 1 168 }, 169 xField: me.BaseFieldName, 170 yField: [String(obj)], 171 tips: { 172 trackMouse: true, 173 renderer: function (storeItem, item) { 174 this.update(storeItem.get(me.BaseFieldName) + ': ' + Ext.util.Format.number(storeItem.get(String(obj)), '0,0')); 175 } 176 } 177 }; 178 }); 179 this.series = objSeries; 180 }, 181 readRecords: function (data) { 182 var me = this; 183 //--是否需要从日期字段中分离数据 184 if (me.IsSplitTurnFieldDate) { 185 me.BaseFieldName = "VDay"; 186 } 187 this.lineFields = new Array(); 188 this.lineFields.push(me.BaseFieldName); 189 var newData = []; 190 var listData = data[me.root]; 191 var newTurnFieldName = me.TurnFieldName; 192 //--重新转换数据 193 Ext.each(listData, function (obj, idx, all) { 194 var objBaseField = obj[me.BaseFieldName]; 195 //--针对日期数据的处理,因为MVC返回的时间字段数据格式是new Data(/12345465/) 196 if (me.IsSplitTurnFieldDate) { 197 objBaseField = Ext.Date.format(Ext.Date.parse(obj[me.TurnFieldName], me.ReturnDateFormat), me.xFormat); 198 } 199 if (!Ext.isEmpty(me.NewTurnFieldFormat)) { 200 201 } 202 //--从已分析好的数据中,查看是否已经存在 203 var newObj = me.findItemByBaseField(newData, objBaseField); 204 //--如果已经存在,那就删除,因为接下来的操作会修改数据 205 newData = Ext.Array.remove(newData, newObj); 206 for (var key in obj) { 207 if (key == me.BaseFieldName) { 208 newObj[key] = obj[key]; 209 } 210 else if (key == me.TurnFieldName) { 211 var objVal = obj[key]; 212 //--针对日期数据的处理 213 if (me.IsSplitTurnFieldDate) { 214 objVal = Ext.Date.parse(objVal, me.ReturnDateFormat); 215 216 newObj[me.BaseFieldName] = objBaseField; 217 objVal = Ext.Date.format(objVal, me.LineFormat); 218 } 219 //--将解析获取的值(如可能是一个日期2013-02-23)作为对象的一个新属性,属性值也是之前配置的一个字段值 220 newObj[String(objVal)] = obj[me.ValueToField]; 221 me.InsertToModelFields(String(objVal)); 222 } 223 } 224 //--修改完,重新放入数据中 225 newData = Ext.Array.push(newData, newObj); 226 }); 227 newData = Ext.Array.sort(newData, function (a, b) { 228 return (a[me.BaseFieldName] - b[me.BaseFieldName]); 229 }); 230 return newData; 231 }, 232 findItemByBaseField: function (array, val) { 233 var newObj = {}; 234 var me = this; 235 Ext.Array.each(array, function (obj, idx, all) { 236 if (obj[me.BaseFieldName] == val) { 237 newObj = obj; 238 return false; 239 } 240 }); 241 return newObj; 242 }, 243 InsertToModelFields: function (modelName) { 244 var me = this; 245 var existsFlag = false; 246 Ext.Array.each(this.lineFields, function (field, idx, all) { 247 var fieldName = undefined; 248 var typeName = typeof (field); 249 if (typeName == "string") { 250 fieldName = field; 251 } 252 else if (typeName == "object") { 253 fieldName = field["name"]; 254 } 255 if (fieldName == modelName) { 256 existsFlag = true; 257 return false; 258 } 259 }); 260 261 262 if (!existsFlag) { 263 this.lineFields.push(modelName); 264 } 265 } 266 });
------------------------------我是分隔线-----------------------------------
这就符合Ext图表空间要求的数据格式了。
接下来就是创建图表了,循环步骤1里的数组,逐一创建曲线,最后绑定数据,就OK了。。。
------------------------------我是分隔线-----------------------------------
PS:能实现的方法确实有很多,我这样做,也是时间所迫,来不及去想更简单方便的方法,现在项目已经上线了,却又没什么精力再返回去继续研究。