本文给出使用 highchart 绘制柱状图的通用方法与接口, 只要指定相应的数据结构和配置, 就可以直接拿来使用。
一、 数据结构与基本接口
一般绘制图形, 会涉及到较复杂的数据结构, 比如使用 jsPlumb 绘制拓扑图的通用接口 。方法是, 首先要弄清楚绘制图形所需要的数据结构,然后根据API文档设计一个公共接口, 并写好详细的文档,避免日后忘记。先从最基本的接口开始, 见下面代码。 这是根据静态示例, 将需要动态生成或配置数据的地方抽取出来做成的接口。
/** * 创建柱状图(基本接口) * @param chartDivName 用来绘制柱状图的 DIV-ID 值 * @param chartData 柱状图数据结构 * categories: ['c1', 'c2', ..., 'Cn'] * series: [ * { name: 'var1', data: [d11, d12, ..., d1n]}, * { name: 'var2', data: [d21, d22, ..., d2n]}, * ..., * { name: 'varN', data: [dn1, dn2, ..., dnn]} * ] * @param chartConfig 柱状图全局配置 * title: 图表标题 * @returns */ function generateColumnChart(chartDivName, chartData, chartConfig) { var displayFormatter = function() { // 当鼠标悬停图表上时, 格式化提示信息 var tipText = '<b>' + this.x + '</b>'; var total = 0; $.each(this.points, function(i, point) { total += point.y; }); $.each(this.points, function(i, point) { tipText += '<br/>'+ point.series.name +': '+ Highcharts.numberFormat((point.y*100 / total), 2) + '%' + '(' + point.y + ')'; }); return tipText; }; var chartObj = obtainCommonChartObj(displayFormatter); chartObj.title.text = chartConfig.title; chartObj.xAxis.categories = chartData.categories; chartObj.series = chartData.series; var seriesNum = (chartData.series == null ? 0 : chartData.series.length); for (var k=0; k < seriesNum; k++) { chartObj.series[k].type = 'column'; } var chartdiv = $('#'+chartDivName); chartdiv.highcharts(chartObj); } function obtainCommonChartObj(displayFormatterFunc) { var commonChartObj = { chart: { zoomType: 'x', events: { click: null }, resetZoomButton: { position: { x: -10, y: 10 }, relativeTo: 'chart' } }, // 去掉 highcharts.com 链接 credits: { enabled: false, text: '' }, plotOptions: { series: { // 去掉点的marker, 使图形更美观 marker: { enabled: false, states: { hover: { enabled: true } } }, turboThreshold: 0, events: { click: null } }, line: { lineWidth: 1.5 } }, series: [], xAxis: { }, yAxis: { title: { text: '' }, min: 0 }, tooltip: { crosshairs: true, shared: true, formatter: displayFormatterFunc }, title: { // 动态显示图表标题 text: '', align: 'center', style: { fontSize: '12px', margin: '3px' } } }; return commonChartObj ; }
二、 对象数组与结构转化
通常, 从服务端后台返回的数据结构是对象数组, 要使用基本接口来绘制, 就需要进行数据结构转化。 因此, 在基本接口之上, 可以构建一个高层接口, 见如下代码所示。 如果要转化的数据结构比较复杂, 那么, 可以拿一个静态的输入/输出数据示例作为范本来辅佐思考, 先通过硬编码来实现目标, 然后再将硬编码用可配置项替换掉, 达到可扩展、灵活的目标。
/** * 创建柱状图(针对对象数组的高层接口) * @param chartDivName 用来绘制柱状图的 DIV-ID 值 * @param chartData 对象数组 * categories: ['c1', 'c2', ..., 'Cn'] * data: * [{'field1': 'v11', 'field2': 'v12', ..., 'fieldN': 'v1N'}, * {'field1': 'v21', 'field2': 'v22', ..., 'fieldN': 'v2N'}, * ..., * {'field1': 'vN1', 'field2': 'vN2', ..., 'fieldN': 'vNN'}] * @param chartConfig 柱状图全局配置 * title: 图表标题 * categoryField: 分类字段 * groupField: 用于创建 legend 的分组字段 * valueField: 用于显示 Y 轴的字段 * @returns */ function generateColumnChartHighLevel(chartDivName, chartData, chartConfig) { var groupField = chartConfig.groupField; var valueField = chartConfig.valueField; var categoryField = chartConfig.categoryField; var categories = chartData.categories; var groupedChartData = groupByField(chartData.data, groupField); var series = []; for (var i=0; i< groupedChartData.length; i++) { var groupName = groupedChartData[i][groupField]; var groupData = groupedChartData[i]['data']; var fieldData = []; for (var k=0; k < groupData.length; k++) { // 每个分类的值必须与相应的分类对应, 应对这样的情况 // 对于每个 groupField, 并不是所有 categories 都有值, 可以通过测试例子看出来 // 苹果在 Q3 对应的值是缺失的, 香蕉在 Q2 对应的值是缺失的 var categoryPosition = getCategoryPosition(categories, groupData[k][categoryField]); if (categoryPosition != -1) { fieldData[categoryPosition] = groupData[k][valueField]; } } for (var j=0; j < categories.length; j++) { // 缺失值填充 if (fieldData[j] == null) { fieldData[j] = 0; } } var subseries = { name: groupName, data: fieldData, }; series.push(subseries); } var data = {}; data.categories = categories; data.series = series; generateColumnChart(chartDivName, data, chartConfig); } /** * 检测 value 在 categories 中的位置 * @param categories * @param value */ function getCategoryPosition(categories, value) { for (var index=0; index < categories.length; index++) { if (categories[index] == value) { return index; } } return -1; } /** * 将指定 chartData 数据按照指定字段的值分类 * eg. [{'timestamp': 'time0', cpu: '0', sys: '15', usr: '20'}, {'timestamp': 'time0', cpu: '1', sys: '16', usr: '21'}, * {'timestamp': 'time1', cpu: '0', sys: '20', usr: '13'}, {'timestamp': 'time1', cpu: '1', sys: '18', usr: '10'}] * 转换为 [{ cpu:'0', data: [{'timestamp': 'time0', cpu: '0', sys: '15', usr: '20'}, {'timestamp': 'time1', cpu: '0', sys: '20', usr: '13'}] } , * { cpu:'1', data: [{'timestamp': 'time0', cpu: '1', sys: '16', usr: '21'}, {'timestamp': 'time1', cpu: '1', sys: '18', usr: '10'}] }] */ var groupByField = function(chartDataGathered, fieldName) { var fieldDataMappingArray = []; var fieldData = {}; var i=0, num = (chartDataGathered == null ? 0 : chartDataGathered.length); for (i=0; i<num; i++) { var fieldValue = chartDataGathered[i][fieldName]; fieldData = obtainFieldData(fieldDataMappingArray, fieldName, fieldValue); if (fieldData == null) { fieldData = {}; fieldData[fieldName] = fieldValue; fieldData['data'] = []; fieldDataMappingArray.push(fieldData); } fieldData['data'].push(chartDataGathered[i]); } return fieldDataMappingArray; } /** * 在 fieldDataMappingArray 中检测是否有 fieldName = fieldValue 的对象, 若有则返回; 若没有则返回 null * @param fieldDataMappingArray [{'fieldName': 'fieldValue1', 'data':[]}, {'fieldName': 'fieldValue2', data: []}] * @param fieldName the name of field * @param fieldValue the value of field */ var obtainFieldData = function(fieldDataMappingArray, fieldName, fieldValue) { var k=0, dataArrayLength = (fieldDataMappingArray == null ? 0 : fieldDataMappingArray.length); var fieldData = {}; var existFieldData = {}; if (dataArrayLength == 0) { return null; } for (k=0; k<dataArrayLength; k++) { existFieldData = fieldDataMappingArray[k]; if (existFieldData[fieldName] == fieldValue) { return existFieldData; } } return null; }
三、 使用接口
<!doctype html public "-//w3c//dtd html 4.01//en" "http://www.w3.org/tr/html4/strict.dtd"> <html> <head> <meta http-equiv="content-type" content="text/html; charset=utf-8"> <title>highcharts 绘图示例</title> <script src="jquery-1.10.1.min.js"></script> <script src="highcharts.js"></script> <script src="draw_highcharts.js"></script> <script type="text/javascript"> $(document).ready(function() { /** * 一个用于测试基本接口的示例 * @param chartDivId */ function testGenerateColumnChart(chartDivId) { var categories = ['2013-11', '2013-12', '2014-01']; var series = [ { name: '苹果', data: [1500, 1300, 1200] }, { name: '桔子', data: [3500, 5000, 2500] }, { name: '香蕉', data: [2000, 1800, 1600] } ]; var data = { categories: categories, series: series }; var chartConfig = { title: '第四季度水果销量' }; generateColumnChart(chartDivId, data, chartConfig); } testGenerateColumnChart('testBasicColumnchartdiv'); /** * 测试绘制柱状图高层接口的测试例子 * @param chartDivId */ function testGenerateColChartHighLevel(chartDivId) { var data = [ { time: 'Q1' , fruit: '苹果', sale: 1500 }, { time: 'Q1' , fruit: '桔子', sale: 1300 }, { time: 'Q1' , fruit: '香蕉', sale: 1400 }, { time: 'Q2' , fruit: '苹果', sale: 1500 }, { time: 'Q2' , fruit: '桔子', sale: 1900 }, { time: 'Q3' , fruit: '桔子', sale: 1700 }, { time: 'Q3' , fruit: '香蕉', sale: 1800 } ]; var categories = ['Q1', 'Q2', 'Q3']; var chartData = { categories: categories, data: data }; var chartConfig = { title: '季度水果销量', categoryField: 'time', groupField: 'fruit', valueField: 'sale' } generateColumnChartHighLevel(chartDivId, chartData, chartConfig); } testGenerateColChartHighLevel('testAdvancedColumnchartdiv'); }); </script> <style> body { font-family: '微软雅黑', '宋体', 'san-serif'; } .chartdiv { width: 90%; height: 250px; } </style> </head> <body> <div id="testBasicColumnchartdiv" class="chartdiv"></div> <div id="testAdvancedColumnchartdiv" class="chartdiv"></div> </body> </html>
四、 效果图
五、 小结
要绘制柱状图, 需要对数据结构和算法有较好的掌握, 能够自由地在各种数据结构中进行转换。通过此例, 是想再次说明了结构与算法在实际开发工作中的应用。 当然, 文中给出的代码并非是最优的, 作为一个基本的解法, 里面还是有很多可改进之处。