在Extjs 中, 单一的 Column Chart 的展示效果如上。
定义的步骤如下:
1. 创建一个 Ext.chart.Chart
2. 创建两个坐标轴, axes
一个 Category 类型的横坐标用来显示日期
一个Numeric 类型的纵坐标用来显示数据
3. 配置显示的图 series
配置 column 类型的柱状图。
具体代码如下:
<!-- Author : oscar999 Date : ALL RIGHTS RESERVED --> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Insert title here</title> <script type="text/javascript" src="../lib/extjs/ext-all.js"></script> <link rel="stylesheet" type="text/css" href="../lib/extjs/resources/ext-theme-neptune/ext-theme-neptune-all.css" /> <script> Ext.onReady(function(){ window.generateData = function(n, floor){ var data = [], p = (Math.random() * 11) + 1, i; floor = (!floor && floor !== 0)? 20 : floor; for (i = 0; i < (n || 12); i++) { data.push({ name: Ext.Date.monthNames[i % 12], data1: Math.floor(Math.max((Math.random() * 100), floor)), data2: Math.floor(Math.max((Math.random() * 100), floor)), data3: Math.floor(Math.max((Math.random() * 100), floor)), data4: Math.floor(Math.max((Math.random() * 100), floor)), data5: Math.floor(Math.max((Math.random() * 100), floor)), data6: Math.floor(Math.max((Math.random() * 100), floor)), data7: Math.floor(Math.max((Math.random() * 100), floor)), data8: Math.floor(Math.max((Math.random() * 100), floor)), data9: Math.floor(Math.max((Math.random() * 100), floor)) }); } return data; }; var store1 = Ext.create('Ext.data.JsonStore', { fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5', 'data6', 'data7', 'data9', 'data9'], data: generateData() }); var chart = Ext.create('Ext.chart.Chart', { style: 'background:#fff', animate: true, shadow: true, store: store1, //maxWidth: 500, //columnWidth : 0.1, axes: [{ type: 'Numeric', position: 'left', fields: ['data1'], label: { renderer: Ext.util.Format.numberRenderer('0,0') }, title: 'Number of Hits', grid: true, minimum: 0 }, { type: 'Category', position: 'bottom', fields: ['name'], //categoryNames:new String("111"), title: 'Month of the Year' }], series: [{ type: 'column', axis: 'left', highlight: true, tips: { trackMouse: true, width: 140, height: 28, renderer: function(storeItem, item) { this.setTitle(storeItem.get('name') + ': ' + storeItem.get('data1') + ' $'); } }, label: { display: 'insideEnd', 'text-anchor': 'middle', field: 'data1', //renderer: Ext.util.Format.numberRenderer('0'), orientation: 'vertical', color: '#FFF' }, style:{ opacity: 0.95 //,width:100 }, //xPadding:{left:100,right:100}, xField: 'name', yField: 'data1' }] }); var win = Ext.create('Ext.window.Window', { width: 800, height: 600, minHeight: 400, minWidth: 550, hidden: false, maximizable: true, title: 'Column Chart', autoShow: true, layout: 'fit', tbar: [{ text: 'Save Chart', handler: function() { Ext.MessageBox.confirm('Confirm Download', 'Would you like to download the chart as an image?', function(choice){ if(choice == 'yes'){ chart.save({ type: 'image/png' }); } }); } }, { text: 'Reload Data', handler: function() { // Add a short delay to prevent fast sequential clicks window.loadTask.delay(100, function() { store1.loadData(generateData()); }); } }], items: chart }); }); </script> </head> <body> </body> </html>
针对上面的例子, 坐标轴的长度是Extjs根据数据大小自动运算并设置的。
如果需要手动定义这个长度的话,改如何设置呢?
对于Numeric这样的坐标轴来说, 有maximum 和 minimun 这样的参数可以配置。
而且配置也很简单了, 就不多介绍了。
这里只是对红色的部分做一个伏笔(这种设置对于配置 堆叠的图形不使用)
有的状况下, 可能会使用多个图形,
因为图形的范围不同,可能需要使用两个纵坐标轴。
类似的情景可以是这样:
有三个图, 两个柱状图, 一个折线图
折线图和柱状图的数据范围或是单位可能不同。
这里看上去好像是一个折线图和一个柱状图。
其实是有两个柱状图, 只不过一个被另一个盖住了。
出现这种状况的原因是在定义的时候, 往series 中添加了两个column 的chart.
这种状况的源码是:
<!-- Author : oscar999 Date : ALL RIGHTS RESERVED --> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Insert title here</title> <script type="text/javascript" src="../lib/extjs/ext-all.js"></script> <link rel="stylesheet" type="text/css" href="../lib/extjs/resources/ext-theme-neptune/ext-theme-neptune-all.css" /> <script> Ext.onReady(function(){ window.generateData = function(n, floor){ var data = [], p = (Math.random() * 11) + 1, i; floor = (!floor && floor !== 0)? 20 : floor; for (i = 0; i < (n || 12); i++) { data.push({ name: Ext.Date.monthNames[i % 12], data1: (i+1)*8, data2: (i+1)*10, data3: (i+1)*8 }); } return data; }; var store1 = Ext.create('Ext.data.JsonStore', { fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5', 'data6', 'data7', 'data9', 'data9'], data: generateData() }); var chart = Ext.create('Ext.chart.Chart', { style: 'background:#fff', animate: true, shadow: true, store: store1, legend:'right', axes: [{ type: 'Numeric', position: 'left', fields: ['data1','data2'], label: { renderer: Ext.util.Format.numberRenderer('0,0') }, title: 'Number of Hits', grid: true },{ type: 'Numeric', position: 'right', fields: ['data3'], label: { renderer: Ext.util.Format.numberRenderer('0,0') }, //title: 'Number of Hits', grid: true }, { type: 'Category', position: 'bottom', fields: ['name'], title: 'Month of the Year' }], series: [{ type: 'column', axis: 'left', highlight: true, tips: { trackMouse: true, width: 140, height: 28, renderer: function(storeItem, item) { this.setTitle(storeItem.get('name') + ': ' + storeItem.get('data1') + ' $'); } }, label: { display: 'insideEnd', 'text-anchor': 'middle', field: 'data1', //renderer: Ext.util.Format.numberRenderer('0'), orientation: 'vertical', color: '#FFF' }, style:{ opacity: 0.95 //,width:100 }, //xPadding:{left:100,right:100}, xField: 'name', yField: ['data1'] } ,{ type: 'column', axis: 'left', highlight: true, tips: { trackMouse: true, width: 140, height: 28, renderer: function(storeItem, item) { this.setTitle(storeItem.get('name') + ': ' + storeItem.get('data2') + ' $'); } }, label: { display: 'insideEnd', 'text-anchor': 'middle', field: ['data1','data2'], //renderer: Ext.util.Format.numberRenderer('0'), orientation: 'vertical', color: '#FFF' }, style:{ opacity: 0.95 //,width:100 }, //xPadding:{left:100,right:100}, xField: 'name', yField: ['data2'] } , { type: 'line', axis: 'right', highlight: true, tips: { trackMouse: true, width: 140, height: 28, renderer: function(storeItem, item) { this.setTitle(storeItem.get('name') + ': ' + storeItem.get('data3') + ' $'); } }, label: { display: 'insideEnd', 'text-anchor': 'middle', field: 'data3', //renderer: Ext.util.Format.numberRenderer('0'), orientation: 'vertical', color: '#FFF' }, style:{ opacity: 0.95 //,width:100 }, //xPadding:{left:100,right:100}, xField: 'name', yField: 'data3' }] }); var win = Ext.create('Ext.window.Window', { width: 800, height: 600, minHeight: 400, minWidth: 550, hidden: false, maximizable: true, title: 'Column Chart', autoShow: true, layout: 'fit', tbar: [{ text: 'Save Chart', handler: function() { Ext.MessageBox.confirm('Confirm Download', 'Would you like to download the chart as an image?', function(choice){ if(choice == 'yes'){ chart.save({ type: 'image/png' }); } }); } }, { text: 'Reload Data', handler: function() { // Add a short delay to prevent fast sequential clicks window.loadTask.delay(100, function() { store1.loadData(generateData()); }); } }], items: chart }); }); </script> </head> <body> </body> </html>
我们要的效果应该是这样:
这是完美的呈现方式。
完美方式实现的思想是:
1 创建两个坐标轴, axes
一个 Category 类型的横坐标用来显示日期
一个Numeric 类型的纵坐标用来显示柱状图数据一个Numeric 类型的纵坐标用来显示折线图数据
2. series 添加一个 column Chart. stacked.
以上定义完成之后, 会发生基本上都正常, 但是有一点就是, 左右的两个纵坐标的尺度可能不同。
通过配置maximum 和maximum 来设置坐标并不会生效。
这里就要提到上面框出的maximum配置的红色部分了, 因为这个配置对于堆叠的图已经不适用了。
为什么会出现左右坐标不一致的状况, 看一看Extjs 的 Ext.chart.axis.Numeric 的定义
原来是这个地方有限制。这应该是Extjs 有意为之了, 至于原因是什么,尚不可知。
不管这样,先去除这个限制得到想要的效果。
解决方案就是定义一个和 Ext.chart.axis.Numeric 类似的坐标定义
/** * Add by Oscar999 */ Ext.define('Ext.chart.axis.StackedNumeric', { /* Begin Definitions */ extend: 'Ext.chart.axis.Axis', alternateClassName: 'Ext.chart.StackedNumericAxis', /* End Definitions */ type: 'StackedNumeric', // @private isNumericAxis: true, alias: 'axis.stackednumeric', uses: ['Ext.data.Store'], constructor: function(config) { var me = this, hasLabel = !!(config.label && config.label.renderer), label; me.callParent([config]); label = me.label; if (config.constrain == null) { me.constrain = (config.minimum != null && config.maximum != null); } if (!hasLabel) { label.renderer = function(v) { return me.roundToDecimal(v, me.decimals); }; } }, roundToDecimal: function(v, dec) { var val = Math.pow(10, dec || 0); return Math.round(v * val) / val; }, /** * @cfg {Number} minimum * The minimum value drawn by the axis. If not set explicitly, the axis * minimum will be calculated automatically. It is ignored for stacked charts. */ minimum: NaN, /** * @cfg {Number} maximum * The maximum value drawn by the axis. If not set explicitly, the axis * maximum will be calculated automatically. It is ignored for stacked charts. */ maximum: NaN, /** * @cfg {Boolean} constrain * If true, the values of the chart will be rendered only if they belong between minimum and maximum. * If false, all values of the chart will be rendered, regardless of whether they belong between minimum and maximum or not. * Default's true if maximum and minimum is specified. It is ignored for stacked charts. */ constrain: true, /** * @cfg {Number} decimals * The number of decimals to round the value to. */ decimals: 2, /** * @cfg {String} scale * The scaling algorithm to use on this axis. May be "linear" or * "logarithmic". Currently only linear scale is implemented. * @private */ scale: "linear", // @private constrains to datapoints between minimum and maximum only doConstrain: function() { var me = this, chart = me.chart, store = chart.getChartStore(), items = store.data.items, d, dLen, record, series = chart.series.items, fields = me.fields, ln = fields.length, range = me.calcEnds(), min = range.from, max = range.to, i, l, useAcum = false, value, data = [], addRecord; for (d = 0, dLen = items.length; d < dLen; d++) { addRecord = true; record = items[d]; for (i = 0; i < ln; i++) { value = record.get(fields[i]); if (me.type == 'Time' && typeof value == "string") { value = Date.parse(value); } if (+value < +min) { addRecord = false; break; } if (+value > +max) { addRecord = false; break; } } if (addRecord) { data.push(record); } } chart.setSubStore(new Ext.data.Store({ model: store.model, data: data })); }, /** * @cfg {String} position * Indicates the position of the axis relative to the chart */ position: 'left', /** * @cfg {Boolean} adjustMaximumByMajorUnit * Indicates whether to extend maximum beyond data's maximum to the nearest * majorUnit. */ adjustMaximumByMajorUnit: false, /** * @cfg {Boolean} adjustMinimumByMajorUnit * Indicates whether to extend the minimum beyond data's minimum to the * nearest majorUnit. */ adjustMinimumByMajorUnit: false, // applying constraint processView: function() { var me = this; if (me.constrain) { me.doConstrain(); } }, // @private apply data. applyData: function() { this.callParent(); return this.calcEnds(); } });
下面给出一个放在同一份文件中完整的源码:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Insert title here</title> <script type="text/javascript" src="../lib/extjs/ext-all.js"></script> <script> /*Ext.define('mtk.chart.axis.Numeric', { extend : 'Ext.chart.axis.Numeric', type: 'stackedNumeric', initComponent: function(config) { this.processView = function() { var me = this, chart = me.chart, series = chart.series.items, i, l; for (i = 0, l = series.length; i < l; i++) { if (series[i].stacked) { // Do not constrain stacked charts (bar, column, or area). delete me.minimum; delete me.maximum; me.constrain = false; break; } } if (me.constrain) { me.doConstrain(); } }; this.callParent([config]); } });*/ Ext.define('Ext.chart.axis.StackedNumeric', { /* Begin Definitions */ extend: 'Ext.chart.axis.Axis', alternateClassName: 'Ext.chart.StackedNumericAxis', /* End Definitions */ type: 'StackedNumeric', // @private isNumericAxis: true, alias: 'axis.stackednumeric', uses: ['Ext.data.Store'], constructor: function(config) { var me = this, hasLabel = !!(config.label && config.label.renderer), label; me.callParent([config]); label = me.label; if (config.constrain == null) { me.constrain = (config.minimum != null && config.maximum != null); } if (!hasLabel) { label.renderer = function(v) { return me.roundToDecimal(v, me.decimals); }; } }, roundToDecimal: function(v, dec) { var val = Math.pow(10, dec || 0); return Math.round(v * val) / val; }, /** * @cfg {Number} minimum * The minimum value drawn by the axis. If not set explicitly, the axis * minimum will be calculated automatically. It is ignored for stacked charts. */ minimum: NaN, /** * @cfg {Number} maximum * The maximum value drawn by the axis. If not set explicitly, the axis * maximum will be calculated automatically. It is ignored for stacked charts. */ maximum: NaN, /** * @cfg {Boolean} constrain * If true, the values of the chart will be rendered only if they belong between minimum and maximum. * If false, all values of the chart will be rendered, regardless of whether they belong between minimum and maximum or not. * Default's true if maximum and minimum is specified. It is ignored for stacked charts. */ constrain: true, /** * @cfg {Number} decimals * The number of decimals to round the value to. */ decimals: 2, /** * @cfg {String} scale * The scaling algorithm to use on this axis. May be "linear" or * "logarithmic". Currently only linear scale is implemented. * @private */ scale: "linear", // @private constrains to datapoints between minimum and maximum only doConstrain: function() { var me = this, chart = me.chart, store = chart.getChartStore(), items = store.data.items, d, dLen, record, series = chart.series.items, fields = me.fields, ln = fields.length, range = me.calcEnds(), min = range.from, max = range.to, i, l, useAcum = false, value, data = [], addRecord; for (d = 0, dLen = items.length; d < dLen; d++) { addRecord = true; record = items[d]; for (i = 0; i < ln; i++) { value = record.get(fields[i]); if (me.type == 'Time' && typeof value == "string") { value = Date.parse(value); } if (+value < +min) { addRecord = false; break; } if (+value > +max) { addRecord = false; break; } } if (addRecord) { data.push(record); } } chart.setSubStore(new Ext.data.Store({ model: store.model, data: data })); }, /** * @cfg {String} position * Indicates the position of the axis relative to the chart */ position: 'left', /** * @cfg {Boolean} adjustMaximumByMajorUnit * Indicates whether to extend maximum beyond data's maximum to the nearest * majorUnit. */ adjustMaximumByMajorUnit: false, /** * @cfg {Boolean} adjustMinimumByMajorUnit * Indicates whether to extend the minimum beyond data's minimum to the * nearest majorUnit. */ adjustMinimumByMajorUnit: false, // applying constraint processView: function() { /*var me = this, chart = me.chart, series = chart.series.items, i, l; for (i = 0, l = series.length; i < l; i++) { if (series[i].stacked) { // Do not constrain stacked charts (bar, column, or area). delete me.minimum; delete me.maximum; me.constrain = false; break; } } if (me.constrain) { me.doConstrain(); }*/ var me = this; if (me.constrain) { me.doConstrain(); } }, // @private apply data. applyData: function() { this.callParent(); return this.calcEnds(); } }); </script> <link rel="stylesheet" type="text/css" href="../lib/extjs/resources/ext-theme-neptune/ext-theme-neptune-all.css" /> <script> Ext.onReady(function(){ window.generateData = function(n, floor){ var data = [], p = (Math.random() * 11) + 1, i; floor = (!floor && floor !== 0)? 20 : floor; for (i = 0; i < (n || 12); i++) { data.push({ name: Ext.Date.monthNames[i % 12], /*data1: Math.floor(Math.max((Math.random() * 100), floor)), data2: Math.floor(Math.max((Math.random() * 100), floor)), data3: Math.floor(Math.max((Math.random() * 100), floor)), data4: Math.floor(Math.max((Math.random() * 100), floor)), data5: Math.floor(Math.max((Math.random() * 100), floor)), data6: Math.floor(Math.max((Math.random() * 100), floor)), data7: Math.floor(Math.max((Math.random() * 100), floor)), data8: Math.floor(Math.max((Math.random() * 100), floor)), data9: Math.floor(Math.max((Math.random() * 100), floor))*/ data1: (i+1)*8, data2: (i+1)*8, data3: (i+1)*8, data4: (i+1)*8, data5: (i+1)*8, data6: (i+1)*8, data7: (i+1)*8, data8: (i+1)*8, data9: (i+1)*8 }); } return data; }; var store1 = Ext.create('Ext.data.JsonStore', { fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5', 'data6', 'data7', 'data9', 'data9'], data: generateData() }); var chart = Ext.create('Ext.chart.Chart', { style: 'background:#fff', animate: true, shadow: true, store: store1, //maxWidth: 500, //columnWidth : 0.1, legend:'right', axes: [{ type: 'StackedNumeric', position: 'left', fields: ['data1','data2'], label: { renderer: Ext.util.Format.numberRenderer('0,0') }, title: 'Number of Hits', grid: true, minimum: 0, maximum:200 },{ type: 'StackedNumeric', position: 'right', fields: ['data3'], label: { renderer: Ext.util.Format.numberRenderer('0,0') }, //title: 'Number of Hits', grid: true, minimum: 0, maximum:200 }, { type: 'Category', position: 'bottom', fields: ['name'], //categoryNames:new String("111"), title: 'Month of the Year' }], series: [{ type: 'column', axis: 'left', //stacked:false, stacked:true, highlight: true, tips: { trackMouse: true, width: 140, height: 28, renderer: function(storeItem, item) { this.setTitle(storeItem.get('name') + ': ' + storeItem.get('data1') + ' $'); } }, label: { display: 'insideEnd', 'text-anchor': 'middle', field: ['data1','data2'], //renderer: Ext.util.Format.numberRenderer('0'), orientation: 'vertical', color: '#FFF' }, style:{ opacity: 0.95 //,width:100 }, //xPadding:{left:100,right:100}, xField: 'name', yField: ['data1','data2'] } , { type: 'line', axis: 'right', highlight: true, tips: { trackMouse: true, width: 140, height: 28, renderer: function(storeItem, item) { this.setTitle(storeItem.get('name') + ': ' + storeItem.get('data3') + ' $'); } }, label: { display: 'insideEnd', 'text-anchor': 'middle', field: 'data3', //renderer: Ext.util.Format.numberRenderer('0'), orientation: 'vertical', color: '#FFF' }, style:{ opacity: 0.95 //,width:100 }, //xPadding:{left:100,right:100}, xField: 'name', yField: 'data3' }] }); var win = Ext.create('Ext.window.Window', { width: 800, height: 600, minHeight: 400, minWidth: 550, hidden: false, maximizable: true, title: 'Column Chart', autoShow: true, layout: 'fit', tbar: [{ text: 'Save Chart', handler: function() { Ext.MessageBox.confirm('Confirm Download', 'Would you like to download the chart as an image?', function(choice){ if(choice == 'yes'){ chart.save({ type: 'image/png' }); } }); } }, { text: 'Reload Data', handler: function() { // Add a short delay to prevent fast sequential clicks window.loadTask.delay(100, function() { store1.loadData(generateData()); }); } }], items: chart }); }); </script> </head> <body> </body> </html>