DevSecOps

8 mai, 2009

Gráfico Cascata no Flex

Publicidade

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

O gráfico de cascata (waterfall, ou 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.

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

  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 seis 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:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" xmlns:ns1="br.com.igormusardo.component.chart.*" backgroundGradientAlphas="[1.0, 1.0]" backgroundGradientColors="[#FFFFFF, #FFFFFF]">
<ns1:WaterfallChart id="wfc" top="10" left="10" right="10" bottom="10" showDataTips="true" calculateRange="false" best="1">
<ns1:dataProvider>
<mx:Array>
<mx:Object eixo="ROB" valor="12850" isSaldo="true"/>
<mx:Object eixo="(-) Deduções" valor="-1500" isSaldo="false"/>
<mx:Object eixo="ROL" valor="11350" isSaldo="true"/>
<mx:Object eixo="(-) Custos" valor="-8200" isSaldo="false"/>
<mx:Object eixo="Lucro Bruto" valor="3150" isSaldo="true"/>
<mx:Object eixo="(-) Desp. Op." valor="-3150" isSaldo="false"/>
<mx:Object eixo="(-) Desp. Fi." valor="-1200" isSaldo="false"/>
<mx:Object eixo="Res. Não Op. " valor="3500" isSaldo="false"/>
<mx:Object eixo="Lajir" valor="2300" isSaldo="true"/>
</mx:Array>
</ns1:dataProvider>
</ns1:WaterfallChart>
</mx:Application>

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.

Divirta-se.

*

artigo originalmente publicado em http://www.igormusardo.com.br/