Representación gráfica de una función

prev.gif (997 bytes)chapter.gif (1105 bytes)home.gif (1232 bytes)next.gif (998 bytes)

Ejemplos completos

Diseño del applet

Respuesta a las acciones del usuario sobre los controles

El área de la representación gráfica

Definición de la función

La clase que describe el canvas

El código fuente


El objetivo de este página es la de describir los pasos para representar una función. Sea la función

Image32.gif (1600 bytes)

que es la fórmula de Maxwell para la distribución de velocidades de las moléculas de un gas ideal. Nos da el número dn de moléculas que se mueven con una velocidad comprendida entre v y v+dv independientemente de la dirección del movimiento, cuando el gas ideal está a una temperatura T. m es la masa de las moléculas y k es la constante de Boltzmann 1.38 10-23 J/K.

Queremos representar en el eje Y de las ordenadas el número dn de moléculas que se mueven con una velocidad comprendida entre v y v+dv, y en el eje X la velocidad de las moléculas, luego cambiaremos de temperatura o de gas y representaremos la nueva distribución en otro color.

Diseño del applet

Como en muchos otros applets se han creado dos clases, la que deriva de Applet y la que deriva de Canvas. El applet describe el interfaz o comunicación entre el usuario y el programa, y en el canvas se muestran los resultados en forma de representación gráfica.

grafico1.gif (3586 bytes)

Se dispone el canvas en la parte superior del applet, y los controles sobe un panel en la parte inferior. Para disponer los controles se han empleado la aproximación de paneles anidados, como puede verse en el modo diseño del IDE de JBuilder.

Sobre el panel inferior se disponen tres paneles: en el panel de la izquierda se sitúan un control etiqueta (Label) y un control selección (Choice). En el panel central se dispone un control etiqueta (Label) y un control de edición (Edit). En el panel situado a la derecha se sitúan dos botones. El diseño del interfaz es muy rápido, si lo tenemos previamente trazado sobre papel o nos imaginamos la disposición de los controles en el applet.

Ponemos un control selección (Choice) en vez de un control lista para ahorrar el máximo espacio posible. Un control selección tiene una altura similar a un control de edición o a un botón, y se pueden disponer sin desentonar en una barra que incluya estos controles.

El control selección (Choice) contiene los nombre de varios gases ideales, y se inicializa de la siguiente forma

    Choice chGases = new Choice();
    //...
    String str[]={"Hidrógeno (H2)", "Oxígeno (O2)", "Nitrógeno (N2)", 
	"Helio (He)", "Neón (Ne)", "Argón (Ar)"};
    for(int i=0; i<str.length; i++){
          chGases.addItem(str[i]);
    }

El primer elemento de la lista aparecerá seleccionado por defecto. Inicializamos también el control de edición, de este modo el usuario, puede hacerse una idea acerca de los valores que puede introducir en dicho control

    TextField tTemperatura = new TextField();
    //...
    tTemperatura.setColumns(5);
    tTemperatura.setText("500");

 

Respuesta a las acciones del usuario sobre los controles

Haciendo doble-clic sobre cada uno de los botones JBuilder asocia el botón con un objeto una clase anónima que gestiona las acciones del usuario sobre dichos controles. El programador solamente tiene que preocuparse de introducir el código entre las llaves de apertura y cierre de la función respuesta.

Cuando pulsamos el botón titulado Gráfica se llama a la función respuesta btnGrafica_actionPerformed, se lee el texto del control de edición tTemperatura y se convierte el string en un número, que se guarda en la variable local temperatura. Posteriormente, leemos el nombre del gas seleccionado en el control de selección chGases. En vez del nombre, obtenemos el índice mediante getSelectedIndex, el número entero lo guardamos en la variable local indice. Finalmente le pasamos estos dos datos al canvas mediante la llamada a setNuevo.

  void btnGrafica_actionPerformed(ActionEvent e) {
     double temperatura=Double.valueOf(tTemperatura.getText()).doubleValue();
     int indice=chGases.getSelectedIndex();
     canvas.setNuevo(temperatura, indice);
  }

Al pulsar en el botón Borrar se llama a la función respuesta btnBorrar_actionPerformed. Dentro de ella se hace una llamada a la función paint de la clase derivada de Canvas que borra los gráficos dibujados en su área de trabajo.

  void btnBorrar_actionPerformed(ActionEvent e) {
    canvas.repaint();
  }

 

El área de la representación gráfica

Para representar gráficamente una función hemos de establecer un origen y unos ejes. Primero, obtenemos la anchura y la altura del canvas mediante getSize

     anchoCanvas=getSize().width;
     altoCanvas=getSize().height;

 

El área y los márgenes de la representación gráfica

A continuación determinamos el área del canvas en el que se va a representar la gráfica. No es conveniente aprovechar toda la superficie del canvas, sino dejar un margen alrededor, y espacio suplemetario suficiente a la izquierda y en la parte inferior para poner etiquetas a las divisiones de los ejes.

grafico2.gif (1617 bytes)

Para situar el origen tomamos como referencia en vez del pixel, la medida de la anchura y la altura de un carácter de la fuente de texto por defecto.

     charAlto=g.getFontMetrics().getHeight();
     charAncho=g.getFontMetrics().stringWidth("0");

Situamos el origen en la parte inferior izquierda del canvas, a 5 caracteres hacia la derecha y dos hacia arriba

     orgX=5*charAncho;
     orgY=altoCanvas-2*charAlto;

Dejamos un margen en la parte superior, y en la parte derecha.

 

Las escalas

La determinación de las escalas es lo que más tiempo lleva si queremos que las distintas representaciones gráficas se vean con claridad. Habitualmente, la determinación de una escala es el resultado de muchas pruebas.

En el eje horizontal vamos a representar las velocidades entre 0 y 3000 m/s, y en el eje vertical representamos la distribución entre 0 y 20 tomado arbitrariamente un valor de N (número de partículas) igual a 10000.

La determinación de una escala es una simple regla de tres, a la anchura del área de representación gráfica (véase la figura) anchoCanvas - origenX - margenDerecho le corresponde 3000. Luego la escala horizontal es

     escalaX=(double)(anchoCanvas-orgX-3*charAncho)/3000;

A la altura del área de la representación gráfica altoCanvas - margenSuperior - margenInferior le corresponde 20. Luego, la escala vertical es

     escalaY=(double)(altoCanvas-3*charAlto)/20;

Una vez determinados el origen y las escalas, podemos proyectar cualquier punto (x, y) del espacio real en un punto del área de la representación gráfica (x1, y1). Otros sistemas como Windows disponen de funciones que realizan una proyección (mapping) entre estos dos sistemas de coordendas.

               x1=orgX+(int)(x*escalaX);
               y1=orgY-(int)(y*escalaY);

Recuérdese, que los argumentos de las funciones gráficas son enteros, por lo que es preciso realizar una conversión (casting) de double a int.

 

Los ejes y las divisiones

Una vez establecido el origen y las escalas, dibujamos los ejes y les ponemos etiquetas

//eje horizontal
     g.drawLine(orgX-charAncho, orgY, anchoCanvas, orgY);
     g.drawString("v(m/s)", anchoCanvas-4*charAncho, orgY);
//eje vertical
     g.drawLine(orgX, 0, orgX, altoCanvas-charAlto);
     g.drawString("dn/dv", orgX+charAncho, charAlto);

A continuación, ponemos divisiones al eje horizontal. Tres divisiones grandes en las posiciones que corresponden a 1000, 2000 y 3000 m/s. Subdividimos cada una de ellas en cinco, de modo que tenemos divisiones pequeñas a 200, 400, 600, 800; 1200, 1400, 1600, 1800 m/s, etc. Para mayor claridad ponemos solamente etiquetas a las divisiones grandes. Dichas etiquetas se centran horizontalmente con la correspondiente división mayor. En letra negrita señalamos la parte del código que centra horizontalmente una etiqueta en una división mayor, cuya posición es x1.

     for(int i=0; i<=3; i++){
          x1=orgX+(int)(1000*i*escalaX);
          g.drawLine(x1, orgY+charAncho/2, x1, orgY-charAncho/2);
          String str=String.valueOf(i*1000);
          g.drawString(str, x1-g.getFontMetrics().stringWidth(str)/2, 
			orgY+charAlto);
          if(i==3) break;
          for(int j=1; j<5; j++){
               x1=orgX+(int)((1000*i+(double)(1000*j)/5)*escalaX);
               g.drawLine(x1, orgY+charAncho/4, x1, orgY-charAncho/4);
          }
     }

La división grande tiene una longitud igual a la altura de un carácter y la división pequeña tiene una longitud igual a mitad de dicha altura

Hacemos lo mismo con el eje vertical. Ponemos divisiones grandes en 5, 10, 15 y 20, y subdividimos cada intervalo en 5. De modo, que tenemos divisiones pequeñas a 1, 2, 3, 4, 6, 8, 9, ...Se ponen etiquetas en las divisiones grandes y se centran verticalmente con la correspondiente división mayor. En letra negrita marcamos la parte del código que centra verticamente una etiqueta en una división mayor.

     for(int i=0; i<=20; i+=5){
          y1=orgY-(int)(i*escalaY);
          g.drawLine(orgX+charAncho/2, y1, orgX-charAncho/2, y1);
          String str=String.valueOf(i);
          g.drawString(str, orgX-g.getFontMetrics().stringWidth(str)-
			charAncho/2, y1+charAlto/2-descent);
          if(i==20) break;
          for(int j=1; j<5; j++){
               y1=orgY-(int)((i+(double)(j))*escalaY);
               g.drawLine(orgX+charAncho/4, y1, orgX-charAncho/4, y1);
          }
     }

Como se observa en el código, se emplea funciones de la clase FontMetrics para obtener la altura de un carácter, la anchura de una cadena de caracteres, y el parámetro descent de la fuente de texto actual. La porción de código que realiza esta tarea es la siguiente.

     FontMetrics fm=g.getFontMetrics();
     int charAlto=fm.getHeight();
     int charAncho=fm.stringWidth("0");
     int descent=fm.getDescent();

 

Definición de la función

Tenemos que trasladar la expresión matemática a código

Image32.gif (1600 bytes)

Proporcionamos la masa en unidades de masa atómica de cada uno de los gases que aparecen en el control selección

Gas masa (u.m.a)
Hidrógeno (H2) 2
Oxígeneo (O2) 32
Nitrógeno (N2) 28
Helio (He) 4
Neon (Ne) 10
Argon (Ar) 18

Hallamos el cociente m/2kT y lo guardamos en una variable denominada cociente. Una unidad de masa atómica vale 1.672 10-27 kg y la constante de Boltzmann vale 1.38 10-23 J/K.

     cociente=(1.67e-27*masa[indice])/(2*temperatura*1.38e-23);

Definimos la función a representar. Para cada valor de v, devuelve la proporción de partículas de un gas ideal cuyas velocidades están comprendidas entre v y v+dv. Dentro de la definición de la función usamos las funciones de la clase Math: Math.pow para hallar la potencia de exponente 3/2 y Math.exp para hallar la potencia del número e.

  double f(double v){
    double y=4*N*Math.PI*Math.pow(cociente/Math.PI, 1.5)*
		Math.exp(-cociente*v*v)*v*v;
    return y;
  }

Para representar la función simplemente hacemos un bucle for, desde el valor v inicial hasta el valor v final, con un determinado paso dv. La elección del paso es importante para que la función se vea como continua. Como la resolución de la ventana es limitada por las dimensiones vertical y horizontal del área de la representación gráfica, la elección de un paso muy pequeño, no tendría efecto visual, ya que los puntos muy próximos se superpondrían sobre el mismo pixel. La elección de un paso grande dará lugar a que la curva continua se transforme en una polilínea u unión de un conjunto de segmentos rectilíneos.

La representación gráfica de una curva se realiza en dos pasos. En primer lugar, se determina las coordendas del punto inicial. Se calcula el nuevo punto, se une mediante una recta el punto inicial y el actual, a continuación, el punto actual se convierte en el punto inicial para el siguiente paso.

//punto inicial
     int x1=orgX, y1=orgY, x2, y2;
     g.setColor(color[nGrafica]);

     for(double v=0; v<3000; v+=10){
//punto actual
          y2=orgY-(int)(funcion(v)*escalaY);
          x2=orgX+(int)(v*escalaX);
          g.drawLine(x1, y1, x2, y2);
//el punto actual se convierte en inicial 
          x1=x2; y1=y2;
     }

 

La clase que describe el canvas

Todo lo que se refiere a la representación gráfica lo describimos mediante la clase MiCanvas derivada de Canvas. Creamos un canvas, u objeto de la clase MiCanvas y la ponemos por encima del panel que contiene los controles.

    this.add(panel1, BorderLayout.SOUTH);
    this.add(canvas, BorderLayout.CENTER);

Cuando se pulsa el botón titulado Gráfica, se llama a la función setNuevo, y le pasa el índice del gas ideal que ha sido seleccionado y el valor de la temperatura que se ha introducido en el control de edición. En dicha función, se calcula el cociente m/2kT y se guarda en una variable denominada cociente, que se empleará en la definición de la función f. Esta función miembro, se encarga también de determinar el índice del color nGrafica con el que se dibujará la gráfica. Finalmente, llama a la función que dibuja la gráfica dibujaFuncion.

  void setNuevo(double temperatura, int indice){
     cociente=(1.67e-27*masa[indice])/(2*temperatura*1.38e-23);
     nGrafica++;
     if(nGrafica>13){
          nGrafica=0;
     }
     dibujaFuncion();
  }

Ahora tenemos una disyuntiva, si queremos dibujar una función cada vez, ponemos el código que la dibuja en la redefinición de paint, y en setNuevo hacemos una llamada a paint mediante repaint. Véase los ejemplos de la primera página que estudia el control Canvas.

Nuestro propósito es el de dibujar varias curvas y poder comparar las distintas representaciones gráficas. Por ejemplo, la distribución de velocidades de las moléculas de un gas a distintas temperaturas, o bien, la distribución de las velocidades de las moléculas de varios gases ideales a la misma temperatura. Para realizar esta tarea, emplearemos la aproximación que se sugiere en la segunda página dedicada al estudio del control Canvas.

La función dibujaFuncion obtiene el contexto gráfico g del componente mediante getGraphics y dibuja la función f sobre dicho contexto, en el color dado por el índice nGrafica del array color. No debemos de olvidarnos de liberar los recursos asociados al contexto gráfico g mediante dispose, cuando se obtiene con la función getGraphics. No es necesario hacerlo cuando el contexto g, nos lo suministra la función paint o update, en su único parámetro.

  void dibujaFuncion(){
     Graphics g=getGraphics();
     int x1=orgX, y1=orgY, x2, y2;
     g.setColor(color[nGrafica]);

     for(double v=0; v<3000; v+=10){
               y2=orgY-(int)(f(v)*escalaY);
               x2=orgX+(int)(v*escalaX);
               g.drawLine(x1, y1, x2, y2);
               x1=x2; y1=y2;
     }
     g.dispose();
  }

Redefinimos la función paint, para dibujar los ejes. La función paint se llama cuando se crea el applet o cuando se redimensiona, o bien, indirectamente a través de repaint. Cuando aparece el applet se crea el objeto canvas, y se llama a su función miembro paint, se determina el origen y se dibujan los ejes, preparando el applet para que el usuario seleccione un gas, introduzca un valor para la temperatura y pulse el botón titulado Gráfica.

 public void paint(Graphics g){
     origen(g);
     dibujaEjes(g);
 }

Cuando se han acumulado varias gráficas y no se distingue bien entre ellas se pulsa el botón titulado Borrar, que hace una llamada a paint, para preparar al applet para dibujar nuevas funciones con nuevos datos.

  void btnBorrar_actionPerformed(ActionEvent e) {
    canvas.repaint();
  }

El código completo de la clase MiCanvas, es el siguiente

package grafica;

import java.awt.*;
public class MiCanvas extends Canvas {
//anchura y altura del canvas
     int anchoCanvas, altoCanvas;
//origenes
     int orgY, orgX;
//escalas
     double escalaX, escalaY;
//masas de las moléculas en u.m.a
     final int masa[]={2, 32, 28, 4, 10, 18};
     double cociente;       
//número de partículas
     final int N=10000;
//colores de las funciones
     int nGrafica=-1;
     static final Color color[]={new Color(255,0,0), new Color(0, 255,0), new Color(0, 0, 255),
          new Color(0, 255, 255), new Color(255, 0, 255), new Color(255, 255, 0),
          new Color(128, 0, 0), new Color(255, 128, 0), new Color(0, 64, 64),
          new Color(128, 128, 255), new Color(128, 0, 64), new Color(255, 0, 128),
          new Color(192, 192, 192), new Color(128, 128, 0)};

  public MiCanvas() {
     setBackground(Color.white);
  }
  void setNuevo(double temperatura, int indice){
     cociente=(1.67e-27*masa[indice])/(2*temperatura*1.38e-23);
     nGrafica++;
     if(nGrafica>13){
          nGrafica=0;
     }
     dibujaFuncion();
  }

  void origen(Graphics g){
     anchoCanvas=getSize().width;
     altoCanvas=getSize().height;
     FontMetrics fm=g.getFontMetrics();
     int charAlto=fm.getHeight();
     int charAncho=fm.stringWidth("0");
//orígenes
     orgX=5*charAncho;
     orgY=altoCanvas-2*charAlto;
//escalas
     escalaX=(double)(anchoCanvas-orgX-3*charAncho)/3000;
     escalaY=(double)(altoCanvas-3*charAlto)/20;
  }

  void dibujaEjes(Graphics g){
     FontMetrics fm=g.getFontMetrics();
     int descent=fm.getDescent();
     int charAlto=fm.getHeight();
     int charAncho=fm.stringWidth("0");
//borra el canvas
     g.setColor(getBackground());
     g.fillRect(0,0, anchoCanvas, altoCanvas);
     g.setColor(Color.black);
//eje horizontal
     g.drawLine(orgX-charAncho, orgY, anchoCanvas, orgY);
     g.drawString("v(m/s)", anchoCanvas-4*charAncho, orgY);
     int x1, y1;
     for(int i=0; i<=3; i++){
          x1=orgX+(int)(1000*i*escalaX);
          g.drawLine(x1, orgY+charAncho/2, x1, orgY-charAncho/2);
          String str=String.valueOf(i*1000);
          g.drawString(str, x1-fm.stringWidth(str)/2, orgY+charAlto);
          if(i==3) break;
          for(int j=1; j<5; j++){
               x1=orgX+(int)((1000*i+(double)(1000*j)/5)*escalaX);
               g.drawLine(x1, orgY+charAncho/4, x1, orgY-charAncho/4);
          }
     }

//eje vertical
     g.drawLine(orgX, 0, orgX, altoCanvas-charAlto);
     g.drawString("dn/dv", orgX+charAncho, charAlto);
     for(int i=0; i<=20; i+=5){
          y1=orgY-(int)(i*escalaY);
          g.drawLine(orgX+charAncho/2, y1, orgX-charAncho/2, y1);
          String str=String.valueOf(i);
          g.drawString(str, orgX-fm.stringWidth(str)-charAncho/2, y1+charAlto/2-descent);
          if(i==20) break;
          for(int j=1; j<5; j++){
               y1=orgY-(int)((i+(double)(j))*escalaY);
               g.drawLine(orgX+charAncho/4, y1, orgX-charAncho/4, y1);
          }
     }
  }
  double f(double v){
    double y=4*N*Math.PI*Math.pow(cociente/Math.PI, 1.5)*Math.exp(-cociente*v*v)*v*v;
    return y;
  }

  void dibujaFuncion(){
     Graphics g=getGraphics();
     int x1=orgX, y1=orgY, x2, y2;
     g.setColor(color[nGrafica]);

     for(double v=0; v<3000; v+=10){
               y2=orgY-(int)(f(v)*escalaY);
               x2=orgX+(int)(v*escalaX);
               g.drawLine(x1, y1, x2, y2);
               x1=x2; y1=y2;
     }
     g.dispose();
  }

 public void paint(Graphics g){
     origen(g);
     dibujaEjes(g);
 }
}

 

El código fuente

disco.gif (1035 bytes)grafica: GraficaApplet.java, MiCanvas.java