Gráficos de barra com seletor de data

Prezados, venho compartilhar uma necessidade que vem aparecendo em alguns projetos e que consegui vencer parcialmente. Assim, quero registrar aqui o que foi feito para que outros colegas possam usufruir da solução e também gostaria da colaboração para resolver os gargalos onde eu travei :sweat_smile:

ESCOPO DA DEMANDA
Eu trabalho desenvolvendo supervisórios para usinas fotovoltaicas, então os dados de geração dos inversores e da usina como um todo são informações que o cliente gosta de ter uma visualização rápida e podendo selecionar os períodos. A maioria das soluções do mercado e fornecidas pelos fabricantes já disponibilizam os dados assim, então acaba sendo uma necessidade para não ficar pra trás.

Como os scripts nativos do ScadaBR/Scada-LTS não são muito amigáveis pra ajustar, busquei soluções aqui no fórum em outras postagens e acabei me adaptando melhor ao uso da biblioteca do Google Charts, baseado nessa postagem inicial:
http://forum.scadabr.com.br/t/criar-grafico-de-barras/848/34

RESULTADO INICIAL
Com as orientações dos colegas, consegui desenvolver um código pra apresentar os dados de maneira mais agradável, usando a lib do Google, mas como o gráfico precisa de um input de quantos dados serão apresentados ‘reading_number’. Assim, dei uma “roubada” e defini um gráfico que apresenta apenas os dados de geração diária dos últimos 30 dias, conforme a imagem a seguir:

Como eu comentei, atende por agora mas não é o cenário ideal. Pensando nisso, comecei a buscar alternativas para apresentar os dados de maneira mais interativa, colocando um campo para selecionar data inicial e final para darmos display no gráfico.

PRIMEIRA VERSÃO FUNCIONAL
A ideia então foi implementar a seguinte lógica:

  1. Capturar as datas inicial e final via script para o servidor com tags input text

  2. Alterar o código do gráfico para que a variável que define a quantidade de dias ‘reading_number’ receba os dados da diferença de dias das datas

Segue a imagem de como ficou esse primeiro teste:

Consegui implementar com sucesso a ideia inicial, mas com uma série de poréns que vou listar a seguir e que peço a ajuda dos colegas mais experientes com HTML e CSS para conseguirmos arredondar.

  1. Não consigo travar o input de ser enviado em caso dos valores não estarem dentro do padrão estabelecido (yyyy-mm-dd); tentei filtrar usando regex e outras tags do HTML mas sem sucesso. A ideia é evitar de salvar dados inválidos e comprometer a geração do gráfico

  2. Não consigo ajustar o gráfico em caso de erros, pra evitar que ele suma com o elemento gráfico e retorne a mensagem de erro na tela; idealmente minha ideia seria ou substituir por uma mensagem de erro ou manter sem alterar até ter um formato válido dos dados de data.

  3. Em último caso a ideia seria colocar um botão para dar um start/refresh no gráfico caso os dados de input estejam ok

A parte de gráficos é uma carência do ScadaBR/Scada-LTS, então creio que se conseguirmos arredondar uma solução que seja de fácil ajuste todos irão se beneficiar!

Agradeço desde já pela atenção de todos, seguimos!

OBS.: vou mandar os códigos nas postagens a seguir, pra não ficar muito ruim de ler.

1 curtida

GRÁFICO DE BARRAS DOS ÚLTIMOS 30 DIAS

// GRÁFICO PARA APRESENTAR GERAÇÃO DIÁRIA - ÚLTIMOS 30 DIAS

// How many values will be included in the chart?

var reading_number = 30;

// Points to be included in chart (ID or XID)

var reading_points = [ point.id ];

//Ajustes do gráfico

//Personalização do gráfico

var titulo_grafico = "Geração dos últimos 30 dias";

var graph_color = "#1e2d41";

var bg_color = "#757e8b";

var text_color = "#1e2d41";

var gridlines_color = "#444444";

var tamanho_fonte = "14";

var altura = "150px";

var largura = "150px";

var id_chart = "kwh";

// Cria o array JSON para alimentar o gráfico em ordem decrescente
function createDataArray() {
var foo = "[";

//acessa os 30 últimos valores
var val = new com.serotonin.mango.db.dao.PointValueDao();
var val = val.getLatestPointValues(point.id, reading_number);

//ajusta o tamanho do loop for
var size = val.size() - 1;
for (var i = size; i >=0; i--) {
var time = val.get(i).time;
var value = val.get(i).value;
var dia = new Date(time).getDate();
var mes = new Date(time).getMonth()+1;
var ano = new Date(time).getFullYear();

var data = dia.toString()+"/"+mes.toString()+"/"+ano.toString();

if (i > 0) { foo += "'" + data + "'," + value + "], ["; }

else foo += "'" + data + "'," + value;

}

foo += "],";
return foo;
}

var s = "" ;

s+=" <script type=\"text/javascript\"> ";

s+=" google.charts.load('current', {'packages':['corechart']}); ";

s+=" google.charts.setOnLoadCallback(drawChart); ";

s+=" function drawChart() { ";

s+=" var data = google.visualization.arrayToDataTable([ ";

s+=" ['ts', 'kWh'], " + createDataArray() + "]); ";

s+=" var options = {";

s+=" chartArea:{left:35, top:25,width:'90%',height:'80%'}, backgroundColor: '"+ bg_color +"', ";

s+=" colors: ['" + graph_color + "'],";

s+=" title: '"+ titulo_grafico +"', titleTextStyle: {color: '"+ text_color +"', fontSize: "+ tamanho_fonte + "},";

s+=" hAxis: { textStyle: { color: '"+ bg_color +"', fontSize: "+ tamanho_fonte +", bold: false }, gridlines: {color: '"+ gridlines_color +"'}, baselineColor: '"+ gridlines_color +"' },";

s+=" vAxis: { gridlines: {color: '"+ gridlines_color +"'}, baselineColor: '"+ gridlines_color +"', ";

s+=" textStyle: { color: '"+ text_color +"', fontSize: "+ tamanho_fonte +", bold: false } }";

s+=" }; ";

s+=" var chart3 = new google.visualization.ColumnChart(document.getElementById('" + id_chart + "')); ";

s+="  chart3.draw(data, options); ";

s+="  } ";

s+=" </script> ";

s+=" <div id=" + id_chart + " style=\"width: "+ largura +"; height: "+ altura +";\"&gt;</div>";

return s;

//DEBUG: VALIDAR O QUE ESTÁ SENDO MONTADO PELA FUNÇÃO createDataArray()

//return "<b>;<font size=4 face=Arial color=#f0eee9>" + createDataArray() + "</font>;";
1 curtida

SCRIPT DATA INICIAL

image

// INPUT DE DATA E HORA INICIAL
function regexOK(inputString) {
    // Expressão regular para o formato yyyy-mm-dd
    const regexDataHora = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2]\d|3[0-1])$/;
    
    return regexDataHora.test(inputString);
}



var s = "";
s += "<label for='data-inicial'>Data Inicial: </label>";
s += "<input type=\"text\" id=\"data-inicial\" placeholder=\"yyyy-mm-dd\" onChange='mango.view.setPoint("+ point.id +", \""+ pointComponent.id +"\", this.value);return false;' value=" + renderedText + ">";
if(regexOK(value)){
s += "<br><p> Input OK! </p>";
}
else{
s += "<br><b><p style=\"color:red;\"> Input fora do padrão, inserir novamente </b></p>";
}
return s;

SCRIPT DATA FINAL

image

// INPUT DE DATA E HORA FINAL
function regexOK(inputString) {
    // Expressão regular para o formato yyyy-mm-dd
    const regexDataHora = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2]\d|3[0-1])$/;
    
    return regexDataHora.test(inputString);
}


var s = "";
s += "<label for='data-final'>Data Final: </label>";
s += "<input type=\"text\" id=\"data-final\" placeholder=\"yyyy-mm-dd\" onChange='mango.view.setPoint("+ point.id +", \""+ pointComponent.id +"\", this.value);return false;' value=\"\">";
if(regexOK(value)){
s += "<br><p> Input OK! </p>";
}
else{
s += "<br><b><p style=\"color:red;\"> Input fora do padrão, inserir novamente </b></p>";
}

return s;
1 curtida

GRÁFICO COM AJUSTE DE DATAS

// GRÁFICO PARA APRESENTAR GERAÇÃO DIÁRIA COM SELEÇÃO DE DATA
// Qual variável vai ser apresentada no gráfico (ID or XID)
var reading_points = [ point.id ];

// Definição de quantos pontos serão lidos calculando a diferença de dias das datas de início e fim
function calcularDiferencaEmDias(dataInicial, dataFinal) {
	// Converte as strings de data para objetos Date
	const dataInicialObj = new Date(dataInicial);
	const dataFinalObj = new Date(dataFinal);
  
	// Calcula a diferença em milissegundos
	const diferencaEmMilissegundos = dataFinalObj - dataInicialObj;
  
	// Converte a diferença em milissegundos para dias
	const umDiaEmMilissegundos = 24 * 60 * 60 * 1000; // 1 dia em milissegundos
	const diferencaEmDias = Math.floor(diferencaEmMilissegundos / umDiaEmMilissegundos);
  
	return diferencaEmDias;
}

// Busca os valores nas variáveis com dpid 242 e 243
var dataInicial = new com.serotonin.mango.db.dao.PointValueDao().getLatestPointValue('242').value;
var dataFinal = new com.serotonin.mango.db.dao.PointValueDao().getLatestPointValue('243').value;

// Variável com o número de dias a serem apresentados no gráfico
var reading_number = calcularDiferencaEmDias(dataInicial, dataFinal);



//Ajustes do gráfico
//Personalização do gráfico
var titulo_grafico = "Consumo por hora";
var graph_color = "#1e2d41";
var bg_color = "#ffffffff";
var text_color = "#1e2d41";
var gridlines_color = "#444444";
var tamanho_fonte = "14";
var altura = "350px";
var largura = "350px";
var id_chart = 	"kwh";




// Cria o array JSON para alimentar o gráfico em ordem decrescente
function createDataArray() {
	var foo = "[";
	//acessa os 30 últimos valores
	var val = new com.serotonin.mango.db.dao.PointValueDao();
	var val = val.getLatestPointValues(point.id, reading_number);
	//ajusta o tamanho do loop for
	var size = val.size() - 1;

	for (var i = size; i >=0; i--) {
		var time = val.get(i).time;
		var value = val.get(i).value;
		var dia = new Date(time).getDate();
		var mes = new Date(time).getMonth()+1;
		var ano = new Date(time).getFullYear();
		var data = dia.toString()+"/"+mes.toString()+"/"+ano.toString();

		if (i > 0) {
			foo += "'" + data + "'," + value + "], [";
		}
        else foo += "'" + data + "'," + value;
	}
	
	foo += "],";
	return foo;
}



var s = "" ;
s+=" <script type=\"text/javascript\"> ";
s+="       google.charts.load('current', {'packages':['corechart']}); ";
s+="       google.charts.setOnLoadCallback(drawChart); ";
s+="       function drawChart() { ";
s+="         var data = google.visualization.arrayToDataTable([ ";
s+="           ['ts', 'kWh'], " + createDataArray() + "]); ";
s+="         var options = {";
s+="            chartArea:{left:35, top:25,width:'90%',height:'80%'}, backgroundColor: '"+ bg_color +"', ";
s+="            colors: ['" + graph_color + "'],";
s+="            title: '"+ titulo_grafico +"', titleTextStyle: {color: '"+ text_color +"', fontSize: "+ tamanho_fonte + "},";
s+="            hAxis: { textStyle: { color: '"+ bg_color +"', fontSize: "+ tamanho_fonte +", bold: false }, gridlines: {color: '"+ gridlines_color +"'}, baselineColor: '"+ gridlines_color +"' },";
s+="            vAxis: { gridlines: {color: '"+ gridlines_color +"'}, baselineColor: '"+ gridlines_color +"', ";
s+="            textStyle: { color: '"+ text_color +"', fontSize: "+ tamanho_fonte +", bold: false } }";
s+="          }; ";
s+="         var chart3 = new google.visualization.ColumnChart(document.getElementById('" + id_chart + "')); ";
s+="         chart3.draw(data, options); ";
s+="       } ";
s+="     </script> ";
s+="     <div id=" + id_chart + " style=\"width: "+ largura +"; height: "+ altura +";\"></div>";
return s;

//DEBUG: VALIDAR O QUE ESTÁ SENDO MONTADO PELA FUNÇÃO createDataArray()
//return "<b><font size=4 face=Arial color=#f0eee9>" +  createDataArray() + "</font>";
1 curtida

Uma dica para gerar um input de data que seja amigável para o usuário e que ao mesmo tempo converta “por baixo dos panos” para o formato yyyy-mm-dd é utilizar a biblioteca Flatpickr. Eu particularmente gosto bastante desta biblioteca na parte de gerar calendários para escolhas de data/hora.

1 curtida

@Celso mais uma vez salvando o dia! rs

Apanhei um pouco aqui pra conseguir implementar um exemplo, mas deu certo! Vou compartilhar como ficou esse ajuste de seleção de datas.

A lib do flatpickr precisa ser iniciada na página para que o calendário apareça certinho. Usando um código simples de teste em um elemento HTML funciona na hora, segue código de exemplo HTML:

<head>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
  <script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
</head>

<div class="group">
    <label class="input-label">Data inicial:</label>
    <input type="text" id="data-inicial-graf">
</div>

<script>
    flatpickr("input[type=text]", {});
</script> 

A questão é que, na hora de transpor para um script para o servidor, mesmo colocando esse cabeçalho <head> na string de retorno, por algum motivo o Scada-LTS não consegue chamar a lib e aí não funciona.

Pra contornar isso eu criei um elemento HTML na tela pra chamar a lib (código a seguir):

<head>
	<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
	<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
</head>

E então chamei um elemento script para servidor para receber o input (código a seguir), aí então que deu certo!

var s = "";

s += "<head>";
s += "  <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css\">";
s += "  <script src=\"https://cdn.jsdelivr.net/npm/flatpickr\"></script>";
s += "</head>";

s += "<label for='data-final'>Data TESTE: </label>";
s += "<input type=\"text\" id=\"data-teste\" placeholder=\"yyyy-mm-dd\" onChange='mango.view.setPoint("+ point.id +", \""+ pointComponent.id +"\", this.value);return false;' value=" + renderedText + ">";

s += "<script>";
s += "    flatpickr(\"input[type=text]\", {});";
s += "</script>";

return s;

Resultado em tela dessas aplicações:

Posso estar deixando algum detalhe besta passar batido, mas foi a maneira que consegui pra resolver o problema :sweat_smile:

Obrigado pela sugestão, Celso!

1 curtida