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.
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)
O Plotchart faz parte do metapacote Tklib e portanto para instalar o pacote basta o comando:
#
apt-get install tklib
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.
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
canvas - nome do widget canvas que irá acomodar o gráfico
eixo_x - lista com 3 elementos contendo: mínimo, máximo e intervalo de cada marcação do eixo x (nessa ordem). Para um eixo em ordem decrescente colocar (nessa ordem): máximo, mínimo e intervalo de cada marcação (com sinal negativo).
eixo_y - lista com 3 elementos contendo: mínimo, máximo e intervalo do eixo y (nessa ordem). Para um eixo em ordem decrescente colocar (nessa ordem): máximo, mínimo e intervalo (com sinal negativo).
opções - zero ou mais opções que determinam a forma de exibição do gráfico:
-xlabels {lista_de_labels} labels (rótulos ou etiquetas) que são exibidos nas marcações do eixo x substituindo as marcações automáticas. Mas são funcionam se for omitido o intervalo de cada marcação no eixo x. Se os labels forem numéricos serão posicionados de acordo com a escala dada, caso contrário serão posicionados em espaços equidistantes com base no número de labels.
-ylabels {lista_de_labels} da mesma forma que xlabels, são labels personalizados para o eixo y.
Veja outras opções disponíveis na seção https://core.tcl.tk/tklib - Plotchart
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 é:
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"
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.
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
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 [...”.