3 This file is part of Ext JS 4
5 Copyright (c) 2011 Sencha Inc
7 Contact: http://www.sencha.com/contact
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file. Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
16 * Creates a Bar Chart. A Bar Chart is a useful visualization technique to display quantitative information for
17 * different categories that can show some progression (or regression) in the dataset. As with all other series, the Bar
18 * Series must be appended in the *series* Chart array configuration. See the Chart documentation for more information.
19 * A typical configuration object for the bar series could be:
21 * {@img Ext.chart.series.Bar/Ext.chart.series.Bar.png Ext.chart.series.Bar chart series}
23 * var store = Ext.create('Ext.data.JsonStore', {
24 * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
26 * {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
27 * {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
28 * {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
29 * {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
30 * {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
34 * Ext.create('Ext.chart.Chart', {
35 * renderTo: Ext.getBody(),
45 * renderer: Ext.util.Format.numberRenderer('0,0')
47 * title: 'Sample Values',
54 * title: 'Sample Metrics'
64 * renderer: function(storeItem, item) {
65 * this.setTitle(storeItem.get('name') + ': ' + storeItem.get('data1') + ' views');
69 * display: 'insideEnd',
71 * renderer: Ext.util.Format.numberRenderer('0'),
72 * orientation: 'horizontal',
74 * 'text-anchor': 'middle'
81 * In this configuration we set `bar` as the series type, bind the values of the bar to the bottom axis and set the
82 * xField or category field to the `name` parameter of the store. We also set `highlight` to true which enables smooth
83 * animations when bars are hovered. We also set some configuration for the bar labels to be displayed inside the bar,
84 * to display the information found in the `data1` property of each element store, to render a formated text with the
85 * `Ext.util.Format` we pass in, to have an `horizontal` orientation (as opposed to a vertical one) and we also set
86 * other styles like `color`, `text-anchor`, etc.
88 Ext.define('Ext.chart.series.Bar', {
90 /* Begin Definitions */
92 extend: 'Ext.chart.series.Cartesian',
94 alternateClassName: ['Ext.chart.BarSeries', 'Ext.chart.BarChart', 'Ext.chart.StackedBarChart'],
96 requires: ['Ext.chart.axis.Axis', 'Ext.fx.Anim'],
104 * @cfg {Boolean} column Whether to set the visualization as column chart or horizontal bar chart.
109 * @cfg style Style properties that will override the theming series styles.
114 * @cfg {Number} gutter The gutter space between single bars, as a percentage of the bar width
119 * @cfg {Number} groupGutter The gutter space between groups of bars, as a percentage of the bar width
124 * @cfg {Number} xPadding Padding between the left/right axes and the bars
129 * @cfg {Number} yPadding Padding between the top/bottom axes and the bars
133 constructor: function(config) {
134 this.callParent(arguments);
136 surface = me.chart.surface,
137 shadow = me.chart.shadow,
139 Ext.apply(me, config, {
149 "stroke-opacity": 0.05,
150 stroke: 'rgb(200, 200, 200)',
157 "stroke-opacity": 0.1,
158 stroke: 'rgb(150, 150, 150)',
165 "stroke-opacity": 0.15,
166 stroke: 'rgb(100, 100, 100)',
173 me.group = surface.getGroup(me.seriesId + '-bars');
175 for (i = 0, l = me.shadowAttributes.length; i < l; i++) {
176 me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i));
181 // @private sets the bar girth.
182 getBarGirth: function() {
184 store = me.chart.store,
186 ln = store.getCount(),
187 gutter = me.gutter / 100;
189 return (me.chart.chartBBox[column ? 'width' : 'height'] - me[column ? 'xPadding' : 'yPadding'] * 2) / (ln * (gutter + 1) - gutter);
192 // @private returns the gutters.
193 getGutters: function() {
196 gutter = Math.ceil(me[column ? 'xPadding' : 'yPadding'] + me.getBarGirth() / 2);
197 return me.column ? [gutter, 0] : [0, gutter];
200 // @private Get chart and data boundaries
201 getBounds: function() {
204 store = chart.substore || chart.store,
205 bars = [].concat(me.yField),
206 barsLen = bars.length,
207 groupBarsLen = barsLen,
208 groupGutter = me.groupGutter / 100,
210 xPadding = me.xPadding,
211 yPadding = me.yPadding,
212 stacked = me.stacked,
213 barWidth = me.getBarGirth(),
217 groupBarWidth, bbox, minY, maxY, axis, out,
218 scale, zero, total, rec, j, plus, minus;
223 //Skip excluded series
225 for (j = 0, total = me.__excludes.length; j < total; j++) {
226 if (me.__excludes[j]) {
233 axis = chart.axes.get(me.axis);
235 out = axis.calcEnds();
236 minY = out.from || axis.prevMin;
237 maxY = mmax(out.to || axis.prevMax, 0);
241 if (me.yField && !Ext.isNumber(minY)) {
242 axis = Ext.create('Ext.chart.axis.Axis', {
244 fields: [].concat(me.yField)
246 out = axis.calcEnds();
247 minY = out.from || axis.prevMin;
248 maxY = mmax(out.to || axis.prevMax, 0);
251 if (!Ext.isNumber(minY)) {
254 if (!Ext.isNumber(maxY)) {
257 scale = (column ? bbox.height - yPadding * 2 : bbox.width - xPadding * 2) / (maxY - minY);
258 groupBarWidth = barWidth / ((stacked ? 1 : groupBarsLen) * (groupGutter + 1) - groupGutter);
259 zero = (column) ? bbox.y + bbox.height - yPadding : bbox.x + xPadding;
263 store.each(function(record, i) {
264 total[0][i] = total[0][i] || 0;
265 total[1][i] = total[1][i] || 0;
266 for (j = 0; j < barsLen; j++) {
267 if (me.__excludes && me.__excludes[j]) {
270 rec = record.get(bars[j]);
271 total[+(rec > 0)][i] += mabs(rec);
274 total[+(maxY > 0)].push(mabs(maxY));
275 total[+(minY > 0)].push(mabs(minY));
276 minus = mmax.apply(math, total[0]);
277 plus = mmax.apply(math, total[1]);
278 scale = (column ? bbox.height - yPadding * 2 : bbox.width - xPadding * 2) / (plus + minus);
279 zero = zero + minus * scale * (column ? -1 : 1);
281 else if (minY / maxY < 0) {
282 zero = zero - minY * scale * (column ? -1 : 1);
288 groupBarsLen: groupBarsLen,
290 groupBarWidth: groupBarWidth,
295 signed: minY / maxY < 0,
301 // @private Build an array of paths for the chart
302 getPaths: function() {
305 store = chart.substore || chart.store,
306 bounds = me.bounds = me.getBounds(),
307 items = me.items = [],
308 gutter = me.gutter / 100,
309 groupGutter = me.groupGutter / 100,
310 animate = chart.animate,
313 enableShadows = chart.shadow,
314 shadowGroups = me.shadowGroups,
315 shadowAttributes = me.shadowAttributes,
316 shadowGroupsLn = shadowGroups.length,
318 xPadding = me.xPadding,
319 yPadding = me.yPadding,
320 stacked = me.stacked,
321 barsLen = bounds.barsLen,
322 colors = me.colorArrayStyle,
323 colorLength = colors && colors.length || 0,
328 j, yValue, height, totalDim, totalNegDim, bottom, top, hasShadow, barAttr, attrs, counter,
329 shadowIndex, shadow, sprite, offset, floorY;
331 store.each(function(record, i, total) {
332 bottom = bounds.zero;
337 for (j = 0, counter = 0; j < barsLen; j++) {
339 if (me.__excludes && me.__excludes[j]) {
342 yValue = record.get(bounds.bars[j]);
343 height = Math.round((yValue - ((bounds.minY < 0) ? 0 : bounds.minY)) * bounds.scale);
345 fill: colors[(barsLen > 1 ? j : 0) % colorLength]
350 width: mmax(bounds.groupBarWidth, 0),
351 x: (bbox.x + xPadding + i * bounds.barWidth * (1 + gutter) + counter * bounds.groupBarWidth * (1 + groupGutter) * !stacked),
356 // draw in reverse order
357 offset = (total - 1) - i;
359 height: mmax(bounds.groupBarWidth, 0),
360 width: height + (bottom == bounds.zero),
361 x: bottom + (bottom != bounds.zero),
362 y: (bbox.y + yPadding + offset * bounds.barWidth * (1 + gutter) + counter * bounds.groupBarWidth * (1 + groupGutter) * !stacked + 1)
368 barAttr.height = mabs(height);
370 barAttr.x = top + height;
371 barAttr.width = mabs(height);
376 top += height * (column ? -1 : 1);
378 bottom += height * (column ? -1 : 1);
380 totalDim += mabs(height);
382 totalNegDim += mabs(height);
385 barAttr.x = Math.floor(barAttr.x) + 1;
386 floorY = Math.floor(barAttr.y);
387 if (!Ext.isIE9 && barAttr.y > floorY) {
391 barAttr.width = Math.floor(barAttr.width);
392 barAttr.height = Math.floor(barAttr.height);
396 value: [record.get(me.xField), yValue],
398 point: column ? [barAttr.x + barAttr.width / 2, yValue >= 0 ? barAttr.y : barAttr.y + barAttr.height] :
399 [yValue >= 0 ? barAttr.x + barAttr.width : barAttr.x, barAttr.y + barAttr.height / 2]
401 // When resizing, reset before animating
402 if (animate && chart.resizing) {
406 width: barAttr.width,
412 height: barAttr.height
414 if (enableShadows && (stacked && !hasShadow || !stacked)) {
417 for (shadowIndex = 0; shadowIndex < shadowGroupsLn; shadowIndex++) {
418 shadow = shadowGroups[shadowIndex].getAt(stacked ? i : (i * barsLen + j));
420 shadow.setAttributes(attrs, true);
424 //update sprite position and width/height
425 sprite = group.getAt(i * barsLen + j);
427 sprite.setAttributes(attrs, true);
432 if (stacked && items.length) {
433 items[i * counter].totalDim = totalDim;
434 items[i * counter].totalNegDim = totalNegDim;
439 // @private render/setAttributes on the shadows
440 renderShadows: function(i, barAttr, baseAttrs, bounds) {
443 surface = chart.surface,
444 animate = chart.animate,
445 stacked = me.stacked,
446 shadowGroups = me.shadowGroups,
447 shadowAttributes = me.shadowAttributes,
448 shadowGroupsLn = shadowGroups.length,
449 store = chart.substore || chart.store,
454 shadowIndex, shadowBarAttr, shadow, totalDim, totalNegDim, j, rendererAttributes;
456 if ((stacked && (i % bounds.groupBarsLen === 0)) || !stacked) {
457 j = i / bounds.groupBarsLen;
459 for (shadowIndex = 0; shadowIndex < shadowGroupsLn; shadowIndex++) {
460 shadowBarAttr = Ext.apply({}, shadowAttributes[shadowIndex]);
461 shadow = shadowGroups[shadowIndex].getAt(stacked ? j : i);
462 Ext.copyTo(shadowBarAttr, barAttr, 'x,y,width,height');
464 shadow = surface.add(Ext.apply({
466 group: shadowGroups[shadowIndex]
467 }, Ext.apply({}, baseAttrs, shadowBarAttr)));
470 totalDim = items[i].totalDim;
471 totalNegDim = items[i].totalNegDim;
473 shadowBarAttr.y = zero - totalNegDim;
474 shadowBarAttr.height = totalDim;
477 shadowBarAttr.x = zero - totalNegDim;
478 shadowBarAttr.width = totalDim;
483 rendererAttributes = me.renderer(shadow, store.getAt(j), shadowBarAttr, i, store);
484 me.onAnimate(shadow, { to: rendererAttributes });
487 rendererAttributes = me.renderer(shadow, store.getAt(j), Ext.apply(shadowBarAttr, { hidden: true }), i, store);
488 shadow.setAttributes(rendererAttributes, true);
492 rendererAttributes = me.renderer(shadow, store.getAt(j), Ext.apply(shadowBarAttr, { hidden: false }), i, store);
493 shadow.setAttributes(rendererAttributes, true);
495 shadows.push(shadow);
502 * Draws the series for the current chart.
504 drawSeries: function() {
507 store = chart.substore || chart.store,
508 surface = chart.surface,
509 animate = chart.animate,
510 stacked = me.stacked,
512 enableShadows = chart.shadow,
513 shadowGroups = me.shadowGroups,
514 shadowGroupsLn = shadowGroups.length,
516 seriesStyle = me.seriesStyle,
517 items, ln, i, j, baseAttrs, sprite, rendererAttributes, shadowIndex, shadowGroup,
518 bounds, endSeriesStyle, barAttr, attrs, anim;
520 if (!store || !store.getCount()) {
524 //fill colors are taken from the colors array.
525 delete seriesStyle.fill;
526 endSeriesStyle = Ext.apply(seriesStyle, this.style);
527 me.unHighlightItem();
528 me.cleanHighlights();
534 baseAttrs = column ? {
542 // Create new or reuse sprites and animate/display
543 for (i = 0; i < ln; i++) {
544 sprite = group.getAt(i);
545 barAttr = items[i].attr;
548 items[i].shadows = me.renderShadows(i, barAttr, baseAttrs, bounds);
551 // Create a new sprite if needed (no height)
553 attrs = Ext.apply({}, baseAttrs, barAttr);
554 attrs = Ext.apply(attrs, endSeriesStyle || {});
555 sprite = surface.add(Ext.apply({}, {
561 rendererAttributes = me.renderer(sprite, store.getAt(i), barAttr, i, store);
562 sprite._to = rendererAttributes;
563 anim = me.onAnimate(sprite, { to: Ext.apply(rendererAttributes, endSeriesStyle) });
564 if (enableShadows && stacked && (i % bounds.barsLen === 0)) {
565 j = i / bounds.barsLen;
566 for (shadowIndex = 0; shadowIndex < shadowGroupsLn; shadowIndex++) {
567 anim.on('afteranimate', function() {
569 }, shadowGroups[shadowIndex].getAt(j));
574 rendererAttributes = me.renderer(sprite, store.getAt(i), Ext.apply(barAttr, { hidden: false }), i, store);
575 sprite.setAttributes(Ext.apply(rendererAttributes, endSeriesStyle), true);
577 items[i].sprite = sprite;
580 // Hide unused sprites
581 ln = group.getCount();
582 for (j = i; j < ln; j++) {
583 group.getAt(j).hide(true);
585 // Hide unused shadows
587 for (shadowIndex = 0; shadowIndex < shadowGroupsLn; shadowIndex++) {
588 shadowGroup = shadowGroups[shadowIndex];
589 ln = shadowGroup.getCount();
590 for (j = i; j < ln; j++) {
591 shadowGroup.getAt(j).hide(true);
598 // @private handled when creating a label.
599 onCreateLabel: function(storeItem, item, i, display) {
601 surface = me.chart.surface,
602 group = me.labelsGroup,
604 endLabelStyle = Ext.apply({}, config, me.seriesLabelStyle || {}),
606 return surface.add(Ext.apply({
609 }, endLabelStyle || {}));
612 // @private callback used when placing a label.
613 onPlaceLabel: function(label, storeItem, item, i, display, animate, j, index) {
614 // Determine the label's final position. Starts with the configured preferred value but
615 // may get flipped from inside to outside or vice-versa depending on space.
618 groupBarWidth = opt.groupBarWidth,
621 chartBBox = chart.chartBBox,
622 resizing = chart.resizing,
623 xValue = item.value[0],
624 yValue = item.value[1],
627 rotate = config.orientation == 'vertical',
628 field = [].concat(config.field),
629 format = config.renderer,
630 text = format(storeItem.get(field[index])),
631 size = me.getLabelSize(text),
633 height = size.height,
636 insideStart = 'insideStart',
637 insideEnd = 'insideEnd',
643 label.setAttributes({
647 label.isOutside = false;
649 if (display == outside) {
650 if (height + offsetY + attr.height > (yValue >= 0 ? zero - chartBBox.y : chartBBox.y + chartBBox.height - zero)) {
654 if (height + offsetY > attr.height) {
656 label.isOutside = true;
659 x = attr.x + groupBarWidth / 2;
660 y = display == insideStart ?
661 (zero + ((height / 2 + 3) * (yValue >= 0 ? -1 : 1))) :
662 (yValue >= 0 ? (attr.y + ((height / 2 + 3) * (display == outside ? -1 : 1))) :
663 (attr.y + attr.height + ((height / 2 + 3) * (display === outside ? 1 : -1))));
666 if (display == outside) {
667 if (width + offsetX + attr.width > (yValue >= 0 ? chartBBox.x + chartBBox.width - zero : zero - chartBBox.x)) {
672 if (width + offsetX > attr.width) {
674 label.isOutside = true;
677 x = display == insideStart ?
678 (zero + ((width / 2 + 5) * (yValue >= 0 ? 1 : -1))) :
679 (yValue >= 0 ? (attr.x + attr.width + ((width / 2 + 5) * (display === outside ? 1 : -1))) :
680 (attr.x + ((width / 2 + 5) * (display === outside ? -1 : 1))));
681 y = attr.y + groupBarWidth / 2;
697 if (animate && resizing) {
699 x = attr.x + attr.width / 2;
703 y = attr.y + attr.height / 2;
705 label.setAttributes({
710 label.setAttributes({
721 me.onAnimate(label, { to: finalAttr });
724 label.setAttributes(Ext.apply(finalAttr, {
731 * Gets the dimensions of a given bar label. Uses a single hidden sprite to avoid
732 * changing visible sprites.
735 getLabelSize: function(value) {
736 var tester = this.testerLabel,
738 endLabelStyle = Ext.apply({}, config, this.seriesLabelStyle || {}),
739 rotated = config.orientation === 'vertical',
743 tester = this.testerLabel = this.chart.surface.add(Ext.apply({
748 tester.setAttributes({
752 // Flip the width/height if rotated, as getBBox returns the pre-rotated dimensions
753 bbox = tester.getBBox();
757 width: rotated ? h : w,
758 height: rotated ? w : h
762 // @private used to animate label, markers and other sprites.
763 onAnimate: function(sprite, attr) {
765 return this.callParent(arguments);
768 isItemInPoint: function(x, y, item) {
769 var bbox = item.sprite.getBBox();
770 return bbox.x <= x && bbox.y <= y
771 && (bbox.x + bbox.width) >= x
772 && (bbox.y + bbox.height) >= y;
775 // @private hide all markers
776 hideAll: function() {
777 var axes = this.chart.axes;
778 if (!isNaN(this._index)) {
779 if (!this.__excludes) {
780 this.__excludes = [];
782 this.__excludes[this._index] = true;
784 axes.each(function(axis) {
790 // @private show all markers
791 showAll: function() {
792 var axes = this.chart.axes;
793 if (!isNaN(this._index)) {
794 if (!this.__excludes) {
795 this.__excludes = [];
797 this.__excludes[this._index] = false;
799 axes.each(function(axis) {
806 * Returns a string with the color to be used for the series legend item.
809 getLegendColor: function(index) {
811 colorLength = me.colorArrayStyle.length;
813 if (me.style && me.style.fill) {
814 return me.style.fill;
816 return me.colorArrayStyle[index % colorLength];
820 highlightItem: function(item) {
821 this.callParent(arguments);
825 unHighlightItem: function() {
826 this.callParent(arguments);
830 cleanHighlights: function() {
831 this.callParent(arguments);