En esta última parte de la serie de artículos sobre el dibujo de gráficos con datos de sensores conectados a Internet de las cosas, toca hablar sobre cómo generar o modificar con JavaScript los dibujos en formato SVG y algunos de los elementos HTML que sirven de contenedor o que presentan información complementaria a los gráficos.
A los usuarios destino de este tutorial se les supone formando un perfil de electrónica y programación de microcontroladores, es posible que no estén familiarizados con HTML, CSS o SVG; por eso, en las anteriores entregas se hacía una pequeña introducción al lenguaje o a la tecnología correspondiente. En esta última parte el enfoque es un poco diferente, ya que seguramente los lectores saben programar, es posible que usando el lenguaje C++ que, como JavaScript, comparte sintaxis básica con C y podrá tomarse como referencia para omitir la mayoría de los conceptos básicos de programación y así centrarnos en las diferencias y en el uso concreto que nos interesa para realizar los gráficos de sensores en la IoT.
De la primera diferencia da una pista el nombre: JavaScript es un lenguaje de programación de script (guión) y como tal, es interpretado, no es necesario compilarlo; el contexto en el que funcione el script (un navegador web, por ejemplo) irá leyendo, traduciendo y ejecutando las órdenes. Para ser precisos, en la mayoría de los casos se produce una compilación en tiempo de ejecución (JIT), pero para el proceso de escritura de código JavaScript no nos afecta, simplemente escribimos el código y ya puede funcionar.
El nombre contiene también la primera confusión: JavaScript no tiene la menor relación con Java. Inicialmente, cuando lo desarrolló Netscape para su navegador, se llamaba primero Mocha y luego LiveScript, menos confuso. Después de su éxito de implantación en los navegadores, y de trascenderlos, se estandarizó como ECMAScript (la ECMA-262, versión 6 en el momento de escribir este texto) para hacerse neutral con respecto a los navegadores que lo implementan. Actualmente también existe un estándar ISO de la versión 5, de 2011 (ISO/IEC 16262:2011 en el momento de escribir el artículo)
Variables, tipos de datos básicos y objetos en JavaScript
A diferencia de lo que ocurre, por ejemplo, en C++, en JavaScript no se incluye el tipo de datos al declarar una variable y además el tipo asociado a una variable no es fijo, es posible asignar un valor de un tipo diferente a lo largo de la ejecución del programa.
1 2 3 4 5 6 7 |
var cosa; cosa="texto"; console.log(typeof cosa); // Debería mostrar string en la consola cosa=123; console.log(typeof cosa); // Debería mostrar number en la consola cosa={temperatura:22,corriente:1.5}; console.log(typeof cosa); // Debería mostrar object en la consola |
En el ejemplo anterior se ha declarado la variable «cosa» (sin indicar el tipo de dato) luego se asignan datos de diferente tipo y se consulta con typeof
el tipo que JavaScript que ha interpretado. Para depurar el código se puede escribir en la consola del inspector del navegador web (lo que no afectará a la presentación de la web) con console.log()
.
Para forzar la conversión de un dato a un tipo concreto, especialmente los de texto a numéricos, se pueden utilizar funciones como parseInt()
o parseFloat()
que convierten a enteros o a números en coma flotante respectivamente. La conversión contraria puede realizarse con String()
, aunque es improbable que sea necesario utilizarla ya que la conversión automática suele ser suficiente. Con parseFloat()
, por ejemplo, puede obtenerse el valor de una propiedad de la página web, por ejemplo el ancho o el alto de un objeto, que incluya unidades; de esta forma, la expresión parseFloat("50px");
devolverá 50, un valor numérico, como resultado.
En JavaScript no hay distinción entre comillas dobles y simples; el tipo de dato en ambos casos es string
, y cada una de ellas puede incluir a la otra sin necesidad de códigos de escape.
1 2 3 4 5 6 7 8 9 10 |
var texto; console.log(typeof texto); // Debería mostrar string en la undefined texto="esto es un texto"; console.log(typeof texto); // Debería mostrar string en la consola texto='A'; console.log(typeof texto); // Debería mostrar string en la consola texto="esto es un 'texto'"; console.log(typeof texto); // Debería mostrar string en la consola texto='"A"'; console.log(typeof texto); // Debería mostrar string en la consola |
En el ejemplo anterior puede verse que una variable, cuando ha sido declarada (existe) pero no se le ha asignado ningún valor, contiene un tipo de dato indefinido (undefined
). Un objeto sin asignar tiene el valor null
; es decir, el objeto existe, pero sin valor; una variable que hiciera referencia a él no tendría un typeof
undefined
sino object
. Un objeto también puede estar vacío, es decir, no ser nulo pero no tener ninguna propiedad.
Para definir un objeto en JavaScript se encierran entre llaves ({
y }
) las propiedades o métodos, separando por el signo de dos puntos (:
) nombre de propiedad de valor de propiedad y por coma (,
) las diferentes propiedades. Puedes encontrar más información sobre esta manera de expresar un objeto en el artículo sobre el formato JSON.
Aunque se puede utilizar una sintaxis que pueda llegar a hacer pensar lo contrario, en JavaScript no existen clases sino prototipos, es decir, para que un objeto herede propiedades y métodos se crea otro (el prototipo) que usan los demás (los hijos) como referencia. La sintaxis más cercana al estilo de JavaScript para usar un prototipo es Object.create
aunque también es posible (y útil a veces) usar new
como en otros lenguajes orientados a objetos.
1 2 3 4 |
var perro=new Mamifero(); // Esto funciona, pero no es exactamente el nuevo estilo JavaScript console.log(perro instanceof Mamifero); var gato=Object.create(Mamifero); // Crear un objeto usando un prototipo al estilo JavaScript console.log(Mamifero.isPrototypeOf(gato)); |
Para consultar si un objeto es una instancia de otro, si lo usa como prototipo, si se hereda sus propiedades, en definitiva, se puede utilizar instanceof
(creado con new
) o isPrototypeOf
(creado con Object.create
) que se evaluará a verdadero cuando el objeto use el prototipo y a falso cuando no.
Una vez creado un objeto utilizando otro como prototipo, es decir, una vez instanciado un objeto, se pueden añadir propiedades nuevas o sobrescribir propiedades del prototipo usando la sintaxis de punto como en gato.peso=2.5
.
La matrices (arrays) en JavaScript son diferentes de las que seguramente conozcas en C. Para empezar, se declaran sin necesidad de indicar su longitud, sólo con los signos de abrir y cerrar corchetes ([
y ]
), los componentes pueden ser heterogéneos (diferentes tipos de datos en la misma matriz) y se pueden añadir nuevos elementos sin restringirse a un límite. Las matrices de JavaScript son en realidad listas (colecciones) de elementos a los que se hace referencia por un índice numérico o por un nombre. Una matriz puede contener simultáneamente índices numéricos y nombres de elementos pero lo habitual es usar objetos (propiedades) para explotar el segundo tipo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// Declarar matrices (arrays) var preparada=[]; // La matriz ha sido declarada pero (todavía) no contiene valores var cosas=["silla","mesa","caja"]; // Matriz declarada con componentes formada por cadenas de texto var valores=[200,"lleno",0.5,true,"simple",false,false,10]; // Matriz declarada con componentes heterogéneos var ramas=[20,"abc",[1,2,3],false,[10,20,["uno","dos"]]]; // Matriz que contiene matrices var demode=new Array(10,20,30,4,3,2,1); // La sintaxis con new no es la preferida de JavaScript aunque funciona… var peligrosa=new Array(10); // …pero con el riesgo de confundir índices con elementos: la matriz peligrosa tiene 10 elementos, no un elemento de valor 10 // Acceder a los valores de la matriz preparada.push(33.33); // Añade un nuevo valor al final de la matriz console.log("La matriz 'preparada' contiene "+preparada.length+" elementos"); // Ahora contine 1 elemento console.log(cosas[0]); // Muestra en la consola el primer valor de la matriz (las matrices empiezan en el índice cero) cosas[2]="tarro"; preparada[10]=50; // Los índices no tienen que ser consecutivos console.log("La matriz 'preparada' contiene "+preparada.length+" elementos"); // Ahora contine 11 elementos console.log("Elemento sexto: "+preparada[5]); // undefined // Verificar si una variable es (apunta a) una matriz console.log(Array.isArray(cosas)); // Nuevas versiones de JavaScript (ECMAScript versión 5 o superior) console.log(cosas instanceof Array); // Implementaciones de JavaScript (ECMAScript) más viejas // Para esto es mejor usar objetos var frutas=[]; frutas["peras"]=20; frutas["manzanas"]=30; frutas[4]=10; console.log(frutas.peras); console.log(frutas["manzanas"]); console.log(frutas[4]); console.log(frutas[3]); // undefined |
Como puede verse en el ejemplo anterior, para saber si una variable corresponde con una instancia de una matriz (es un objeto matriz) se puede utilizar instanceof
, como ya se ha usado con objetos genéricos o, en versiones más recientes de JavaScript se puede recurrir a Array.isArray()
Para acceder a los elementos de la matriz se puede utilizar su índice (matriz[7]
) o por el nombre de la propiedad con el nombre entre corchetes (matriz["nombre"]
) o con la sintaxis de punto habitual de los objetos (matriz.nombre
). Como el nombre es una cadena de texto, se puede utilizar una expresión, incluyendo variables, para componerlo. Para recorrer una matriz con propiedades puede utilizarse un bucle con el formato for(propiedad in matriz)
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
var matriz=[]; matriz["color"]="verde"; matriz["grosor"]=10; matriz["estado"]="nuevo"; matriz[0]=25.0; matriz[1]="uno"; for(propiedad in matriz) { console.log(propiedad+" valor "+matriz[propiedad]); } /* El resultado en la consola será: 0 valor 25 1 valor uno color valor verde grosor valor 10 estado valor nuevo */ |
Es interesante para nuestro objetivo tratar el objeto Date
, con el que representar y gestionar fecha y hora en JavaScript. El objeto se puede instanciar sin datos, de manera que tomará la fecha y hora actual, o se puede crear indicando una fecha como un valor, ya sea en milisegundos desde el 1 de enero de 1970 (como el tiempo Unix o tiempo POSIX pero expresado en milisegundos en lugar de en segundos) o especificando los valores separados de año, mes, día, hora…
El objeto incluye una completa serie de métodos para consultar o establecer la fecha y la hora:
-
now()
Devuelve la fecha y hora actual expresada en milisegundos desde el 1 de enero de 1970 -
getTime()
|setTime()
Obtiene o cambia, respectivamente, el valor de la hora en milisegundos desde el 1 de enero de 1970. UsandovalueOf()
, que es un método presente en la mayoría de objetos, también se obtiene el valor del objeto Date que corresponde, comogetTime()
con tiempo Unix o tiempo POSIX expresado en ms. -
getMilliseconds()
|setMilliseconds()
Sirve para consultar o para establecer la parte fracción de milisegundos del objetoDate
sobre el que se ejecute. Si se consulta, el valor obtenido está comprendido entre 0 y 999 pero se pueden asignar valores mayores que se acumularán en la fecha y hora total por lo que, como el resto de métodos get-, sirve para incrementar el valor del objetoDate
(o decrementarlo, si se usan valores negativos). -
getSeconds()
|setSeconds()
Devuelve o cambia, respectivamente, el valor de los segundos del objetoDate
. -
getMinutes()
|setMinutes()
Sirve para consultar o establecer los minutos del objetoDate
. -
getHours()
|setHours()
Permite consultar o modificar las horas (de 0 a 23) del objetoDate
. -
getDay()
Devuelve el día de la semana de la fecha, expresado como un valor de 0 a 6 (de domingo a sábado). -
getDate()
|setDate()
Devuelve o cambia el día del mes del objetoDate
sobre el que se aplique. -
getMonth()
|setMonth()
Sirve para consultar o modificar el número del mes del objetoDate
. -
getFullYear()
|setFullYear()
Consulta o establece el valor del año en el objeto que contiene fecha y hora.
Los anteriores métodos de Date
incluyen una versión UTC para poder trabajar directamente con la hora universal sin tener que hacer cálculos intermedios. En ese sentido, por ejemplo, getHours()
tiene una versión getUTCHours()
o getMilliseconds()
una alternativa getUTCMilliseconds()
para trabajar alternativamente con la hora oficial (legal) o la universal. Con getTimezoneOffset()
se puede saber la diferencia que existe entre la hora universal y la oficial local.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
var dia_semana=["domingo","lunes","martes","miércoles","jueves","viernes","sábado"]; var nombre_mes=["enero","febrero","marzo","abril","mayo","junio","julio","agosto","septiembre","octubre","noviembre","diciembre"]; var digitos_hora; var hoy=new Date(); var texto_hoy=""; texto_hoy+="Hoy es "; texto_hoy+=dia_semana[hoy.getDay()]; texto_hoy+=", "; texto_hoy+=hoy.getDate(); texto_hoy+=" de "; texto_hoy+=nombre_mes[hoy.getMonth()]; texto_hoy+=" de "; texto_hoy+=hoy.getFullYear(); texto_hoy+=" y son las "; digitos_hora=hoy.getHours(); texto_hoy+=digitos_hora>9?digitos_hora:"0"+digitos_hora; texto_hoy+=":"; digitos_hora=hoy.getMinutes(); texto_hoy+=digitos_hora>9?digitos_hora:"0"+digitos_hora; texto_hoy+=":"; digitos_hora=hoy.getSeconds(); texto_hoy+=digitos_hora>9?digitos_hora:"0"+digitos_hora; |
Funciones JavaScript
Si estás leyendo esto seguramente sabes programar microcontroladores en C o en C++ y conozcas el concepto de función. Aunque la idea base es la misma, en JavaScript la forma de definirlas y usarlas es un poco distinta. Para empezar, ya se ha dicho, JavaScript no usa explícitamente tipos de datos así que no hay que indicarlo al definir la función. Para seguir, no es obligatorio que una función tenga nombre, pueden ser anónimas. Pueden asociarse a una variable para invocarlas pero también puede que no sea necesario ya que, a veces, es útil invocarlas inmediatamente, para lo cual se añaden los paréntesis y los parámetros después de la definición de la función.
Para definir una función se antepone function
, si procede se escribe el nombre, entre paréntesis los argumentos (los parámetros que se pasan a la función) y entre llaves el código que se ejecutará cuando se invoque a la función.
1 2 3 4 5 |
function doble(numero) { var resultado=numero*2; return resultado; } |
Ciertamente, en el ejemplo anterior no hacía falta para nada la variable «resultado», pero es una buena excusa para recordar el ámbito de las variables, que funciona como esperas: la variable «resultado» sólo existe dentro de la función «doble». En JavaScript también puede usarse let
, en lugar de var
, para definir el ámbito de una variable a un contexto de bloque de código (encerrada entre llaves, {
y }
)
Al hablar de objetos en el anterior apartado faltaba algo fundamental: se han definido propiedades pero no se han definido métodos. Como cabe esperar, los métodos de los objetos son funciones, no tienen nombre y se utilizan (se invocan) desde el nombre (de propiedad) que le asigna la definición del objeto.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var termostato= { temperatura_actual:0.0, temperatura_frio:18.5, temperatura_calor:22.0, consumo:0, ver_temperatura: function() { console.log("Temperatura actual: "+this.temperatura_actual+" °C"); } } |
En el anterior ejemplo, ya existe un método, «ver_temperatura», que muestra por la consola el valor de la propiedad «temperatura_actual». No es muy útil, pero da una idea más completa de cómo es la definición de un objeto en JavaScript.
Para acceder desde los métodos de un objeto (funciones) a sus propiedades se utiliza this
, como en el ejemplo anterior en la línea 11, al utilizar la propiedad «temperatura_actual».
Acceder al modelo de objetos del documento (DOM) con JavaScript
Desde JavaScript se tiene acceso al contenido de la página web en la que se ejecuta, así como a algunos aspectos del navegador que muestra esa página, aunque no a los recursos del sistema. La estructura de datos que soporta las propiedades y los métodos a los que se tiene acceso desde JavaScript parte del objeto window, en concreto, el contenido del objeto (el documento HTML) corresponde con el objeto document
. Aunque se utiliza a veces por claridad, no es necesario preceder de window a los métodos o propiedades para referirse a ellos, basta, por ejemplo, con usar document
, no es necesario escribir el nombre del objeto raíz como en window.document
, siempre que se haga referencia a la ventana actual.
La forma más usada de encontrar un objeto dentro del documento HTML es por medio del método getElementById()
, al que se le pasa como argumento el id que se indicó al crear el código HTML. Por lo explicado en apartados anteriores, es fácil suponer que también se puede acceder a los componentes que hay dentro del objeto document
usando la sintaxis de punto (document.componente
) o de corchetes usando tanto el nombre (document["componente"]
), lo más útil, como el índice numérico, difícil de usar y poco práctico cuando se accede al contenido de una página web compuesta manualmente.
Con JavaScript se puede obtener el elemento que contiene a otro elemento (elemento o nodo padre) consultando su propiedad parentNode
o bien su propiedad parentElement
, la diferencia es que el elemento padre (parentElement
) del elemento final de la cadena del DOM es nulo (null
) y el nodo padre (parentNode
) es el propio documento (document
).
Para modificar el contenido de un elemento HTML, por ejemplo el de una etiqueta <div>
, se puede utilizar innerHTML
y para cambiar sus propiedades se puede optar por asignarle una clase diferente con className
o alterar sus propiedades individualmente con style
. Para consultar el estilo mostrado por un elemento en la página web no necesariamente sirve style
ya que es posible que dependa de varios factores o simplemente no se haya especificado explícitamente. Para consultar el estilo de un elemento mostrado finalmente en la página web se utiliza el método getComputedStyle.
A un elemento del documento HTML se le pueden asignar varias clases con las que determinar su aspecto y su comportamiento, para gestionar la lista de clases de un objeto desde JavaScript se puede recurrir a classList
que ofrece los métodos add
para añadir una nueva clase a la lista, remove
para eliminarla, toggle
para sustituirla o consultar el contenido de la lista de clases de un elemento con item
y con contains
, que devuelve la clase que ocupa cierta posición en la lista y un valor true
o false
si cierta clase está o no en la lista.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var contenedor_temperatura=document.getElementById("temperatura"); // Encontrar el div con id="temperatura" contenedor_temperatura.innerHTML=""; // Eliminar el contenido provisionalmente para que no se vean los cambios contenedor.className="bloque_temperatura"; // Asignar una nueva clase if(temperatura>20) // Si la temperatura es mayor que 20 °C… { contenedor.style.color="#FF6666"; // …usar el color rojo en lugar del color normal de la clase } contenedor_temperatura.innerHTML="La temperatura es "+temperatura+" °C"; // Cuando el aspecto esté preparado mostrar el valor if(document["titulo"].classList.contains("estilo_titulo")) // Si el objeto "titulo" tiene la clase "estilo_titulo"… { if(document["titulo"].classList.contains("general")) // …y la clase "general"… { document["titulo"].classList.remove("general"); // …quitar la clase "general" } } else // Si el objeto "titulo" no tiene la clase "estilo_titulo"… { document["titulo"].classList.add("estilo_titulo"); // …añadir la clase "estilo_titulo" (da igual que tenga o no la clase "general") } |
En el ejemplo anterior se localiza con getElementById
el objeto que se quiere manipular (un elemento <div>
por su id
), antes de cambiar el aspecto, se borra el contenido asignando con innerHTML
una cadena de texto vacía, se le asigna una nueva clase con className
y se modifica su estilo con style
según el valor del contenido (la temperatura), cambiando el color, si procede, por medio de la propiedad color
. Una vez establecido el aspecto, se escribe el valor usando otra vez innerHTML
.
En la segunda parte del ejemplo de arriba (líneas 9 a 19) se accede a un elemento del código HTML utilizando la sintaxis document[]
y la propiedad id
del elemento para alterar su lista de clases con el método classList.remove()
y con el métodoclassList.add()
, en función del resultado de varias consultas que se realizan en ejecuciones condicionales, que comparan usando classList.contains()
.
Cuando se va a hacer referencia a un elemento HTML varias veces a lo largo del código JavaScript, es un poco más eficiente asignarlo a una variable o usar su índice en lugar del nombre ya que, en caso contrario, el método que usaría JavaScript para obtenerlo cada vez requeriría buscar su nombre, consumiendo algo más de tiempo que si se accediera a una variable.
Para añadir nuevos objetos al documento HTML, se pueden crear primero con el método createElement
de document
y posteriormente incorporarlos al resto de los elementos en el punto del árbol que sea necesario con appendChild
. Para crear un objeto XML, como los objetos SVG que usamos para dibujar la gráfica de los sensores IoT, se puede utilizar createElementNS
(NS por name space, espacio de nombres). Como se explicó al hablar del formato SVG, el espacio de nombres que le corresponde (por la versión actual) es http://www.w3.org/2000/svg
, que debe pasarse a createElementNS
como argumento junto con el tipo de elemento, svg
, en este caso.
Una alternativa a innerHTML
para añadir un texto como contenido de un elemento del documento HTML es el método createTextNode()
del objeto document
. Con esta alternativa se puede crear un texto nuevo (al que se tiene posterior acceso si se asigna a una variable) que se incorpora al árbol del objeto con el método appendChild()
. Como alternativa a appendChild()
, que añade el nuevo contenido al final del que ya exista en el nodo al que se incorpora, se puede utilizar el método insertBefore()
, que añade un nuevo objeto delante de otro que ya exista. Usar insertBefore()
en lugar de appendChild()
aporta un método que sirve, por ejemplo, para ordenar nuevos objetos frente a los existentes cuando un elemento deba estar delante de otro (como en una lista) o bien tapar o ser tapado en una estructura gráfica en la que existan elementos más cerca del primer plano o del fondo.
Reaccionar a los eventos con JavaScript
Cuando se explicó la forma de usar una página web como contenedor para los gráficos de sensores conectados a la IoT se utilizó onload
en la etiqueta <body>
para iniciar el dibujo del gráfico. Esta propiedad, asociada los objetos del código HTML, hace referencia a los eventos JavaScript. Como ya se ha explicado, ejecuta una función cuando la página se ha cargado. Aunque se ha asociado al código HTML para tenerla más presente, se podría haber escrito en el código JavaScript como body.onload=dibujar;
siendo dibujar
el nombre de la función que se debe iniciar al cargar la página web.
En las versiones más recientes de JavaScript se pueden asociar eventos a funciones usando addEventListener
con el formato objeto.addEventListener(evento,función);
o utilizando la sintaxis objeto.evento=función;
que funciona también en las implementaciones más antiguas. Para desligar la función asociada al evento se dispone de removeEventListener
que tiene el mismo formato que addEventListener
.
JavaScript es capaz de reaccionar a multitud de eventos que se pueden producir en una página web. Por ejemplo, puede detectar cuando se pulsa sobre un elemento HTML con onmousedown
, o cuando se hace clic sobre él con onclick
, cuando se presiona una tecla con onkeydown
, al accionar la barra de desplazamiento con onscroll
. Para nuestro propósito nos basta con detectar la carga de la página con onload
y su redimensionado con onresize
. Asociaremos estos eventos a los objetos body
y window
del DOM respectivamente. El primero puede asignarse en el código HTML, como se ha visto y el segundo dentro del código JavaScript dentro de la función llamada por el primero y con el formato window.onresize=redimensionar;
siendo redimensionar
la función que se invocará cada vez que la ventana cambie de tamaño.
Ejecutar después de un intervalo de tiempo
JavaScript dispone de dos recursos para la ejecución diferida: setTimeout
, que ejecuta una función pasado un intervalo de tiempo y setInterval
que ejecutará una función cada cierto intervalo de tiempo. Ambos métodos necesitan como parámetros (1) la función invocada y (2) el intervalo de tiempo expresado en milisegundos. Para detener su funcionamiento, se puede asignar el resultado devuelto por estas funciones a variables y pasarlas como argumento a clearTimeout
o a clearInterval
cuando no se desee volver a invocar (o cuando no se desee que se lleguen a ejecutar por primera vez) setTimeout
o setInterval
respectivamente.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
var cuenta_atras=setTimeout(descansar,1000*60*20); // Recordar que hay que descansar cuando pasen 20 minutos var repeticion=setInterval(consultar_correo,1000*60*5); // Revisar el correo cada 5 minutos function descansar() { alert("Puedes descansar un rato"); // Esto solamente aparecerá una vez } function consultar_correo() { alert("Revisa el correo electrónico"); // Esto aparecerá cada cinco minutos } function detener_cuenta_atras() // No utiliza argumentos sino la variable global { clearTimeout(cuenta_atras); // Detener la cuenta atrás para avisar a los 20 minutos } function no_avisar_lectura_correo() // No utiliza argumentos sino la variable global { clearInterval(repeticion); // Dejar de avisar cada 5 minutos } |
En el ejemplo anterior se introduce el método alert
que sirve para mostrar un letrero de aviso. Aunque se utilizó bastante en el pasado, actualmente está casi proscrito del código JavaScript por lo agresivo (intrusivo) que resulta tapar con un cuadro de diálogo la página web.
En un programa escrito para un microcontrolador de una serie pequeña (como el de la placa Arduino Uno) es habitual utilizar variables globales, como pasa en el ejemplo anterior en JavaScript, ya que el código es breve y no resulta especialmente confuso, porque muchas veces las funciones se implementan ad hoc y porque el uso de variables globales permite predecir de manera muy sencilla e intuitiva el uso de memoria, lo cual es crítico en sistemas con pocos recursos. En cambio, en JavaScript es habitual reducir al mínimo posible el uso de variables globales porque no necesita apurar el uso de memoria, ya que se ejecuta normalmente en una CPU con recursos muy superiores a los de un MCU, porque es probable que coexista con mucho código de terceros con el que debe funcionar sin interferir y al ser un sistema abierto no se puede predecir el contexto de ejecución futuro (el programa de un microcontrolador pequeño determina por completo su funcionamiento sin que se añada más código una vez que se encuentra en explotación) y porque las dimensiones de las aplicaciones podrían dificultar la lectura si el código no encapsula su funcionamiento haciendo los métodos lo más auto-contenidos que sea posible.
Operaciones matemáticas con el objeto Math de JavaScript
Las operaciones matemáticas de cálculo matemático más complicado se agrupan en el objeto Math
. Este objeto se usa directamente, no es necesario instanciarlo para usar los métodos o las propiedades (constantes) que incorpora.
Math.abs(n)
Valor absoluto del parámetro nMath.acos(n)
Arcocoseno del parámetro n (resultado en radianes)Math.asin(n)
Arcoseno del parámetro n (resultado en radianes)Math.atan(n)
Arcotangente del parámetro n (resultado en radianes)Math.atan2(n,m)
Arcotangente de n/m (resultado en radianes)Math.ceil(n)
Redondea el parámetro al entero más cercano hacia arribaMath.cos(α)
Coseno del parámetro α (α en radianes)Math.E
Número e (≃2.718281828459045)Math.exp(n)
e elevado al parámetro n: enMath.floor(n)
Redondea el parámetro n al entero más cercano hacia abajoMath.log(n)
Logaritmo natural (base e) del parámetro nMath.LN2
Logaritmo natural (base e) de 2 (≃0.6931471805599453)Math.LN10
Logaritmo natural (base e) de 10 (≃2.302585092994046)Math.LOG2E
Logaritmo con base 2 de e (≃1.4426950408889634)Math.LOG10E
Logaritmo con base 10 de e (≃0.4342944819032518)Math.max(a,b,c,…)
Mayor valor de la lista de parámetros que se pasenMath.min(a,b,c,…)
Menor valor de la lista de parámetros que se pasenMath.PI
Número π (≃3.141592653589793)Math.pow(n,m)
Primer parámetro n elevado al segundo parámetro m: nmMath.random()
Número (casi) aleatorio comprendido entre 0.0 y 1.0Math.round(n)
Redondea el parámetro n al entero más cercanoMath.sin(α)
Seno del parámetro α (α en radianes)Math.sqrt(n)
Raíz cuadrada del parámetro nMath.SQRT1_2
Raíz cuadrada de 1/2 (≃0.7071067811865476)Math.SQRT2
Raíz cuadrada de 2 (≃1.4142135623730951)Math.tan(α)
Tangente del parámetro α (α en radianes)
Cargar datos del servidor con AJAX
El método que se sigue para dibujar la información almacenada en la IoT consiste en cargar los datos del servidor cada cierto tiempo y redibujar con ellos la gráfica con la que se representan. Para leer los datos del servidor se utiliza la tecnología AJAX (Asynchronous JavaScript And XML) por medio de un objeto XMLHttpRequest
de JavaScript. El trazado de la gráfica de los datos se hace reutilizando un objeto SVG que ya está en el código HTML y que contiene un trazado cuyas coordenadas se modifican para hacerlas corresponder con los nuevos datos cargados.
En el ejemplo de esta propuesta, además de actualizar el dibujo, también se actualiza un texto en la página web que muestra la fecha y el valor del último dato medido para cada gráfica.
Del lado del servidor hay una base de datos que contiene la información que los sensores conectados a la IoT han ido monitorizando. Esta base de datos se lee por la petición del objeto XMLHttpRequest
respondiendo con información codificada en el formato JSON, pese a que el nombre del método utilizado sugiere relación con el formato XML.
En el primer tutorial de polaridad.es sobre el almacenamiento de datos IoT se puede ver un ejemplo de una infraestructura para gestionar desde el lado del servidor la información aportada por los dispositivos conectados a la Internet de las cosas. En esa serie de artículos se utiliza como recurso un servidor Apache desde el que se puede usar el lenguaje de programación PHP para acceder a una base de datos MySQL o MariaDB. En servidores utilizados para dar soporte a IoT es muy común encontrar bases de datos MongoDB (NoSQL) y el lenguaje de programación JavaScript sobre Node.js como infraestructura software.
La siguiente función es la que se encarga de solicitar al servidor los últimos datos de uno de los sensores. En la llamada a la función se utiliza como argumento el objeto JavaScript que soporta los datos que se dibujan. Si el mismo gráfico representa varios valores, por ejemplo para buscar visualmente una correlación, se puede hacer una petición al servidor para que devuelva varios simultáneamente, un método más óptimo por la forma de trabajar del protocolo HTTP.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
function consultar_ultimo_valor_sensores(objeto_grafico) { var consulta='zona='+objeto_grafico.sufijo_nombre; var pagina="ultimo_valor_sensor.php"; var resultado; var ajax; if(window.XMLHttpRequest) { ajax=new XMLHttpRequest(); // ajax=Object.create(XMLHttpRequest); } else // Versiones antiguas de MS Internet Explorer { ajax=new ActiveXObject("Microsoft.XMLHTTP"); } ajax.onreadystatechange= function() { if(ajax.readyState==4&&ajax.status==200&&ajax.responseType=="json") { resultado=JSON.parse(ajax.responseText); if(resultado.fecha>objeto_grafico.fecha[objeto_grafico.fecha.length-1]) { // Normalmente se gestionará la respuesta utilizando el objeto redibujar_grafico(objeto_grafico,resultado); // Si los datos son sencillos y tienen una estructura clara puede ser más práctico usarla directamente // redibujar_grafico(objeto_grafico,[resultado.fecha,resultado.temperatura]); } } } ajax.open("POST",pagina); ajax.setRequestHeader("Method","POST "+pagina+" HTTP/1.1"); ajax.setRequestHeader("Content-type","application/x-www-form-urlencoded"); ajax.setRequestHeader("Content-length",consulta.length); ajax.setRequestHeader("Connection","close"); ajax.send(consulta); } |
En la tercera línea del anterior ejemplo se prepara la consulta que se hará al servidor, en la que se pasará el argumento «zona» cuyo valor será el del nombre o código del lugar monitorizado ya que en la misma base de datos puede coexistir información de diferentes sensores (por ejemplo, termómetros que miden la temperatura en diferentes estancias). Se espera que el parámetro que se pasa a la función anterior, el objeto con los datos del gráfico, incluya una propiedad con el nombre de la estancia («sufijo_nombre»).
Entre las líneas 7 y 14 del anterior código se crea el objeto XMLHttpRequest
que se almacena en la variable «ajax». Antes de elegir la forma de crear el objeto, se busca en window
por si XMLHttpRequest
no estuviera disponible (cosa que ocurría en versiones antiguas del explorador de Microsoft y aunque queda muy atrás, sirve como ejemplo de alternativas para crear el objeto usando la sintaxis (más nativa) Object.create
o new
, similar a la de otros lenguajes orientados a objetos.
Para poder gestionar la respuesta inmediatamente, en las líneas 15 a 26 se prepara el código que la atiende antes de realizar la petición al servidor.
La forma de realizar la consulta HTTP al servidor consiste en abrir una conexión con open
indicando tipo y página (opcionalmente usuario y clave), preparar las cabeceras del protocolo con setRequestHeader
y enviar la petición con send
. La cabecera HTTP Content-length
necesitará saber la longitud de la consulta (número de caracteres) que se calcula usando length
.
Cuando la petición AJAX está lista se ejecuta la función asociada al evento onreadystatechange
. En lugar de asignar una función, en el ejemplo anterior se define sobre la marcha una función anónima que gestionará la recepción de los datos que lleguen del servidor. En primer lugar, en la línea 18, se verifica que el estado de la petición es «terminada», lo que corresponde con el valor 4
de la propiedad readyState
, que el estado sea el «OK» del protocolo HTTP (código 200
) que se puede obtener de la propiedad status
y que los datos que han llegado están el formato JSON, consultando la propiedad responseType
.
Una vez verificado que el estado de la respuesta es el esperado, en la línea 20 del ejemplo anterior se crea un objeto con el resultado, convirtiendo el texto JSON. En la respuesta se ha previsto que se devuelva una fecha, esto permite ver si el resultado que el servidor envía ya se había representado anteriormente en la gráfica, lo que se verifica en la línea 21. Si el dato es nuevo, en la línea 23 se llama a la función que se encarga de redibujar el gráfico con la nueva información.
La idea al plantear este método de lectura es que se van a refrescar los datos con mucha frecuencia. Si la información que se presenta corresponde a un plazo largo (como las temperaturas de un día o de una semana) puede implementarse una petición inicial que recoja todos los datos disponibles y luego una, similar a la del ejemplo, que los actualice en el plazo correspondiente.
Generar datos aleatorios para las pruebas
Cuando toda la infraestructura del servidor y del cliente esté lista, una función como la del apartado anterior se encargará de leer los datos y dibujar con ellos el gráfico pero en la fase de pruebas puede ser más práctico utilizar números aleatorios dentro de un rango controlado para ver si el código que se va escribiendo es correcto. La siguiente función puede servir como ejemplo para obtener datos mientras se construye la aplicación definitiva.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
function consultar_ultimo_valor_sensores ( objeto_grafico,// Objeto dentro del elemento SVG que representa el trazado valor_maximo, // Valor máximo valor_minimo, // Valor mínimo margen_valor // Cantidad extra que se representa sobre/bajo el valor máximo/mínimo ) { // La forma más genérica es usar objetos /* var nuevo_valor_inventado= { fecha:Math.round(Date.now()+Math.random()*1000), temperatura:Math.random()*Math.abs(valor_maximo-valor_minimo)+valor_minimo }; */ // En este caso concreto, como son pocos datos y con una estructura concreta es más práctico usar un vector var nuevo_valor_inventado= [ Math.round(Date.now()+Math.random()*1000), Math.random()*Math.abs(valor_maximo-valor_minimo)+valor_minimo ]; redibujar_grafico(objeto_grafico,nuevo_valor_inventado); } |
En lugar de leer la información de una base de datos, el ejemplo de arriba los genera aleatoriamente y los pasa a la función encargada de dibujar el gráfico. El dato inventado es un vector formado por una fecha expresada como un valor en milisegundos, el momento de grabar la información del sensor, y el dato monitorizado, que se encuentra entre un valor máximo y un valor mínimo.
En este ejemplo, al generar una fecha se puede retrasar hasta un segundo (1000 milisegundos) con respecto a al fecha del momento de inventarla. Como Math.random()
genera un número entre 0.0 y 1.0, multiplicándolo por 1000 se obtiene un número entre 0 y 1000 que se convierte después en entero. De la misma forma, el valor se obtiene multiplicando el número aleatorio por el rango (máximo menos mínimo) y añadiéndole el mínimo.
Dibujar el gráfico de los sensores IoT con un trazado SVG
Ya que se ha visto cómo se pueden obtener los valores que se desean representar (temperatura, en el ejemplo) y su localización temporal, que se pueden expresar conjuntamente en forma de coordenadas, en el ejemplo de más abajo se muestra una función para dibujar un trazado que une esos puntos y opcionalmente una zona coloreada delimitada por esa línea en la parte superior. El resultado sería como el de la siguiente imagen.
El eje horizontal (X) del gráfico representa el tiempo y el vertical (Y) los valores que los sensores conectados a la IoT han ido monitorizando. El intervalo horizontal es de unos pocos segundos ya que en esta propuesta el gráfico se actualiza con mucha frecuencia (cada segundo, por ejemplo) para dar información casi en tiempo real del estado de los sensores.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
function actualizar_grafico ( grafico, // Objeto SVG con el que se dibuja la gráfico coordenada, // Matriz con las coordenadas del trazado formada por pares [tiempo,valor] ordenados primero el más antiguo (tiempo menor) último el más nuevo tiempo_total_representado, // Tiempo total representado por la gráfico (en milisegundos) valor_maximo, // Valor máximo aceptable antes de emitir una alarma valor_minimo, // Valor mínimo aceptable antes de emitir una alarma margen_valor, // Cantidad extra que se representa sobre/bajo el valor máximo/mínimo parametro_cerrado, // Valor booleano que indica si el trazado se cierra o no (por defecto false) parametro_ancho_caja, // Ancho de la caja que contiene la gráfico (por defecto 100.0) parametro_alto_caja // Alto de la caja que contiene la gráfico (por defecto 100.0) ) { var cerrado=parametro_cerrado||false; // Valor booleano que indica si el trazado se cierra o no (por defecto false) var ancho_caja=parametro_ancho_caja||100.0; // Ancho de la caja que contiene la gráfico (por defecto 100.0) var alto_caja=parametro_alto_caja||100.0; // Alto de la caja que contiene la gráfico (por defecto 100.0) var coordenadas_trazado="M "; // Cadena de texto que representa la propiedad "d" del trazado SVG var desplazamiento=[]; // Desplazamientos X e Y para posicionar el tiempo y el valor representado dentro del rango del gráfico var escala=[]; // Coeficientes X e Y para calcular el tamaño al representar el gráfico var sin_recortar=true; // False si la gráfico se sale de la caja (si se sale, si recorta, se hace false para no seguir dibujando puntos) var contador_valor=coordenada.length-1; // Variable para recorrer los valores (índice) var posicion=[]; // Variable intermedia (para hacer más legible el código) con la que calcular la posición horizontal y vertical de un punto del trazado escala[0]=ancho_caja/tiempo_total_representado; // Coeficiente que multiplica a los valores horizontales (tiempo) para calcular las coordenadas X escala[1]=alto_caja/(Math.abs(valor_maximo-valor_minimo)+margen_valor*2); // Coeficiente que multiplica a los valores (vertical) para calcular las coordenadas Y desplazamiento[0]=coordenada[coordenada.length-1][0]-tiempo_total_representado; // Valor desde el que se empieza a contar el tiempo: el valor mayor (último) menos el rango de tiempo representado desplazamiento[1]=margen_valor-valor_minimo; // Valor menor mostrado (al menor se le añade un margen para visualizar el principio de los valores fuera del rango permitido) if(cerrado) // Si se dibuja un path (trazado) cerrado… { coordenadas_trazado+=ancho_caja+","+alto_caja+" L "; // …se empieza por la parte inferior de la caja } while(contador_valor>=0&&sin_recortar) // Mientras queden valores por representar y no se haya llegado al borde izquierdo del gráfico… { posicion[0]=(coordenada[contador_valor][0]-desplazamiento[0])*escala[0]; // Calcular la X restando al tiempo el desplazamiento y convirtiéndolo a la escala del gráfico con el coeficiente horizontal posicion[1]=alto_caja-(coordenada[contador_valor][1]+desplazamiento[1])*escala[1]; // Calcular la Y restando del alto de la caja (la Y crece hacia abajo en SVG) coordenadas_trazado+=posicion[0]+","+posicion[1]; // Formar la coordenada con la X y la Y if(posicion[0]>0) // Si no se ha rebasado el margen izquierdo… { coordenadas_trazado+=contador_valor>0?" L ":""; // …y quedan valores que represntar, añadir una nueva línea (código L) para el próximo contador_valor--; // Pasar al siguiente valor } else // Si se ha rebasado el margen izquierdo… { sin_recortar=false; // …abandonar el modo sin recorte (lo que terminará de calcular coordenadas) } } if(cerrado) // Si se dibuja un trazado (path) cerrado… { coordenadas_trazado+=" L "+posicion[0]+","+alto_caja+" Z"; // …se termina por la parte inferior de la caja y se añade Z para cerrarlo en SVG } grafico.setAttribute("d",coordenadas_trazado); // Cambiar las coordenadas del trazado (propiedad "d") por las que se han calculado } |
En el código anterior hay dos aspectos interesantes, en primer lugar el cálculo que permite adaptar el rango de valores que se representan y en segundo lugar la construcción de la propiedad d
que indica las coordenadas de los puntos del trazado (path
).
Para adaptar el rango de valores representados se desplazan desde un mínimo y se escalan para que la magnitud visible corresponda con el tamaño del gráfico. En el caso del tiempo, el desplazamiento se obtiene al restar al tiempo mayor (la fecha y la hora más cercana a la actual) el rango que se quiere mostrar (20 segundos en el ejemplo). El desplazamiento de los valores de la temperatura es el del margen inferior (un grado) menos el valor menor, de manera que los datos que se muestran más abajo son los más parecidos al valor menor permitido pero dejando un margen que permite apreciar los que lo rebasen.
El coeficiente que multiplica a los valores de tiempo para obtener las coordenadas horizontales del gráfico se obtiene dividiendo el ancho total del gráfico (100 unidades en el ejemplo) entre el rango de tiempo representado (20 segundos en el ejemplo). Para obtener el coeficiente con el escalar los valores de temperatura hay que recordar que el rango representado va desde un margen por debajo del valor mínimo hasta un margen por encima del máximo, un grado en ambos casos. De esta forma, el coeficiente de escala vertical resulta de dividir el alto del gráfico (100 unidades en el ejemplo), entre el valor máximo, menos el mínimo más el margen superior e inferior. Como estos valores podrían desarrollarse por completo en temperaturas negativas, se utiliza Math.abs()
para usar el valor absoluto de la diferencia.
La propiedad d
del objeto path
se construye concatenando las coordenadas de los puntos en un texto. Cada par de coordenadas va precedida por un código SVG L
, que dibuja una linea desde la posición actual hasta un valor absoluto que es el indicado por las coordenadas. Los valores de X e Y se separan por comas y cada operación SVG se separa por un espacio de la siguiente.
Para empezar el trazado se utiliza el código M
(mover hasta una coordenada absoluta). En el caso del trazado cerrado y relleno se empieza por la parte inferior derecha, en el caso del trazado abierto que dibuja el perfil de los datos se empieza por el último valor representado (el más reciente). Para terminar el trazado cerrado se utiliza el código Z
añadiendo como último punto el que tiene el mismo valor de coordenada X que el último punto de la línea y como coordenada Y el valor menor representado.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
function dibujar_grafico() { var tiempo_mostrado=20000; // Se representan 20 segundos (20000 milisegundos) var valor_maximo=10; // 10 grados sobre cero var valor_minimo=-5; // Cinco grados bajo cero var fecha_hora=Date.now(); // Hora actual var matriz_de_coordenadas_de_prueba=[]; // Preparar el vector de coordenadas for(var contador=0;contador<20;contador++) { fecha_hora+=500+1500*Math.random(); // Añadir medio segundo a la hora anterior y entre 0 y segundo y medio aleatoriamente matriz_de_coordenadas_de_prueba[contador]=[]; // Preparar el siguiente punto del vector de coordenadas matriz_de_coordenadas_de_prueba[contador][0]=fecha_hora; // En la coordenada horizontal, situar la hora matriz_de_coordenadas_de_prueba[contador][1]=Math.random()*Math.abs(valor_maximo-valor_minimo)+valor_minimo; // En la coordenada vertical situar el valor del sensor } actualizar_grafico ( document.getElementById("relleno_temperatura"), // Trazado para el relleno definido en el código HTML matriz_de_coordenadas_de_prueba, tiempo_mostrado, valor_maximo, // Diez grados de valor máximo valor_minimo, // Cinco grados bajo cero como valor mínimo 1, // Un grado por encima y por debajo de las temperatura mínimas y máximas respectivamente true // Cerrar el trazado para representar el área rellena ); actualizar_grafico ( document.getElementById("linea_temperatura"), // Trazado para el relleno definido en el código HTML matriz_de_coordenadas_de_prueba, tiempo_mostrado, valor_maximo, // Diez grados de valor máximo valor_minimo, // Cinco grados bajo cero como valor mínimo 1, // Un grado por encima y por debajo de las temperatura mínimas y máximas respectivamente false // No cerrar el trazado para representar la linea que une los puntos que representan las temperaturas ); } |
En este ejemplo, la función dibujar_grafico()
, que es la llamada al cargar la página, obtiene los valores iniciales para probar (no el último valor en tiempo real) y prepara el rango en el que se representarán los datos: 20 segundos (20000 ms) en horizontal y 15 °C en vertical desde -5 °C hasta +10 °C con un grado de margen superior e inferior. Realiza dos llamadas a actualizar_grafico()
, en la primera pasa true
como argumento, que indica que el gráfico debe cerrarse para representar una zona rellena, y en la segunda llamada pasa false
para dibujar la línea. En cada caso, el objeto path
modificado es el que tiene el aspecto correspondiente, con relleno y sin borde en el primer caso y con cierto grosor de línea y sin relleno en el segundo.
La función actualizar_grafico()
trabaja sobre un objeto SVG que utiliza como contenedor el siguiente código HTML. El objeto SVG contiene dos trazados, uno para dibujar la línea y otro para dibujar la zona rellena. Al cargar la página web, desde el elemento <body>
se llama automáticamente a la función anterior, dibujar_grafico()
gracias al evento JavaScript onload
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<!DOCTYPE html> <html lang="es"> <head> <meta charset="utf-8"> <title>Temperatura</title> <script type="text/javascript" src="grafico.js"></script> </head> <body onload="dibujar_grafico();" style="margin:0;"> <!-- Cuerpo del documento HTML. Al cargar el contenido llama a la función JavaScript dibujar_grafico() --> <div id="temperatura"> <div id="bloque_temperatura" style="width:820px;height:150px"> <svg id="grafico_temperatura" width="100%" height="100%" viewBox="0 0 100 100" preserveAspectRatio="none"> <path id="relleno_temperatura" d="" style="fill:#A8C3EA;stroke:none;" vector-effect="non-scaling-stroke" /> <path id="linea_temperatura" d="" style="fill:none;stroke:#205587;stroke-width:4;stroke-opacity:1;" vector-effect="non-scaling-stroke" /> </svg> </div> </div> </body> </html> |
En la línea 10 del código HTML de arriba se establece un ancho (de ejemplo) de 820 px y un alto de 150 px en el estilo (algo que, en la versión definitiva convendrá hacer con una clase y un documento CSS). Parece raro que en las líneas 13 y 14 se defina el tamaño del objeto SVG como el 100 % de ancho y de alto (lo que casa mejor con las dimensiones de la ventana, 100×100). Como ya se ha mencionado, la razón de hacerlo así es trabajar siempre con unas dimensiones conocidas y ajustar a ella los valores representados. Las otras alternativas serían, calcular el espacio del gráfico cada vez y reajustar después los valores o forzar unas dimensiones fijas para el gráfico, a las que el documento tendrá que ceñirse.
Al haber optado por un gráfico cuyas dimensiones cambian según el código HTML, es necesario incluir la propiedad vector-effect
con el valor non-scaling-stroke
para evitar que los grosores de líneas se deformen cuando el gráfico no mantenga las proporciones elegidas 1:1 en la página web en la que se muestra, como ocurre en la propuesta anterior.
Para «recortar» el gráfico y mostrar solamente la zona que se elija, se utiliza viewBox
. En este caso se ha elegido ver la parte del gráfico que empieza en 0,0 (esquina superior izquierda) y mide 100×100 hacia abajo y hacia la derecha. La parte del dibujo situada en coordenadas con valores negativos o mayores que 100 no se mostrarán en la página web aunque existan en el objeto SVG
Añadir nuevos elementos al dibujo SVG
En el anterior ejemplo, la función actualizar_grafico()
utiliza un trazado SVG al que se le cambia la propiedad d
, que es la que expresa la cadena de coordenadas. La alternativa sería crear el objeto completo cada vez que se redibuja. La ventaja de la primera opción es que el aspecto gráfico (como el grosor o el color) está definido en el código HTML, la limitación es que los objetos deben estar previamente creados.
Para crear los objetos SVG se utiliza createElementNS()
, que permite incluir el espacio de nombres. En el ejemplo de abajo se crea un nuevo objeto texto (text
) y se asocia a un elemento SVG que ya existe en el código HTML de la página web. Una vez creado el nuevo elemento, se asignan sus propiedades con setAttribute()
y se añade al SVG con appendChild()
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
function rotular ( objeto_grafico, texto, inicio=[0,0], altura=10.0, tipo_letra="SircuitoRegularMedium", color_texto="#000000", color_fondo="#FFFFFF" ) { nuevo_objeto_svg=document.createElementNS("http://www.w3.org/2000/svg","text"); nuevo_objeto_svg.setAttribute("x",inicio[0]); nuevo_objeto_svg.setAttribute("y",inicio[1]); nuevo_objeto_svg.setAttribute("font-family",tipo_letra); nuevo_objeto_svg.setAttribute("font-size",altura); nuevo_objeto_svg.setAttribute("fill",color_texto); nuevo_objeto_svg.textContent=texto; objeto_grafico.appendChild(nuevo_objeto_svg); } //rotular(document.getElementById("cosa_svg"),"HOLA",[10,10]); |
Modificar la proporción de los elementos del dibujo
Si has probado a rotular con la función del ejemplo del apartado anterior, habrás visto que el texto aparece deformado cuando la proporción del objeto en la página web (width
y height
del código HTML) no es igual a la de la zona representada (viewBox
). Para adaptar la proporción es necesario conocer las medidas del objeto SVG para lo que se puede consultar el estilo del objeto, o del contenedor HTML, si el objeto SVG cede esta propiedad. Asignando la propiedad transform
a los objetos SVG que dependan de la proporción se puede corregir la deformación aplicando una operación de escala scale()
en la que el coeficiente en X es diferente al de Y.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
function rotular ( objeto_grafico, texto, inicio=[0,0], altura=10.0, proporcion=1.0, tipo_letra="SircuitoRegularMedium", color_texto="#000000", color_fondo="#FFFFFF" ) { var escala_horizontal=parseFloat(getComputedStyle(objeto_grafico).height)/parseFloat(getComputedStyle(objeto_grafico).width); nuevo_objeto_svg=document.createElementNS("http://www.w3.org/2000/svg",'text'); nuevo_objeto_svg.setAttribute("transform","scale("+escala_horizontal+",1.0)"); // scale permite cambiar la escala en X e Y //nuevo_objeto_svg.setAttribute("transform","scaleX("+escala_horizontal+")"); // Como se sabe que sólo cambia la escala en X, se puede usar scaleX nuevo_objeto_svg.setAttribute("x",inicio[0]); nuevo_objeto_svg.setAttribute("y",inicio[1]); nuevo_objeto_svg.setAttribute("font-family",tipo_letra); nuevo_objeto_svg.setAttribute("font-size",altura); nuevo_objeto_svg.setAttribute("fill",color_texto); nuevo_objeto_svg.textContent=texto; objeto_grafico.appendChild(nuevo_objeto_svg); } //rotular(document.getElementById("cosa_svg"),"HOLA",[10,10]); |
SVG permite agrupar varios objetos formando un nuevo elemento compuesto que también soporta propiedades, como los objetos simples. Para aplicar la misma transformación a una serie de objetos a la vez en lugar de a cada objeto por separado se pueden agrupar según este recurso y aplicar una única propiedad transform
a todos ellos.
Como se explicó al hablar del formato SVG, los elementos de un grupo se encierran entre las etiquetas <g>
y </g>
. Para añadir desde JavaScript elementos a un grupo SVG se utiliza, como se ha visto en el ejemplo anterior, appendChild()
una vez definido el nuevo objeto.
Para establecer un origen al aplicar las transformaciones puede utilizarse en los objetos la propiedad SVG transform-origin
, cuyo valor son las coordenadas X e Y del punto desde el que empieza la transformación. Si no se indica expresamente un valor para el origen de la transformación (en el navegador web) se utiliza el centro de coordenadas. Desafortunadamente, en el momento de escribir este texto, la especificación del comportamiento de las transformaciones usando un origen diferente de el usado por defecto no es homogénea en todos los navegadores y debe usarse con cautela.
Junto con la transformación de escala con scale
existen otras, como la rotación con rotation
y el desplazamiento con translate
, que ofrecen una alternativa a la representación del gráfico: en lugar de obtener nuevas coordenadas se pueden representar en su propio espacio y transformar el gráfico para que encajen con el formato en el que se quiere representar.
Añadir referencias al gráfico
Ahora que la parte principal del gráfico está resuelta trazando los valores con un perfil y una zona rellena, se puede completar con referencias que ayuden a su lectura. Como ejemplo, empecemos por dibujar unas referencias horizontales (líneas) que marquen los valores máximos y mínimos aceptables así como un valor deseado. Como se ha explicado, se puede optar por añadir los objetos al SVG directamente desde JavaScript o incluirlos manualmente en el código HTML y modificarlos después con JavaScript.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
var CONTENEDOR_SVG; // Objeto SVG que contiene el gráfico que representa los valores monitorizados por los sensores en la IoT var NS="http://www.w3.org/2000/svg"; // Nombre del espacio de nombres (name space NS) var ANCHO_CAJA=100.0; // Ancho del objeto SVG (aunque al dibujarlo con el código HTML tomará otras dimensiones var ALTO_CAJA=100.0; // Alto del objeto SVG (aunque al dibujarlo con el código HTML tomará otras dimensiones var VALOR_MAXIMO=10.0; // Mayor valor admisible en el gráfico SVG. Los valores mayores se saldrán del gráfico salvo por un margen que permite tener una idea de la tendencia var VALOR_MINIMO=-5.0; // Menor valor admisible en el gráfico SVG. Los valores menores se saldrán del gráfico salvo por un margen que permite tener una idea de la tendencia var VALOR_DESEADO=5.0; // Mejor valor del parámetro medido (temperatura). Sirve para tener una idea rápida de cómo de correcto es el estado del sistema var MARGEN_VALOR=1.0; // Zona por encima del valor mayor y por debajo del valor menor que se representa para tener una idea aproximada de la tendencia cuando los datos monitorizados rebasen los valores máximo y/o mínimo function inicializar_grafico() { CONTENEDOR_SVG=document.getElementById("contenedor_svg"); crear_referencia_horizontal_svg(VALOR_MAXIMO,"#FF0000"); crear_referencia_horizontal_svg(VALOR_DESEADO,"#00FF00"); crear_referencia_horizontal_svg(VALOR_MINIMO,"#0000FF"); } function crear_referencia_horizontal_svg ( altura=0.0, color="#000000", grosor=0.5, opacidad=1.0 ) { var altura_corregida=ALTO_CAJA-(altura+MARGEN_VALOR-VALOR_MINIMO)*ALTO_CAJA/(Math.abs(VALOR_MAXIMO-VALOR_MINIMO)+MARGEN_VALOR*2); var referencia_horizontal=document.createElementNS(NS,'line'); referencia_horizontal.setAttribute("x1",0.0); referencia_horizontal.setAttribute("x2",ANCHO_CAJA); referencia_horizontal.setAttribute("y1",altura_corregida); referencia_horizontal.setAttribute("y2",altura_corregida); referencia_horizontal.style.stroke=color; referencia_horizontal.style.strokeWidth=grosor; referencia_horizontal.style.strokeOpacity=opacidad; CONTENEDOR_SVG.appendChild(referencia_horizontal); } //inicializar_grafico(); |
Parece lógico rotular estas referencias horizontales con un texto que aclare el valor que representan. Para resaltar el texto, se pueden utilizar rectángulos que lo destacarán del fondo y del gráfico. Como a los textos habrá que aplicarles una transformación de escala par compensar la deformación, se pueden agrupar todos en un objeto al que se le aplique la escala; la ventaja principal de hacerlo así es poder modificarlos en una única operación si el contenedor del gráfico (la ventana del navegador) se redimensiona y cambia esa proporción que la escala corrige.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
// Este código de ejemplo usa constantes para hacerlo más legible a desarrolladores de aplicaciones para microcontroladores de series pequeñas. La opción más recomendable, y más propia del estilo JavaScript, es crear un objeto cuyas propiedades serían las constantes y que incluiría los métodos (aquí funciones) que generan o modifican el gráfico o en este caso las referencias var CONTENEDOR_SVG; // Objeto SVG que contiene el gráfico que representa los valores monitorizados por los sensores en la IoT var NS="http://www.w3.org/2000/svg"; // Nombre del espacio de nombres (name space NS) var ANCHO_CAJA=100.0; // Ancho del objeto SVG (aunque al dibujarlo con el código HTML tomará otras dimensiones var ALTO_CAJA=100.0; // Alto del objeto SVG (aunque al dibujarlo con el código HTML tomará otras dimensiones var TIEMPO_REPRESENTADO=30000; // Milisegundos visibles en el gráfico empezando en el valor mayor (fecha y hora del último valor monitorizado) var VALOR_MAXIMO=10.0; // Mayor valor admisible en el gráfico SVG. Los valores mayores se saldrán del gráfico salvo por un margen que permite tener una idea de la tendencia var VALOR_MINIMO=-5.0; // Menor valor admisible en el gráfico SVG. Los valores menores se saldrán del gráfico salvo por un margen que permite tener una idea de la tendencia var VALOR_OPTIMO=5.0; // Mejor valor del parámetro medido (temperatura). Sirve para tener una idea rápida de cómo de correcto es el estado del sistema var MARGEN_VALOR=2.0; // Zona por encima del valor mayor y por debajo del valor menor que se representa para tener una idea aproximada de la tendencia cuando los datos monitorizados rebasen los valores máximo y/o mínimo var desplazamiento_valor=desplazamiento(Date.now()); var escala_valor=escala(); var TIPOGRAFIA="SircuitoRegularMedium"; // Tipografía con la que se rotula todo el gráfico (se usa una constante buscando la uniformidad, pero se puede rotular usando diferentes tipos de letra si es necesario) var ALTURA_TEXTO=10.0; // Altura de los textos en valor absoluto (píxeles) var COLOR_MINIMO="#621D87"; // Color de la referencia que indica el valor mínimo var COLOR_OPTIMO="#1D8762"; // Color de la referencia que indica el valor máximo var COLOR_MAXIMO="#871E35"; // Color de la referencia que indica el valor óptimo var COLOR_TIPOGRAFIA="#A8C3EA"; // Color del tipo de letra con que se rotulan las referencias var GROSOR_REFERENCIA=0.5; // Grosor de la línea que se dibuja como referencia en valor absoluto (píxeles) var OPACIDAD_REFERENCIA=1.0; // Opacidad de la línea de referencia. Si se dibuja sobre el gráfico con cierta transparencia permite ver el dibujo bajo ella var RELLENO_FONDO_REFERENCIA=4.0; // Margen entre el fondo de la referencia y el texto medido en valor absoluto (la línea empieza en el margen izquierdo y recorre todo el gráfico) var MARGEN_FONDO_REFERENCIA=10.0; // Separación de la referencia y el borde izquierdo del gráfico medido en valor absoluto (la línea empieza en el margen izquierdo y recorre todo el gráfico) var ANCHO_FONDO_REFERENCIA=34.0; // Medida horizontal del rectángulo que hace de fondo al texto de la referencia medido en valor absoluto function proporcion_grafico() { return parseFloat(getComputedStyle(CONTENEDOR_SVG).height)/parseFloat(getComputedStyle(CONTENEDOR_SVG).width); // La escala debe calcularse cada vez ya que no se sabe si se ha redimensionado el objeto HTML que contiene al objeto SVG y que le da el tamaño } function medida_grafico() { var proporcion=[]; // Coeficiente que calcula la medida absoluta en píxeles en función del ancho/alto base del gráfico y de la representación en la página web // Coeficiente que calcula la medida absoluta en píxeles en función del ancho/alto base del gráfico y de la representación en la página web proporcion[0]=ANCHO_CAJA/parseFloat(getComputedStyle(CONTENEDOR_SVG).width); proporcion[1]=ALTO_CAJA/parseFloat(getComputedStyle(CONTENEDOR_SVG).height); return proporcion; } function desplazamiento(valor_mayor) { var desplazamiento_valor=[]; desplazamiento_valor[0]=valor_mayor-TIEMPO_REPRESENTADO; // Valor desde el que se empieza a contar el tiempo: el valor mayor (último) menos el rango de tiempo representado desplazamiento_valor[1]=MARGEN_VALOR-VALOR_MINIMO; // Valor menor mostrado (al menor se le añade un margen para visualizar el principio de los valores fuera del rango permitido) return desplazamiento_valor; } function escala() { var escala_valor=[]; escala_valor[0]=ANCHO_CAJA/TIEMPO_REPRESENTADO; // Coeficiente que multiplica a los valores horizontales (tiempo) para calcular las coordenadas X escala_valor[1]=ALTO_CAJA/(Math.abs(VALOR_MAXIMO-VALOR_MINIMO)+MARGEN_VALOR*2); // Coeficiente que multiplica a los valores (vertical) para calcular las coordenadas Y return escala_valor; } function crear_referencia_horizontal_svg ( posicion=0.0, color_dibujo="#000000", grosor_linea=GROSOR_REFERENCIA, opacidad_linea=OPACIDAD_REFERENCIA, color_texto=COLOR_TIPOGRAFIA, altura_texto=ALTURA_TEXTO ) { var proporcion_horizontal=proporcion_grafico(); // La escala debe calcularse cada vez ya que no se sabe si se ha redimensionado el objeto HTML que contiene al objeto SVG y que le da el tamaño var coeficiente_medida=medida_grafico(); var posicion_corregida=ALTO_CAJA-(posicion+desplazamiento_valor[1])*escala_valor[1]; var referencia_horizontal=document.createElementNS(NS,'line'); // El orden en el que se crean los objetos determina qué tapa (lo último) y que es tapado (lo primero) var fondo_referencia=document.createElementNS(NS,'rect'); // El rectángulo (opaco) se creará después de la línea (que puede ser un poco transparente) para definir con claridad el fondo del texto var texto_referencia=document.createElementNS(NS,'text'); // El texto se creará en último lugar para que quede sobre los otros objetos referencia_horizontal.setAttribute("x1",0.0); referencia_horizontal.setAttribute("x2",ANCHO_CAJA); referencia_horizontal.setAttribute("y1",posicion_corregida); referencia_horizontal.setAttribute("y2",posicion_corregida); referencia_horizontal.style.stroke=color_dibujo; referencia_horizontal.style.strokeWidth=grosor_linea*coeficiente_medida[1]; referencia_horizontal.style.strokeOpacity=opacidad_linea; CONTENEDOR_SVG.appendChild(referencia_horizontal); // Añadir la línea de referencia lo más abajo fondo_referencia.setAttribute("x",MARGEN_FONDO_REFERENCIA*coeficiente_medida[0]); fondo_referencia.setAttribute("y",posicion_corregida-(ALTURA_TEXTO+RELLENO_FONDO_REFERENCIA*2.0)*coeficiente_medida[1]/2.0); fondo_referencia.setAttribute("width",ANCHO_FONDO_REFERENCIA*coeficiente_medida[0]); fondo_referencia.setAttribute("height",(ALTURA_TEXTO+RELLENO_FONDO_REFERENCIA*2.0)*coeficiente_medida[1]); fondo_referencia.style.fill=color_dibujo; fondo_referencia.style.fillOpacity=1.0; fondo_referencia.style.strokeWidth=0; CONTENEDOR_SVG.appendChild(fondo_referencia); texto_referencia.setAttribute("x",(MARGEN_FONDO_REFERENCIA+RELLENO_FONDO_REFERENCIA)*coeficiente_medida[0]/proporcion_horizontal); texto_referencia.setAttribute("y",posicion_corregida+ALTURA_TEXTO/2.0*coeficiente_medida[1]); texto_referencia.setAttribute("font-family",TIPOGRAFIA); texto_referencia.setAttribute("font-size",ALTURA_TEXTO*coeficiente_medida[1]); texto_referencia.setAttribute("fill",COLOR_TIPOGRAFIA); texto_referencia.setAttribute("transform","scale("+proporcion_horizontal+",1.0)"); texto_referencia.textContent=(posicion>=0?"+":"")+posicion; CONTENEDOR_SVG.appendChild(texto_referencia); } function inicializar_grafico() { CONTENEDOR_SVG=document.getElementById("contenedor_svg"); crear_referencia_horizontal_svg(VALOR_MAXIMO,COLOR_MAXIMO); crear_referencia_horizontal_svg(VALOR_OPTIMO,COLOR_OPTIMO); crear_referencia_horizontal_svg(VALOR_MINIMO,COLOR_MINIMO); } //inicializar_grafico(); |
Hay varios aspectos interesantes en el anterior código de ejemplo. En primer lugar, comentar que se han usado constantes (variables globales) para que el ejemplo sea más legible a usuarios que llegan desde la programación de microcontroladores en C o en C++. Como se verá más adelante, la forma óptima de programarlo en JavaScript sería usando objetos que contendrían estos valores y los métodos que gestionarían las referencias en este ejemplo o el gráfico, en general, en un sistema en producción.
Por otra parte, avanzando cómo sería el código más genérico, se han desarrollado funciones separadas que calculan los diferentes coeficientes que corrigen la proporción del gráfico para ajustar el texto proporcion_grafico()
, la escala de los valores en función de su rango escala()
y un factor de corrección de las medidas que se conocen en valor absoluto, como las medidas de las referencias medida_grafico()
.
La lectura de este código debería ayudar a aclarar cómo es el contexto en el que funciona una aplicación como esta, que dibuja gráficos en tiempo real y que debe ser flexible para presentarse en varios contextos gráficos (varios tamaños y proporciones, cuando menos). En primer lugar deben generarse los objetos SVG, ya sea «manualmente» en el código HTML, ya sea por medio de código JavaScript y en cualquier caso, posteriormente han de obtenerse referencias a estos objetos para manipularlos desde JavaScript de manera que se puedan dibujar nuevos gráficos y adaptar la representación de un gráfico ya dibujado a un cambio en el medio en el que se presenta.
Otra de las referencias que pueden ayudar interpretar un gráfico fácilmente son los puntos que representan valores concretos (los nodos de la línea). En este ejemplo, en el que representamos una única magnitud, no es crítica la elección de un símbolo pero si se superponen varios valores diferentes para buscar correlación, es interesante distinguir, además de usando otros recursos como el color, dibujando símbolos diferentes. El grafismo que se use para el nodo de la línea deberá modificarse en tamaño y en proporción, como ocurre, por ejemplo, con los textos, para que sus dimensiones sean absolutas y para que su proporciones se mantengan aunque cambien las de la caja que contiene el gráfico.
En el ejemplo anterior ya se vio cómo calcular los diferentes coeficientes para rescalar y corregir la proporción del dibujo; con respecto a cómo implementar la gestión de los símbolos de los nodos o vértices del gráfico, una posible solución puede ser almacenar los objetos SVG en un vector y modificar su posición cuando se actualice el gráfico con la lectura de un nuevo valor, o cuando se redibuje al redimensionar el contenedor. En el primer caso habría que modificar su posición y en el segundo su proporción con la propiedad transform
y el valor de scale
. El siguiente código es una modificación de la función actualizar_grafico()
para incluir el reposicionado de los símbolos de los vértices del gráfico.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
function actualizar_grafico_puntos ( grafico, // Objeto SVG con el que se dibuja la gráfico coordenada, // Matriz con las coordenadas del trazado formada por pares [tiempo,valor] ordenados primero el más antiguo (tiempo menor) último el más nuevo puntos, // Matriz con los objetos SVG que se representan en los nodos de la línea (los valores reales) tiempo_total_representado, // Tiempo total representado por la gráfico (en milisegundos) valor_maximo, // Valor máximo aceptable antes de emitir una alarma valor_minimo, // Valor mínimo aceptable antes de emitir una alarma margen_valor, // Cantidad extra que se representa sobre/bajo el valor máximo/mínimo parametro_cerrado, // Valor booleano que indica si el trazado se cierra o no (por defecto false) parametro_ancho_caja, // Ancho de la caja que contiene la gráfico (por defecto 100.0) parametro_alto_caja // Alto de la caja que contiene la gráfico (por defecto 100.0) ) { var cerrado=parametro_cerrado||false; // Valor booleano que indica si el trazado se cierra o no (por defecto false) var ancho_caja=parametro_ancho_caja||100.0; // Ancho de la caja que contiene la gráfico (por defecto 100.0) var alto_caja=parametro_alto_caja||100.0; // Alto de la caja que contiene la gráfico (por defecto 100.0) var coordenadas_trazado="M "; // Cadena de texto que representa la propiedad "d" del trazado SVG var desplazamiento=[]; // Desplazamientos X e Y para posicionar el tiempo y el valor representado dentro del rango del gráfico var escala=[]; // Coeficientes X e Y para calcular el tamaño al representar el gráfico var sin_recortar=true; // False si la gráfico se sale de la caja (si se sale, si recorta, se hace false para no seguir dibujando puntos) var contador_valor=coordenada.length-1; // Variable para recorrer los valores (índice) var posicion=[]; // Variable intermedia (para hacer más legible el código) con la que calcular la posición horizontal y vertical de un punto del trazado escala[0]=ancho_caja/tiempo_total_representado; // Coeficiente que multiplica a los valores horizontales (tiempo) para calcular las coordenadas X escala[1]=alto_caja/(Math.abs(valor_maximo-valor_minimo)+margen_valor*2); // Coeficiente que multiplica a los valores (vertical) para calcular las coordenadas Y desplazamiento[0]=coordenada[coordenada.length-1][0]-tiempo_total_representado; // Valor desde el que se empieza a contar el tiempo: el valor mayor (último) menos el rango de tiempo representado desplazamiento[1]=margen_valor-valor_minimo; // Valor menor mostrado (al menor se le añade un margen para visualizar el principio de los valores fuera del rango permitido) if(cerrado) // Si se dibuja un path (trazado) cerrado… { coordenadas_trazado+=ancho_caja+","+alto_caja+" L "; // …se empieza por la parte inferior de la caja } while(contador_valor>=0&&sin_recortar) // Mientras queden valores por representar y no se haya llegado al borde izquierdo del gráfico… { posicion[0]=(coordenada[contador_valor][0]-desplazamiento[0])*escala[0]; // Calcular la X restando al tiempo el desplazamiento y convirtiéndolo a la escala del gráfico con el coeficiente horizontal posicion[1]=alto_caja-(coordenada[contador_valor][1]+desplazamiento[1])*escala[1]; // Calcular la Y restando del alto de la caja (la Y crece hacia abajo en SVG) punto[contador_valor].setAttribute("cx",posicion[0]); punto[contador_valor].setAttribute("cy",posicion[1]); coordenadas_trazado+=posicion[0]+","+posicion[1]; // Formar la coordenada con la X y la Y if(posicion[0]>0) // Si no se ha rebasado el margen izquierdo… { coordenadas_trazado+=contador_valor>0?" L ":""; // …y quedan valores que represntar, añadir una nueva línea (código L) para el próximo } else // Si se ha rebasado el margen izquierdo… { sin_recortar=false; // …abandonar el modo sin recorte (lo que terminará de calcular coordenadas) } contador_valor--; // Pasar al siguiente valor } if(cerrado) // Si se dibuja un trazado (path) cerrado… { coordenadas_trazado+=" L "+posicion[0]+","+alto_caja+" Z"; // …se termina por la parte inferior de la caja y se añade Z para cerrarlo en SVG } grafico.setAttribute("d",coordenadas_trazado); // Cambiar las coordenadas del trazado (propiedad "d") por las que se han calculado for(;contador_valor>=0;contador_valor--) { punto[contador_valor].setAttribute("cx",-10000); punto[contador_valor].setAttribute("cy",0); } } |
Las modificaciones realizadas a la función actualizar_grafico()
para obtener la nueva función actualizar_grafico_puntos()
son las que se encuentran resaltadas en el código del ejemplo anterior. En primer lugar, en la línea 5, se toma un vector de objetos SVG como parámetro. Este vector contendrá los símbolos que hay que reposicionar en los nuevos nodos del gráfico.
En las líneas 39 y 40 se asignan las nuevas coordenadas del centro, cx
y cy
, a las de los valores que se están representando. Si el símbolo no estuviera basado en el centro seguramente será necesario añadir un desplazamiento en cx
de la mitad del ancho y en cy
de la mitad del alto para reposicionarlas exactamente en el nodo del gráfico.
En las líneas 57 a 61 se reposicionan fuera del gráfico los puntos que corresponden a coordenadas que no se dibujan por recortarse con el borde izquierdo. Se ha asignado la coordenada de cy
a cero y la de cx
a un número negativo cualquiera (mayor que el propio punto) para que no se muestre al ser recortado, como la parte izquierda del gráfico, por la ventana del SVG.
Gestionar el gráfico desde un objeto con JavaScript
Todas las operaciones que se han explicado hasta ahora pueden integrarse en un objeto para hacer una gestión del gráfico con un estilo más propio de las nuevas versiones de JavaScript. Esta alternativa de implementación tiene la ventaja añadida de simplificar la incorporación de varios gráficos, de diferentes valores, en la misma página web.
Antes de comentar la implementación, revisemos las formas más habituales de crear objetos con JavaScript y alguna de las peculiaridades de las funciones que afectan a la propuesta de dibujo de gráficos de sensores IoT.
Ya se explicó que la nueva forma de crear objetos en JavaScript (disponible desde la versión 5 de ECMAScript) consiste en usar Object.create
, que conviene acostumbrarse a usar en lugar de la «clásica» new
, que por supuesto sigue funcionando correctamente, aunque su finalidad es más simular el estilo de los lenguajes con objetos basados en clases (JavaScript basa la creación de objetos en prototipos) que una alternativa de trabajo.
1 2 3 4 5 6 7 8 9 10 |
<!DOCTYPE html> <html lang="es"> <head> <meta charset="utf-8"> <title>Crear objetos con new o con Objetct.create</title> <script type="text/javascript" src="crear_objetos.js"></script> </head> <body onload="empezar();"> </body> </html> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
function Objeto_clasico(primero,segundo) { this.primero=primero||1; this.segundo=segundo||2; this.hacer_algo=function() { console.log("El objeto clásico le saluda (primero="+this.primero+" y segundo="+this.segundo+")"); } } var Objeto_ES5= { primero:10, segundo:20, hacer_algo: function() { console.log("El objeto ES5 le saluda (primero="+this.primero+" y segundo="+this.segundo+")"); } } var objeto_clasico=new Objeto_clasico(); var objeto_ES5=Object.create(Objeto_ES5); function empezar() { console.log("Objeto clásico ("+objeto_clasico.primero+","+objeto_clasico.segundo+")"); console.log("Objeto ES5 ("+objeto_ES5.primero+","+objeto_ES5.segundo+")"); objeto_clasico.hacer_algo(); objeto_ES5.hacer_algo(); } |
El código anterior permite recordar las diferencias entre crear los objetos con Object.create
o con new
. También sirve para subrayar que, mientras que la función con la que se crea el objeto con new
puede estar en cualquier parte del código, el objeto ya debe existir antes de poder crear una instancia con Object.create
(objeto Objeto_ES5 no es una función).
En las líneas 3 y 4, para establecer un valor por defecto a las propiedades en la función que crea el objeto con new
, se asigna cada propiedad al valor del argumento correspondiente o (||
), si no se han pasado argumentos, es decir, si están sin definir (undefined
), como esa circunstancia se evalúa como false
, se asigna el valor previsto por omisión.
El contexto en el que se ejecuta una función en JavaScript plantea dos cuestiones que es importante tener presentes y que también pueden resultar confusas cuando se usa este lenguaje de programación después de haber trabajado con otros, como C o C++, en nuestro caso. El contexto incluye las variables definidas en el ámbito de la función (y las globales) lo que, por cierto, plantea un interesante concepto, las «closures» que establece todo un estilo de programación en JavaScript. Dicho lo anterior, podría esperarse que this
, que hace referencia al objeto cuando se usa dentro del código que lo define, se mantuviera el contexto de ejecución en el que se ha definido pero el que utiliza es el contexto desde el cual que se llama a la función. Este comportamiento es transparente en la mayoría de los casos pero hay dos circunstancias en las que puede confundir: una función definida dentro de otra y un método llamado desde un evento del objeto window
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
var primero="Primero global"; var segundo="Segundo global"; var Contexto= { primero:"Primero en contexto", segundo:"Segundo en contexto", probar: function() { console.log("Primero en contexto: "+this.primero); console.log("Segundo en contexto: "+this.segundo); function probar_dentro() { console.log("Primero dentro: "+this.primero); console.log("Segundo dentro: "+this.segundo); } probar_dentro(); } } var probador=Object.create(Contexto); probador.probar(); /* Primero en contexto: Primero en contexto Segundo en contexto: Segundo en contexto Primero dentro: Primero global Segundo dentro: Segundo global */ |
Al ejecutar el código anterior se muestra en la consola el texto comentado al final. Las dos líneas marcadas reflejan el comportamiento que puede ser confuso: el contexto de ejecución de la función probar_dentro()
no es probar()
, como podría esperarse, sino window
, con lo que se muestran las variables globales y no las propiedades de igual nombre. Si no se desea ese comportamiento basta con crear una variable en la función del nivel más alto y se asigna a this
, como en el siguiente código.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
var primero="Primero global"; var segundo="Segundo global"; var Contexto= { primero:"Primero en contexto", segundo:"Segundo en contexto", probar: function() { var esto=this; console.log("Primero en contexto: "+esto.primero); console.log("Segundo en contexto: "+esto.segundo); function probar_dentro() { console.log("Primero dentro: "+esto.primero); console.log("Segundo dentro: "+esto.segundo); } probar_dentro(); } } var probador=Object.create(Contexto); probador.probar(); /* Primero en contexto: Primero en contexto Segundo en contexto: Segundo en contexto Primero dentro: Primero en contexto Segundo dentro: Segundo en contexto */ |
Para controlar el contexto de ejecución cuando se llama un método desde un evento de window
, por ejemplo al redimensionar la ventana del navegador, se puede explotar otra peculiaridad de JavaScript: la posibilidad de programar «fábricas de funciones», es decir, funciones que generan otras funciones, devolviéndolas con return
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var Contexto= { prueba:"objeto", // La propiedad prueba es de los objetos Contexto llamar: function() { esto=this; return function() { console.log("H + "+(Date.now()-hora)+" Contexto: "+esto.prueba); }; } } var prueba="global"; // Esta variable prueba es global (window.prueba) var hora=Date.now(); var probador=Object.create(Contexto); var cosa=probador.llamar(); var pesadez=setInterval(cosa,3000); setTimeout(function(){clearInterval(pesadez)},30100); // Desactivar la llamada periódica |
En el código del ejemplo anterior, el método llamar()
de los objetos Contexto
no realiza el trabajo sino que devuelve una función anónima que se encarga de hacerlo. Para verificar que todo funciona como se espera, hay una variable global de igual nombre que la propiedad que la función muestra en la consola; si el contexto es el correcto se mostrará el valor de la propiedad y no el de la variable global.
JavaScript intenta corregir los signos de punto y coma que omitamos al final de las sentencias. Esto permite un estilo de escritura relajado pero es un arma de doble filo que hay que tratar con cuidado. En la mayoría de los casos, para evitar los efectos indeseables que esto produce en expresiones que ocupan varias líneas se pueden usar paréntesis o adelantarse a la forma en que JavaScript interpretará el código; esa es la razón por la que la línea 8 del ejemplo incluye function
detrás de return
, si hubiera usado otra línea el significado sería muy distinto. A mi criterio, la solución más legible es usar una variable intermedia (prescindible) como en la siguiente versión; obviamente, una vez entendido el comportamiento, la decisión corresponde al programador.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
var Contexto= { prueba:"objeto", // La propiedad prueba es de los objetos Contexto llamar: function() { esto=this; var variable_auxiliar= function() { console.log("H + "+(Date.now()-hora)+" Contexto: "+esto.prueba); }; return variable_auxiliar; } } var prueba="global"; // Esta variable prueba es global (window.prueba) var hora=Date.now(); var probador=Object.create(Contexto); var cosa=probador.llamar(); var pesadez=setInterval(cosa,3000); setTimeout(function(){clearInterval(pesadez)},30100); |
En el mismo sentido de evaluar una expresión como una función, es decir, devolver una función y no el valor que la función devuelve; en la línea 21 del último ejemplo (quedó en la 19 del anterior) se detiene con clearInterval
la función llamada con setInterval
. Para que actúe durante 30 segundos se difiere la detención con setTimeout
, que a su vez necesita una función como primer argumento; para entregar como parámetro la ejecución clearInterval
con la variable que contiene a la llamada periódica (y no la función clearInterval
) es para lo que se crea la función anónima de la última línea.
La elección entre escribir el código integrando la definición de la función, más compacto (como en la línea 21) o usar una variable auxiliar, a mi juicio, más legible (como en las líneas 19 y 20) varía poco en el rendimiento y depende más de estilo y de legibilidad para su mantenimiento.
Para hacer pruebas con el código, antes de tener datos en el servidor, puede usarse un generador de valores aleatorios en el rango buscado o preparar tablas con valores controlados que se simulen el funcionamiento en las condiciones buscada. En el siguiente ejemplo se usa un sencillo generador de datos en todo el rango, por eso aparecen un poco exagerados.
Para hacer pruebas, puedes descargar el código completo del ejemplo formado por una página web escrita en HTML, el estilo CSS y el código JavaScript. Esto último es lo más relevante, ya que los otros componentes son sólo un soporte mínimo, muy simplificado y se encuentra mucho más desarrollado en los artículos de los apartados correspondientes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<!DOCTYPE html> <html lang="es"> <head> <meta charset="utf-8"> <title>Temperatura</title> <script type="text/javascript" src="grafico.js"></script> <link rel="stylesheet" href="estilo.css" type="text/css" media="all"> </head> <body onload="iniciar_grafico('grafico','valor','fecha');"> <div id="temperatura"> <div id="ultimo_valor"> <div id="fecha"></div> <div id="valor"></div> </div> <div id="grafico"></div> </div> </body> </html> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
body { margin:0px 0px 0px 0px; } #ultimo_valor { box-sizing:border-box; width:100%; height:40px; padding:8px 0px 8px 14px; font-family:monospaced; font-size:18px; color:#205587; background-color:#86A7D0; } #fecha { float:left; margin-right:15px; } #valor { float:left; } #grafico { width:100%; height:200px; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 |
var Grafico= { contenedor_svg:void(0), // Objeto SVG con el que se dibuja el gráfico. Se inicializa con void(0) para asegurarse de que es === undefined al principio contenedor_html:void(0), // Objeto HTML que contiene al objeto SVG con el gráfico (seguramente un div). Se inicializa con void(0) para asegurarse de que es === undefined al principio contenedor_valor:void(0), // Objeto HTML en el que se muestra el último valor cargado del servidor contenedor_fecha:void(0), // Objeto HTML para mostrar la fecha del último valor pagina_servidor:"ultimo_valor_sensor.php", // página en la que se consultan los datos consulta:"fr1", // Valor del parámetro con el que se consulta el servidor. La URL sería algo como http://servidoriot.com/ultimo_valor_sensor.php?zona=fr1 NS:"http://www.w3.org/2000/svg", // Nombre del espacio de nombres (name space NS) MINIMO:0, // Índice del menor valor admisible en el gráfico SVG. Los valores menores se saldrán del gráfico salvo por un margen que permite tener una idea de la tendencia OPTIMO:1, // Índice del mejor valor del parámetro medido. Valor deseado para el parámetro monitorizado. Sirve para tener una idea rápida de cómo de correcto es el estado del sistema MAXIMO:2, // Índice del mayor valor admisible en el gráfico SVG. Los valores mayores se saldrán del gráfico salvo por un margen que permite tener una idea de la tendencia maximo_puntos:0, // Número máximo de vértices representados (pueden dibujarse menos si se recortan por la parte izquierda) valor:[], // Últimos valores cargados del servidor trazado:[], // Trazados representados (en principio se trata de representar uno con relleno y otro con línea) cerrado:true, // Cuando vale true se dibuja el perfil y el área (la línea del gráfico y una zona rellena debajo). Cuando vale false solo se dibuja la línea simbolo:[], // Objetos SVG utilizados para marcar los vértices de la línea (círculos) linea_referencia:[], // Líneas horizontales en los valores de referencia texto_referencia:[], // Objetos texto que rotulan los valores de referencia fondo_referencia:[], // Rectángulos bajo los textos de los valores de referencia medida_caja_svg:[100.0,100.0], // Ancho,Ancho del objeto SVG (aunque al dibujarlo con el código HTML tomará otras dimensiones ya que se dimensiona al 100% de su contenedor correccion_caja_svg:[1.0,1.0], // Coeficientes para calcular la medida en el gráfico de un objeto del que se sabe el valor absoluto (Se usa el píxel como unidad) tiempo_representado:0, // Milisegundos visibles en el gráfico empezando en el valor mayor (fecha y hora del último valor monitorizado) valor_referencia:[0,0,0], // Valores de referencia [mínimo,óptimo,máximo] margen_valor:0, // Zona por encima del valor mayor y por debajo del valor menor que se representa para tener una idea aproximada de la tendencia cuando los datos monitorizados rebasen los valores máximo y/o mínimo escala_valor:[1.0,1.0], // Coeficientes que multiplican el valor monitorizado (y almacenado en el servidor) para calcular el representado según las dimensiones del gráfico desplazamiento_valor:[0.0,0.0], // Desplazamiento que se suma al tiempo (X) y al valor (Y) para representarlo en el rango especificado para el gráfico area_bajo_linea:true, // True para dibujar una zona rellena además del perfil (línea) del gráfico medida_simbolo:[5,5], // Valor absoluto (en píxeles) del ancho y el alto del dibujo que se hace en los vértices de la línea del gráfico tipografia:"sans-serif", // Tipo de letra usado para rotular el gráfico (se usa solamente una tipografía por uniformidad) altura_texto:10.0, // Altura de los textos en valor absoluto (píxeles). No todos los navegadores soportan todos los valores intermedios, será necesario coordinarlo con los valores que estén relacionados escala_horizontal_texto:1, // Transformación que se aplica al texto para corregir la que implica la proporción del gráfico color_fondo:"", // Color de fondo del contenedor HTML del objeto SVG (Si es una cadena vacía no se cambia) color_simbolos:"#000000", // Color de los símbolos que se dibujan en los vértices de la línea del gráfico color_referencia:["#000000","#000000","#000000"], // Color de la referencia que indica el valor [mínimo,óptimo,máximo] color_tipografia:"#FFFFFF", // Color del tipo de letra con que se rotulan las referencias grosor_trazado:4, // Grosor de la línea del gráfico color_trazado:"#000000", // Color de la línea del gráfico color_relleno:"#000000", // Color del relleno del gráfico opacidad_relleno:1.0, // Opacidad del relleno del gráfico grosor_referencia:0.5, // Grosor de la línea que se dibuja como referencia en valor absoluto (píxeles) opacidad_referencia:1.0, // Opacidad de la línea de referencia. Si se dibuja sobre el gráfico con cierta transparencia permite ver el dibujo bajo ella relleno_fondo_referencia:7.0, // Margen entre el fondo de la referencia y el texto medido en valor absoluto (la línea empieza en el margen izquierdo y recorre todo el gráfico) margen_fondo_referencia:10.0, // Separación de la referencia y el borde izquierdo del gráfico medido en valor absoluto (la línea empieza en el margen izquierdo y recorre todo el gráfico) ancho_fondo_referencia:50.0, // Medida horizontal del rectángulo que hace de fondo al texto de la referencia medido en valor absoluto texto_valor:["temperatura: "," °C"], // Prefijo y sufico con los que se rotula el valor monitorizado texto_hora:["hora ",""], // Prefijo y sufijo con los que se rotula la hora (cuando solamente se rotula la hora) texto_fecha:["",""], // Prefijo y sufijo con los que se rotula la fecha (cuando solamente se rotula la fecha) texto_fecha_hora:["el "," a las "," | "], // Prefijo con el que se rotula la hora, separador de fecha y hora y sufjo de la hora cuando se rotula la fecha y la hora HORA:0, // Constante que representa la hora para el modo de la fecha/hora FECHA:1, // Constante que representa la fehca para el modo de la fecha/hora FECHA_Y_HORA:2, // Constante que representa rotular la fecha y la hora (para el modo de fecha/hora) modo_fecha:2, // 0->hora, 1-> fecha 2-> fecha y hora repetir:null, // Función llamada por setInterval nueva_proporcion: // Calcular la nueva proporción (al iniciar y al redimensionar el contenedor del gráfico) para transformar el texto function() { if(this.contenedor_svg!==undefined) // Se entiende que si no es undefined se ha asignado un objeto { if(this.contenedor_svg.nodeName==="svg") // Si el objeto asignado es un SVG { this.escala_horizontal_texto=parseFloat(getComputedStyle(this.contenedor_svg).height)/parseFloat(getComputedStyle(this.contenedor_svg).width); // La escala debe calcularse cada vez ya que no se sabe si se ha redimensionado el objeto HTML que contiene al objeto SVG y que le da el tamaño } } }, nueva_correccion_caja_svg: function() { if(this.contenedor_svg!==undefined) // Se entiende que si no es undefined se ha asignado un objeto { if(this.contenedor_svg.nodeName.toLowerCase()==="svg") // Si el objeto asignado es un SVG { this.correccion_caja_svg[0]=this.medida_caja_svg[0]/parseFloat(getComputedStyle(this.contenedor_svg).width); this.correccion_caja_svg[1]=this.medida_caja_svg[1]/parseFloat(getComputedStyle(this.contenedor_svg).height); } } }, nueva_escala: function() { if(this.tiempo_representado>0) // Antes de inicializar el objeto el tiempo representado es cero { this.escala_valor[0]=this.medida_caja_svg[0]/this.tiempo_representado; // Coeficiente que multiplica a los valores horizontales (tiempo) para calcular las coordenadas X this.escala_valor[1]=this.medida_caja_svg[1]/(Math.abs(this.valor_referencia[this.MAXIMO]-this.valor_referencia[this.MINIMO])+this.margen_valor*2); // Coeficiente que multiplica a los valores (vertical) para calcular las coordenadas Y } }, nuevo_desplazamiento: function() { if(this.tiempo_representado>0) // Antes de inicializar el objeto el tiempo representado es cero { this.desplazamiento_valor[0]=this.valor[this.valor.length-1][0]-this.tiempo_representado; // Valor desde el que se empieza a contar el tiempo: el valor mayor (último) menos el rango de tiempo representado this.desplazamiento_valor[1]=this.margen_valor-this.valor_referencia[this.MINIMO]; // Valor menor mostrado (al menor se le añade un margen para visualizar el principio de los valores fuera del rango permitido) } }, crear_svg: // Crear el objeto SVG dentro del objeto HTML function() { if(this.contenedor_html!==null&&this.contenedor_html!==undefined) // Ya se ha asignado el objeto HTML { if(this.contenedor_html.nodeType===1) // Es un objeto HTML válido { // Color de fondo del objeto HTML if(this.color_fondo!=="") { this.contenedor_html.style.backgroundColor=this.color_fondo; } // Contenedor SVG this.contenedor_svg=document.createElementNS(this.NS,"svg"); // Crear un objeto SVG this.contenedor_svg.setAttribute("width","100%"); // Ancho del objeto SVG this.contenedor_svg.setAttribute("height","100%"); // Alto del objeto SVG this.contenedor_svg.setAttribute("viewBox","0 0 "+this.medida_caja_svg[0]+" "+this.medida_caja_svg[1]); // Zona del SVG que se muestra this.contenedor_svg.setAttribute("preserveAspectRatio","none"); // No preservar la proporción para ocupar todo el objeto HTML que hace de contenedor this.contenedor_html.appendChild(this.contenedor_svg); // Añadir al contenedor HTML el objeto SVG (que será el contenedor del gráfico) // Trazado this.trazado[0]=document.createElementNS(this.NS,"path"); // Crear el trazado que soporta el relleno this.trazado[0].style.fill=this.color_relleno; this.trazado[0].style.fillOpacity=this.opacidad_relleno; this.trazado[0].style.stroke="none"; this.contenedor_svg.appendChild(this.trazado[0]); // Añadir el relleno al SVG this.trazado[1]=document.createElementNS(this.NS,"path"); // Crear el trazado que soporta el perfil this.trazado[1].setAttribute("vector-effect","non-scaling-stroke"); // No mantener la proporción en el trazado (no deformarlo) this.trazado[1].style.fill="none"; this.trazado[1].style.stroke=this.color_trazado; this.trazado[1].style.strokeWidth=this.grosor_trazado; //this.trazado[1].style.strokeOpacity=1.0; // La opacidad por defecto es 1.0 this.contenedor_svg.appendChild(this.trazado[1]); // Añadir el perfil al SVG // Símbolos para los vértices var ahora=Date.now()-30000; // Fecha y hora actual menos 30 segundos for(var contador_vertices=0;contador_vertices<this.maximo_puntos;contador_vertices++) { this.valor[contador_vertices]=[]; this.valor[contador_vertices][0]=ahora; // Inicializar los valores a la fecha y hora actual menos 30 segundos this.valor[contador_vertices][1]=this.valor_referencia[this.OPTIMO]; // Inicializar los valores al óptimo this.simbolo[contador_vertices]=document.createElementNS(this.NS,"ellipse"); // Crear una elipse (círculo) para cada vértice this.simbolo[contador_vertices].style.fill=this.color_simbolos; this.simbolo[contador_vertices].style.fillOpacity=1.0; this.simbolo[contador_vertices].style.stroke="none"; this.contenedor_svg.appendChild(this.simbolo[contador_vertices]); // Añadir el símbolo al SVG } //this.nuevo_desplazamiento(); // Necesario si se asignaran las alturas de las referencias for(var contador_referencia=0;contador_referencia<this.valor_referencia.length;contador_referencia++) { // Línea de referencia //var posicion_corregida=this.medida_caja_svg[1]-(this.valor_referencia[contador_referencia]+this.desplazamiento_valor[1])*this.escala_valor[1]; // Necesario si se asignaran las alturas de las referencias this.linea_referencia[contador_referencia]=document.createElementNS(this.NS,"line"); // Crear la línea que representa el valor mínimo this.linea_referencia[contador_referencia].style.stroke=this.color_referencia[contador_referencia]; this.linea_referencia[contador_referencia].style.strokeWidth=this.grosor_referencia; this.linea_referencia[contador_referencia].style.strokeOpacity=this.opacidad_referencia; this.linea_referencia[contador_referencia].setAttribute("x1",0.0); // Las líneas de referencia empiezan en el borde izquierdo… this.linea_referencia[contador_referencia].setAttribute("x2",this.medida_caja_svg[0]); // …y terminan en el derecho //this.linea_referencia[contador_referencia].setAttribute("y1",posicion_corregida); // Ambos extremos a la altura correspondiente a la referencia [mínimo,óptimo,máximo] //this.linea_referencia[contador_referencia].setAttribute("y2",posicion_corregida); // Ambos extremos a la altura correspondiente a la referencia [mínimo,óptimo,máximo] this.contenedor_svg.appendChild(this.linea_referencia[contador_referencia]); // Rectángulo de fondo del texto que indica el valor de referencia this.fondo_referencia[contador_referencia]=document.createElementNS(this.NS,'rect'); this.fondo_referencia[contador_referencia].style.fill=this.color_referencia[contador_referencia]; this.fondo_referencia[contador_referencia].style.fillOpacity=1.0; this.fondo_referencia[contador_referencia].style.stroke="none"; this.contenedor_svg.appendChild(this.fondo_referencia[contador_referencia]); // Texto de referencia this.texto_referencia[contador_referencia]=document.createElementNS(this.NS,"text"); // Crear el texto para rotular la referencia this.texto_referencia[contador_referencia].setAttribute("font-family",this.tipografia); // Asignar el tipo de letra this.texto_referencia[contador_referencia].setAttribute("fill",this.color_tipografia); // Asignar el color //this.texto_referencia[contador_referencia].textContent=""+(this.valor_referencia[contador_referencia]>=0?"+":"")+this.valor_referencia[contador_referencia]; // Texto que se rotula (los valores máximo, óptimo y mínimo ya deben estar asignados) this.contenedor_svg.appendChild(this.texto_referencia[contador_referencia]); } } } }, ajustar_prporcion: function() { this.nueva_proporcion(); this.nueva_correccion_caja_svg(); var posicion_corregida; for(var contador_vertices=0;contador_vertices<this.simbolo.length;contador_vertices++) { this.simbolo[contador_vertices].setAttribute("rx",this.medida_simbolo[0]*this.correccion_caja_svg[0]); this.simbolo[contador_vertices].setAttribute("ry",this.medida_simbolo[1]*this.correccion_caja_svg[1]); } for(var contador_referencia=0;contador_referencia<this.valor_referencia.length;contador_referencia++) { posicion_corregida=this.medida_caja_svg[1]-(this.valor_referencia[contador_referencia]+this.desplazamiento_valor[1])*this.escala_valor[1]; this.fondo_referencia[contador_referencia].setAttribute("x",this.margen_fondo_referencia*this.correccion_caja_svg[0]); this.fondo_referencia[contador_referencia].setAttribute("y",posicion_corregida-(this.altura_texto+this.relleno_fondo_referencia*2.0)*this.correccion_caja_svg[1]/2.0); this.fondo_referencia[contador_referencia].setAttribute("width",this.ancho_fondo_referencia*this.correccion_caja_svg[0]); this.fondo_referencia[contador_referencia].setAttribute("height",(this.altura_texto+this.relleno_fondo_referencia*2.0)*this.correccion_caja_svg[1]); this.linea_referencia[contador_referencia].setAttribute("y1",posicion_corregida); this.linea_referencia[contador_referencia].setAttribute("y2",posicion_corregida); this.texto_referencia[contador_referencia].setAttribute("transform","scale("+this.escala_horizontal_texto+",1.0)"); this.texto_referencia[contador_referencia].setAttribute("font-size",this.altura_texto*this.correccion_caja_svg[1]); this.texto_referencia[contador_referencia].setAttribute("x",(this.margen_fondo_referencia+this.relleno_fondo_referencia)*this.correccion_caja_svg[0]/this.escala_horizontal_texto); this.texto_referencia[contador_referencia].setAttribute("y",posicion_corregida+this.altura_texto/2.0*this.correccion_caja_svg[1]); this.texto_referencia[contador_referencia].textContent=(this.valor_referencia[contador_referencia]>=0?"+":"")+this.valor_referencia[contador_referencia]; // Texto que se rotula } }, rotar_valores: function() { for(var contador_valor=0;contador_valor<this.valor.length-1;contador_valor++) { this.valor[contador_valor][0]=this.valor[contador_valor+1][0]; this.valor[contador_valor][1]=this.valor[contador_valor+1][1]; } }, nuevo_valor_aleatorio: function() { this.rotar_valores(); this.valor[this.valor.length-1][0]=this.valor[this.valor.length-2][0]+1000+Math.random()*1000; // El nuevo tiempo es el anterior más un segundo más un valor entre 0 y un segundo this.valor[this.valor.length-1][1]=Math.random()*Math.abs(this.valor_referencia[this.MAXIMO]-this.valor_referencia[this.MINIMO])+this.valor_referencia[this.MINIMO]; // Un valor aleatorio entre el máximo y el mínimo this.nuevo_desplazamiento(); // Cada nuevo valor cambia el desplazamiento en horizontal this.dibujar_nuevo_valor(true); this.dibujar_nuevo_valor(false); this.rotular_nuevo_valor(); this.ritular_nueva_fecha(); }, cargar_nuevo_valor: function() { var consulta='zona='+this.consulta; var resultado; var ajax; if(window.XMLHttpRequest) { ajax=new XMLHttpRequest(); // ajax=Object.create(XMLHttpRequest); } else // Versiones antiguas de MS Internet Explorer { ajax=new ActiveXObject("Microsoft.XMLHTTP"); } ajax.onreadystatechange= function() { if(ajax.readyState==4&&ajax.status==200&&ajax.responseType=="json") { resultado=JSON.parse(ajax.responseText); if(resultado.fecha>objeto_grafico.fecha[objeto_grafico.fecha.length-1]) { this.rotar_valores(); this.valor[this.valor.length-1][0]=resultado.fecha; this.valor[this.valor.length-1][1]=resultado.temperatura; this.nuevo_desplazamiento(); // Cada nuevo valor cambia el desplazamiento en horizontal this.dibujar_nuevo_valor(true); this.dibujar_nuevo_valor(false); this.rotular_nuevo_valor(); this.ritular_nueva_fecha(); } } } ajax.open("POST",this.pagina_servidor); ajax.setRequestHeader("Method","POST "+this.pagina_servidor+" HTTP/1.1"); ajax.setRequestHeader("Content-type","application/x-www-form-urlencoded"); ajax.setRequestHeader("Content-length",consulta.length); ajax.setRequestHeader("Connection","close"); ajax.send(consulta); }, rotular_nuevo_valor: function() { var valor_redondeado=Math.round(this.valor[this.valor.length-1][1]*100.0)/100.0; this.contenedor_valor.innerHTML=this.texto_valor[0]+valor_redondeado+this.texto_valor[1]; }, ritular_nueva_fecha: function() { var fecha_lectura=new Date(this.valor[this.valor.length-1][0]); var texto_fecha=this.texto_hora[0]; texto_fecha+=(fecha_lectura.getHours()<10?"0":"")+fecha_lectura.getHours(); texto_fecha+=":"; texto_fecha+=(fecha_lectura.getMinutes()<10?"0":"")+fecha_lectura.getMinutes(); texto_fecha+=":"; texto_fecha+=(fecha_lectura.getSeconds()<10?"0":"")+fecha_lectura.getSeconds(); texto_fecha+=this.texto_hora[1]; this.contenedor_fecha.innerHTML=texto_fecha; }, dibujar_nuevo_valor: // Dibujar el gráfico cuando llega un nuevo valor (que estará almacenado en el vector valor function(cerrado) { // Si cerrado es undefined se evalúa a false, que se toma como valor por defecto var contador_valor=this.valor.length-1; // Variable para recorrer los valores (índice) var sin_recortar=true; // False si la gráfico se sale de la caja (si se sale, si recorta, se hace false para no seguir dibujando puntos) var posicion=[]; // Variable intermedia (para hacer más legible el código) con la que calcular la posición horizontal y vertical de un punto del trazado var coordenadas_trazado="M "; // Cadena de texto que representa la propiedad "d" del trazado SVG if(cerrado) // Si el trazado que se está dibujando está cerrado (no confundir con this.cerrado que determina si se dibujan los dos trazados, la línea y el relleno) { coordenadas_trazado+=this.medida_caja_svg[0]+","+this.medida_caja_svg[1]+" L "; // …se empieza por la parte inferior de la caja } while(contador_valor>=0&&sin_recortar) // Mientras queden valores por representar y no se haya llegado al borde izquierdo del gráfico… { posicion[0]=(this.valor[contador_valor][0]-this.desplazamiento_valor[0])*this.escala_valor[0]; // Calcular la X restando al tiempo el desplazamiento y convirtiéndolo a la escala del gráfico con el coeficiente horizontal posicion[1]=this.medida_caja_svg[1]-(this.valor[contador_valor][1]+this.desplazamiento_valor[1])*this.escala_valor[1]; // Calcular la Y restando del alto de la caja (la Y crece hacia abajo en SVG) this.simbolo[contador_valor].setAttribute("cx",posicion[0]); this.simbolo[contador_valor].setAttribute("cy",posicion[1]); coordenadas_trazado+=posicion[0]+","+posicion[1]; // Formar la coordenada con la X y la Y if(posicion[0]>0) // Si no se ha rebasado el margen izquierdo… { coordenadas_trazado+=contador_valor>0?" L ":""; // …y quedan valores que representar, añadir una nueva línea (código L) para el próximo contador_valor--; // Pasar al siguiente valor } else // Si se ha rebasado el margen izquierdo… { sin_recortar=false; // …abandonar el modo sin recorte (lo que terminará de calcular coordenadas) } } if(cerrado) // Si se dibuja un trazado (path) cerrado… { coordenadas_trazado+=" L "+posicion[0]+","+this.medida_caja_svg[1]+" Z"; // …se termina por la parte inferior de la caja y se añade Z para cerrarlo en SVG } this.trazado[cerrado?0:1].setAttribute("d",coordenadas_trazado); // Cambiar las coordenadas del trazado (propiedad "d") por las que se han calculado for(;contador_valor>=0;contador_valor--) { this.simbolo[contador_valor].setAttribute("cx",-10000); this.simbolo[contador_valor].setAttribute("cy",0); } } } var temperatura_frigorifico; function iniciar_grafico(nombre_objeto_grafico,nombre_objeto_valor,nombre_objeto_fecha) { temperatura_frigorifico=Object.create(Grafico); temperatura_frigorifico.contenedor_html=document.getElementById(nombre_objeto_grafico); temperatura_frigorifico.contenedor_valor=document.getElementById(nombre_objeto_valor); temperatura_frigorifico.contenedor_fecha=document.getElementById(nombre_objeto_fecha); temperatura_frigorifico.color_fondo="#A8C3EA"; temperatura_frigorifico.color_trazado="#205587"; temperatura_frigorifico.grosor_trazado=3; temperatura_frigorifico.color_relleno="#205587"; temperatura_frigorifico.opacidad_relleno=0.5; temperatura_frigorifico.color_simbolos="#205587"; temperatura_frigorifico.color_referencia[Grafico.MINIMO]="#621D87"; temperatura_frigorifico.color_referencia[Grafico.OPTIMO]="#1D8762"; temperatura_frigorifico.color_referencia[Grafico.MAXIMO]="#871E35"; temperatura_frigorifico.grosor_referencia=1.5; temperatura_frigorifico.opacidad_referencia=0.5; temperatura_frigorifico.tipografia="monospaced"; temperatura_frigorifico.color_tipografia="#A8C3EA"; temperatura_frigorifico.cerrado=true; // Dibujar una línea y una zona rellena debajo (true por defecto) temperatura_frigorifico.maximo_puntos=32; // Un espacio de 30 segundos representados con un intervalo mínimo de 1 segundo necesitan como máximo 31 puntos, se añade uno por seguridad (por si algún valor monitorizado estuviera por debajo del segundo) temperatura_frigorifico.crear_svg(); temperatura_frigorifico.nueva_correccion_caja_svg(); temperatura_frigorifico.tiempo_representado=30000; temperatura_frigorifico.valor_referencia[Grafico.MINIMO]=-5; temperatura_frigorifico.valor_referencia[Grafico.OPTIMO]=5; temperatura_frigorifico.valor_referencia[Grafico.MAXIMO]=10; temperatura_frigorifico.margen_valor=3; temperatura_frigorifico.nueva_escala(); temperatura_frigorifico.nuevo_desplazamiento(); // El desplazamiento se (re)calcula cada vez que se añade un valor temperatura_frigorifico.ajustar_prporcion(); window.addEventListener("resize",function(){temperatura_frigorifico.ajustar_prporcion()}); // Versión moderna temperatura_frigorifico.repetir=setInterval(function(){temperatura_frigorifico.nuevo_valor_aleatorio()},1000); } |
Pedro Colina
Excelente! Gracias por el aporte!
Adonis
Simplemente NOTABLE !!.. gracias …
soy un novato..