Notxor tiene un blog

Defenestrando la vida


Gráfico de perfil en el MiniMult

El último paso de la corrección del test:

MiniMult-Grafico.png

Como se puede ver, el experimento ha terminado. Emacs aún siendo un editor de texto tiene ciertas capacidades gráficas cuando no lo lanzamos para la consola.

El gráfico

En el pasado «articulillo» un poco de las capacidades gráficas de Emacs aunque me guardé la parte sobre SVG para hablarlo cuando terminara el experimento con la generación del gráfico final. Cuando pensé originalmente esta parte utilizaba siempre Emacs en consola con la opción -nw, y no me percaté de esas capacidades hasta más adelante... una grata sorpresa.

Mi primera idea fue que el programa lo dibujara todo, pero me pareció un poco chapuza reiterativa y opté por coger un gráfico SVG y ponerle en su interior algo que permitiera dibujar la línea de forma automática en base a los datos. A base de utilizarlo sólo para texto y en consola desconocía las capacidades gráficas de Emacs y había pensado que al fin y al cabo un SVG es un archivo XML. Pensé en lo sencillo que sería abrir un fichero de texto (XML en este caso) modificar los valores oportunos y guardarlo para abrirlo con otra aplicación o visor. Se pondrían etiquetas del estilo {{etiqueta}} en los lugares adecuados y con una simple regla de tres se calcula la coordenada Y de cada punto en la línea.

Sin embargo, he tenido algunos problemas para hacerlo. Las coordenadas de los puntos y los ejes me han obligado a repasar varias veces los algoritmos de cálculo y cómo procesarlos. En este caso no me ha servido la socorrida regla de tres que esperaba hacer, ha sido todo algo más complejo aunque no lo suficiente para desanimarme.

Os cuento los pasos uno por uno:

Cargando datos

La primera parte de la función se ocupa de cargar datos y preparar variables que luego se utilizarán en los cálculos. Carga la plantilla en un buffer y lo renombra creando un fichero nuevo.

(defun minimult-ver-grafica ()
  "Dibuja la gráfica del MiniMult."
  (interactive)
  ;; Guardamos el estado del buffer actual
  (save-excursion
    (let ((nombre (concat "MiniMult-"
              (org-entry-get (point) "Nombre" t) ".svg"))
      (sexo (org-entry-get (point) "Sexo" t))
      (L    (org-entry-get (point)    "L" t))
      (F    (org-entry-get (point)    "F" t))
      (K    (org-entry-get (point)    "K" t))
      (Hs   (org-entry-get (point)   "Hs" t))
      (D    (org-entry-get (point)    "D" t))
      (Hy   (org-entry-get (point)   "Hy" t))
      (Pd   (org-entry-get (point)   "Pd" t))
      (Pa   (org-entry-get (point)   "Pa" t))
      (Pt   (org-entry-get (point)   "Pt" t))
      (Sc   (org-entry-get (point)   "Sc" t))
      (Ma   (org-entry-get (point)   "Ma" t))
      (Si   (org-entry-get (point)   "Si" t))
      (delta-control 0)
      (valor-antes   0)
      (buffer (find-file-noselect "~/proyectos/minimult/plantilla.svg")))
      (set-buffer buffer)
      ;; Activamos escritura en el buffer
      (setq inhibit-read-only t)
      ;; Guardar la plantilla con otro nombre
      (set-visited-file-name nombre)
      ;; Ir al principio del buffer
      (goto-char (point-min))
      ;; Hacer las sustituciones
      (if (search-forward "{{sexo}}")
      (replace-match sexo))
      (if (search-forward "{{L}}")
          (replace-match L))

      ...

      (if (search-forward "{{Si}}")
          (replace-match Si))
      ...)

Lo primero que hace la función es guardar la posición del punto en el fichero org que estamos trabajando con save-excursion por si se mueve. Luego, en el let se cargan las variables obteniéndolas de las propiedades del org. También carga un fichero de plantilla desde el disco. Resumo la parte de reemplazar los números que aparecen abajo en el gráfico con los valores de las escalas. La etiqueta {{L}} se sustituye con el valor de la variable L que hemos cargado en el let; y se hace de forma directa porque L es un string.

Coordenadas Y de los puntos del perfil

La conversión del valor que cada escala ha alcanzado en las coordenadas era ─en principio─ una aplicación más de la fórmula para ajustar intervalos que ya vimos en otro artículo anterior. Pero puede parecer más complicado porque el eje Y interno del SVG gráfico crece hacia abajo, mientras que los valores representados en él crecen hacia arriba. Es decir, están invertidos uno respecto al otro.

Por ejemplo, la cota de la puntuación 20 de una escala estará en el 495.5 y la cota de la puntuación 120 en el 31.5. Aún así se podría haber utilizado la misma función para hacer los cálculo aunque hubiéramos pasado al parámetro min-t el valor 495.5 y al parámetro max-t el valor 31.5.

Para ahorrar tiempo de cálculo y como todos los datos son siempre los mismos decidí resumir la función a otra más sencilla:

(defun minimult-calcular-perfil (puntos)
  "Calcula la posición en el gráfico de una puntuación."
  (+ 495.5 (* -4.641 (- puntos 20.0))))

Después de devolver el punto al principio del buffer para realizar una nueva búsqueda se realizan los cálculos y sustituciones. Pongo sólo los de las tres escalas de control para no eternizarme con un churro de código, pero con ello puedo explicar uno de los muchos fiascos que me he llevado haciendo el gráfico.

;; Ir al principio del buffer
(goto-char (point-min))
;; Calcular la posicion de las escalas de control
(setq delta-control (minimult-calcular-perfil (string-to-number L)))
(if (search-forward "{{dL}}")
    (replace-match (number-to-string delta-control)))
(setq valor-antes delta-control)
(setq delta-control (minimult-calcular-perfil (string-to-number F)))
(if (search-forward "{{dF}}")
    (replace-match (number-to-string (- delta-control valor-antes))))
(setq valor-antes delta-control)
(setq delta-control (minimult-calcular-perfil (string-to-number K)))
(if (search-forward "{{dK}}")
    (replace-match (number-to-string (- delta-control valor-antes))))

Como se puede apreciar, los cálculos que se realizan para unas escalas no sirven para otras. Eso depende de si el punto que queremos calcular es el que inicia la línea o es una continuación. ¿Por qué? Pues porque los valores de esos puntos no son absolutos sino relativos. El primer punto de la línea (y hay tres que debemos calcular en el gráfico) tiene un valor absoluto. Por eso, una vez calculada su coordenada Y guardándola en la variable delta-control la reemplazamos directamente. Bueno directamente tampoco, recordemos que los datos que se cargaban desde el org son cadenas y necesitamos convertirlos en números para operar con ellos, para eso utilizo string-to-number y cuando los queremos escribir tenemos que devolverlos como number-to-string.

Como decía, el primer punto de cada línea tiene unas coordenadas (x,y) absolutas. El siguiente punto en la línea, sin embargo, se define utilizando el anterior como si fuera el origen de las coordenadas. Esto me obliga a calcular la posición de cada punto guardando el valor anterior (valor-antes) para restarlo a la posición calculada en delta-control. Este proceso se repite también para las otras dos líneas. La idea de pasarle a una función una lista con los valores y que los fuera calculando y sustituyendo se truncó y el código ha quedado hecho un chorizo que necesita una refactorización de manera urgente.

La última chapuza es el refresco del buffer del SVG. Cuando cargo la plantilla la imagen se dibuja sin las líneas del perfil y sin los textos cambiados. He estado haciendo un búsqueda de cómo hacer un refresco gráfico. Como no encontré nada lo que hago es un refresco a las bravas (o a lo tonto)... llamo dos veces seguidas a image-toggle-display la primera cambia el buffer a modo texto y la segunda lo devuelve a modo gráfico y después cambio al buffer del gráfico en otra ventana:

;; Reseteamos el buffer pasándolo a modo texto y de nuevo a gráfico
(image-toggle-display)   ;; es un poco chapuza pero no encuentro
(image-toggle-display)   ;; cómo hacer el refresco de otro modo
;; Mostramos el buffer en otra ventana
(switch-to-buffer-other-window buffer)

Conclusiones

La verdad es que he estado aprendiendo mucho. El programa en sí es una chapuza (siendo suave con la terminología), aunque funciona como versión pre-pre-pre-alfa de lo que debería ser.

Acabo de decir que funciona pero no lo hará salvo en momentos controlados. Porque no se ha hecho prácticamente ninguna comprobación para que todo funcione correctamente. Por ejemplo, no se puede saber qué ocurriría si se llamara a la función de corrección sin tener una variable de respuestas completa. Tampoco se han tenido en cuenta los límites de las escalas. ¿Qué ocurriría si llegara un valor por debajo del mínimo calculado para una escala? ¿o por encima del máximo? Esto último puede pasar en varias escalas que se ajustan mediante K. Se puede dar el caso de una puntuación alta en la escala en cuestión se junta con una puntuación alta en K y el valor puede sobrepasar el límite máximo (vale, en la vida real sería un caso excepcional, pero podría darse).

Seguramente, si empezara a hacerlo ahora, lo plantearía de otra manera. Lisp es un lenguaje sencillo de entender: (casi) todo es una lista encerrada entre paréntesis, Lisp intenta evaluar el primer elemento de una lista con los demás como parámetros, a no ser que tenga una comilla (quote) delante. Eso lo pillas en medio minuto pero luego la forma de pensar los algoritmos es completamente distinta a los lenguajes en los que me encuentro más cómodo. La POO debe de haber anquilosado mis neuronas. Las ha dejado rígidas y sin cintura para flexibilizar mi razonamiento al programar.

Pero creo que poco a poco me iré acostumbrando a los usos y costumbres de Lisp. De hecho, me parece un lenguaje bonito e interesante. Si reflexiono un poco sobre los lenguajes que más me gustan lo pondría en segundo lugar, justo después de smalltalk. Los dos primeros de la lista son pues lenguajes muy «viejunos». No sé si será por la edad o no, pero tienen chispa. En smalltalk todo es un objeto y en lisp todo es una lista y sobre ese supuesto se montan todo un sistema completo sin necesitar una interminable lista de palabras reservadas del lenguaje, sólo un supuesto y sus consecuencias lógicas.


Comentarios

Debido a algunos ataques mailintencionados a través de la herramienta de comentarios, se ha decidido activar un filtro antispam y guardar las direcciones IP con el único objeto de añadir a la lista de bloqueos las que correspondan a spam y otras actividades maliciosas.

Disculpen las molestias.