Tutoriales

Estender às Capacidades de Dashboards com JavaScript

Hoje trazemos aqui uma alternativa para estender as capacidades nativas da componente de Dashboards da Onesait Platform.

O exemplo exposto resultou de um use-case de exploração de dados sobre a COVID-19 em Portugal. Atualmente somos dominados em muitos aspeto da nossa vida pelas consequências da pandemia e, como acontece em muitas áreas de atividade, é vital disponibilizar informação de forma simples, em que as conclusões por parte dos utilizadores finais sejam fáceis de aferir.

Actualmente temos um Dashboard com toda esta informação, que é actualizado diariamente com os dados disponibilizados pelo governo português e que pode consultar a seguir:

Dashboard com dados COVID-19 em Portugal

Se você olhar para ele, a estética é um pouco diferente do que costumamos ter em Dashboards por padrão, já que neste caso específico uma biblioteca gráfica externa foi usada para torná-lo ainda mais atraente.

Ao longo desta postagem, explicamos como projetar e gerar um gadget gráfico usando essa biblioteca de gráficos passo a passo.

A ideia

O assunto do momento em muitos países é o confinamento. Em Portugal defende-se que seja dimensionado de acordo com a quantidade de infetados por 100.000 habitantes nos últimos 14 dias, numa análise realizada concelho a concelho, com escalões de risco tendo por base esta métrica.

Por isso, entendemos que é relevante para cada português, saber qual o escalão de risco da zona onde vive (para saber as limitações associadas), bem como, qual a sua evolução e qual o nível de risco comparativo dentro dessa lista.

Um use-case “perfeito” para se criar um bubble chart, pois apresentar esta informação de forma tabular, é «meio caminho andando» para perdemos a atenção dos interessados. Contudo, para chegar até este gráfico, temos de percorrer o caminho de ter os dados nas condições ideias para a sua apresentação e o seguinte esquema pretende evidenciar o que poderia ser necessário para chegar a essa fase:

Temos por certo que este diagrama, para além de responder ao propósito deste exemplo, poderá ser mapeado com muitas outras necessidades que existem no dia-a-dia das empresas para digitalização de processos. Acreditamos também que reflete o seguinte:

  • A Onesait Platform responde à necessidade do processo end-to-end;
  • Cada subprocesso possui uma ferramenta específica e especializada em cada requisito;
  • Estas ferramentas, embora integradas, são de tal forma autónomas que podem ser suprimidas, substituídas ou ampliadas, permitindo a adequação ótima às realidades de cada processo e de cada cliente;
  • Para o exemplo que estamos a propor, tudo isto poderia ser resumido a único processo desenvolvido numa linguagem de programação. Mas certo seria também, que se tornaria mais difícil de implementar, mais complexo de manter e uma verdadeira dor de cabeça na hora de escalar em termos técnicos e funcionais!

A Onesait Platform serve-nos tudo isto (e muito mais) numa única plataforma e, nos casos mais básicos, sem requerer grandes conhecimentos de programação, como é o exemplo que trazemos hoje.

Isso ocorre porque a maioria destas ferramentas possuem interfaces gráficos de apoio ao desenvolvimento, que permitem uma fácil interpretação e mapeamento entre a tecnologia e o seu objetivo funcional.

O desenho

Compreendidas todas as fases do processo global, iremo-nos agora concentrar no gráfico final que pela natureza do requisito, nos impeliu a ir um pouco mais além da funcionalidade nativa da Onesait Platform, recorrendo a uma biblioteca JavaScript Open Source – ApexCharts. Não obstante, importa salientar qual é output das fases anteriores e que serve de input para a produção do Dashboard.

Assim, em primeiro lugar, chegamos a esta fase com as ontologias (conceito subjacente ao Semantic DataHub) preenchidas com os dados necessários, nomeadamente aquela que recolheu a informação por região de saúde (ARS), concelho e data, detalhando essas dimensões por novos casos nos últimos 14 dias por 100.000 habitantes e a variação face à última data.

Por último, e como resultado de um produto ponderado de diversos fatores, recolhemos o nível de risco em cada momento (tudo facilmente alcançado previamente, utilizando a ferramenta de Notebooks).

No sentido de preparar os dados para utilização no gráfico, criamos ainda um DataSource (conceito da ferramenta de Dashboards) denominado «DS_MINICIP_INCID», que se preconiza num SQL Query e que se concretiza da seguinte forma:

O resultado deste datasource pode ser encontrado no seguinte ficheiro JSON.

Posteriormente, é necessário criar um novo Dashboard cujo processo se descreve a seguir:

Na configuração inicial do dashboard destaca-se a necessidade de incluir a referência para a biblioteca do Apexcharts:

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

Posteriormente, inicia-se a edição do dashboard de acordo com os seguintes passos:

Durante este processo, há que salientar duas opções importantes:

  • Selecionar “AngularJS” como Template Type.
  • Selecionar a Datasource que criamos previamente: «DS_MINICIP_INCID».

Neste ponto ficamos com 2 editores disponíveis: um para o HTML e um outro para o JavaScript. No editor de HTML podemos definir o seguinte:

<!-- Stylesheet para formatar o conteúdo dos elementos -->
<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>

          <!--
 A função sendFilter é responsável por aplicar a opção selecionada como filtro do datasource, passando o valor através da variável «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>

      <!-- Os dados do datasource (ds) podem ser acedidos diretamente no HTML. Aqui recolhemos e apresentamos no subtítulo do gráfico, a data do primeiro registo (report_date) que, neste dataset, é igual em todos os registos -->	
      <label id="subtitle1">Last update on {{ ds[0].report_date }}</label>

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

<!-- DIV Container do gráfico, cujo o conteúdo será gerado em Javascript -->
<div id="chart1"></div>

Agora é necessário fazer o código JavaScript que gera o gráfico. A abordagem adotada passou por criar uma função destinada exclusivamente a fazer a renderização do gráfico utilizando a biblioteca do Apexcharts:

/*
A função recebe como parâmetro os dados devolvidos pelo datasource (data). O gráfico é definido por uma estrutura JSON cujos os detalhes podem ser encontrados em Apexcharts.
*/
function renderChart1(data) {
  chart1Optn = {
    /*
    Este tipo de gráfico (bubble) permite a apresentação de 3 dimensões de dados. Este nó da estrutura JSON permite definir os dados do gráfico. Neste caso, através de uma array  de 3 elementos: valor do eixo do X (incidence), valor do eixo dos Y (pct_change) e valor do eixo dos Z (risk).
    */
    series: [{
      name: data[0].health_region,
      data: data.map(n => ([n.incidence, n.pct_change, n.risk])),
    }],
    /*
    Aqui definem-se aspetos gerais sobre o gráfico, nomeadamente o tipo de gráfico (bubble), alguns aspetos de apresentação, se deve disponibilizar a toolbar de ferramentas e o suporte para zoom.
    */
    chart: {
      type: 'bubble',
      height: '93%',
      offsetY: -10,
      toolbar: {
        show: true,
        offsetY: -54,
      },
      zoom: {
        type: 'xy',
        enabled: true,
        autoScaleYaxis: true
      },
    },
    /*
    Neste apartado, são definidos detalhes de apresentação das bolhas, nomeadamente a dimensão mínima (e/ou máxima) dessas bolhas.
    */
    plotOptions: {
      bubble: {
        minBubbleRadius: 5,
      }
    },
    /*
    Definição da descrição de cada bolha. Neste caso, optou-se pelo nome do concelho.
    */
    labels: [
      data.map(n => (n.municipality)),
    ],

    /*
    Aqui é possível definir a cor das bolhas. Neste gráfico, pretende-se uma definição dinâmica, dependendo do escalão de risco, razão pela qual evocamos uma função que retorna a cor (detalhada mais adiante).
    */
    colors: [function ({ value, seriesIndex, dataPointIndex, w }) {
      return getColor(w.config.series[seriesIndex].data[dataPointIndex][0]);
    }],

    /*
    Definição de detalhes de apresentação das descrição de cada bolha.
    */
    dataLabels: {
      enabled: true,
      textAnchor: 'start',
      formatter: function (value, { seriesIndex, dataPointIndex, w }) {
        return w.config.labels[seriesIndex][dataPointIndex];
      },
      style: {
        fontSize: '10px',
        colors: ['#1A3B70'],
      },
    },

    /*
    Aqui definem-se aspetos de apresentação do eixo dos Xs.
    */
    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',
      },
    },
    /*
    Aqui definem-se aspetos de apresentação do eixo dos Ys.
    */
    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 (%)',
      },
    },
    /*
    Definição do tipo de preenchimento da cor das bolhas.
    */
    fill: {
      opacity: 1,
    },

    /*
    Neste nó, definem-se aspetos de apresentação dos valores, na tooltip que é acessível para cada bolha.
    */
    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:',
      },
    },

    /*
    Neste nó, definem-se anotações que podem ser adicionadas ao gráfico, nomeadamente linhas horizontais ou verticais em valores específicos dos eixos dos Xs e dos Ys.
 Neste caso, estamos a fazer uma linha horizontal no valor 0 dos Ys, para criar mais destaque entre os concelhos com evolução positiva e negativa.
    */
    annotations: {
      yaxis: [
        {
          y: 0,
          borderColor: '#000',
        }
      ],

    /*
    Neste caso, estamos a fazer uma linha vertical no valor 240 dos Xs, para criar uma separação entre os concelhos de risco moderado e os de alto risco.
    */
      xaxis: [
        {
          x: 240,
          borderColor: '#999',
          label: {
            show: true,
            text: 'High Risk',
            offsetX: 23,
            borderWidth: 0,
            style: {
              color: "#000",
              background: '#FFEA80',
            },
          },
        },
    /*
    Neste caso, estamos a fazer uma linha vertical no valor 480 dos Xs, para criar uma separação entre os concelhos de alto risco e os de risco muito alto.
    */
        {
          x: 480,
          borderColor: '#999',
          label: {
            show: true,
            text: 'Very High Risk',
            offsetX: 23,
            borderWidth: 0,
            style: {
              color: "#fff",
              background: '#F7AC6F',
            },
          },
        },
    /*
    Neste caso, estamos a fazer uma linha vertical no valor 960 dos Xs, para criar uma separação entre os concelhos de risco muito alto e os de risco extremamente alto.
    */
        {          
          x: 960,
          borderColor: '#999',
          label: {
            show: true,
            text: 'Extremely High Risk',
            offsetX: 23,
            borderWidth: 0,
            style: {
              color: "#fff",
              background: '#E88AA2',
            },
          },
        },
      ],
    },
  };
  /*
Por último vamos solicitar a renderização do gráfico (caso ainda não exista) ou a sua atualização, dentro do DIV container “chart1” que criamos em HTML.
  */
  if (chart1) {
    chart1.updateOptions(chart1Optn);
  } else {
    chart1 = new ApexCharts(document.querySelector("#chart1"), chart1Optn);
    chart1.render();
  };
};

De salientar que estamos a utilizar dois variáveis «globais» que devem ser definidas e inicializadas dentro do mesmo editor Javascript, bem como, uma função que devolve a cor das «bolhas» de acordo com o valor da incidência:

/*
Guarda a estrutura JSON com os detalhes do gráfico.
*/
var chart1Optn = {};

/*
Guarda o objeto do gráfico.
*/
var chart1 = undefined;

/*
Função que devolve a cor das “bolhas” de acordo com o valor da incidência.
*/
function getColor(value) {
    if (value >= 960) {
        return '#E88AA2';
    } else if (value >= 480) {
        return '#F7AC6F';
    } else if (value >= 240) {
        return '#FFEA80';
    } else {
        return '#B1CFE5';
    };
};

Vamos utilizar ainda uma das funções que é disponibilizada por defeito na criação do gadget template:

/*
A função vm.drawLiveComponent é automaticamente invocada sempre que seja alterado o dataset resultante do datasource (ex.: como resultado da aplicação de valor diferente para o filtro).
Desta forma, esta função será invocada na criação do dashboard e sempre que for selecionada uma nova opção na drop-down list que criamos na estrutura HTML.
Por sua vez, faz chamada à função renderização do gráfico (supra especificada) com os novos dados.
*/
vm.drawLiveComponent = function (newData, oldData) {
    if (newData) {
        renderChart1(newData);
    }
};

Por último, vamos criar um Datalink que permite fazer a ligação entre opção selecionada drop-down list e o datasource, de forma a automatizar a aplicação do filtro:

Neste caso em particular, o Source Gadget e o Target Gadget são o mesmo (i.e., o gadget template que estamos a criar) e devemos utilizar como Source Field os parâmetros da função sendFilter utilizados na directiva ng-change em HTML. O Target Field deverá ser o campo do datasource ao qual se aplica o filtro:

  • Source Gadget: de acordo com o valor disponível na drop-down, correspondente ao gadget.
  • Source Field: «ars».
  • Target Gadget: de acordo com o valor disponível na drop-down, correspondente ao gadget.
  • Target Field: «health region».

Finalmente, deveremos obter um gráfico idêntico ao seguinte:

✍🏻 Author(s)

Deja una respuesta