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:
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: