Após um longo período sem artigos, volto a ativa com um bem interessante e bastante usual para quem desenvolve sistemas de apoio a decisão e sistemas de business intelligence.

O gráfico de cascata (waterfall, também conhecido como, flying bricks chart em inglês) é uma ferramenta que facilita a demonstração de parcelas de um valor. Ele é muito utilizado na apresentação da divisão da receita e exibição do que resta de lucro em DRE, da entrada de caixa ao saldo final em demonstrativos de Fluxo de Caixa, mas pode-se utilizá-lo em qualquer situação em que seja necessário mostrar a “quebra” de um número.

Neste artigo vou demonstrar como desenvolver um gráfico de cascata em Adobe Flex estendendo um ColumnChart de forma simples obtendo como resultado o exemplo abaixo.
[like-gate]

Primeiro precisamos entender qual a estrutura do gráfico de cascata. Ele é um gráfico de colunas do tipo empilhado (stacked) com 6 séries conforme demonstrado na figura abaixo:

Gráfico Waterfall Modelo

  • 1 – Base, que será invisível;
  • 2 – Vermelho Positivo, série da cor vermelha que será exibida acima do zero, ex. (-) Custos no gráfico acima;
  • 3 – Verde Positivo, série da cor verde que será exibida acima do zero, ex. Resultado não Op.;
  • 4 – Vermelho Negativo, série da cor vermelha que será exibida abaixo do zero, ex. (-) Despesas;
  • 5 – Verde Negativo, série da cor verde que será exibida abaixo do zero, ex. Resultado não Op.;
  • 6 – Saldo, série da cor azul, ex. Lajir.

No componente que criaremos apenas será necessário passar um DataProvider com as colunas: eixo, valor e isSaldo. Com base nos dados deste DataProvider o Gráfico Cascata criará todas as 6 séries necessárias, já com os valores calculados.

Veja como o componente é implementado abaixo.

package br.com.igormusardo.component.chart
{
	import mx.charts.*;
	import mx.charts.chartClasses.IAxis;
	import mx.charts.series.ColumnSeries;
	import mx.charts.series.ColumnSet;
	import mx.charts.series.items.*;
	import mx.collections.ArrayCollection;
	import mx.effects.easing.*;
	import mx.formatters.NumberFormatter;
	import mx.graphics.GradientEntry;
	import mx.graphics.LinearGradient;
	import mx.graphics.SolidColor;
	import mx.graphics.Stroke;
	import mx.utils.ObjectUtil;

	public class WaterfallChart extends ColumnChart
	{
		[Bindable]
		private var _calculateRange:Boolean;

		[Bindable]
		private var _backgroundColor:uint;

		[Bindable]
		private var _best:Number;

		[Bindable]
		private var _precision:Number;

		[Bindable]
		private var _decimalSeparator:String;

		[Bindable]
		private var _thousandsSeparator:String;

		private var dataProviderOriginal:ArrayCollection = new ArrayCollection();
		private var format:NumberFormatter = new NumberFormatter;
		private var	columnBase:ColumnSeries = new ColumnSeries();
		private var	columnSaldo:ColumnSeries = new ColumnSeries();
		private var	columnVermelhoPositivo:ColumnSeries = new ColumnSeries();
		private var	columnVerdePositivo:ColumnSeries = new ColumnSeries();
		private var	columnVermelhoNegativo:ColumnSeries = new ColumnSeries();
		private var	columnVerdeNegativo:ColumnSeries = new ColumnSeries();

		public function WaterfallChart()
		{
			super();
			calculateRange = true;
			backgroundColor = 0xFFFFFF;
			best = 1;
			precision = 0;
			decimalSeparator = ',';
			thousandsSeparator = '.';
			createChart();
		}

		override protected function initializationComplete():void
		{
			changeStyles();
			super.initializationComplete()
		}


		//Propriedades
		////////////////////////////////////////////////////////////////////////////////////////
		//calculateRange, quando Verdadeiro o componente calcula a diferença entre as séries, exibindo somente os valores que somaram ou reduziram de uma série para a outra.
		public function set calculateRange(value:Boolean):void
		{
			_calculateRange = value;
			changeDataProvider();
		}

		public function get calculateRange():Boolean
		{
			return _calculateRange;
		}

		public function set backgroundColor(value:uint):void
		{
			_backgroundColor = value;
			changeStyles();
		}

		public function get backgroundColor():uint
		{
			return _backgroundColor;
		}

		//best, define qual é o melhor (1 melhor para cima, verde quando aumenta e vermelho quando diminui, 2 melhor para baixo, verde quando diminiui e vermelho quando aumente)
		public function set best(value:Number):void
		{
			_best = value;
			changeDataProvider();
		}

		public function get best():Number
		{
			return _best;
		}

		//precision, define o número de casas decimais.
		public function set precision(value:Number):void
		{
			_precision = value;
			format.precision=_precision;
		    invalidateProperties();
		    invalidateDisplayList();
		}

		public function get precision():Number
		{
			return _precision;
		}

		//decimalSeparator, define qual o separador de decimal, padrão ,
		public function set decimalSeparator(value:String):void
		{
			_decimalSeparator = value;
    		format.decimalSeparatorTo=_decimalSeparator;
		    invalidateProperties();
		    invalidateDisplayList();
		}

		public function get decimalSeparator():String
		{
			return _decimalSeparator;
		}

		//thousandsSeparator, define qual o separador de milhar, padrão .
		public function set thousandsSeparator(value:String):void
		{
			_thousandsSeparator = value;
    		format.thousandsSeparatorTo=_thousandsSeparator;
		    invalidateProperties();
		    invalidateDisplayList();
		}

		public function get thousandsSeparator():String
		{
			return _thousandsSeparator;
		}

		private function createChart():void
		{
			seriesFilters=[];
			dataTipMode="single";
			dataTipFunction=chartTipFunction;
			maxLabelWidth=100;

			var columnSet:ColumnSet = new ColumnSet();
			var hAxis:CategoryAxis = new CategoryAxis()
			var vAxis:LinearAxis = new LinearAxis()
			var columnSerie:ColumnSeries = new ColumnSeries();
			var stroke:Stroke;
			var linearGradient:LinearGradient;
			var solidColor:SolidColor;
			var gradientEntry:GradientEntry;
			var gradArray:Array;

			vAxis.labelFunction = alteraLabelVertical;
			verticalAxis = vAxis;

			hAxis.dataProvider = this.dataProvider;
			hAxis.categoryField = "eixo";
			hAxis.labelFunction = alteraLabelHorizontal;
			horizontalAxis = hAxis;

			//Define o columnSet do gráfico
			columnSet.type = "stacked";
			columnSet.allowNegativeForStacked = true;

			/////////////////////////////////////////////////////////////////////////////////
			//Início da configuração da série vlrSaldo
			//Define a configuração da linha
			stroke = new Stroke();
			stroke.alpha = 0;
			stroke.weight = 0;

			//Início configuração Degradê de cores para a série vlrSaldo
			linearGradient = new LinearGradient();
			linearGradient.angle = 0;

			gradientEntry = new GradientEntry()
			gradientEntry.color = 0x003ea5;
			gradientEntry.ratio = 0;
			gradientEntry.alpha = 1;

			gradArray = new Array();
			gradArray.push(gradientEntry);

			gradientEntry = new GradientEntry()
			gradientEntry.color = 0x0060ff;
			gradientEntry.ratio = .5;
			gradientEntry.alpha = 1;
			gradArray.push(gradientEntry);

			gradientEntry = new GradientEntry()
			gradientEntry.color = 0x003ea5;
			gradientEntry.ratio = 1;
			gradientEntry.alpha = 1;
			gradArray.push(gradientEntry);

			linearGradient.entries = gradArray;
			//Fim configuração Degradê de cores para a série vlrSaldo

			columnSaldo.yField = "vlrSaldo";
			columnSaldo.labelField = "vlrSaldo";
			columnSaldo.setStyle("stroke",stroke);
			columnSaldo.setStyle("fill",linearGradient);
			//Fim configuração da série vlrSaldo

			//Inclui a série vlrSaldo às séries do ColumnSet
			columnSet.series.push(columnSaldo);
			/////////////////////////////////////////////////////////////////////////////////

			/////////////////////////////////////////////////////////////////////////////////
			//Início da configuração da série vlrBase

			columnBase.yField = "vlrBase";
			columnBase.setStyle("stroke",stroke);
			//Fim configuração da série vlrSaldo

			//Inclui a série vlrBase às séries do ColumnSet
			columnSet.series.push(columnBase);
			/////////////////////////////////////////////////////////////////////////////////

			/////////////////////////////////////////////////////////////////////////////////
			//Início da configuração da série vlrVermelhoPositivo
			//Define a configuração da linha
			stroke = new Stroke();
			stroke.alpha = 0;
			stroke.weight = 0;

			//Início configuração Degradê de cores para a série vlrVermelhoPositivo
			linearGradient = new LinearGradient();
			linearGradient.angle = 0;

			gradientEntry = new GradientEntry()
			gradientEntry.color = 0xa50000;
			gradientEntry.ratio = 0;
			gradientEntry.alpha = 1;

			gradArray = new Array();
			gradArray.push(gradientEntry);

			gradientEntry = new GradientEntry()
			gradientEntry.color = 0xff0000;
			gradientEntry.ratio = .5;
			gradientEntry.alpha = 1;
			gradArray.push(gradientEntry);

			gradientEntry = new GradientEntry()
			gradientEntry.color = 0xa50000;
			gradientEntry.ratio = 1;
			gradientEntry.alpha = 1;
			gradArray.push(gradientEntry);

			linearGradient.entries = gradArray;
			//Fim configuração Degradê de cores para a série vlrVermelhoPositivo

			columnVermelhoPositivo.yField = "vlrVermelhoPositivo";
			columnVermelhoPositivo.labelField = "vlrVermelhoPositivo";
			columnVermelhoPositivo.setStyle("stroke",stroke);
			columnVermelhoPositivo.setStyle("fill",linearGradient);
			//Fim configuração da série vlrSaldo

			//Inclui a série vlrVermelhoPositivo às séries do ColumnSet
			columnSet.series.push(columnVermelhoPositivo);
			/////////////////////////////////////////////////////////////////////////////////

			/////////////////////////////////////////////////////////////////////////////////
			//Início da configuração da série vlrVerdePositivo
			//Define a configuração da linha
			stroke = new Stroke();
			stroke.alpha = 0;
			stroke.weight = 0;

			//Início configuração Degradê de cores para a série vlrVerdePositivo
			linearGradient = new LinearGradient();
			linearGradient.angle = 0;

			gradientEntry = new GradientEntry()
			gradientEntry.color = 0x00a500;
			gradientEntry.ratio = 0;
			gradientEntry.alpha = 1;

			gradArray = new Array();
			gradArray.push(gradientEntry);

			gradientEntry = new GradientEntry()
			gradientEntry.color = 0x60ff00;
			gradientEntry.ratio = .5;
			gradientEntry.alpha = 1;
			gradArray.push(gradientEntry);

			gradientEntry = new GradientEntry()
			gradientEntry.color = 0x00a500;
			gradientEntry.ratio = 1;
			gradientEntry.alpha = 1;
			gradArray.push(gradientEntry);

			linearGradient.entries = gradArray;
			//Fim configuração Degradê de cores para a série vlrVerdePositivo

			columnVerdePositivo.yField = "vlrVerdePositivo";
			columnVerdePositivo.labelField = "vlrVerdePositivo";
			columnVerdePositivo.setStyle("stroke",stroke);
			columnVerdePositivo.setStyle("fill",linearGradient);
			//Fim configuração da série vlrVerdePositivo

			//Inclui a série vlrVerdePositivo às séries do ColumnSet
			columnSet.series.push(columnVerdePositivo);
			/////////////////////////////////////////////////////////////////////////////////

			/////////////////////////////////////////////////////////////////////////////////
			//Início da configuração da série vlrVermelhoNegativo
			//Define a configuração da linha
			stroke = new Stroke();
			stroke.alpha = 0;
			stroke.weight = 0;

			//Início configuração Degradê de cores para a série vlrVermelhoNegativo
			linearGradient = new LinearGradient();
			linearGradient.angle = 0;

			gradientEntry = new GradientEntry()
			gradientEntry.color = 0xa50000;
			gradientEntry.ratio = 0;
			gradientEntry.alpha = 1;

			gradArray = new Array();
			gradArray.push(gradientEntry);

			gradientEntry = new GradientEntry()
			gradientEntry.color = 0xff0000;
			gradientEntry.ratio = .5;
			gradientEntry.alpha = 1;
			gradArray.push(gradientEntry);

			gradientEntry = new GradientEntry()
			gradientEntry.color = 0xa50000;
			gradientEntry.ratio = 1;
			gradientEntry.alpha = 1;
			gradArray.push(gradientEntry);

			linearGradient.entries = gradArray;
			//Fim configuração Degradê de cores para a série vlrVermelhoNegativo

			columnVermelhoNegativo.yField = "vlrVermelhoNegativo";
			columnVermelhoNegativo.labelField = "vlrVermelhoNegativo";
			columnVermelhoNegativo.setStyle("stroke",stroke);
			columnVermelhoNegativo.setStyle("fill",linearGradient);
			//Fim configuração da série vlrVermelhoNegativo

			//Inclui a série vlrVermelhoNegativo às séries do ColumnSet
			columnSet.series.push(columnVermelhoNegativo);
			/////////////////////////////////////////////////////////////////////////////////

			/////////////////////////////////////////////////////////////////////////////////
			//Início da configuração da série vlrVerdeNegativo
			//Define a configuração da linha
			stroke = new Stroke();
			stroke.alpha = 0;
			stroke.weight = 0;

			//Início configuração Degradê de cores para a série vlrVerdeNegativo
			linearGradient = new LinearGradient();
			linearGradient.angle = 0;

			gradientEntry = new GradientEntry()
			gradientEntry.color = 0x00a500;
			gradientEntry.ratio = 0;
			gradientEntry.alpha = 1;

			gradArray = new Array();
			gradArray.push(gradientEntry);

			gradientEntry = new GradientEntry()
			gradientEntry.color = 0x60ff00;
			gradientEntry.ratio = .5;
			gradientEntry.alpha = 1;
			gradArray.push(gradientEntry);

			gradientEntry = new GradientEntry()
			gradientEntry.color = 0x00a500;
			gradientEntry.ratio = 1;
			gradientEntry.alpha = 1;
			gradArray.push(gradientEntry);

			linearGradient.entries = gradArray;
			//Fim configuração Degradê de cores para a série vlrVerdeNegativo

			columnVerdeNegativo.yField = "vlrVerdeNegativo";
			columnVerdeNegativo.labelField = "vlrVerdeNegativo";
			columnVerdeNegativo.setStyle("stroke",stroke);
			columnVerdeNegativo.setStyle("fill",linearGradient);
			//Fim configuração da série vlrVerdeNegativo

			//Inclui a série vlrVerdeNegativo às séries do ColumnSet
			columnSet.series.push(columnVerdeNegativo);

			/////////////////////////////////////////////////////////////////////////////////
			setStyle("columnWidthRatio",0.9);

			series = [columnSet];
		}

		//Métodos formatação do gráfico
		////////////////////////////////////////////////////////////////////////////////////////
		//chartTipFunction, exibe o popup com os valores ao posicionar o mouse sobre a série.
		private function chartTipFunction(hitData:HitData):String
		{
			return format.format(hitData.item.vlrValor);
		}

		private function alteraLabelHorizontal(item:Object, prevValue:Object, axis:CategoryAxis, categoryItem:Object):String
		{
			var retorno:String="";
			var tamanho:int=String(item).length;

			retorno=String(item)

			return retorno;
		}

		private function alteraLabelVertical(item:Object, prevValue:Object, axis:IAxis):String
		{
			return format.format(item);
		}

		private function changeStyles():void
		{
			//Início configuração da cor de fundo do gráfico
			var bgi:GridLines = new GridLines();
			bgi.setStyle("horizontalStroke", new Stroke(_backgroundColor, 1));
			bgi.setStyle("horizontalFill", new SolidColor(_backgroundColor, 100));
			bgi.setStyle("horizontalAlternateFill", new SolidColor(_backgroundColor, 100));
			backgroundElements = [bgi]
			//Fim configuração da cor de fundo do gráfico

			//Início configuração da cor da série Base
			var solidColor:SolidColor = new SolidColor();
			solidColor.alpha = 1;
			solidColor.color = _backgroundColor;
			columnBase.setStyle("fill",solidColor);
			//Fim configuração da cor da série Base

		    invalidateProperties();
		    invalidateDisplayList();
		}


		//Série base (invisível) do gráfico
		private function calculaSerieBase(vlrSaldoAnterior:Number,vlrValorCorrente:Number):Number
		{
			var retorno:Number;
			var vlrSaldoAtual:Number=vlrSaldoAnterior+vlrValorCorrente;

			if ((vlrSaldoAnterior>0 && vlrSaldoAtual<0)||(vlrSaldoAnterior<0 && vlrSaldoAtual>0))
			{
				retorno=0;
			}
			else
			{
				if (vlrSaldoAnterior>0 && vlrSaldoAtual>0)
				{
					retorno=vlrSaldoAnterior-calculaSerieDebitoPositivo(vlrSaldoAnterior,vlrValorCorrente);
				}
				else
				{
					if (vlrSaldoAnterior<0 && vlrSaldoAtual<0)
					{
						retorno=vlrSaldoAnterior+calculaSerieCreditoNegativo(vlrSaldoAnterior,vlrValorCorrente);
					}
					else
					{
						retorno=0;
					}
				}
			}

			return retorno;
		}

		//Série verde valor positivo
		private function calculaSerieCreditoPositivo(vlrSaldoAnterior:Number, vlrValorCorrente:Number):Number
		{
			var retorno:Number;

			if (vlrValorCorrente<=0)
			{
				retorno=0;
			}
			else
			{
				if (vlrSaldoAnterior<0)
				{
					if (vlrValorCorrente>-vlrSaldoAnterior)
					{
						retorno=vlrValorCorrente+vlrSaldoAnterior;
					}
					else
					{
						retorno=0;
					}
				}
				else
				{
					retorno=vlrValorCorrente;
				}
			}
			return retorno;
		}


		//Série verde valor negativo
		private function calculaSerieCreditoNegativo(vlrSaldoAnterior:Number, vlrValorCorrente:Number):Number
		{
			var retorno:Number;

			if (vlrValorCorrente<=0)
			{
				retorno=0;
			}
			else
			{
				if (vlrSaldoAnterior>=0)
				{
					retorno=0;
				}
				else
				{
					if (vlrValorCorrente>-vlrSaldoAnterior)
					{
						retorno=vlrSaldoAnterior;
					}
					else
					{
						retorno=-vlrValorCorrente;
					}
				}
			}
			return retorno;
		}

		//Série vermelho valor positivo
		private function calculaSerieDebitoPositivo(vlrSaldoAnterior:Number, vlrValorCorrente:Number):Number
		{
			var retorno:Number;

			if (vlrValorCorrente>=0)
			{
				retorno=0;
			}
			else
			{
				if (vlrSaldoAnterior<=0)
				{
					retorno=0;
				}
				else
				{
					if(vlrSaldoAnterior<=Math.abs(vlrValorCorrente))
					{
						retorno=vlrSaldoAnterior;
					}
					else
					{
						retorno=-vlrValorCorrente;
					}
				}
			}
			return retorno;
		}


		//Série vermelho valor negativo
		private function calculaSerieDebitoNegativo(vlrSaldoAnterior:Number, vlrValorCorrente:Number):Number
		{
			var retorno:Number;

			if (vlrValorCorrente>=0)
			{
				retorno=0;
			}
			else
			{
				if (vlrSaldoAnterior>0)
				{
					if(Math.abs(vlrValorCorrente)<=vlrSaldoAnterior)
					{
						retorno=0;
					}
					else
					{
						retorno=vlrSaldoAnterior+vlrValorCorrente;
					}
				}
				else
				{
					retorno=vlrValorCorrente;
				}
			}

			return retorno;
		}

		//Métodos Geração das séries
		////////////////////////////////////////////////////////////////////////////////////////
		override public function set dataProvider(value:Object):void
    	{
			if(value is ArrayCollection)
				dataProviderOriginal = value as ArrayCollection;
			else
				dataProviderOriginal = new ArrayCollection(value as Array);
			changeDataProvider();
     	}

     	private function changeDataProvider():void
     	{
     		if(dataProviderOriginal.length == 0)
     			return;

     		var value:Object = ObjectUtil.copy(dataProviderOriginal);
    		var dataProviderOriginalDP:Array = new Array;
    		var vlrValorCorrente:Number;
    		var vlrSaldoAnterior:Number;

    		//Verifica se o array é válido
    		if(value[0].eixo==null || value[0].valor==null || value[0].isSaldo==null)
    		{
    			throw new Error('Array inválido, deve possuir os campos: eixo:String, valor:Number e isSaldo:Boolean');
    			return;
    		}

    		format.useNegativeSign='-';
    		format.useThousandsSeparator=true;
    		format.rounding="nearest";

			if(calculateRange && value != null)
			{
				for(var index:int = value.length-1; index >= 0; index--)
				{
					if(value[index].isSaldo != true)
						if(index > 0)
							value[index].valor = value[index].valor - value[index-1].valor;
					if(index==value.length-1 && value[index].isSaldo == true)
					{
						value.addItem(ObjectUtil.copy(value[index]));
						value[index].isSaldo = false;
					}
				}
			}

    		//Percorre o array de dataProviderOriginal, montando as séries do gráfico
			for each (var Obj:Object in value)
			{
				if (Obj.isSaldo==true)
					vlrSaldoAnterior=0;

				vlrValorCorrente=Obj.valor;

				dataProviderOriginalDP.push({
							eixo:Obj.eixo,
							vlrSaldo:(Obj.isSaldo==true?vlrValorCorrente:0), //Se for saldo PLOTA esta série
							vlrBase:(Obj.isSaldo==true?0: //Se for saldo NÃO plota esta série
														calculaSerieBase(vlrSaldoAnterior,vlrValorCorrente)),
							vlrVermelhoPositivo:(Obj.isSaldo==true?0 //Se for saldo NÃO plota esta série
																	:(_best==2? //Se for melhor pra baixo inverte a cor do melhor pra cima
																				calculaSerieCreditoPositivo(vlrSaldoAnterior,vlrValorCorrente)
																				:calculaSerieDebitoPositivo(vlrSaldoAnterior,vlrValorCorrente))),
							vlrVerdePositivo:(Obj.isSaldo==true?0 //Se for saldo NÃO plota esta série
																	:(_best==2? //Se for melhor pra baixo inverte a cor do melhor pra cima
																				calculaSerieDebitoPositivo(vlrSaldoAnterior,vlrValorCorrente)
																				:calculaSerieCreditoPositivo(vlrSaldoAnterior,vlrValorCorrente))),
							vlrVermelhoNegativo:(Obj.isSaldo==true?0 //Se for saldo NÃO plota esta série
																	:(_best==2? //Se for melhor pra baixo inverte a cor do melhor pra cima
																				calculaSerieCreditoNegativo(vlrSaldoAnterior,vlrValorCorrente)
																				:calculaSerieDebitoNegativo(vlrSaldoAnterior,vlrValorCorrente))),
							vlrVerdeNegativo:(Obj.isSaldo==true?0 //Se for saldo NÃO plota esta série
																:(_best==2? //Se for melhor pra baixo inverte a cor do melhor pra cima
																			calculaSerieDebitoNegativo(vlrSaldoAnterior,vlrValorCorrente)
																			:calculaSerieCreditoNegativo(vlrSaldoAnterior,vlrValorCorrente))),
							vlrValor:vlrValorCorrente
						});

				vlrSaldoAnterior+=vlrValorCorrente;
			}

    		super.dataProvider = dataProviderOriginalDP;
		    invalidateProperties();
		    invalidateDisplayList();
     	}

	}
}

Para utilizar o novo componente, utilize o código:



	
		
			
				
				
				
				
				
				
				
				
				
			
		
	

O resultado obtido é o demonstrado abaixo.

Se quiser fazer teste com o comportamento do gráfico teste-o logo abaixo…

Se preferir, faça o download do gráfico cascata já compilado em SWC. Download Gráfico Cascata.
[/like-gate] Divirta-se…

About Author

You may also like

2 Response Comments

  • Luiz  04/08/2010 at 19:16

    Por favor, eu ja tenho as formulas para montagen to grafico waterfall mas nao sei como fazer para que as cores das barras mudem automaticamente de acordo com o sinal da variação.
    Qual seria o codigo VBA que eu devo utilizar para fazer com que as barras mudem de cor automaticamente?
    Obrigado

  • Jose  21/10/2010 at 18:53

    Hello,

    Thanks for your example, I need to create the same char type, but I need to put diferent colors to the first 2 columns and the last column.

    Do you know how can I put different colors?.

    Thanks so much.
    Bye