Q. Plotchart - Gráficos com Tcl/Tk

Nesta seção vamos compartilhar algumas informações sobre o uso da biblioteca Plotchart.

Plotchart é uma biblioteca totalmente escrita em Tcl/Tk pelo desenvolvedor Arjen Markus com o objetivo de facilitar a criação de gráficos XY, de barra e outros tipos de gráficos mais comuns.

Nota

Em programação há sempre um compromisso entre facilidade de uso e flexibilidade. Ou seja, para facilitar a vida do usuário é necessário simplificar e reduzir as opções de uso. E nesse caso podemo dizer que o Plotchart prioriza a facilidade em detrimento da flexibilidade.

Algumas referências: ActiveTcl User Guide - Plotchart, https://core.tcl.tk/tklib - Plotchart e http://wiki.tcl.tk/11265.

A figura Q.1 mostra alguns exemplos de gráficos produzidos com Plotchart.

Figura Q.1. Alguns exemplos de gráficos produzidos com o uso do pacote Plotchart. (Fonte: Plotchart gallery)

Alguns exemplos de gráficos produzidos com o uso do pacote Plotchart. (Fonte: Plotchart gallery)

O Plotchart faz parte do metapacote Tklib e portanto para instalar o pacote basta o comando:

# apt-get install tklib

Dica

Após a instalação você encontrará vários exemplos de uso da biblioteca no diretório /usr/share/doc/tklib/examples/plotchart/.

Podemos então criar um gráfico XY simples com os comandos:

#!/usr/bin/env wish

package require Plotchart

#Cria o widget canvas onde será construído o gráfico

canvas .c -background white -width 400 -height 200
pack   .c -fill both

# Cria o gráfico com os eixos x e y

set s [::Plotchart::createXYPlot .c {0.0 100.0 10.0} {0.0 100.0 20.0}]

# Loop foreach que executa o comando "plot" para cada par xy

foreach {x y} {0.0 32.0 10.0 50.0 25.0 60.0 78.0 11.0 } {
    $s plot series1 $x $y
}

# Define o nome do gráfico

$s title "Gráfico XY simples"

O código acima produz o gráfico da figura Q.2.

Figura Q.2. Gráfico XY obtido com apenas 6 linhas.

Gráfico XY obtido com apenas “6” linhas.

Primeiro é criado um widget canvas que irá exibir o gráfico:

canvas .c -background white -width 400 -height 200

Este comando cria o widget canvas com o nome .c e as opções de fundo branco (-background white), largura de 400 pixels (-width 400) e altura de 200 pixels (-height 200).

E o comando pack .c, usa o gerenciador de posição pack para exibir o widget .c expandindo o widget em x e y nos limites disponíveis (-fill both).

O comando ::Plotchart::createXYPlot é um comando da biblioteca Plotchart para a criação de um gráfico XY e apresenta a seguinte estrutura:

::Plotchart::createXYPlot canvas eixo_x eixo_y opções

Por exemplo, se quisermos substituir os labels do eixo x basta editarmos a linha:

    ...
    set s [::Plotchart::createXYPlot .c {0.0 100.0 {}} {0.0 100.0 20.0} -xlabels {"mínima" "média" "máxima"}]
    ...
  

E o resultado é:

Figura Q.3. Alteração nos labels do eixo x com o uso da opção xlabels.

Alteração nos labels do eixo x com o uso da opção xlabels.

Um outro tipo de gráfico muito útil para o monitoramento contínuo de uma variável é o stripchart. A diferença básica do stripchart em relação a um gráfico XY é que o eixo x será automaticamente ajustado quando a coordenada x de um novo ponto exceder o limite máximo.

Por exemplo o código seguinte gera pares xy variando x em incrementos de 15 unidades e y variando aleatoriamente. (Fonte: plotdemo2.tcl)

#!/usr/bin/env wish

package require Plotchart

#Cria o widget canvas onde será construído o gráfico

canvas .c -background white -width 400 -height 200
pack   .c -fill both

set s [::Plotchart::createStripchart .c {0.0 100.0 20.0} {0.0 100.0 20.0}]

proc gendata {slipchart xold xd yold yd} {
   set xnew [expr {$xold+$xd}]
   set ynew [expr {$yold+(rand()-0.5)*$yd}]
   $slipchart plot series1 $xnew $ynew

   if { $xnew < 200 } {
   after 500 [list gendata $slipchart $xnew $xd $ynew $yd]
   }
}

after 100 [list gendata $s 0.0 15.0 50.0 30.0]

$s title "Monitoramento Contínuo"

Q.1. Exibindo Leituras de um Pluviômetro

Vamos usar como exemplo os passos iniciais para a exibição das leituras de um pluviômetro.

Neste projeto utilizamos uma placa Arduino para realizar as leituras do número de descargas do reservatório de um pluviômetro do tipo báscula.

A documentação completa deste projeto está disponível na seção www.c2o.pro.br/proj/pluviometro.

Implementamos o código no Arduino para permitir a comunicação serial com o PC pelo envio de mensagens com o formato: [instrumento];[valor];[unidade].

Onde [instrumento] é o nome do instrumento (pluviômetro), [valor] é um número inteiro que corresponde ao número de descargas (escoamento) do reservatório e [unidade] (D) é a unidade da leitura. No caso do pluviômetro criamos uma unidade arbitrária "D" que significa Descarga (Discharge) do reservatório do pluviômetro.

Para permitir a visualização de dados usamos a função random(min, max) no Sketch do Arduino para gerar números inteiros aleatórios no intervalo definido por min e max.

O código do Arduino usado apenas para demonstrar os funcionamento da biblioteca Plotchart ficou:

  /*
  Pluviometro OO
  */
  
int reed_switch_pin = 8;
boolean last_reed_switch_state = LOW;
unsigned long tc = millis();
unsigned long ts = millis();
int clicks;
float volume;

void setup() {
  // initialize serial communication at 9600 bits per second:
  Serial.begin(9600);

  pinMode(reed_switch_pin, INPUT);
}

void loop() {
  unsigned long int_click = millis() - tc;
  
  if ( int_click > 50 ) {
    readPin();
  } 
  
  unsigned long int_send = millis() - ts;
  
  if ( int_send > 5000 ) {
    sendData();
  }

//Outras futuras atividades

}

void readPin () {
  
  int reed_switch_state = digitalRead(reed_switch_pin);

  if (last_reed_switch_state == LOW && reed_switch_state == HIGH) {
    clicks++;
//    Serial.print("Click - ");
//    Serial.println(clicks);
  }
  last_reed_switch_state = reed_switch_state;
  tc = millis();    
}

//7.02 é o volume de cada báscula medido com o frasco de Mariotte
void sendData () {
  volume = clicks * 7.02;
  
//  Serial.print("Clicks em 10 segundos - ");
//  Serial.println(clicks);
//  Serial.print("Volume em 10 segundos - ");
//  Serial.println(volume);
  clicks = random(0, 10);
  Serial.print("pluviometro;");
  Serial.print(clicks);
  Serial.println(";D");
  clicks = 0;
  ts = millis();
}

Com esse código a placa Arduino envia a cada 5 segundos uma mensagem com 3 campos separados por ";" do tipo: pluviometro;5;D

Para fazer a leitura dessas mensagens e exibir em um gráfico XY fizemos um código inicial simplificado com as funcionalidades essenciais para uma demonstração:

#!/bin/sh
#A proxima linha reinicia usando wish \
    exec wish "$0" "$@"

package require Plotchart

#Cria o canvas para exibir o gráfico
set canvas_grafico [canvas .c -background white -width 450 -height 200]

#Exibe o canvas com o gerenciador de posição pack
pack $canvas_grafico

#Cria o gráfico XY com os limites de 0 a 60 e intervalos de 5 unidades no eixo X
#e limites de 0 a 10 e intervalo de 1 unidade no eixo Y
set grafico_pluviometro [::Plotchart::createXYPlot $canvas_grafico {0 60 5} {0 10 1}]

#Procedimento para criar um canal de comunicação serial
proc criarCanal { } {
    
    global canal tempo_inicial

    #cria o canal
    set canal [ open /dev/ttyACM0 r+ ]

    #configura os parâmetros de comunicação
    fconfigure $canal -mode 9600,n,8,1

    #configura para não esperar caractere de final de linha
    #e não bloquear a execução do procedimento
    fconfigure $canal -blocking 0

    #determina o instante inicial do monitoramento
    set tempo_inicial [clock seconds]

    #associa a chamada da rotina lerCanal quando houver dados para leitura no canal
    fileevent $canal readable [list lerCanal $canal]

}

#Procedimento que é executado sempre que houver dados para leitura na porta serial
proc lerCanal { canal } {
    
    global tempo_inicial grafico_pluviometro

    #armazena na variável leitura a mensagem no canal
    set leitura [gets $canal]

    #verifica se os dados enviados são caracteres não imprimíveis (lixo)
    if {$leitura == ""} { return }

    #determina o instante da leitura
    set tempo_final [clock seconds]

    #calcula o intervalo da leitura
    set tempo_leitura [expr $tempo_final - $tempo_inicial]

    #verifica se o canal foi encerrado
    if { [eof $canal ] } {
	   puts stderr "Fechando $canal"
	   catch { close $canal }
	   return }

    #converte o conteúdo da variável leitura em uma lista
    #usando o caractere ; como delimitador	   
    set lista_leitura [split $leitura ";"]

    #extrai o segundo elemento da lista
    set N [lindex $lista_leitura 1]

    #comando para exibir as leituras no gráfico
    $grafico_pluviometro plot serie_leitura $tempo_leitura $N
    
    #força a atualização do gráfico na tela
    update
}

criarCanal

update

Foi possível visualizar o gráfico da figura Q.4.

Figura Q.4. Exemplo de gráfico simples exibindo leituras aleatórias enviadas pela placa Arduino.

Exemplo de gráfico simples exibindo leituras aleatórias enviadas pela placa Arduino.

Gastei algum tempo até perceber que a placa Arduino envia alguns caracteres não imprimíveis que disparam a chamada do procedimento lerCanal. Por isso foi necessário incluir o teste if {$leitura == ""} { return } logo no início do procedimento, mas também foi necessário incluir a opção fconfigure $canal -blocking 0 no procedimento criarCanal.

Podemos usar o stripchart, um tipo de gráfico XY onde o eixo horizontal é ajustado automaticamente para valores que ultrapassam o limite original, mantendo a amplitude dos dados no eixo X.

Basta modificar a linha de criação do gráfico substituindo ::Plotchart::createXYPlot por ::Plotchart::createStripchart.

  .
  .
  .
  set grafico_pluviometro [::Plotchart::createXYPlot $canvas_grafico {0 60 5} {0 10 1}]
  .
  .
  .

Outra opção é o gráfico de barras (Barchart), ou gráfico de colunas, e consiste em um gráfico com barras retangulares e comprimento proporcional aos valores que ele representa. As barras podem ser desenhadas verticalmente ou horizontalmente.

Formato do comando para Barchart:

::Plotchart::createBarchart w xlabels yaxis noseries args

Onde>

  • w - nome do canvas que irá exibir o gráfico

  • xlabels - lista dos identificadores (labels) para o eixo x

  • yaxis - lista com 3 elementos contendo: mínimo, máximo e intervalo do eixo y (nessa ordem)

  • noseries e args - opções adicionais que determinam a forma de exibição do gráfico (Ver ::Plotchart::createBarchart)

Código para exibição das leituras em gráfico de barras (Barchart) e o resultado na figura Q.5:

#!/bin/sh
#A proxima linha reinicia usando wish \
    exec wish "$0" "$@"

package require Plotchart


#Incluindo cálculo do volume e legendas com unidades

#Volume do reservatório V = 7.02 mL = 7,02 cm³ = 7020 mm³
#Área de coleta do pluviômetro A = 165,13 cm² = 16513 mm²
#Volume do reservatório em mm (V_mm) = V / A = 0.425 mm

set V_mm 0.425

set canvas_grafico [canvas .c -background white -width 800 -height 400]

pack $canvas_grafico

set list_xlabels {60 55 50 45 40 35 30 25 20 15 10 5 0}

set list_yaxis {0 0 0 0 0 0 0 0 0 0 0 0 0}

set count_loop 0

set grafico_pluviometro [::Plotchart::createBarchart $canvas_grafico $list_xlabels {0 5 1} 2.5]

$grafico_pluviometro xtext "Tempo (s)"

$grafico_pluviometro ytext "Volume (mm)"

puts "grafico_pluviometro: $grafico_pluviometro"

$grafico_pluviometro plot serie_leitura $list_yaxis blue

proc criarCanal { } {
    
    global canal tempo_inicial
    
    set canal [ open /dev/ttyACM0 r+]
	    
    fconfigure $canal -mode 9600,n,8,1

    fconfigure $canal -blocking 0
    
    set tempo_inicial [clock seconds]

    fileevent $canal readable [list lerCanal $canal]

    puts "criarCanal: canal -> $canal tempo_inicial $tempo_inicial"
}

proc lerCanal { canal } {
    
    global tempo_inicial

    global canvas_grafico grafico_pluviometro

    global list_xlabels list_yaxis

    global V_mm
    
    set tempo_final [clock seconds]

    set tempo_leitura [expr $tempo_final - $tempo_inicial]

    puts "tempo_inicial: $tempo_inicial tempo_final: $tempo_final tempo_leitura: $tempo_leitura"

    set leitura [gets $canal]

    if {$leitura == ""} { return }
    
    if { [eof $canal ] } {
    puts stderr "Fechando $canal"
    catch { close $canal }
    return }
    
    set lista_leitura [split $leitura ";"]
    
    set N [lindex $lista_leitura 1]
    
    set V_total_mm [expr $N * $V_mm]
    
    lappend list_yaxis $V_total_mm
    
    set new_list_yaxis [ lrange $list_yaxis 1 end ]
       
    set list_yaxis $new_list_yaxis

    $canvas_grafico delete all

    set grafico_pluviometro [::Plotchart::createBarchart $canvas_grafico $list_xlabels {0 5 1} 1]
       
    $grafico_pluviometro xtext "Tempo (s)"

    $grafico_pluviometro vtext "Volume (mm)"

    $grafico_pluviometro plot serie_leitura $new_list_yaxis blue
       
    update
}

criarCanal

update

Figura Q.5. Exemplo de gráfico de barras exibindo leituras aleatórias enviadas pela placa Arduino.

Exemplo de gráfico de barras exibindo leituras aleatórias enviadas pela placa Arduino.

O programa funcionou mas aparecia um erro na centésima atualização com a seguinte mensagem:

  bad window path name "0.c"
bad window path name "0.c"
    while executing
"winfo width $w"
    (procedure "WidthCanvas" line 13)
    invoked from within
"WidthCanvas $w"
    (procedure "MarginsRectangle" line 75)
    invoked from within
"MarginsRectangle $w $args"
    (procedure "::Plotchart::createBarchart" line 21)
    invoked from within
"::Plotchart::createBarchart $canvas_grafico $list_xlabels {0 10 1} 1.5"
    (procedure "lerCanal" line 50)
    invoked from within
    "lerCanal file6"

Para tentar identificar e corrigir o erro incluí algumas saídas (tracer) nos arquivos plotchart.tcl (::Plotchart::createBarchart) e plotpriv.tcl (::Plotchart::widthcanvas, ::Plotchart::MarginsRectangle e NewPlotInCanvas), para visualizar o status de algumas variáveis.

Para tentar remediar o problema do limite de 100 atualizações, comentei a linha:

incr scaling($c,plots)

no procedimento ::Plotchart::NewPlotInCanvas do arquivo plotpriv.tcl.

Mas esse procedimento é usado por diversos outros procedimentos dentro de plotchart.tcl.

Mais tarde, tentando uma solução mais adequada, descomentei a linha de incremento (incr scaling($c,plots)) e resolvi alterar os procedimentos que extraem uma substring a partir de uma string nos seguintes procedimentos:

O comando original (set c [string range $w 2 end]) extraía uma substring c a partir do segundo caracter da string w. Até 99 atualizações esse processo funcionava (99.c -> .c) mas a partir de 100 atualizações (100.c -> 0.c) a substring incluía um número antes do ponto.

Usamos o comando [string first "." $w] para obter a posição do caracter "." na string e usar esse índice para extrair corretamente a substring de w correspondente ao widget canvas.

Também alterei o procedimento ::Plotchart::WidthCanvas no arquivo plotpriv.tcl:

#Identifica a posição do ponto na string w
    #para extrair apenas a partir do ponto e armazenar
    #na variável c
     set position_point [string first "." $w]
    
    if { [string match {[0-9]*} $w] } {
	#  set w [string range $w 2 end]
	set w [string range $w $position_point end]
    }

E o procedimento ::Plotchart::DrawMask no arquivo plotpriv.tcl.

Todos esses arquivos estão no diretório /usr/share/tcltk/tklib0.6/plotchart.

Também pedi ajuda na lista comp.lang.tcl e foi feita a mesma sugestão ou então baixar os arquivos mais atualizados no link https://core.tcl.tk/tklib/dir?ci=tip clicando no link que vem depois de "Files of check-in [....