Gráficas con ng2-charts (Chart.js) y Angular 8. Ordena, filtra, mapea y reduce arreglos de objetos.
Demo: GLTrack 4ta parte. Una aplicación para monitorear niveles de glucosa para pacientes de diabetes.
Introducción
Esta es la cuarta y última parte de la serie de posts “GLTrack”. Durante los 4 posts hemos construido una aplicación que permite a los pacientes de diabetes llevar un registro de sus mediciones diarias de niveles de glucosa en la sangre.
En el post anterior, comenzamos a construir la página principal de la aplicación e implementamos un componente que muestra una tabla con los datos de niveles de glucosa capturador por el usuario.
Hoy implementaremos gráficas de línea, barras y pastel para representar de manera visual los datos cargados en el sistema.
Comenzaremos corrigiendo un bug que encontré mientras trabajaba en este post. El bug quedó corregido también en los repositorios de GitHub de los posts anteriores.
NOTA: Esta es la parte 4 de 4 de la serie de posts "GLTrack"
- GLTrack parte 1: "Schematics, Componentes y Ruteo con Angular + Material".
- GLTrack parte 2: "Modelos, servicios, dependency injection, formas y data binding con Angular + Material".
- GLTrack parte 3: "Reutiliza componentes. Interacción entre componentes usando @Input() y EventEmmiter con Angular 8".
- GLTrack parte 4: "Gráficas con ng2-charts (Chart.js) y Angular 8. Ordena, filtra, mapea y reduce arreglos de objetos.".
Paso 0: Corregir bug en el componente “tabla-registros”
El bug consiste en que la paginación de la tabla de registros no funciona correctamente. Esto es causado por que los elementos del paginador y el sorteador se conectan a la fuente de datos de la tabla en el constructor del componente.
Para corregir esto, debemos mover este código al método ngOnInit(). Modifica el siguiente código en “tabla-registros.component.ts”:
...
dataSource = new MatTableDataSource<MedicionGlucosa>(this._registros);
...
constructor() {
}
ngOnInit() {
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
}
...
El primer cambio consiste en declarar e inicializar la tabla con los datos en la propiedad “this._registros”.
Después eliminamos todo el contenido del método constructor del componente y lo pasamos al método “ngOnInit()”. Ya no es necesario inicializar los datos de la tabla así que solo dejamos el código que conecta el paginador y el sorteador a la tabla.
Después de hacer estos cambios, el paginador de la tabla funciona correctamente.
Paso 1: Generar registros de prueba (opcional)
Para generar registros con datos aleatorios para visualizarlos en las gráficas que implementaremos en el dashboard, agrega el siguiente código en el servicio “mediciones.service.ts”:
...
export class MedicionesService {
debugMode = true;
...
constructor() {
...
if (this.debugMode) {
this.CrearDatosDePrueba();
}
}
...
private CrearDatosDePrueba() {
if (this.GetAll().length < 2000) {
localStorage.setItem(this.storageKey, JSON.stringify([] as MedicionGlucosa[]));
const fechaA = new Date();
const fechaB = new Date();
fechaA.setMonth(fechaA.getMonth() - 24);
while (fechaA < fechaB) {
let medicion = this.CrearMedicionRandom('Desayuno', fechaA);
this.Create(medicion);
medicion = this.CrearMedicionRandom('Comida', fechaA);
this.Create(medicion);
medicion = this.CrearMedicionRandom('Cena', fechaA);
this.Create(medicion);
fechaA.setDate(fechaA.getDate() + 1);
}
}
}
private CrearMedicionRandom(comida: string, fecha: Date) {
let rnd = (Math.random() * 130) + 60;
const nivelGlucosa = rnd;
rnd = (Math.floor(Math.random() * 100) % 2);
const antesODespues = rnd === 0 ? 'Antes' : 'Despues';
const medicion: MedicionGlucosa = {
Id: this.NewGuid(),
Timestamp: new Date(),
Nivel: nivelGlucosa,
Comida: comida,
AntesDespues: antesODespues,
Fecha: fecha
};
return medicion;
}
}
Los cambios consisten en lo siguiente:
- Creamos una propiedad booleana “debugMode”.
- En el método constructor, si la propiedad “debugMode” es igual a “true” el servicio va a precargar datos de prueba en la aplicación llamando al método “CrearDatosDePrueba()”.
- El método “CrearDatosDePrueba()” genera 3 registros diarios de mediciones por cada día de los últimos 2 años utilizando el método “CrearMedicionRandom()”.
- El método “CrearMedicionRandom()” genera valores pseudoaleatorios para generar registros de niveles de glucosa que se encuentren dentro de rangos realistas.
Para deshabilitar este comportamiento, cambia el valor de la propiedad “debugMode” a “false”.
Paso 2: Actualizar estilos en el dashboard de la aplicación
La página principal de la aplicación debe mostrar el contenido de manera ordenada para que todos los datos y graficas sean más fácil de entender. Para esto vamos a agregar los siguientes estilos en el archivo “dashboard.component.css”:
.flex-row {
display: flex;
width: 100%;
flex-wrap: wrap;
padding-bottom: 20px;
}
.flex-row-item.uno {
width: 100%;
}
.flex-row-item.dos {
width: 50%;
}
.flex-row-item mat-card {
margin-right: 10px;
}
.flex-row-item:last-child mat-card {
margin-right: 0px;
}
Ahora agregaremos algunos componentes “mat-card” de Angular Material que nos servirán como contenedores para las gráficas que se van a mostrar en la aplicación. Actualiza el contenido de “dashboard.component.html”:
<div class="flex-row">
<div class="flex-row-item uno">
<mat-card>
<p>RCKSTR1.GitHub.io</p>
</mat-card>
</div>
</div>
<div class="flex-row">
<div class="flex-row-item dos">
<mat-card>
<p>RCKSTR1.GitHub.io</p>
</mat-card>
</div>
<div class="flex-row-item dos">
<mat-card>
<p>RCKSTR1.GitHub.io</p>
</mat-card>
</div>
</div>
<div class="flex-row">
<div class="flex-row-item uno">
<mat-card>
<mat-card-title>Mis registros</mat-card-title>
<app-tabla-registros [permitirEliminar]="false" [registros]="registros">
</app-tabla-registros>
</mat-card>
</div>
</div>
Paso 3: Instalar ng2-charts y Chart.js
Ng2-charts es un paquete de directivas de Angular para Chart.js que estaremos utilizando para crear graficas en Angular 8 utilizaremos ng2-charts.
En la terminal, ejecuta los siguientes comandos:
Instala ng2-charts
npm install --save ng2-charts
Instala Chart.js
npm install --save chart.js
Importa ng2-charts
Agrega el siguiente código en “App.module.ts”:
...
import { ChartsModule } from 'ng2-charts';
@NgModule({
declarations: [
...
],
imports: [
...
ChartsModule
],
...
})
export class AppModule { }
Instala los schematics de ng2-charts para generar componentes que implementen graficas más rápidamente.
npm install --save-dev ng2-charts-schematics
Paso 4: Crear gráficas utilizando schematics de ng2-charts
A continuación, generaremos de manera automática componentes con implementaciones de gráficas de Charts.js utilizando los schematics que instalamos en el paso anterior. Ejecuta los siguientes comandos en la terminal para generar los nuevos componentes:
ng generate ng2-charts-schematics:line components/historico-mediciones-chart
ng generate ng2-charts-schematics:bar components/comparativo-mensual-chart
ng generate ng2-charts-schematics:pie components/global-niveles-chart
Implementar los nuevos componentes al dashboard
Para mostrar los componentes que acabamos de generar, actualiza el contenido de “dashboard.component.html”:
<div class="flex-row">
<div class="flex-row-item uno">
<mat-card>
<app-historico-mediciones-chart>
</app-historico-mediciones-chart>
</mat-card>
</div>
</div>
<div class="flex-row">
<div class="flex-row-item dos">
<mat-card>
<app-comparativo-mensual-chart>
</app-comparativo-mensual-chart>
</mat-card>
</div>
<div class="flex-row-item dos">
<mat-card>
<app-global-niveles-chart>
</app-global-niveles-chart>
</mat-card>
</div>
</div>
...
Ahora se muestran los nuevos componentes que acabamos de generar.
Paso 5: Configurar las graficas
En este paso modificaremos los componentes que incluyen las gráficas para ajustar aspectos estéticos, pero también para que los componentes reciban y procesen los datos que alimentan nuestras graficas.
Actualizar componente “histórico-mediciones-chart”
Comenzaremos a modificar la primera gráfica, ajustando la altura a 100 en el archivo “histórico-mediciones-chart.component.html”:
Para que el componente sea capaz de recibir y procesar los datos de las mediciones guardadas por el usuario, actualiza el archivo “historico-mediciones-chart.component.ts”:
...
export class HistoricoMedicionesChartComponent implements OnInit {
public lineChartData: ChartDataSets[] = [
{ data: [], label: 'Nivel de glucosa' },
];
public lineChartLabels: Label[] = [];
public lineChartOptions: ChartOptions = {
responsive: true,
scales: {
xAxes: [{
display: false
}]
}
};
...
@Input()
set mediciones(datos: MedicionGlucosa[]) {
const fechaInicial = new Date();
fechaInicial.setMonth(fechaInicial.getMonth() - 1);
const ultimoMes = datos.filter(f =>
new Date(Date.parse(f.Fecha.toString())) >= fechaInicial);
this.lineChartData[0].data = ultimoMes.map(m => m.Nivel);
this.lineChartLabels = ultimoMes
.map(m => `${m.Comida} ${m.AntesDespues} ${new Date(Date.parse(m.Fecha.toString())).toDateString()}`);
}
}
Los cambios consisten en lo siguiente:
- Eliminar los datos precargados en el componente y actualizar la etiqueta principal.
- Eliminar las etiquetas precargadas en el componente.
- Configurar la gráfica para que no muestre las etiquetas en el eje X (opcional).
- Agregar una propiedad de entrada “mediciones” con una función setter.
Esta función intercepta los valores de la propiedad “mediciones” y se ejecuta cada vez que el valor cambia. En este caso, cuando el valor de “mediciones” cambia, la gráfica se actualiza para mostrar los valores de todas las mediciones del último mes.
El método filter()
En la función setter de “mediciones” filtramos los objetos del arreglo “datos” por fecha para guardar en la constante “ultimoMes” solamente los registros del último mes (línea 49). Para esto utilizamos el método filter().
El método filter() crea un nuevo array con todos los elementos que cumplan la condición implementada por la función dada.
En este caso, primero guardamos la fecha actual en la constante “fechaInicial” y le restamos 1 mes. Después, la función dada a datos.filter() evalúa que la fecha de cada registro sea mayor o igual a la guardada en “fechaInicial”.
El método map()
Puedes observar que en la función setter de “mediciones” también utilizamos el método map() para crear un arreglo de los niveles de glucosa a partir del arreglo de objetos de tipo MedicionGlucosa (línea 51).
El método map() crea un nuevo array con los resultados de la llamada a la función indicada aplicados a cada uno de sus elementos.
En este caso, la función regesa el valor de la propiedad “Nivel” de cada una de las mediciones en el arreglo.
Pasar los datos de la aplicación desde el dashboard al componente
Ahora necesitamos pasar los registros de la aplicación a nuestro componente a través de la propiedad de entrada “mediciones”. Actualiza el archivo “dashboard.component.html”:
De esta manera, el dashboard pasa los datos de la aplicación al componente de la gráfica a través de la propiedad de entrada “mediciones”.
Actualizar componente “comparativo-mensual-chart”
Igual que con el componente anterior, eliminaremos los datos precargados en la gráfica. Modifica el código del archivo “comparativo-mensual-chart.component.ts”:
export class ComparativoMensualChartComponent implements OnInit {
...
public barChartLabels: Label[] = [];
...
public barChartData: ChartDataSets[] = [];
nombreMeses = [
'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio',
'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'
];
...
}
Estos cambios consisten en:
- Eliminar las etiquetas precargadas en el componente.
- Eliminar los datos precargados en el componente.
- Crear la propiedad “nombreMeses” que contiene un arreglo con los nombres de los meses del año.
Ahora, para mostrar los datos comparativos de cada mes, crearemos una propiedad de entrada “mediciones” con una función setter en el archivo “comparativo-mensual-chart.component.ts”:
export class ComparativoMensualChartComponent implements OnInit {
...
@Input()
set mediciones(datos: MedicionGlucosa[]) {
// Ordenar los datos por fecha
datos.sort((a, b) => {
return Date.parse(a.Fecha.toString()) - Date.parse(b.Fecha.toString());
});
// Obtener un arreglo de años disponibles
let anos = datos.map(m => new Date(m.Fecha.toString()).getFullYear());
// Filtrar duplicados
anos = anos.filter((val, i) => anos.indexOf(val) === i);
const anoActual = new Date().getFullYear();
const anoAnterior = anoActual - 1;
// Eliminar datos existentes en la gráfica
this.barChartData = [{ label: anoAnterior.toString(), data: [] }, { label:
anoActual.toString(), data: [] }];
this.barChartLabels = [];
const mesActual = new Date().getMonth();
let iMes = 0;
while (iMes <= mesActual) {
// Agregar etiquetas con los nombres de cada mes desde Enero hasta el mes actual
this.barChartLabels.push(this.nombreMeses[iMes]);
// Obtener promedio mensual del año actual y el anterior
this.barChartData[0].data.push(this.getPromedioMediciones(anoAnterior, iMes, datos));
this.barChartData[1].data.push(this.getPromedioMediciones(anoActual, iMes, datos));
iMes++;
}
}
getPromedioMediciones(ano: number, mes: number, datos: MedicionGlucosa[]) {
let promedio = 0;
let datosFiltrados: MedicionGlucosa[] = [];
let cantidadMediciones = 0;
let suma = 0;
// Filtrar las mediciones del mes y año que se desea promediar
datosFiltrados = datos.filter(f =>
new Date(f.Fecha.toString()).getFullYear() === ano
&& new Date(f.Fecha.toString()).getMonth() === mes
);
if (datosFiltrados.length > 0) {
// Obtener la suma de los valores de las mediciones a promediar
suma = datosFiltrados.map(m => m.Nivel)
.reduce((a, b) => a + b);
cantidadMediciones = datosFiltrados.length;
promedio = suma / cantidadMediciones;
}
return promedio;
}
}
De esta manera, cada vez que los datos del usuario cambien, se ejecutara esta función que procesa los datos para generar un promedio mensual y comparar estos datos con los del año anterior.
El método sort()
En la función setter de “mediciones” ordenamos los objetos en el arreglo “datos” por fecha utilizando el método sort() (línea 51).
El método sort() ordena los elementos de un array de objetos y devuelve el array ordenado de acuerdo al valor que retorna la función de comparación dada. Siendo a y b dos elementos comparados, entonces:
- Si el valor retornado es menor que 0, se sitúa “a” en un índice menor que “b”. Es decir, “a” viene primero.
- Si el valor retornado es 0, se deja “a” y “b” sin cambios entre ellos, pero ordenados con respecto a todos los elementos diferentes.
- Si el valor retornado es mayor que 0, se sitúa “b” en un índice menor que “a”.
En este caso, la función de comparación regresa el resultado de la resta entre las fechas de diferentes registros de mediciones.
El método reduce()
En la función “getPromedioMediciones()” estamos obteniendo la suma de los niveles de glucosa para después calcular el promedio de los niveles recibidos. Para esto, primero utilizamos el método map() para obtener un arreglo de los niveles de glucosa, “[1,2,3,4,5]” y después obtenemos la suma de las mediciones con el método reduce().
El método reduce() ejecuta una función reductora sobre cada elemento de un array, devolviendo como resultado un único valor.
En esta función, utilizamos el método reduce para obtener la suma de todos los elementos sin necesidad de utilizar un ciclo while, for, o el método forEach.
Pasar los datos de la aplicación desde el dashboard al componente
De la misma manera que con el primer componente, necesitamos pasar los registros de la aplicación a través de la propiedad de entrada “mediciones”. Actualiza el archivo “dashboard.component.html”:
Actualizar componente “global-niveles-chart”
Para terminar, actualizaremos el ultimo componente para eliminar datos precargados y crear la propiedad de entrada “mediciones”. Modifica el código del archivo “global-niveles-chart.component.ts”:
export class GlobalNivelesChartComponent implements OnInit {
...
// public pieChartLabels: Label[] = ['Download Sales', 'In-Store Sales', 'Mail Sales'];
public pieChartLabels: Label[] = [
'Hipoglucemia', 'Normal',
'Nivel elevado', 'Altamente elevado'
];
// public pieChartData: SingleDataSet = [300, 500, 100];
public pieChartData: SingleDataSet = [];
...
@Input()
set mediciones(datos: MedicionGlucosa[]) {
let hipoglucemia = 0;
let normal = 0;
let elevado = 0;
let altamenteElevado = 0;
datos.forEach(m => {
if (m.Nivel <= 70) {
hipoglucemia++;
} else {
if (m.Nivel <= 115) {
normal++;
} else {
if (m.Nivel <= 180) {
elevado++;
} else {
altamenteElevado++;
}
}
}
});
this.pieChartData = [hipoglucemia, normal,
elevado, altamenteElevado];
}
constructor() { }
ngOnInit() {
}
}
Los cambios consisten en lo siguiente:
- Reemplazamos las etiquetas precargadas con las 4 categorías en las que clasificaremos los datos.
- Eliminamos los datos precargados en el componente.
- Creamos la propiedad de entrada “mediciones” con una función setter que se encarga de clasificar los datos disponibles en las categorías que definimos en el paso 1. Para después, llenar la gráfica con la cantidad de mediciones por categoría.
Para concluir, actualiza el archivo “dashboard.component.html” para pasar los datos de la aplicación a este componente:
Conclusión
En este post terminamos de desarrollar GLTrack, una aplicación para registro de mediciones de glucosa en la sangre para pacientes de diabetes, implementando gráficas de Chart.js con el paquete de directivas para angular ng2-charts.
Procesamos la información de niveles de glucosa del usuario para convertirla en datos que pudiéramos representar visualmente en las gráficas utilizando mapeando, filtrando y reduciendo arreglos de objetos con las funciones map(), filter() y reduce().
También creamos propiedades de entrada en los componentes de angular, con el decorador Input() y funciones setter para interceptar cambios en las propiedades de entrada.
De esta manera concluimos el desarrollo del prototipo de GLTrack.
El código fuente completo de la aplicación de ejemplo puedes encontrarlo en GitHub, en https://github.com/RCKSTR1/GLTrack4 o si prefieres, puedes clonar el repositorio directo en https://github.com/RCKSTR1/GLTrack4.git
Si tuviste algún problema o duda al seguir los pasos, o si tienes alguna pregunta acerca de este post, contáctame en Twitter, Facebook o LinkedIn.
Es cuánto.
Fuentes
- Vive con Diabetes. "Tablas de los rangos de los niveles de azúcar en la sangre". https://www.vivecondiabetes.com/viviendo-con-diabetes/tratamiento-y-cuidados/9203-tablas-de-los-rangos-de-los-niveles-de-az%C3%BAcar-en-la-sangre.html
- ng2-charts. "Angular2 directives for Chart.js". https://valor-software.com/ng2-charts/
- MDN web docs. "Array.prototype.map()". https://developer.mozilla.org/es/docs/Web/JavaScript/Referencia/Objetos_globales/Array/map
- MDN web docs. "Array.prototype.filter()". https://developer.mozilla.org/es/docs/Web/JavaScript/Referencia/Objetos_globales/Array/filter
- MDN web docs. "Array.prototype.reduce()". https://developer.mozilla.org/es/docs/Web/JavaScript/Referencia/Objetos_globales/Array/reduce
- MDN web docs. "Array.prototype.sort()". https://developer.mozilla.org/es/docs/Web/JavaScript/Referencia/Objetos_globales/Array/sort