XY++
Biblioteca extensível de classes para gráficos bidimensionais


Manual do Usuário
versão 1.0

ATENÇÃO: Documentação desatualizada. Veja o código atualizado em "Documentação Interna".


Objetivo

XY++ é uma biblioteca de classes construída para facilitar o processo de geração de gráficos bidimensionais em geral. A biblioteca tem as seguintes características:

Visão Geral

Essa biblioteca está foi desenvolvida usando-se C++ e CD. Está apta a ser usada em ambientes DOS, WINDOWS, UNIX/MOTIF e VMS/MOTIF.

Em seu aspecto global, o XY++ consiste num conjunto de objetos que interagem entre si, respeitando uma hierarquia definida de forma a tornar essas relações mais eficientes, evitando-se ao máximo duplicações e ambiguidades. Observe a hierarquia desses objetos no esquema abaixo.


As setas indicam o relacionamento entre as classes e os símbolos meia-lua indicam derivação de classes.

As classes básicas são XYObject que é a primeira nessa hierarquia e contém o que é comum aos demais membros; XYGraph que é o local onde títulos, eixos, curvas e legenda irão ser representados; XYText que é responsável pelos textos; XYAxis que representa um eixo; XYMask que é responsável pela representação visual de um conjunto de dados; XYSeries que é o próprio conjunto de dados; XYLegend que faz a legenda, XYMarker que define um marcador de região no gráfico e XYPosition que indica a posição absoluta de um objeto ou a relativa a outro objeto. Cada classe será vista com mais detalhes a seguir.


Descrição

Faremos, agora, uma descrição detalhada das classes do XY++ com o objetivo de torná-lo mais transparente. Antes de começar, porém, é preciso que se entenda alguns conceitos sobre os sistemas de coordenadas envolvidos. Consideraremos o canvas do CD como ambiente onde o resultado visual da ação do XY++ será apresentado.


A área do gráfico é fornecida em coordenadas normalizadas com relação ao canvas do CD. Por exemplo, se à área do gráfico forem atribuidos os valores xmin = 0.1, ymin = 0.1, xmax = 0.7 e ymax = 0.5, então a posição dele no canvas do CD será:


As coordenadas dos textos, dos eixos, dos marcadores e da legenda são normalizadas com relação a área do gráfico. Por isso se, por exemplo, forem atribuidos para um eixo os valores x = 0.1, y = 0.1 e size = 0.4, então a posição dele será:


A região onde são desenhadas as máscaras (mask area) é obtida indiretamente como resultado de um cálculo interno baseado na orientação dos eixos envolvidos.


Usando XY

Para utilizar o pacote XY basta linkar sua aplicação com a biblioteca XYpassivo.lib da plataforma desejada, não esquecendo de incluir também biblioteca do CD para plataforma em questão. Além disso, talvez seja necessário definir o símbolo __BOOL_H caso o compilador utilizado já implemente o tipo básico bool em C++ (Ex : VC++5.x, gcc).


Exemplo

Veremos, agora, um exemplo prático de como usar esta ferramenta. Recomenda-se que ele seja aproveitado como base para os primeiros ensaios do usuário até que o mesmo se familiarize com a biblioteca e possa trabalhar com ela com naturalidade.

Aplicação

Código

Neste exemplo, tem-se como objetivo a visualização do gráfico acima com todos os seus detalhes. Para esse fim usaremos um sistema portátil de interface com o usuário conhecido como IUP. Fica claro que essa é uma necessidade da aplicação e não do XY++, nele será mapeado o canvas do CD onde esta ferramenta atua.

O que se segue de agora em diante é o código-fonte, comentado, da rotina principal da aplicação proposta.

#include "iup.h"         // includes da biblioteca IUP
#include "cd.h"          // includes da biblioteca CD
#include "cdiup.h"       // includes da biblioteca CD
#include "xy.h"          // includes da biblioteca XY++
#include "xyserfil.h"    // classe derivada da propria aplicacao
Após se incluir as bibliotecas necessárias, começaremos a definir os elementos presentes no gráfico; ou seja, título(s), eixo(s), escala(s), grade(s), curva(s) e legenda.

Para se obter os dois títulos que aparecem no topo do canvas, basta defini-los como feito abaixo:

XYText tipo_de_curva(
         "Curva de Destilação",
         XY_BLACK, 
         XYText::large, 
         XYText::helvetica, 
         XYText::italic, 
         XYText::north, 
         XYText::horizontal, 
         true);

XYText período(
         "Amostra do Mês de Junho/96", 
         XY_BLUE, 
         XYText::standard, 
         XYText::timesRoman, 
         XYText::bold, 
         XYText::north, 
         XYText::horizontal);
Na sequência, o próximo passo seria definir os dois eixos presentes no gráfico. Proceda como a seguir, observe que os dois eixos definidos são lineares, mas existem outros tipos:

XYLinearAxis eixo_porcentagem(
                 0.0, 96.0, 
                 0.11, 0.2, 
                 XY_RED, 
                 0.7,  
                 0.0, 
                 4., 
                 &decorator_porcentagem,
                 &tit_porcentagem);

XYLinearAxis eixo_temperatura(
                (-300, 950, 
                 0.11, 0.2, 
                 XY_RED, 
                 0.6, 
                 90.0, 
                 50., 
                 &decorator_temperatura,
                 &tit_temperatura);
Pode-se verificar que na definição dos eixos, feita acima, aparecem novos dados que precisam ser definidos; quais sejam: o título e o decorador para cada um deles. Para se obter o resultado do exemplo, basta definí-los como segue.

XYText tit_porcentagem (
        "Porcentagem (%)", 
        0.5, 0.15, 
        XY_RED, 
        XYText::standard, 
        XYText::timesRoman, 
        XYText::plain, 
        XYText::center, 
        XYText::horizontal);
 
XYText tit_temperatura (
        "Graus Centígrados", 
        0.03, 0.5, 
        XY_RED, 
        XYText::standard, 
        XYText::timesRoman,
        XYText::plain, 
        XYText::center, 
        XYText::vertBotTop);
 
XYNumericalScaleDecorator decorador_porcentagem ("%.1f", &escala_porcentagem);
 
XYNumericalScaleDecorator decorador_temperatura ("%.1f", &escala_temperatura);
Como se vê, esses decoradores precisam que se defina as escalas onde eles irão atuar. Seguindo o exemplo, considere:

XYText escala_porcentagem(
           XY_BLACK, 
           XYText::small, 
           XYText::timesRoman, 
           XYText::plain, 
           XYText::north, 
           XYText::horizontal);
 
XYText escala_temperatura(
           XY_BLACK, 
           XYText::small, 
           XYText::timesRoman, 
           XYText::plain, 
           XYText::east,  
           XYText::horizontal);
Definamos, agora, a grade que aparece na região de desenho da curva, observe que esse elemento tem que ser especificado para as duas direções consideradas:

XYGrid grv (   0.,  96.,    0., XY_GRAY,  0.,  4.);
XYGrid grh (-300., 950., -300., XY_GRAY, 90., 50.);
A essa altura do exemplo dado, falta apenas definir a máscara (representação da curva que para esse exemplo será adquirida via leitura de um arquivo) e a legenda.

A máscara escolhida é a cartesiana de linha (existem outras) e sua definição é a que se tem abaixo:

XYCartesianLineMask marquivo(
          &mask_titulo, 
          &arquivo,   
          &eixo_porcentagem, &eixo_temperatura, 
          XY_RED,      
          1, 
          XYCartesianLineMask::continuous);
Observe que a definição acima precisa do acréscimo das especificações de título, eixos e série ao qual esta máscara está relacionada.

A definição do título da máscara pode ser feita assim:

XYText mask_titulo(
          "Óleo CRU",
          XY_BLACK, 
          XYText::tile, 
          XYText::timesRoman, 
          XYText::plain, 
          XYText::north, 
          XYText::horizontal);
A definição do tipo da série a qual a máscara deve representar é de responsabilidade inteira da aplicação no sentido de que nesse momento se faz uso de uma classe implementada especialmente, pelo usuário, para esse fim. Porém é um passo simples e para o nosso caso essa definição é feita abaixo (código listado mais a adiante), onde o tipo já identifica a série como sendo obtida a partir da leitura de um arquivo cujo nome, no exemplo, é "dado.dat":

XYSeriesFile arquivo ("dado.dat");
Finalmente, definimos a legenda. Observe que o nome que aparecerá nela não consta em sua lista de atributos, vem da consulta que ela faz a lista de máscaras envolvidas; no nosso exemplo, apenas um:

XYLegend legend (0.83, 0.3, 1, 1);
Por último, vamos definir o tipo de gráfico, dentre os disponíveis, que será desenhado no canvas.

XYCartesian *grafico;
Observe-se que a sequência de apresentacão dos elementos envolvidos não se dá necessariamente nessa ordem, ela foi escolhida aqui por ser a mais didática.

Findo o processo de definição dos objetos, passemos às funções úteis a visualização do exemplo dado. Começamos pela função de repaint que, neste caso, é bastante simples:

int frepaint (Ihandle *)
{
   grafico -> clear();
   grafico -> draw();

   return IUP_DEFAULT;
}
Nesse nosso caso específico, a função principal seria:

void main(void)
{ 
   IupOpen ();
   Ihandle *c = IupCanvas ("arepaint");
   Ihandle *d = IupDialog (c);
   IupSetAttributes (d," SIZE = 400x300, TITLE = XY++");
   IupSetFunction ("arepaint", (Icallback) frepaint);

   IupMap (d);

   grafico = new XYCartesian (c, 0.0, 0.0, 1.0, 1.0, &grv, &grh);

   // Insere lista de títulos
   grafico -> insert (&período);
   grafico -> insert (&tipo_de_curva);

   // define cor de fundo para o gráfico
   grafico -> color (XY_GRAY);

   // Insere lista de eixos
   grafico -> insert (&eixo_temperatura);
   grafico -> insert (&eixo_porcentagem);
   
   // define região de desenho para as máscaras baseado nos dois eixos dados
   grafico -> calcMaskArea (0);

   // define cor da região de desenho das máscaras
   grafico -> maskAreaColor (XY_WHITE);

   // Insere lista de máscaras
   grafico -> insert (&marquivo);

   // define a legenda do gráfico
   grafico -> legend (&legend);

   // exibe o diálogo que foi definido
   IupShow (d);
   
   // imprime: deixar ativo apenas se a impressão do gráfico for desejada  
   grafico -> print(); 

   // passa o controle para o IUP, interage com o usuário
   IupMainLoop ();

   // fecha o IUP
   IupClose ();
}
Como foi dito anteriormente, o armazenamento de eixos, curvas (máscaras) e títulos é feito usando-se uma pilha simples, o que precisa do cuidado de se incluir, por exemplo, títulos e curvas na ordem inversa da desejada para que sejam plotadas na ordem correta (inversa a esta) sobre o canvas.

Nesse exemplo, se trocarmos a ordem de inclusão dos títulos colocando o tipo_de_curva acima do período o resultado visual seria aparecer a frase "Amostra do Mês de Junho/96" acima de "Curva de Destilação".

O que vem a seguir são as rotinas auxiliares a implementação desse exemplo, isto é, as que são extensão das já existentes no pacote XY++ (através do mecanismo de herança). Nesse caso específico, a classe que faz a leitura dos dados da série a partir de um arquivo de dados não consta do pacote e seu código fonte é listado.

Na lista de includes desse nosso exemplo aparece "xyserfil.h" que é a classe mencionada no parágrafo anterior e é dela que trataremos a seguir.

Rotinas de Responsabilidade da Aplicação

Veremos agora as rotinas desse nosso exemplo cuja implementação são de responsabilidade exclusiva da aplicação.

Classe que faz leitura de dados a partir de um arquivo

#include "xyser.h"
 
class XYSeriesFile : public XYSeries
{
   public:

   // construtor da classe XYSeriesFile
   XYSeriesFile (const char *filename);

   // destrutor da classe XYSeriesFile
   virtual ~XYSeriesFile (void);

   // define o domínio da série
   inline void domain (double begin, double end);
   // consulta o domínio da série
   inline void domain (double& begin, double& end) const;

   // define o número de pontos dentro do domínio considerado 
   inline unsigned numPoints(void) const;

   // consulta n-ésimo ponto no domínio
   inline bool point (unsigned n, double& x, double& y) const;

   protected:

   double   _begin;      // início do domínio
   double   _end;        // final do domínio
   unsigned _first;      // índice do primeiro x que está dentro do domínio
   double   *_x, *_y;    // coordenadas dos pontos
   unsigned _np;	       // número de pontos
   unsigned _np_dominio; // número de pontos no domínio
};     
Essa classe tem como objetivo a aquisição dos dados da série via leitura de arquivo. Os métodos que ela possui são obrigatórios pois são definidos como virtuais puros na classe-pai "xyser.h" e seus codigos-fonte são listados abaixo:

void XYSeriesFile::domain (double begin, double end)
{
   _begin = begin;
   _end   = end;
   _first = 0;
 
   // atualiza o índice do primeiro x que pertence ao domínio
   while ( _first < _np && _x[_first] < _begin )
      _first++;
 
   // índice do último x que está dentro do domínio
   unsigned last = _first; 
 
   // atualiza o índice do último x que pertence ao domínio
   while ( last < _np && _x[last] < _end )
      last++;

   _np_dominio = last - _first;
}
 
void XYSeriesFile::domain (double& begin, double& end) const
{
   begin = _begin;
   end   = _end; 
}
 
unsigned XYSeriesFile::numPoints (void) const
{
   return _np_dominio;
}
 
bool XYSeriesFile::point (unsigned n, double& x, double& y) const
{
   if (_np_dominio == 0)
      return false;

   if (n >= _np_dominio)
      return false;

   n += _first;  // desloca para o início do domínio

   x = _x[n];
   y = _y[n];
   return true;
}
Como se pode ver, é uma classe de implementação bastante simples. Observe que seus métodos serão usados para que o XY++ possa identificar o domínio, a quantidade de pontos e a forma de acesso aos dados que devem ser plotados.

Construtor da classe que faz leitura de dados a partir de um arquivo

#include <stdio.h>
#include <malloc.h>
#include "xyserfil.h"	

XYSeriesFile::XYSeriesFile (const char *filename)
{
   FILE *f = fopen (filename, "r");

   if (f == 0) return;

   double x, y;
   unsigned num_alocados = 16;

   _np = 0;
   _x = (double *) malloc (sizeof (double) * num_alocados);

   if (_x == 0)
   {
      fclose (f);
      return;
   }

   _y = (double *) malloc (sizeof (double) * num_alocados);

   if (_y == 0)
   {
      free (_x);
      _x = 0;

      fclose (f);
      return;
   }

   while (fscanf (f, "%lf %lf", &x, &y) == 2)
   {
      if (_np == num_alocados)
      {
         num_alocados *= 2;
         _x = (double *) realloc (_x,sizeof (double) * num_alocados);
         _y = (double *) realloc (_y,sizeof (double) * num_alocados);
      }

      _x[_np] = x;
      _y[_np] = y;

      ++_np;
   }
   
   --_np;

   _first = 0;
   _begin = _x[0];
   _end   = _x[_np];
   _np_dominio = _np;
   
   fclose (f);
}

XYSeriesFile::~XYSeriesFile (void)
{
   if (_x)
      free(_x);

   if (_y)
      free(_y);
}
A essa altura nosso objetivo inicial já terá sido atingido. Depois desse exemplo simples, pode-se querer dotar o gráfico já existente de um leque mais robusto de elementos. Neste caso, ainda a título de exemplo, podemos acrescentar algumas funções que nos permitam:

Viabilizar o pick, isto é, termos como feedback de um click sobre um ponto do canvas uma janela com mensagem sobre onde estamos fazendo o click (sobre qual elemento do gráfico). Uma função para isso seria:

int fbutton (Ihandle *, int b, int m, int x, int y, char *)
{
   if (m == 0) 				// botão solto, retorne
    return IUP_DEFAULT;

   cdCanvas2Raster (&x, &y);

   if (grafico -> pickMask (x, y) != 0)
     IupMessage ("Pick","Mascara Selecionada");
   else if (grafico -> pickAxis (x, y) != 0)
     IupMessage ("Pick","Eixo Selecionado");
   else if (grafico -> pickText (x, y) != 0)
     IupMessage ("Pick","Texto Selecionado");
   else if (grafico -> pickLegend (x, y) != 0)
     IupMessage ("Pick","Legenda Selecionada");

   return IUP_DEFAULT;
}
No entanto, para que essa função funcione é preciso acrescentar mais duas linha a função main(), isto é, onde antes se via

void main(void)
{ 
   IupOpen ();
   Ihandle *c = IupCanvas ("arepaint");
   Ihandle *d = IupDialog (c);
   IupSetAttributes (d," SIZE = 400x300, TITLE = XY++");
   IupSetFunction ("arepaint", (Icallback) frepaint);

   IupMap (d);

   ...
}
Agora faça:

void main(void)
{ 
   IupOpen ();
   Ihandle *c = IupCanvas ("arepaint");
   Ihandle *d = IupDialog (c);
   IupSetAttributes (d," SIZE = 400x300, TITLE = XY++");
   IupSetFunction ("arepaint", (Icallback) frepaint);
   IupSetAttribute (c, IUP_BUTTON_CB, "fbutton");		// linha nova
   IupSetFunction ("fbutton", (Icallback) fbutton);		// linha nova
   
   IupMap (d);

   ...
}
Viabilizar o scroll, isto é, podermos mover a curva na direção que desejarmos. Segue aqui as funções para se fazer scroll para a direita e para a esquerda de um passo da grade vertical:

int fscrollright (Ihandle *)  // 4. = passo da grade vertical nesse exemplo
{
   arquivo.displacement(4.);  
   grafico -> scroll(grv.sizeWorld2sizePixel(4.), 0);
   return IUP_DEFAULT;
}

int fscrollleft (Ihandle *) // 4. = passo da grade vertical nesse exemplo
{
   arquivo.displacement(-4.);  
   grafico -> scroll(-grv.sizeWorld2sizePixel(4.), 0);
   return IUP_DEFAULT;
}
Nesse caso, a função repaint muda. Ficando assim:

int frepaint (Ihandle *)
{
   grafico -> setTransformation();

   eixo_porcentagem.size(0.7);  // tamanho dado a esse eixo quando definido
   eixo_porcentagem.adjustSize ();

   grafico -> remove(&eixo_temperatura);
   grafico -> calcMaskArea (0);
   grafico -> insert(&eixo_temperatura);


   grafico -> clear();
   grafico -> draw();
   return IUP_DEFAULT;
}
E a função main() precisa do acréscimo de mais quatro linhas para absorver mais esses detalhes. Ou seja, ela passa a ser:

void main(void)
{ 
   IupOpen ();
   Ihandle *c = IupCanvas ("arepaint");
   Ihandle *d = IupDialog (c);
   IupSetAttributes (d," SIZE = 400x300, TITLE = XY++");
   IupSetFunction ("arepaint", (Icallback) frepaint);
   IupSetAttribute (c, IUP_BUTTON_CB, "fbutton");   
   IupSetFunction ("fbutton", (Icallback) fbutton);   
   IupSetAttribute (c, IUP_BUTTON_CB, " fscrollleft ");       // linha nova
   IupSetFunction ("fscrollleft ", (Icallback) fscrollleft);  // linha nova
   IupSetAttribute (c, IUP_BUTTON_CB, " fscrollright ");      // linha nova
   IupSetFunction ("fscrollright ", (Icallback) fscrollright);// linha nova
   
   IupMap (d);

   ...
}
Uma informação útil é que esse scroll só terá efeito de continuidade sobre a curva se a mesma tiver pontos a acrescentar nas duas direções.

Assim, pode-se ir incluindo cada vez mais elementos na aplicação de forma a satisfazer a necessidade para a qual ela tenha sido idealizada.

Para atingir a intenção de fazer esse manuscrito tão auto-suficiente quanto possível faz-se necessário dar uma idéia da implementação de mais uma das classes de responsabilidade exclusiva da aplicação, mas que aparece no diagrama de hierarquia do XY++. Digamos que a título de curiosidade segue também a listagem da classe "xysersin.h" responsável pela representação da curva seno de um ângulo:

Classe que representa a curva seno de um ângulo

#ifndef __XYSERIESSIN_H
#define __XYSERIESSIN_H

#include "xyser.h"
#include "xymath.h"

class XYSeriesSin : public XYSeries
{
   public:

   // construtor da classe XYSeriesSin
   XYSeriesSin (
       double b,  // início do domínio em graus
       double e,  // final do domínio em graus
       double s)  // passo
       : _begin(b), 
         _end(e), 
         _step(s), 
         _displacement(0.),
         _np((unsigned) ((e - b) / s)) {};

   // destrutor da classe XYSeriesSin
   virtual ~XYSeriesSin (void){};
   // consulta o passo na série
   inline double step (void) const;

   //define deslocamento: para testar scroll da região de desenho das 
   // máscaras do gráfico
   inline void displacement (double  d);

   // define o domínio da série
   inline void domain (double begin, double end);
   // consulta o domínio da série
   inline void domain (double* begin, double* end) const;

   // consulta o número de pontos dentro do domínio
   virtual unsigned numPoints (void) const;

   // consulta o n-ésimo ponto no domínio
   virtual bool point (unsigned n, double& x, double& y) const;

   private:

   double   _begin;         // início do domínio
   double   _end;           // final do domínio
   double   _step;          // passo
   double   _displacement;  // deslocamento para o scroll
   unsigned _np;            // número de pontos
};
Essa classe foi desenvolvida para dar o efeito de continuidade quando da aplicação de um scroll. Implemente-a e acrescente-a a aplicação já existente. O código-fonte de seus métodos estão a seguir.

double XYSeriesSin::step (void) const
{
   return _step;
}

void XYSeriesSin::displacement (double d)
{
   _displacement += d;
}

void XYSeriesSin::domain (double begin, double end)
{
   _begin = begin;
   _end   = end; 
   _np    = (unsigned) ((_end - _begin) / _step);
}

void XYSeriesSin::domain (double* begin, double* end) const
{
   *begin = _begin;
   *end   = _end; 
}

unsigned XYSeriesSin::numPoints (void) const
{
   return _np;
}

bool XYSeriesSin::point (unsigned n, double& x, double& y) const
{
   if (_np == 0)
      return false;

   if (n > _np)
      n = 0;                        
   x = n * step() + _begin;
   y = sin((x + _displacement) * PI / 180.0);
   return true;
}

#endif
Observe que a forma de acesso aos dados que devem ser plotados é diferente aqui, pois o valor de cada ponto não é mais obtido da leitura de uma rquivo e sim da função "seno".

Caso exista interesse nas demais classes presentes no diagrama do XY++ e que são de inteira responsabilidade da aplicação, basta saber que as mesmas são idênticas a anterior alterando-se apenas o valor que a variável "y" receberá. Em outras palavras, temos:

Para "xysercos.h":

inline bool XYSeriesCos::point (unsigned n, double& x, double& y) const
{
   ...

   y = cos((x + _displacement) * PI / 180.0);
   return true;
}
Para "xyserncos.h":

inline bool XYSeriesNCos::point (unsigned n, double& x, double& y) const
{
   ...

   y = -cos((x + _displacement) * PI / 180.0);
   return true; 
}