Visualização de Dados com F#
F#
Olá pessoa!
Já faz um tempo desde o último post sobre F# com Type Providers, hoje vamos fazer mais um! Dessa vez focando um pouco mais na visualização dos dados.
Este post faz parte de uma série sobre Type Providers! Para visualizar a série inteira clique aqui
No post de hoje usaremos os providers para o world bank e para requisições HTTP com o retorno em JSON.
Para seguir, é recomendável que você leia os posts sobre os dois type providers citados:
Como em toda utilização de providers, precisaremos do pacote FSharp.Data
, vamos instalá-lo a partir do nuget:
PM> Install-Package FSharp.Data
No post de hoje, vamos mostrar a visualização dos resultados de forma mais rica que apenas uma saída em console, então vamos aproveitar um pouco do Google Charts
. Para isso, basta instalarmos seu pacote:
PM> Install-Package XPlot.GoogleCharts
Com tudo instalado, já podemos pensar em nossa implementação.
Coletando dados globais
A ideia para este post é buscarmos informações dos países ao redor do mundo, vamos trabalhar com dois tipos de informação: Emissão de CO2 e Temperatura.
Vamos usar esses dois indicadores, buscando-os de fontes diferentes. E nos dois casos vamos gerar um HTML com o mapa mundi do gráfico.
Vamos começar com a emissão de CO2, para isso, vamos utilizar os indicadores do World Bank, como fizemos no post citado anteriormente.
Vamos começar com as importações necessárias:
open FSharp.Data
open XPlot.GoogleCharts
open System
Com isso, já podemos utilizar os providers tranquilamente.
[<EntryPoint>]
let main argv =
let bancoDadosGlobal = WorldBankData.GetDataContext()
Agora vamos extrair a emissão de CO2 dos países através do indicador "CO2 emissions (metric tons per capita)"
. Para exibir o gráfico vamos utilizar o ano de 2014 como referência, mas é sempre melhor deixarmos esta configuração parametrizável.
Vamos retornar os dados em uma tupla contendo o nome do país e o indicador, conforme código:
let emissaoCO2Global
(bancoDadosGlobal: WorldBankData.ServiceTypes.WorldBankDataService)
ano =
bancoDadosGlobal.Countries
|> Seq.map( fun pais ->
pais.Name,
pais.Indicators.``CO2 emissions (metric tons per capita)``.[ano]
)
Ótimo, agora já conseguimos consumir nossa função:
[<EntryPoint>]
let main argv =
let bancoDadosGlobal = WorldBankData.GetDataContext()
emissaoCO2Global bancoDadosGlobal 2014
|> Seq.iter (fun pais -> Console.WriteLine(pais))
Console.ReadKey() |> ignore
Com isso já temos o resultado:
Agora é a hora de transformarmos isso em um gráfico bonitão!
Construindo a Visualização dos Dados
A função para obter o gráfico é bastante simples, mesmo esperando diversos parâmetros. A ideia aqui é exibirmos um mapa mundi colorindo os países de acordo com a quantidade de emissão, por exemplo, caso o país emita pouco, pintaremos ele de verde, caso emita muito, de vermelho.
Para isso, precisamos de um array definindo os valores de emissão para cada cor, e outro array para informarmos o hexadecimal de cada cor. Além disso, também precisaremos informar a descrição do tooltip do mapa e os valores propriamente ditos.
Isso nos dá uma assinatura relativamente grande:
let obterGrafico cores valoresEixo descricao (valores: seq<string * float>) =
...
Vamos quebrar isso em apenas dois parâmetros diferentes: configurações do gráfico e valores.
Para isso, vamos criar um novo tipo:
type ConfiguracoesGrafico = {
CoresEixo : string array
ValoresEixo : int array
Descricao : string
}
E vamos alterar a assinatura do método:
let obterGrafico configuracoes (valores: seq<string * float>) =
...
Agora precisamos criar o gráfico propriamente dito, para isso, precisamos criar o eixo de cores (ColorAxis) com os valores da configuração. Por fim usaremos as funções disponíveis em Chart
, para gerarmos o gráfico e retornar o HTML, conforme código:
let obterGrafico configuracoes (valores: seq<string * float>) =
let eixo = ColorAxis(values = configuracoes.ValoresEixo, colors = configuracoes.CoresEixo)
let chart =
valores
|> Chart.Geo
|> Chart.WithOptions(Options(colorAxis=eixo))
|> Chart.WithLabel configuracoes.Descricao
chart.GetHtml()
Vamos voltar para a função main
e criar a configuração do nosso gráfico. Vamos dividir os valores da seguinte maneira:
- Emissão baixa(valores de 0 até 5) na cor verde (#98f442);
- Emissão média (valores de 5 até 10) na cor amarela (#f1f441);
- Emissão alta (valores de 10 até 20) na cor laranja (#efaf39);
- Emissão muito alta (valores acima 20) na cor vermelha (#ef5a39);
let configuracoesCO2 = {
CoresEixo = [| "#98f442";"#f1f441";"#efaf39";"#ef5a39" |]
ValoresEixo = [| 0;+5;+10;+20 |]
Descricao = "Emissão CO2"
}
Agora já conseguimos obter a string contendo o HTML do gráfico:
emissaoCO2Global bancoDadosGlobal 2014
|> obterGrafico configuracoesCO2
Precisamos utilizar esta string para escrever um arquivo HTML, faremos isso utilizando a classe System.IO.File
do .NET.
open System.IO
let escreverArquivoHtml html =
File.AppendAllLines ("map.html",[html])
Agora podemos concluir a função main
, gerando o arquivo HTML:
let main argv =
let bancoDadosGlobal = WorldBankData.GetDataContext()
let configuracoesCO2 = {
CoresEixo = [| "#98f442";"#f1f441";"#efaf39";"#ef5a39" |]
ValoresEixo = [| 0;+5;+10;+20 |]
Descricao = "Emissão CO2"
}
emissaoCO2Global bancoDadosGlobal 2014
|> obterGrafico configuracoesCO2
|> escreverArquivoHtml
0
Acessando o diretório da aplicação (“\bin\debug\netcoreapp2.1”) você encontrará o arquivo HTML gerado, ao abrir você encontrará o gráfico:
Legal né? E o gráfico é interativo, permitindo visualizar os dados de cada país individualmente.
Mas vamos facilitar um pouco nosso trabalho, que tal fazermos uma chamada ao sistema operacional para que ele abra o navegador com o gráfico gerado?
É bem simples fazer isso, basta iniciarmos o processo do navegador informando o diretório do arquivo. Para iniciar um processo precisamos utilizar o namespace System.Diagnostics
, então, vamos dar um open nele antes de partir para o código.
open System.Diagnostics
Agora basta utilizarmos a função Process.Start
, informando os parâmetros. No meu caso usarei o navegador Google Chrome:
//...
emissaoCO2Global bancoDadosGlobal 2014
|> obterGrafico configuracoesCO2
|> escreverArquivoHtml
Process.Start (@"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
"file:\\" + Directory.GetCurrentDirectory() + "\\map.html")
|> ignore
Pronto! Se executarmos novamente o código, o navegador será aberto automaticamente!
Obtendo Dados de um Serviço
Agora vamos mostrar o mesmo tipo de gráfico, só que desta vez unindo informações do World Bank com um outro serviço (também aberto). Vamos usar o openweather, é possível utilizá-lo de forma gratuita, mas é necessário o registro.
Faça o registro no site e gere um App Key, você precisará dela para fazer as requisições.
Com a App Key em mãos podemos começar a implementação.
Primeiro usaremos o JsonProvider
para gerar o tipo de forma dinâmica:
[<Literal>]
let urlBase =
"http://api.openweathermap.org/data/2.5/forecast?appid={SUA APP KEY}&units=metric&q="
type Clima =
JsonProvider<"http://api.openweathermap.org/data/2.5/forecast?appid={SUA APP KEY}&units=metric&q=London,UK">
Com o tipo definido já podemos criar a função que irá buscar a temperatura atual através desta API. Essa função terá de receber um local por parâmetro, com o formato indicado no JsonProvider
: "London,UK"
.
Por enquanto vamos assumir que já receberemos o dado formato, portanto teremos apenas um parâmetro chamado local
, conforme código:
let obterTemperatura local =
//...
A função é bastante simples, precisamos utilizar a função Load
do tipo Clima
, conforme já vimos no post sobre JsonProvider
. O retorno de nossa chamada contém a previsão dos próximos cinco dias, mas vamos usar apenas a previsão do dia seguinte.
Então basta obtermos a temperatura (Main.Temp
) do primeiro valor da lista (head
), conforme código:
let obterTemperatura local =
let clima = Clima.Load(urlBase + local)
let amanha = Seq.head clima.List
(float) amanha.Main.Temp
Note que estamos fazendo um cast do tipo decimal
para o tipo float
, isso é feito apenas para compatibilidade com nossa função geradora do gráfico criada anteriormente.
Aqui temos um pequeno problema, existem casos onde a descrição retornada do World Bank não é compatível com a descrição esperada nesta API. Apesar disso acontecer poucas vezes, precisamos realizar um tratamento, caso contrário nosso código irá quebrar.
Mesmo sendo bastante contra o uso de exceções, vamos utilizá-las por aqui. Caso o Load
do type provider falhe, iremos simplesmente retornar zero. Faremos isso capturando a exceção WebException
, conforme código.
let obterTemperatura local =
try
let clima = Clima.Load(urlBase + local)
let amanha = Seq.head clima.List
(float) amanha.Main.Temp
with :? System.Net.WebException as ex ->
0.0
Não se preocupe em causar distorções no mapa, afinal, esses valores acabam sendo ignorados no mapa e não são pintados de nenhuma cor.
Agora que já temos a função que obtém a temperatura de um local específico precisamos criar a função que realiza múltiplas chamadas dela. Dessa forma, teremos a temperatura de vários locais.
Vamos utilizar a capital de cada país como referência, conforme código:
let temperaturaGlobal
(bancoDadosGlobal: WorldBankData.ServiceTypes.WorldBankDataService) =
bancoDadosGlobal.Countries
|> Seq.map( fun pais ->
pais.Name,
obterTemperatura (pais.CapitalCity + "," + pais.Name)
)
Simples né?
Refatorando o Uso dos Indicadores
Se você é atento, deve ter notado que a função temperaturaGlobal
e a função emissaoCO2Global
são praticamente idênticas. A única coisa que está diferente é o indicador retornado, veja:
let emissaoCO2Global
(bancoDadosGlobal: WorldBankData.ServiceTypes.WorldBankDataService)
ano =
bancoDadosGlobal.Countries
|> Seq.map( fun pais ->
pais.Name,
pais.Indicators.``CO2 emissions (metric tons per capita)``.[ano]
)
let temperaturaGlobal
(bancoDadosGlobal: WorldBankData.ServiceTypes.WorldBankDataService) =
bancoDadosGlobal.Countries
|> Seq.map( fun pais ->
pais.Name,
obterTemperatura (pais.CapitalCity + "," + pais.Name)
)
Vamos refatorá-las e transformar a função que obtém o indicador em um parâmetro:
let obterIndicadorGlobal
(bancoDadosGlobal: WorldBankData.ServiceTypes.WorldBankDataService)
indicador =
bancoDadosGlobal.Countries
|> Seq.map( fun pais ->
pais.Name,
indicador pais
)
Agora com a função obterIndicadorGlobal
podemos informar a função no parâmetro indicador
, veja:
let emissaoCO2Global
(bancoDadosGlobal: WorldBankData.ServiceTypes.WorldBankDataService)
ano =
obterIndicadorGlobal bancoDadosGlobal
(fun pais -> pais.Indicators.``CO2 emissions (metric tons per capita)``.[ano])
let temperaturaGlobal
(bancoDadosGlobal: WorldBankData.ServiceTypes.WorldBankDataService) =
obterIndicadorGlobal bancoDadosGlobal
(fun pais -> obterTemperatura (pais.CapitalCity + "," + pais.Name))
Finalizando
Por fim, teremos que criar as configurações do gráfico de temperatura e adicionarmos ele ao HTML gerado. Para as configurações utilizaremos as seguintes cores e valores:
- Temperaturas muito baixas (valores até -20) na cor azul clara (#d8fffc);
- Temperaturas baixas (valores de -20 até 0) na cor azul (#7badfc);
- Temperaturas um pouco baixas (valores de 0 até 15) na cor verde (#98F442);
- Temperaturas agradáveis (valores de 15 até 30) na cor amarela (#f1f441);
- Temperaturas altas (valores de 30 até 40) na cor vermelho (#ef5a39);
- Temperaturas muito altas (valores até 60) na cor vermelho forte (#ff3916);
let configuracoesTemperatura ={
CoresEixo = [| "#d8fffc";"#7badfc"; "#98f442";"#f1f441";"#ef5a39";"#ff3916" |]
ValoresEixo = [| -20; 0;+15;+30;+40;+60 |]
Descricao = "Temperatura"
}
Agora é só fazermos o mesmo procedimento para gerar o gráfico e adicionar ao HTML:
temperaturaGlobal bancoDadosGlobal
|> obterGrafico configuracoesTemperatura
|> escreverArquivoHtml
Executando novamente, teremos o HTML com os dois gráficos!
Atenção
Você pode fazer o download da página HTML com os gráficos aqui
Os demais gráficos disponíveis fica para um post no futuro, quem sabe até com alguma implementação de machine learning!
Qualquer dúvida ou sugestão, deixem nos comentários!
E até mais.
Sempre vale lembrar que as informações e textos aqui no blog representam minha opinião pessoal, o que pode não ser igual à sua ou de qualquer outra pessoa, incluindo a empresa para qual eu trabalho. Portanto as publicações inseridas aqui estão relacionadas somente a mim.