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:
22 * var store = Ext.create('Ext.data.JsonStore', {
23 * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
25 * { 'name': 'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13 },
26 * { 'name': 'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3 },
27 * { 'name': 'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7 },
28 * { 'name': 'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23 },
29 * { 'name': 'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33 }
33 * Ext.create('Ext.chart.Chart', {
34 * renderTo: Ext.getBody(),
44 * renderer: Ext.util.Format.numberRenderer('0,0')
46 * title: 'Sample Values',
53 * title: 'Sample Metrics'
63 * renderer: function(storeItem, item) {
64 * this.setTitle(storeItem.get('name') + ': ' + storeItem.get('data1') + ' views');
68 * display: 'insideEnd',
70 * renderer: Ext.util.Format.numberRenderer('0'),
71 * orientation: 'horizontal',
73 * 'text-anchor': 'middle'
80 * In this configuration we set `bar` as the series type, bind the values of the bar to the bottom axis and set the
81 * xField or category field to the `name` parameter of the store. We also set `highlight` to true which enables smooth
82 * animations when bars are hovered. We also set some configuration for the bar labels to be displayed inside the bar,
83 * to display the information found in the `data1` property of each element store, to render a formated text with the
84 * `Ext.util.Format` we pass in, to have an `horizontal` orientation (as opposed to a vertical one) and we also set
85 * other styles like `color`, `text-anchor`, etc.
87 Ext.define('Ext.chart.series.Bar', {
89 /* Begin Definitions */
91 extend: 'Ext.chart.series.Cartesian',
93 alternateClassName: ['Ext.chart.BarSeries', 'Ext.chart.BarChart', 'Ext.chart.StackedBarChart'],
95 requires: ['Ext.chart.axis.Axis', 'Ext.fx.Anim'],
103 * @cfg {Boolean} column Whether to set the visualization as column chart or horizontal bar chart.
108 * @cfg style Style properties that will override the theming series styles.
113 * @cfg {Number} gutter The gutter space between single bars, as a percentage of the bar width
118 * @cfg {Number} groupGutter The gutter space between groups of bars, as a percentage of the bar width
123 * @cfg {Number} xPadding Padding between the left/right axes and the bars
128 * @cfg {Number} yPadding Padding between the top/bottom axes and the bars
132 constructor: function(config) {
133 this.callParent(arguments);
135 surface = me.chart.surface,
136 shadow = me.chart.shadow,
138 Ext.apply(me, config, {
148 "stroke-opacity": 0.05,
149 stroke: 'rgb(200, 200, 200)',
156 "stroke-opacity": 0.1,
157 stroke: 'rgb(150, 150, 150)',
164 "stroke-opacity": 0.15,
165 stroke: 'rgb(100, 100, 100)',
172 me.group = surface.getGroup(me.seriesId + '-bars');
174 for (i = 0, l = me.shadowAttributes.length; i < l; i++) {
175 me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i));
180 // @private sets the bar girth.
181 getBarGirth: function() {
183 store = me.chart.getChartStore(),
185 ln = store.getCount(),
186 gutter = me.gutter / 100;
188 return (me.chart.chartBBox[column ? 'width' : 'height'] - me[column ? 'xPadding' : 'yPadding'] * 2) / (ln * (gutter + 1) - gutter);
191 // @private returns the gutters.
192 getGutters: function() {
195 gutter = Math.ceil(me[column ? 'xPadding' : 'yPadding'] + me.getBarGirth() / 2);
196 return me.column ? [gutter, 0] : [0, gutter];
199 // @private Get chart and data boundaries
200 getBounds: function() {
203 store = chart.getChartStore(),
204 bars = [].concat(me.yField),
205 barsLen = bars.length,
206 groupBarsLen = barsLen,
207 groupGutter = me.groupGutter / 100,
209 xPadding = me.xPadding,
210 yPadding = me.yPadding,
211 stacked = me.stacked,
212 barWidth = me.getBarGirth(),
216 groupBarWidth, bbox, minY, maxY, axis, out,
217 scale, zero, total, rec, j, plus, minus;
222 //Skip excluded series
224 for (j = 0, total = me.__excludes.length; j < total; j++) {
225 if (me.__excludes[j]) {
232 axis = chart.axes.get(me.axis);
234 out = axis.calcEnds();
240 if (me.yField && !Ext.isNumber(minY)) {
241 axis = Ext.create('Ext.chart.axis.Axis', {
243 fields: [].concat(me.yField)
245 out = axis.calcEnds();
250 if (!Ext.isNumber(minY)) {
253 if (!Ext.isNumber(maxY)) {
256 scale = (column ? bbox.height - yPadding * 2 : bbox.width - xPadding * 2) / (maxY - minY);
257 groupBarWidth = barWidth / ((stacked ? 1 : groupBarsLen) * (groupGutter + 1) - groupGutter);
258 zero = (column) ? bbox.y + bbox.height - yPadding : bbox.x + xPadding;
262 store.each(function(record, i) {
263 total[0][i] = total[0][i] || 0;
264 total[1][i] = total[1][i] || 0;
265 for (j = 0; j < barsLen; j++) {
266 if (me.__excludes && me.__excludes[j]) {
269 rec = record.get(bars[j]);
270 total[+(rec > 0)][i] += mabs(rec);
273 total[+(maxY > 0)].push(mabs(maxY));
274 total[+(minY > 0)].push(mabs(minY));
275 minus = mmax.apply(math, total[0]);
276 plus = mmax.apply(math, total[1]);
277 scale = (column ? bbox.height - yPadding * 2 : bbox.width - xPadding * 2) / (plus + minus);
278 zero = zero + minus * scale * (column ? -1 : 1);
280 else if (minY / maxY < 0) {
281 zero = zero - minY * scale * (column ? -1 : 1);
287 groupBarsLen: groupBarsLen,
289 groupBarWidth: groupBarWidth,
294 signed: minY / maxY < 0,
300 // @private Build an array of paths for the chart
301 getPaths: function() {
304 store = chart.getChartStore(),
305 bounds = me.bounds = me.getBounds(),
306 items = me.items = [],
307 gutter = me.gutter / 100,
308 groupGutter = me.groupGutter / 100,
309 animate = chart.animate,
312 enableShadows = chart.shadow,
313 shadowGroups = me.shadowGroups,
314 shadowAttributes = me.shadowAttributes,
315 shadowGroupsLn = shadowGroups.length,
317 xPadding = me.xPadding,
318 yPadding = me.yPadding,
319 stacked = me.stacked,
320 barsLen = bounds.barsLen,
321 colors = me.colorArrayStyle,
322 colorLength = colors && colors.length || 0,
327 j, yValue, height, totalDim, totalNegDim, bottom, top, hasShadow, barAttr, attrs, counter,
328 shadowIndex, shadow, sprite, offset, floorY;
330 store.each(function(record, i, total) {
331 bottom = bounds.zero;
336 for (j = 0, counter = 0; j < barsLen; j++) {
338 if (me.__excludes && me.__excludes[j]) {
341 yValue = record.get(bounds.bars[j]);
342 height = Math.round((yValue - mmax(bounds.minY, 0)) * bounds.scale);
344 fill: colors[(barsLen > 1 ? j : 0) % colorLength]
349 width: mmax(bounds.groupBarWidth, 0),
350 x: (bbox.x + xPadding + i * bounds.barWidth * (1 + gutter) + counter * bounds.groupBarWidth * (1 + groupGutter) * !stacked),
355 // draw in reverse order
356 offset = (total - 1) - i;
358 height: mmax(bounds.groupBarWidth, 0),
359 width: height + (bottom == bounds.zero),
360 x: bottom + (bottom != bounds.zero),
361 y: (bbox.y + yPadding + offset * bounds.barWidth * (1 + gutter) + counter * bounds.groupBarWidth * (1 + groupGutter) * !stacked + 1)
367 barAttr.height = mabs(height);
369 barAttr.x = top + height;
370 barAttr.width = mabs(height);
375 top += height * (column ? -1 : 1);
377 bottom += height * (column ? -1 : 1);
379 totalDim += mabs(height);
381 totalNegDim += mabs(height);
384 barAttr.x = Math.floor(barAttr.x) + 1;
385 floorY = Math.floor(barAttr.y);
386 if (!Ext.isIE9 && barAttr.y > floorY) {
390 barAttr.width = Math.floor(barAttr.width);
391 barAttr.height = Math.floor(barAttr.height);
395 value: [record.get(me.xField), yValue],
397 point: column ? [barAttr.x + barAttr.width / 2, yValue >= 0 ? barAttr.y : barAttr.y + barAttr.height] :
398 [yValue >= 0 ? barAttr.x + barAttr.width : barAttr.x, barAttr.y + barAttr.height / 2]
400 // When resizing, reset before animating
401 if (animate && chart.resizing) {
405 width: barAttr.width,
411 height: barAttr.height
413 if (enableShadows && (stacked && !hasShadow || !stacked)) {
416 for (shadowIndex = 0; shadowIndex < shadowGroupsLn; shadowIndex++) {
417 shadow = shadowGroups[shadowIndex].getAt(stacked ? i : (i * barsLen + j));
419 shadow.setAttributes(attrs, true);
423 //update sprite position and width/height
424 sprite = group.getAt(i * barsLen + j);
426 sprite.setAttributes(attrs, true);
431 if (stacked && items.length) {
432 items[i * counter].totalDim = totalDim;
433 items[i * counter].totalNegDim = totalNegDim;
438 // @private render/setAttributes on the shadows
439 renderShadows: function(i, barAttr, baseAttrs, bounds) {
442 surface = chart.surface,
443 animate = chart.animate,
444 stacked = me.stacked,
445 shadowGroups = me.shadowGroups,
446 shadowAttributes = me.shadowAttributes,
447 shadowGroupsLn = shadowGroups.length,
448 store = chart.getChartStore(),
453 shadowIndex, shadowBarAttr, shadow, totalDim, totalNegDim, j, rendererAttributes;
455 if ((stacked && (i % bounds.groupBarsLen === 0)) || !stacked) {
456 j = i / bounds.groupBarsLen;
458 for (shadowIndex = 0; shadowIndex < shadowGroupsLn; shadowIndex++) {
459 shadowBarAttr = Ext.apply({}, shadowAttributes[shadowIndex]);
460 shadow = shadowGroups[shadowIndex].getAt(stacked ? j : i);
461 Ext.copyTo(shadowBarAttr, barAttr, 'x,y,width,height');
463 shadow = surface.add(Ext.apply({
465 group: shadowGroups[shadowIndex]
466 }, Ext.apply({}, baseAttrs, shadowBarAttr)));
469 totalDim = items[i].totalDim;
470 totalNegDim = items[i].totalNegDim;
472 shadowBarAttr.y = zero - totalNegDim;
473 shadowBarAttr.height = totalDim;
476 shadowBarAttr.x = zero - totalNegDim;
477 shadowBarAttr.width = totalDim;
482 rendererAttributes = me.renderer(shadow, store.getAt(j), shadowBarAttr, i, store);
483 me.onAnimate(shadow, { to: rendererAttributes });
486 rendererAttributes = me.renderer(shadow, store.getAt(j), Ext.apply(shadowBarAttr, { hidden: true }), i, store);
487 shadow.setAttributes(rendererAttributes, true);
491 rendererAttributes = me.renderer(shadow, store.getAt(j), Ext.apply(shadowBarAttr, { hidden: false }), i, store);
492 shadow.setAttributes(rendererAttributes, true);
494 shadows.push(shadow);
501 * Draws the series for the current chart.
503 drawSeries: function() {
506 store = chart.getChartStore(),
507 surface = chart.surface,
508 animate = chart.animate,
509 stacked = me.stacked,
511 enableShadows = chart.shadow,
512 shadowGroups = me.shadowGroups,
513 shadowGroupsLn = shadowGroups.length,
515 seriesStyle = me.seriesStyle,
516 items, ln, i, j, baseAttrs, sprite, rendererAttributes, shadowIndex, shadowGroup,
517 bounds, endSeriesStyle, barAttr, attrs, anim;
519 if (!store || !store.getCount()) {
523 //fill colors are taken from the colors array.
524 delete seriesStyle.fill;
525 endSeriesStyle = Ext.apply(seriesStyle, this.style);
526 me.unHighlightItem();
527 me.cleanHighlights();
533 baseAttrs = column ? {
541 // Create new or reuse sprites and animate/display
542 for (i = 0; i < ln; i++) {
543 sprite = group.getAt(i);
544 barAttr = items[i].attr;
547 items[i].shadows = me.renderShadows(i, barAttr, baseAttrs, bounds);
550 // Create a new sprite if needed (no height)
552 attrs = Ext.apply({}, baseAttrs, barAttr);
553 attrs = Ext.apply(attrs, endSeriesStyle || {});
554 sprite = surface.add(Ext.apply({}, {
560 rendererAttributes = me.renderer(sprite, store.getAt(i), barAttr, i, store);
561 sprite._to = rendererAttributes;
562 anim = me.onAnimate(sprite, { to: Ext.apply(rendererAttributes, endSeriesStyle) });
563 if (enableShadows && stacked && (i % bounds.barsLen === 0)) {
564 j = i / bounds.barsLen;
565 for (shadowIndex = 0; shadowIndex < shadowGroupsLn; shadowIndex++) {
566 anim.on('afteranimate', function() {
568 }, shadowGroups[shadowIndex].getAt(j));
573 rendererAttributes = me.renderer(sprite, store.getAt(i), Ext.apply(barAttr, { hidden: false }), i, store);
574 sprite.setAttributes(Ext.apply(rendererAttributes, endSeriesStyle), true);
576 items[i].sprite = sprite;
579 // Hide unused sprites
580 ln = group.getCount();
581 for (j = i; j < ln; j++) {
582 group.getAt(j).hide(true);
584 // Hide unused shadows
586 for (shadowIndex = 0; shadowIndex < shadowGroupsLn; shadowIndex++) {
587 shadowGroup = shadowGroups[shadowIndex];
588 ln = shadowGroup.getCount();
589 for (j = i; j < ln; j++) {
590 shadowGroup.getAt(j).hide(true);
597 // @private handled when creating a label.
598 onCreateLabel: function(storeItem, item, i, display) {
600 surface = me.chart.surface,
601 group = me.labelsGroup,
603 endLabelStyle = Ext.apply({}, config, me.seriesLabelStyle || {}),
605 return surface.add(Ext.apply({
608 }, endLabelStyle || {}));
611 // @private callback used when placing a label.
612 onPlaceLabel: function(label, storeItem, item, i, display, animate, j, index) {
613 // Determine the label's final position. Starts with the configured preferred value but
614 // may get flipped from inside to outside or vice-versa depending on space.
617 groupBarWidth = opt.groupBarWidth,
620 chartBBox = chart.chartBBox,
621 resizing = chart.resizing,
622 xValue = item.value[0],
623 yValue = item.value[1],
626 rotate = config.orientation == 'vertical',
627 field = [].concat(config.field),
628 format = config.renderer,
629 text = format(storeItem.get(field[index])),
630 size = me.getLabelSize(text),
632 height = size.height,
635 insideStart = 'insideStart',
636 insideEnd = 'insideEnd',
642 label.setAttributes({
646 label.isOutside = false;
648 if (display == outside) {
649 if (height + offsetY + attr.height > (yValue >= 0 ? zero - chartBBox.y : chartBBox.y + chartBBox.height - zero)) {
653 if (height + offsetY > attr.height) {
655 label.isOutside = true;
658 x = attr.x + groupBarWidth / 2;
659 y = display == insideStart ?
660 (zero + ((height / 2 + 3) * (yValue >= 0 ? -1 : 1))) :
661 (yValue >= 0 ? (attr.y + ((height / 2 + 3) * (display == outside ? -1 : 1))) :
662 (attr.y + attr.height + ((height / 2 + 3) * (display === outside ? 1 : -1))));
665 if (display == outside) {
666 if (width + offsetX + attr.width > (yValue >= 0 ? chartBBox.x + chartBBox.width - zero : zero - chartBBox.x)) {
671 if (width + offsetX > attr.width) {
673 label.isOutside = true;
676 x = display == insideStart ?
677 (zero + ((width / 2 + 5) * (yValue >= 0 ? 1 : -1))) :
678 (yValue >= 0 ? (attr.x + attr.width + ((width / 2 + 5) * (display === outside ? 1 : -1))) :
679 (attr.x + ((width / 2 + 5) * (display === outside ? -1 : 1))));
680 y = attr.y + groupBarWidth / 2;
696 if (animate && resizing) {
698 x = attr.x + attr.width / 2;
702 y = attr.y + attr.height / 2;
704 label.setAttributes({
709 label.setAttributes({
720 me.onAnimate(label, { to: finalAttr });
723 label.setAttributes(Ext.apply(finalAttr, {
730 * Gets the dimensions of a given bar label. Uses a single hidden sprite to avoid
731 * changing visible sprites.
734 getLabelSize: function(value) {
735 var tester = this.testerLabel,
737 endLabelStyle = Ext.apply({}, config, this.seriesLabelStyle || {}),
738 rotated = config.orientation === 'vertical',
742 tester = this.testerLabel = this.chart.surface.add(Ext.apply({
747 tester.setAttributes({
751 // Flip the width/height if rotated, as getBBox returns the pre-rotated dimensions
752 bbox = tester.getBBox();
756 width: rotated ? h : w,
757 height: rotated ? w : h
761 // @private used to animate label, markers and other sprites.
762 onAnimate: function(sprite, attr) {
764 return this.callParent(arguments);
767 isItemInPoint: function(x, y, item) {
768 var bbox = item.sprite.getBBox();
769 return bbox.x <= x && bbox.y <= y
770 && (bbox.x + bbox.width) >= x
771 && (bbox.y + bbox.height) >= y;
774 // @private hide all markers
775 hideAll: function() {
776 var axes = this.chart.axes;
777 if (!isNaN(this._index)) {
778 if (!this.__excludes) {
779 this.__excludes = [];
781 this.__excludes[this._index] = true;
783 axes.each(function(axis) {
789 // @private show all markers
790 showAll: function() {
791 var axes = this.chart.axes;
792 if (!isNaN(this._index)) {
793 if (!this.__excludes) {
794 this.__excludes = [];
796 this.__excludes[this._index] = false;
798 axes.each(function(axis) {
805 * Returns a string with the color to be used for the series legend item.
808 getLegendColor: function(index) {
810 colorLength = me.colorArrayStyle.length;
812 if (me.style && me.style.fill) {
813 return me.style.fill;
815 return me.colorArrayStyle[index % colorLength];
819 highlightItem: function(item) {
820 this.callParent(arguments);
824 unHighlightItem: function() {
825 this.callParent(arguments);
829 cleanHighlights: function() {
830 this.callParent(arguments);