Extendiendo las capacidades de los Dashboards con JavaScript

Header - ApexCharts

Hoy traemos una alternativa para entender las capacidades nativas del componente de Dashboards de la Onesait Platform.

Un ejemplo de caso de uso puede ser la exploración de datos relacionados con la pandemia de COVID-19 en Portugal. Actualmente, muchos aspectos de nuestra vida se encuentran dominados por las consecuencias de dicha pandemia y, como en muchas otras áreas de actividad, es de vital importancia brindar información de una manera sencilla en donde las conclusiones de los usuarios finales sean fáciles de evaluar.

Actualmente contamos con un Dashboard con toda esa información, la cual se actualiza a diario con los datos proporcionados por el gobierno portugués y que podéis consultar a continuación:

Dashboard con los datos de COVID-19 en Portugal

Si os fijáis, la estética es un poco diferente a la que solemos tener en los Dashboards por defecto, ya que en este caso en concreto se ha usado una librería de gráficas externa para hacerlo aun más vistoso.

A lo largo de esta entrada os contamos cómo diseñar y generar un gadget de gráficos usando esta librería de gráficas paso a paso.

La idea

El confinamiento es el tema del momento en muchos países. En el caso de Portugal, se busca dimensionarlo en función del número de infectados por cada 100.000 habitantes en los últimos 14 días, análisis que se realiza de municipio en municipio, delimitando los niveles de riesgo usando dicha métrica.

Por ello, consideramos que es muy relevante que cada portugués conozca el nivel de riesgo de la zona en la que vive (y conocer así las limitaciones asociadas a dicho nivel) así como cuál es su evolución y cuál es el nivel de riesgo comparativo dentro de esa lista.

Es por tanto un caso de uso ideal para generar gráficos de información, ya que mostrar la información en forma de tabla es «quedarse a mitad del camino», consiguiendo perder la atención de los interesados.

Sin embargo, para llegar a obtener este tipo de gráfico, hay que seguir un camino que implica tener los datos en condiciones ideales para su presentación. Por ello, en el siguiente esquema resaltamos qué sería necesario para llegar a ello:

Estamos seguros de que este diagrama, además de responder a la finalidad de este ejemplo, pueden verse muchas otras necesidades que existen en el día a día de las empresas para los procesos de digitalización. Podemos decir que refleja lo siguiente:

  • La Onesait Platform responde a la necesidad del proceso completamente, de principio a fin.
  • Cada subproceso tiene una herramienta específica y especializada para cada requerimiento.
  • Estas herramientas, aunque integradas, son autónomas entre sí, por lo que pueden ser suprimidas, reemplazadas o ampliadas sin problema, permitiendo la óptima adaptación a las realidades de cada proceso y cliente.
  • Para el ejemplo que proponemos, todo esto se podría solucionar en un único proceso desarrollado en un lenguaje de programación. Sin embargo, esto implicaría que sería más difícil de implementar, más complejo de mantener y un verdadero dolor de cabeza a la hora de escalar en términos técnicos y funcionales.

La Onesait Platform disponibiliza todo esto (¡y más!) en una única plataforma y, en las situaciones más sencillas, sin requerir grandes conocimientos de programación, como podríamos decir del ejemplo que traemos hoy.

Esto se debe a que la mayoría de las herramientas que vamos a utilizar cuentan con interfaces gráficas para apoyar su desarrollo, lo que permite una fácil interpretación y mapeo entre la tecnología y su objetivo funcional.

El diseño

Comprendidas todas las fases globales del proceso, ahora vamos a concentrarnos en el gráfico final a realizar que, por la naturaleza de las necesidades, nos ha obligado a ir un poco más allá de las funcionalidades nativas de la Plataforma, haciendo uso de la librería JavaScript Open Source de ApexCharts. Sin embargo, es importante resaltar cuál es el resultado de las fases anteriores, y cuál sirve como entrada para la producción del panel de mando.

Así, en primer lugar empezaríamos preparando las ontologías (concepto subyacente al Semantic DataHub) con todos los datos necesarios; es decir, la recopilación de información por región de salud (ARS), municipio y fecha, detallando estas dimensiones para los nuevos casos por 100.000 habitantes en los últimos 14 días (incidencia acumulada), así como la variación desde la última fecha.

Finalmente, y como resultado de un producto ponderado por diversos factores, seleccionamos el nivel de de riesgo de cada momento, nivel calculado previamente mediante un Notebook.

Para preparar los datos para su utilización en los gráficos, creamos un DataSource (una herramienta conceptual de los Dashboards) de nombre «DS_MINICIP_INCID», el cual lleva a cabo una consulta SQL sobre la ontología interesada, y que se realiza de la siguiente manera:

La respuesta de este DataSource se puede consultar desde este archivo JSON.

Seguidamente, se procede a crear el Dashboard desde cero, tal como se describe a continuación:

En la configuración inicial del Dashboard, hay que incluir la URL de la librería de ApexCharts. Eso se hace añadiendo la siguiente línea de código:

<script src = "https://cdn.jsdelivr.net/npm/apexcharts"> </script>

Una vez creado el Dashboard, el cual estará vacío, se comienza añadiendo un Gadget de tipo Template y editándolo de acuerdo con los siguientes pasos:

Dos puntos importantes a tener en cuenta son que:

  • Hay que seleccionar «AngularJS» como el tipo de plantilla.
  • Como DataSource, seleccionaremos el creado previamente: «DS_MINICIP_INCID».

Bueno, pues llegado a este punto, en el configurador del gadget contamos con dos editores disponibles: uno para HTML (a la izquierda) y otro para JavaScript (a la derecha).

En el primer editor, el de HTML definimos lo siguiente (hemos incluido comentarios explicativos en el código para entender lo que se está haciendo):

<!-- Estilos para formatear los contenidos -->
<style>
  #chart1 {
    max-width: 100% auto;
  }

  md-card {
    box-shadow: none;
  }

  #title1 {
    padding-top: 0pt;
    padding-left: 10pt;
    color: #1A3B70;
    font-weight: bold;
    font-size: 12pt;
  }
  
  #subtitle1 {
    padding-top: 0pt;
    padding-left: 10pt;
    color: #1A3B70;
    font-size: x-small;
  }
</style>


<!-- Contenedores -->
<div layout="column">
  <div layout="row" layout-align="start center">
    <md-card>
      <md-card-content style="max-height:20;width:200">
        <md-input-container style="margin:0;padding:0;width:100%;">
          <label>Health Region (ARS)</label>

          <!--
 La función sendFilter se encarga de aplicar la opción seleccionada como filtro de fuente de datos, pasando el valor a través de la variable 'ars' -->	
          <md-select ng-model="c" placeholder="Choose Health Region"
            ng-change="sendFilter('ars',c)">

            <md-option value="Norte">Norte</md-option>
            <md-option value="Centro">Centro</md-option>
            <md-option value="Lisboa e Vale do Tejo" ng-selected="true">
              Lisboa e Vale do Tejo
            </md-option>
            <md-option value="Alentejo">Alentejo</md-option>
            <md-option value="Algarve">Algarve</md-option>
            <md-option value="Madeira">Madeira</md-option>
            <md-option value="Açores">Açores</md-option>
          </md-select>
        </md-input-container>
      </md-card-content>
    </md-card>
    <div>
      <label id="title1">
        Relation Between Municipalities Incidence (14 days per 100K people)
        and Incidence Grow Rate (%)
      </label>
      <br>

      <!-- Se puede acceder a los datos de origen de datos (ds) directamente en HTML. Aquí recopilamos y presentamos en el subtítulo del gráfico, la fecha del primer registro (report_date) que, en este conjunto de datos, es el mismo en todos los registros -->	
      <label id="subtitle1">Last update on {{ ds[0].report_date }}</label>

    </div>
  </div>
</div>	

<!-- Contenedor DIV del gráfico, cuyo contenido se generará con Javascript -->
<div id="chart1"></div>

Seguidamente, es necesario preparar el código Javascript que genere el gráfico. La idea es crear una función diseñada exclusivamente para renderizar el gráfico utilizando la librería de ApexCharts:

/*
Esta función recibirá como parámetro los datos devueltos por la fuente de datos (data). El gráfico está definido por una estructura JSON cuyos detalles se pueden encontrar en Apexcharts.
*/
function renderChart1(data) {
  chart1Optn = {
    /*
    Este tipo de gráfico (de burbujas) permite la presentación de 3 dimensiones de datos. Este nodo de estructura JSON permite definir los datos del gráfico. En este caso, mediante una matriz de 3 elementos: valor del eje X (incidencia), valor del eje Y (pct_change) y valor del eje Z (riesgo).
    */
    series: [{
      name: data[0].health_region,
      data: data.map(n => ([n.incidence, n.pct_change, n.risk])),
    }],
    /*
    Aquí se definen las propiedades generales del gráfico: el tipo de gráfico (burbuja), algunos aspectos de representación, si muestra la barra de herramientas y si el soporte para zoom debe estar disponible.
    */
    chart: {
      type: 'bubble',
      height: '93%',
      offsetY: -10,
      toolbar: {
        show: true,
        offsetY: -54,
      },
      zoom: {
        type: 'xy',
        enabled: true,
        autoScaleYaxis: true
      },
    },
    /*
    En este apartado se definen los detalles de la representación del gráfico: tamaño mínimo y/o máximo de estas burbujas.
    */
    plotOptions: {
      bubble: {
        minBubbleRadius: 5,
      }
    },
    /*
    Definición de la descripción de cada burbuja. En este caso, se eligió el nombre del municipio.
    */
    labels: [
      data.map(n => (n.municipality)),
    ],

    /*
    Aquí definimos el color de las burbujas. En este gráfico se pretende hacer una definición dinámica, en función del tramo de riesgo, por lo que se hace uso de una función que devuelve el color (se detalla a continuación).
    */
    colors: [function ({ value, seriesIndex, dataPointIndex, w }) {
      return getColor(w.config.series[seriesIndex].data[dataPointIndex][0]);
    }],

    /*
    Definición de los detalles de representación de la descripción de cada burbuja.
    */
    dataLabels: {
      enabled: true,
      textAnchor: 'start',
      formatter: function (value, { seriesIndex, dataPointIndex, w }) {
        return w.config.labels[seriesIndex][dataPointIndex];
      },
      style: {
        fontSize: '10px',
        colors: ['#1A3B70'],
      },
    },

    /*
    Aquí se definen las propieades de representación del eje X.
    */
    xaxis: {
      type: 'numeric',
      min: 0,
      max: Math.max(...data.map(n => (n.incidence))) * 1.1,
      labels: {
        style: {
          fontSize: '10px',
          fontFamily: 'Soho',
        },
        minHeight: 70,
        hideOverlappingLabels: false,
      },
      title: {
        text: 'Nr. Cases last 14 Days per 100.000 People',
      },
    },
    /*
    Y aquí las del eje Y.
    */
    yaxis: {
      labels: {
        formatter: function (value, timestamp, index) {
          return value.toFixed(2) + '%';
        },
      },
      max: Math.abs(Math.max(...data.map(n => (n.pct_change)))) * 1.1,
      min: Math.abs(Math.min(...data.map(n => (n.pct_change)))) * -1.1,
      forceNiceScale: true,
      title: {
        text: 'Incidence Grow Rate (%)',
      },
    },
    /*
    Definición de las propiedades de color de relleno de las burbujas.
    */
    fill: {
      opacity: 1,
    },

    /*
    Aquí se definen las propiedades del tooltip de cada burbuja, configurando la información a mostrar y sus etiquetas.
    */
    tooltip: {
      enabled: true,
      x: {
        show: true,
        formatter: (n) => n.toFixed(2),
      },
      y: {
        formatter: (n) => n.toFixed(2) + '%',
        title: {
          formatter: (n) => 'Grow Rate:',
        },
      },
      z: {
        formatter: (n) => n.toFixed(2),
        title: 'Risk:',
      },
    },

    /*
    En este punto se definen las anotaciones que se pueden agregar al gráfico; es decir, líneas horizontales o verticales en valores específicos de los ejes X e Y como referencia. En este caso, se va a incluir una línea horizontal en el valor 0 para las Ys, para destacar los municipios con evolución positiva y negativa.
    */
    annotations: {
      yaxis: [
        {
          y: 0,
          borderColor: '#000',
        }
      ],

    /*
    A continuación, se genera una línea vertical en la cantidad de 240 en el eje de las X para hacer una separación entre los municipios de riesgo moderado y de alto riesgo...
    */
      xaxis: [
        {
          x: 240,
          borderColor: '#999',
          label: {
            show: true,
            text: 'High Risk',
            offsetX: 23,
            borderWidth: 0,
            style: {
              color: "#000",
              background: '#FFEA80',
            },
          },
        },
    /*
    ... otra línea vertical en la cantidad de 480 de las X, para crear una separación entre los municipios de alto riesgo y los de muy alto riesgo...
    */
        {
          x: 480,
          borderColor: '#999',
          label: {
            show: true,
            text: 'Very High Risk',
            offsetX: 23,
            borderWidth: 0,
            style: {
              color: "#fff",
              background: '#F7AC6F',
            },
          },
        },
    /*
    ... y una última línea con valor de 960 en el eje X, para crear una separación entre los municipios de riesgo muy alto y de riesgo extremadamente alto.
    */
        {          
          x: 960,
          borderColor: '#999',
          label: {
            show: true,
            text: 'Extremely High Risk',
            offsetX: 23,
            borderWidth: 0,
            style: {
              color: "#fff",
              background: '#E88AA2',
            },
          },
        },
      ],
    },
  };
  /*
Por último, generaremos el renderizado del gráfico (en caso de que aún no existiese) o lo actualizaremos.
  */
  if (chart1) {
    chart1.updateOptions(chart1Optn);
  } else {
    chart1 = new ApexCharts(document.querySelector("#chart1"), chart1Optn);
    chart1.render();
  };
};

Aquí queremos indicar que estamos utilizando dos variables de tipo global, que deben definirse e inicializarse dentro del mismo editor de JavaScript, así como una función que devuelve el color de las «burbujas» según el valor de la incidencia:

/*
Esta variable almacena con estructura JSON los detalles del gráfico.
*/
var chart1Optn = {};

/*
Esta otra variable almacena el objeto del gráfico.
*/
var chart1 = undefined;

/*
Esta es la función que colorea las burbujas del gráfico según el valor de la incidencia indicada como parámetro de entrada.
*/
function getColor(value) {
    if (value >= 960) {
        return '#E88AA2';
    } else if (value >= 480) {
        return '#F7AC6F';
    } else if (value >= 240) {
        return '#FFEA80';
    } else {
        return '#B1CFE5';
    };
};

Aparte, también vamos a usar una de las funciones que están disponible por defecto en la creación de la plantilla de gadget:

/*
La función vm.drawLiveComponent se llama automáticamente cada vez que se cambia el conjunto de datos que llegan del DataSource (por ejemplo, como resultado de aplicar un valor diferente al filtro).
 De esta forma, esta función será ejecutada nada más crear el Dashboard, y siempre que se seleccione una nueva opción en la lista desplegable que creamos en la estructura HTML.
 A su vez, realiza una llamada a la función de representación gráfica (antes explicada) con los nuevos datos.
*/
vm.drawLiveComponent = function (newData, oldData) {
    if (newData) {
        renderChart1(newData);
    }
};

Bueno, pues una vez hecho todo esto, guardamos y compilamos el Gadget. Hecho esto, crearemos en el DataLink una conexión entre la opción de la lista desplegable seleccionada y la fuente de datos, con el fin de automatizar la aplicación del filtro:

En este caso particular, el Gadget de origen y el Gadget de destino son el mismo (el Gadget Template que hemos creado). Además, vamos a usar como campo de origen los parámetros de la función SendFilter usados ​​en la parte de «ng-change» en el HTML, y como campo de destino el campo de la fuente de datos a la que se aplica el filtro:

Gadget de origen: el nombre del gadget en el desplegable de orígenes.
Campo de origen: «ars».
Gadget de destino: el nombre del gadget en el desplegable de destinos.
Campo de destino: «health region».

Hecho esto, deberíamos obtener una gráfica similar a la siguiente:

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *