Dashboards com Python (Parte 3)

24 minute read

Desenvolvendo gráficos com matplotlib, Plotly e Dash (Parte 3 de 3)!

Olá 😀!

Neste texto vou dar sequência a criação de Dashboards com Python,onde vou estudar como gerar Dashboards com Dash. Na Parte 1 preparei os dados e criei os gráficos com o matplotlib. Já na Parte 2 criei os gráficos com o Plotly.

Você pode baixar o notebook construído até aqui neste link e a planilha com os dados neste outro link.

Introdução

Agora finalmente vamos construir o Dashboard. Dashboards são planilhas interativas que o usuário pode alterar algumas propriedades de forma bem simples, mantendo a interatividade. O que vou fazer aqui é um Dashboard para um bioma, e o usuário irá poder trocar de bioma utilizando um dropdown. O potencial aqui é muito grande, mas é necessário ter um servidor rodando Python para que ele funcione. Por isso o Dashborad será hospedado no heroku (plataforma gratuita até certo ponto) que permite rodar Python online. Mas como eu utilizo esta plataforma para testes, o dashboard não ficará disponível por muito tempo. Mas os códigos e os prints vão estar aqui, não se preocupe.

Então vamos lá!

Para começar precisamos importar o Dash, juntamente com os componentes de html, os core, e as dependências de Input e Output:

import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output

O dash funciona justamente com tags de html, onde geramos uma tag no dash que vai fazer a função que a mesma tag faria no HTML. Então temos os Div, o H1, H2, …, H6, p, etc. Dai, vamos criando uma aplicação com diversos blocos (Divs) que contém os elementos (gráficos, tabelas, texto, etc). É importante ter em mente como é a estrutura da aplicação, pois facilita ter um rumo na vida. Além das divisões com os elementos, podemos utilizar funções que ficam “ouvindo” o Dashborad o tempo todo esperando por alterações. Quando a alteração é feita pelo usuário, a função é chamada e ocorre a mudança no Dashboard. Vou utilizar esta função para trocar os dados de um bioma para o outro.

A primeira coisa é ter um rumo na vida:

Figura 1 - Esquema inicial do Dashboard desenhado a mão.

A figura mostra um esquema que contém um titulo, seguido de um dropdown, um gráfico de dispersão, um gráfico de barras e um gráfico de pizza

A ideia inicial é ter um titulo, um dropdown (para selecionar o bioma), e três gráficos, um abaixo do outro.

A estrutura mínima que precisamos montar um Dashboard com Dash é esta:

  • Criar uma instância da aplicação:
    app = dash.Dash()
    
  • Criar um layout (é aqui que vai todos os blocos):
    app.layout = html.Div([])
    
  • Rodar o servidor (é necessário ter um servidor rodando por trás):
    if __name__ == '__main__':
      app.run_server(debug=True, use_reloader=False) # os parâmetros são para facilitar o desenvolvimento, mas devem ser removidos nos finalmentes</code></pre>
    

Titulo do Dashboard

Para começar, vou criar apenas o titulo do Dashboard. Basta apenas adicionar um elemento H1 ao elemento Div inicial:

app = dash.Dash() # Criando a aplicação

# Criando o layout
app.layout = html.Div([
    html.H1("Esse é o titulo do Dashboard") # adicionando um titulo
])
# Rodando a aplicação através de um servidor
if __name__ == '__main__':
    app.run_server(debug = True, use_reloader = False)

Figura 2 - Dashboard com apenas o título.

Print do dashboard gerado com apenas um titulo

Mas o título sozinho dessa forma não fica bom. Para melhorar eu vou expandir esse titulo para um cabeçalho, que vai ter o título e um dropdown, e acima desse dropdown eu vou colocar um aviso para o usuário alterar o Bioma.

Vou começar apenas recolocando o titulo dentro de uma Div, e vou aproveitar e adicionar estilo para deixar o titulo centralizado, com outra fonte e com um espaçamento acima para que não fique grudado no início da página.

app = dash.Dash() # Criando a aplicação

# Criando o layout
app.layout = html.Div([
    # cabeçalho
    html.Div([
        # Titulo
        html.H1("Dados de focos de queimadas por Bioma no Brasil entre os anos de 1998 e 2020",
                style = {'textAlign': 'center', # alinhando o titulo ao centro
                         'fontFamily': 'Roboto', # alterando a fonte do H1
                         'paddingTop': 20}), # adicionando um padding no topo
    ])
])
# Rodando a aplicação através de um servidor
if __name__ == '__main__':
    app.run_server(debug = True, use_reloader = False)

Figura 3 - Dashboard com apenas o título modificado.

Print do dashboard gerado com apenas um titulo, mas modificado.

Dropdown

Para adicionar o dropdown basta colocar elemento de Dropdown abaixo do elemento de H1. Mas eu vou coloca-lo dentro de uma outra Div para que o estilo seja aplicado especificamente na Div onde o Dropdown está. Precisamos passar alguns parâmetros dentro do elemento de Dropdown. Dentre os diversos parâmetros posíveis, três são essenciais:

  • O parâmetro id: Este vai ser utilizado para o Dashboard saber qual valor esta selecionado.
  • O parâmetro value: Este parâmetro vai ser o parâmetro inicial, o que vai aparecer quando o usuário entrar no Dashboard.
  • O parâmetro options: Este parâmetro vai fornecer quais são as opções para o usuário escolher (neste caso, os diferentes biomas). Ele deve ser uma lista onde cada elemento da lista contém um dicionário com as chaves 'label' e 'value'. O 'label' é o que vai aparecer para o usuário escolher e deve ser um string. Já o 'value' é o valor que será utilizado nos códigos para acessar o DataFrame com os dados. Por enquanto vou utilizar apenas alguns valores gerais.

Mas eu não quero que este dropdown ocupe a tela inteira, e por isso vou adicionar um estilo a Div em que o elemento do dropdown foi criado de forma que ele ocupe apenas uma parte da tela.

app = dash.Dash() # Criando a aplicação

# Criando o layout
app.layout = html.Div([
    # cabeçalho
    html.Div([
        # Titulo
        html.H1("Dados de focos de queimadas por Bioma no Brasil entre os anos de 1998 e 2020",
                style = {'textAlign': 'center', # alinhando o titulo ao centro
                         'fontFamily': 'Roboto', # alterando a fonte do H1
                         'paddingTop': 20}), # adicionando um padding no topo
        # Dropdown
        html.Div([
            dcc.Dropdown(id = 'biome-picker', # id do dropdown
                     value = 'Amazônia', # seta o valor inicial,
                     options = [{'label': 'Amazônia', 'value': 0},
                               {'label': 'Pampa', 'value': 1},
                               {'label': 'Pantanal', 'value': 2}], # as opções que vão aparecer no dropdown
                    )
        ], style = {'width': '33%',
                            'display': 'inline-block'})
    ])
])
# Rodando a aplicação através de um servidor
if __name__ == '__main__':
    app.run_server(debug = True, use_reloader = False)

Figura 4 - Dashboard com título e dropdown.

Print do dashboard gerado com título e Dropdown.

Para concluir a parte visual do cabeçalho, falta apenas adicionar um aviso para o usuário que ele pode alterar o bioma. Para fazer isto, vou adicionar um parágrafo antes do dropdown.

# Aviso
  html.P("Selecione um bioma:",
        style = {'fontFamily': 'Roboto'}),</code></pre>

Gráfico de dispersão

Agora vou adicionar o gráfico de dispersão. Para fazer isto, vou criar uma nova Div após o parênteses da Div do cabeçalho (precisa de uma vírgula aqui). Eu vou colocar dentro dessa Div um titulo (H3) para o gráfico (não irei utilizar o titulo do próprio gráfico) e o próprio gráfico, mas vou focar apenas nesste título por enquanto.

Eu quero que este título seja atualizado com o nome do bioma toda vez que o usuário alterar o bioma. Mas para fazer isto, eu preciso adicionar uma função que fica aguardando a mudança no dropdown, e que quando o valor no dropdown for alterado, que essa função seja chamada e altere o valor dentro deste título.

Dentro deste elemento H3 eu vou adicionar apenas um id e um estilo:

html.Div([
    # Titulo do gráfico de dispersão
        html.H3(id='titulo-scatter',
                style={'textAlign': 'center',
                       'fontFamily' : "Roboto",
                       'paddingTop': 15
                      }
               )
])

Agora vou criar a função que vai ficar atualizando este H3 que tem id='titulo-scatter'. Vou chamar esta função de update_titulo_scatter(), e dentro dela eu vou passar um parâmetro qualquer (não importa o nome), que eu vou chamar de selected_biome. Essa função vai apenas retornar o texto que vai aparecer no Dashboard:

return "Número de focos de queimadas por mês no bioma: " + selected_state

Mas agora que vem o pulo do gato do Dash: o uso de decorators. Através de um decorator o Dash permite chamar a função callback() que vai fazer o papel de receber o valor atual e retornar um novo valor. A estrutura do callback é esta:

@app.callback(Output('ID do TITULO', 'o que será alterado'),
        [Input('ID DO Dropdown', 'value')])

Para o título do gráfico de dispersão, temos este bloco de código:

# Titulo do gráfico de dispersão
@app.callback(Output('titulo-scatter', 'children'),
             [Input('biome-picker', 'value')])
def update_titulo_scatter(selected_biome):
    return "Número de focos de queimadas por mês no bioma: " + str(selected_biome)

Repare que por enquanto é necessário transformar o selected_biome em string, pois o children recebe o valor correspondente ao label fornecido em options, e eu utilizei números (por enquanto).

O código está assim neste momento:

app = dash.Dash() # Criando a aplicação

# Criando o layout
app.layout = html.Div([
    # cabeçalho
    html.Div([
        # Titulo
        html.H1("Dados de focos de queimadas por Bioma no Brasil entre os anos de 1998 e 2020",
                style = {'textAlign': 'center', # alinhando o titulo ao centro
                         'fontFamily': 'Roboto', # alterando a fonte do H1
                         'paddingTop': 20}), # adicionando um padding no topo
        # Aviso
        html.P("Selecione um bioma:",
              style = {'fontFamily': 'Roboto'}),
        # Dropdown
        html.Div([
            dcc.Dropdown(id = 'biome-picker', # id do dropdown
                     value = 'Amazônia', # seta o valor inicial,
                     options = [{'label': 'Amazônia', 'value': 0},
                               {'label': 'Pampa', 'value': 1},
                               {'label': 'Pantanal', 'value': 2}], # as opções que vão aparecer no dropdown
                    )
        ], style = {'width': '33%',
                            'display': 'inline-block'})
    ]),
    html.Div([
        # Titulo do gráfico de dispersão
            html.H3(id='titulo-scatter',
                    style={'textAlign': 'center',
                           'fontFamily' : "Roboto",
                           'paddingTop': 15
                          }
                   )
    ])
])



# Titulo do gráfico de dispersão
@app.callback(Output('titulo-scatter', 'children'),
             [Input('biome-picker', 'value')])
def update_titulo_scatter(selected_biome):
    return "Número de focos de queimadas por mês no bioma: " + str(selected_biome)


# Rodando a aplicação através de um servidor
if __name__ == '__main__':
    app.run_server(debug = True, use_reloader = False)

Figura 5 - Dashboard com título do gráfico de dispersão responsivo.

Print do dashboard gerado com título responsivo.

Agora vou adicionar o gráfico de dispersão. Dentro do layout e dentro da Div do gráfico de dispersão e abaixo do H3 do título interativo, vou adicionar o elemento de gráfico que é o dcc.Graph(). Vou passar apenas uma id para o gráfico, e vou aproveitar e colocar um estilo na Div que contém o titulo do gráfico e o gráfico, para que o elemento todo fica centralizado e com um espaço nas margens:

# O gráfico de dispersão
dcc.Graph(id = 'scatter-plot')
 ], style = {'paddingLeft': '10%',
             'padingRight': '10%',
             'width': '80%',
             'display': 'inline-block'}),

E agora é preciso criar o gráfico e colocar ele dentro do elemento com id='scatter-plot'. Vou fazer isto utilizando a função callback. Esta função vai fazer exatamente a mesma coisa que a função update_titulo_scatter(), ou seja, vai receber o gráfico que está inicialmente setado, e quando houver mudança no dropdown ele vai atualizar o gráfico.

A estrutura basica da função é esta:

# Grafico de dispersão
@app.callback(Output('scatter-plot', 'figure'),
              [Input('biome-picker', 'value')])
def update_scatter(selected_biome):

    return pass

O retorno da função vai ser um dicionário com os traços e o layout do gráfico, de forma muito similar ao que fizemos para gerar os gráficos no Plotly. Dentro da função precisamos apenas gerar os traços.

Começamos filtrando os dados:

df_aux = df[df['Bioma'] == selected_biome] # data frame filtrado baseado no selected_biome
df_aux.reset_index(drop=True, inplace=True) # resetando o indice para facilitar a vida

Observação: é importante que a leitura dos dados ocorra apenas uma vez (ou o mínimo de vezes possível) ao longo da aplicação (logo quando a aplicação é iniciada). Dessa forma o tempo de execução é menor. Então no algoritmo finalizado vamos adicionar uma linha logo após a inicialização do app para ler os dados. Depois, sempre filtramos os dados para economizar tempo de execução.

Agora criamos os traços dentro de um for loop (copie e colei do que fizemos no plotly):

tracos = [] # lista vazia para apendar os traços
for i in range(df_aux.shape[0]):
    tracos.append(go.Scatter(
                        x = df_aux.columns.values[1:13], # os dados do eixo x
                        y = df_aux.loc[i][1:13], # acessando os dados dos meses (dados do eixo y)
                        mode = 'lines+markers', # define o tipo de gráfico, neste caso vai ter linhas e marcadores
                        name = str(df_aux['Ano'][i]), # adiciono o nome do traço
                        hovertemplate = df_aux.columns.values[1:13] + ' de ' + str(df_aux['Ano'][i]) + '<br>nº de focos: '
                        + [str(i) for i in list(df_aux.loc[i][1:13])] , # alterando o template do hover
    ))

E agora basta retornar os traços e o layout do gráfico. A estrutura de retorno do gráfico é a seguinte:

return {
    'data': tracos,
    'layout': go.Layout('código do estilo')
    }

Então vou apenas copiar o estilo que fizemos para este gráfico (no Plotly) e colar em cima de go.Layout().

return {
        'data': tracos,
        'layout': go.Layout(
                           showlegend=True, # garante que a legenda será mostrada
                           hovermode = "closest", # garante que o hover irá mostrar os dados do ponto mais próximo a seta do mouse
                           hoverlabel=dict(bgcolor="white", # altera a cor de fundo
                                            font_size=16, # altera o tamanho da fonte
                                            font_family="Roboto"), # altera a fonte de texto
                           xaxis = dict(title = 'Meses', linecolor='rgba(0,0,0,1)'), # Nome do eixo x / adiciona uma linha preta em y=0
                           yaxis = dict(title = 'Número de focos de queimadas', linecolor='rgba(0,0,0,1)'),
                          )
        }

Mas para que funcione corretamente, precisamos de uma lista contendo os dicionários de opções para o dropdown. Eu vou criar uma lista contendo os diferentes biomas. Neste caso, tanto o label como o value vão ter exatamente o mesmo valor.

biome_options = []
for biome in df['Bioma'].unique():
    biome_options.append({'label': biome, 'value': biome})

Agora é só substituir a lista contendo dicionários dentro do Dropdown por biome_options. Vou aproveitar e adicionar o parâmetro clearable = False para não permitir que o Dropdown fique em branco.

O código por enquanto está assim:

df = pd.read_csv('biomas_dados_historicos.csv', encoding='latin-1') # lendo os dados

app = dash.Dash() # Criando a aplicação


# criando uma variável para armazenar os estados, para o usuario poder utilizar o dropdown e trocar de estado
biome_options = []
for biome in df['Bioma'].unique():
    biome_options.append({'label': biome, 'value': biome})

# Criando o layout
app.layout = html.Div([
    # cabeçalho
    html.Div([
        # Titulo
        html.H1("Dados de focos de queimadas por Bioma no Brasil entre os anos de 1998 e 2020",
                style = {'textAlign': 'center', # alinhando o titulo ao centro
                         'fontFamily': 'Roboto', # alterando a fonte do H1
                         'paddingTop': 20}), # adicionando um padding no topo
        # Aviso
        html.P("Selecione um bioma:",
              style = {'fontFamily': 'Roboto'}),
        # Dropdown
        html.Div([
            dcc.Dropdown(id = 'biome-picker', # id do dropdown
                     value = 'Amazônia', # seta o valor inicial,
                     options = biome_options, # as opções que vão aparecer no dropdown
                     clearable = False, # permite remover o valor (acho importante manter false para evitar problemas)
                     )
        ], style = {'width': '33%',
                            'display': 'inline-block'})
    ]),
    html.Div([
        # Titulo do gráfico de dispersão
        html.H3(id='titulo-scatter',
                style={'textAlign': 'center',
                       'fontFamily' : "Roboto",
                       'paddingTop': 15
                      }
               ),
        # O gráfico de dispersão
        dcc.Graph(id = 'scatter-plot')
         ], style = {'paddingLeft': '10%',
                     'padingRight': '10%',
                     'width': '80%',
                     'display': 'inline-block'}),

])


# Grafico de dispersão
@app.callback(Output('scatter-plot', 'figure'),
              [Input('biome-picker', 'value')])
def update_scatter(selected_biome):

    df_aux = df[df['Bioma'] == selected_biome] # data frame filtrado baseado no selected_biome
    df_aux.reset_index(drop=True, inplace=True) # resetando o indice para facilitar a vida

    tracos = [] # lista vazia para apendar os traços
    for i in range(df_aux.shape[0]):
        tracos.append(go.Scatter(
                            x = df_aux.columns.values[1:13], # os dados do eixo x
                            y = df_aux.loc[i][1:13], # acessando os dados dos meses (dados do eixo y)
                            mode = 'lines+markers', # define o tipo de gráfico, neste caso vai ter linhas e marcadores
                            name = str(df_aux['Ano'][i]), # adiciono o nome do traço
                            hovertemplate = df_aux.columns.values[1:13] + ' de ' + str(df_aux['Ano'][i]) + '<br>nº de focos: '
                            + [str(i) for i in list(df_aux.loc[i][1:13])] , # alterando o template do hover
        ))
    return {
        'data': tracos,
        'layout': go.Layout(
                           showlegend=True, # garante que a legenda será mostrada
                           hovermode = "closest", # garante que o hover irá mostrar os dados do ponto mais próximo a seta do mouse
                           hoverlabel=dict(bgcolor="white", # altera a cor de fundo
                                            font_size=16, # altera o tamanho da fonte
                                            font_family="Roboto"), # altera a fonte de texto
                           xaxis = dict(title = 'Meses', linecolor='rgba(0,0,0,1)'), # Nome do eixo x / adiciona uma linha preta em y=0
                           yaxis = dict(title = 'Número de focos de queimadas', linecolor='rgba(0,0,0,1)'),
                          )
    }

# Titulo do gráfico de dispersão
@app.callback(Output('titulo-scatter', 'children'),
             [Input('biome-picker', 'value')])
def update_titulo_scatter(selected_biome):
    return "Número de focos de queimadas por mês no bioma: " + str(selected_biome)


# Rodando a aplicação através de um servidor
if __name__ == '__main__':
    app.run_server(debug = True, use_reloader = False)

Figura 6 - Dashboard com título do gráfico de dispersão responsivo finalizado.

Print do dashboard gerado com título e gráfico de dispersão responsivos.

Os próximos passos para os demais gráficos são exatamente os mesmos, apenas tendo de alterar o gráfico em si.

Gráfico de barras

Começamos com o layout, montando uma nova Div apenas para o gráfico de barras. Dentro dessa Div, criamos um elemento H3 para colocar o titulo do gráfico, e um elemento de gráfico para colocar o gráfico de barras. E a Div recebe um estilo para centralizar o gráfico com o mesmo tamanho de margem do gráfico de dispersão.

# Grafico de barras
    html.Div([
        # Titulo do gráfico de dispersão
        html.H3(id = 'titulo-barplot',
               style = {'textAlign': 'center',
                        'fontFamily': 'Roboto',
                        'paddingTop': 10
                       }
               ),
        # Gráfico de barras
        dcc.Graph(id = 'bar-plot')
    ], style = {'paddingLeft': '10%',
                     'padingRight': '10%',
                     'width': '80%',
                     'display': 'inline-block'}
    ),

E agora precisamos criar as duas funções de callback, uma para o título:

# Titulo do gráfico de barras
@app.callback(Output('titulo-barplot', 'children'),
             [Input('biome-picker', 'value')])
def update_titulo_barplot(selected_biome):
    return "Número TOTAL focos de queimadas por ano durante todo o período no bioma: " + str(selected_biome)

E outra para o gráfico em si. Vou utilizar o mesmo código que construí quando montei o gráfico no Plotly, mas alguns ajustes são necessários (ajustar o name para selected_biome e remover o titulo).

# Grafico de barras
@app.callback(Output('bar-plot', 'figure'),
              [Input('biome-picker', 'value')])
def update_bar_plot(selected_biome):

    df_aux = df[df['Bioma'] == selected_biome] # data frame filtrado baseado no selected_biome
    df_aux.reset_index(drop=True, inplace=True) # resetando o indice para facilitar a vida

    traco = [go.Bar(
            x = df_aux['Ano'], # os dados do eixo x
            y = df_aux['Total'], # dados do eio y
            name = selected_biome, # nome do bioma
            hovertemplate = ['Total de focos de queimadas: ' + i for i in [str(i) for i in (df_aux['Total'])]],
                )
            ]
    return {
            'data': traco,
            'layout': go.Layout(
                              xaxis = dict(title = 'Anos', linecolor='rgba(0,0,0,1)', tickmode = 'array', tickvals = df_aux['Ano'], ticktext = df_aux['Ano']), # adicionando nome eo eixo x, barra (y=0) na cor preta, e fixando o ano abaixo de todas as barras
                              yaxis = dict(title = 'Total de queimadas por ano', linecolor='rgba(0,0,0,1)', tickformat=False), # adicionando nome no eixo y, passando uma linha preta em x = 0, e removendo a formatação padrão dos ticks, para que não apareça o K
                              showlegend=True, # adicionando a legenda
                              hoverlabel=dict(bgcolor="white", # alterando a cor de fundo do hover
                                                font_size=16, # alterando o tamanho da letra no hover
                                                font_family="Roboto") # alterando a fonte do hover

                                )
                }

Figura 7 - Dashboard com gráfico de dispersão e gráfico de barras.

Print do dashboard gerado com título e gráfico de dispersão e gráfico barras responsivos.

Gráfico de pizza

Agora falta apenas o gráfico de pizza, e os passos são exatamente os mesmos.

Começamos com o layout, montando uma nova Div apenas para o gráfico de pizza. Dentro dessa Div, criamos um elemento H3 para colocar o titulo do gráfico, e um elemento de gráfico para colocar o gráfico de pizza. E a Div recebe um estilo para centralizar o gráfico com o mesmo tamanho de margem do gráfico de dispersão.

# Grafico de pizza
    html.Div([
        #Titulo de gráfico de rosquinha
        html.H3(id = 'titulo-pie',
               style = {'textAlign': 'center',
                        'fontFamily': 'Roboto',
                        'paddingTop': 10
                       }
               ),
        # Gráfico de rosquinha
        dcc.Graph(id = 'pie-plot')
    ], style = {'paddingLeft': '10%',
                     'padingRight': '10%',
                     'width': '80%',
                     'display': 'inline-block'}
    ),

E agora precisamos criar as duas funções de callback, uma para o título:

# Titulo do gráfico de rosquinha
@app.callback(Output('titulo-pie', 'children'),
             [Input('biome-picker', 'value')])
def update_titulo_pie(selected_biome):
    return "Porcentagem do TOTAL focos de queimadas por ano durante todo o período no bioma: " + str(selected_biome)

E outra para o gráfico em si. Vou utilizar o mesmo código que construí quando montei o gráfico no Plotly, mas é necessário remover o titulo.

# Grafico de rosquinha
@app.callback(Output('pie-plot', 'figure'),
              [Input('biome-picker', 'value')])
def update_pie_plot(selected_biome):

    df_aux = df[df['Bioma'] == selected_biome] # data frame filtrado baseado no selected_biome
    df_aux.reset_index(drop=True, inplace=True) # resetando o indice para facilitar a vida

    traco = [go.Pie(
                labels = df_aux['Ano'], # adicionando os labels das fatias de pizza
                values = df_aux['Total'], # adicionando o tamanho das fatais de pizza
                insidetextorientation='radial', # mudando a orientação do texto dentro das fatias
                hole=.3, # transformando a pizza em uma rosquinha
                )
            ]
    return {
            'data': traco,
            'layout': go.Layout(
                    annotations=[dict(text = 'Total', # Colocando o que será inserido dentro do buraco
                                      x = .5, # posição de x do centro do buraco da rosquinha
                                      y = 0.5, # posição de y do centro do buraco da rosquinha
                                      font_size = 24, # tamanho da fonte
                                      font_family = "Roboto", # alterando a fonte do texto
                                      showarrow = False, # removendo a seta que vem por padrão inserida
                                     )],
                  )
            }

Figura 8 - Dashboard com gráfico de dispersão, gráfico de barras e gráfico de pizza responsivos.

Print do dashboard gerado com título e gráfico de dispersão, e gráfico barras e gráfico de pizza responsivos.

Ajustes finais

Para finalizar este Dashboard eu vou colocar um link no final da página que leva ao site do INPE, que é a fonte dos dados. Para isso, basta colocar outra Div depois do gráfico de rosquinha, e chamar o elemento Label, e colocar um trecho com o elemento html.A, que é a tag responsável pelo link.

E eu vou aproveitar e colocar nesta mesma Div meu contato, mas sem links.

# Referencia
    html.Div([
        html.Label(["Fonte: ",
                html.A('queimadas.dgi.inpe.br',
                       href='http://queimadas.dgi.inpe.br/queimadas/portal-static/estatisticas_estados/'),
                        ". Acesso em 27/01/2021"
                   ]),
        html.Label([
            html.P(["Desenvolvido por Anderson Canteli (andersonmdcanteli@gmail.com)"])
        ])
            ], style={'textAlign': 'center',
                       'fontFamily' : "Roboto",
                       'paddingTop': 15
                      }
    )

Figura 9 - Dashboard do número de focos de queimadas nos biomas brasileiros entre 1998 e 2020.

Print do dashboard finalizado.

Conclusão

Uhuull 😇!Foi uma longa jornada neste estudo para gerar Dashboards. É uma ferramenta com muito potencial, e certamente vou utiliza-la em meus projetos! A interatividade do usuário com os elementos é bem tranquila, e consigo vislumbrar alguns projetos. Só tem de verificar a parte legal do uso comercial do Dash.

Eu peguei este Dashboard e hospedei ele no Heroku, e funcionou corretamente. Eu segui os passos indicados pelo próprio Heroku para fazer o deployment. A dificuldade que apareceu foi relacionado as versões do NumPy na hora de criar o ambiente virtual, mas uma vez que isso foi solucionado, foi bem tranquilo. O problema com o NumPy provavelmente ocorreu devido a versão do Python que utilizei (3.7) não ser compatível com a versão 1.20 do NumPy (isso é um chute, ok?!). Instalando a versão 1.16, o deployment foi tranquilo. PS: o NumPy é necessário pois ele é uma dependência do Pandas.

Talvez você consiga acessar ele clicando aqui, mas eu faço atualizações neste site as vezes (utilizo como teste mesmo), então provavelmente o link estará quebrado ou te levará a um outro projeto.

A planilha com os dados pode ser baixada clicando aqui.

O notebook final pode ser baixado clicando aqui.

De qualquer forma, o código completo (incluindo os outros arquivos necessários para o deployment, como o Procfile, requirments, etc) esta disponível abaixo e no meu git-hub.

import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
import pandas as pd
import plotly.offline as pyo
import plotly.graph_objs as go

df = pd.read_csv('biomas_dados_historicos.csv', encoding='latin-1') # lendo os dados

app = dash.Dash() # Criando a aplicação


# criando uma variável para armazenar os estados, para o usuario poder utilizar o dropdown e trocar de estado
biome_options = []
for biome in df['Bioma'].unique():
    biome_options.append({'label': biome, 'value': biome})

# Criando o layout
app.layout = html.Div([
    # cabeçalho
    html.Div([
        # Titulo
        html.H1("Dados de focos de queimadas por Bioma no Brasil entre os anos de 1998 e 2020",
                style = {'textAlign': 'center', # alinhando o titulo ao centro
                         'fontFamily': 'Roboto', # alterando a fonte do H1
                         'paddingTop': 20}), # adicionando um padding no topo
        # Aviso
        html.P("Selecione um bioma:",
              style = {'fontFamily': 'Roboto'}),
        # Dropdown
        html.Div([
            dcc.Dropdown(id = 'biome-picker', # id do dropdown
                     value = 'Amazônia', # seta o valor inicial,
                     options = biome_options, # as opções que vão aparecer no dropdown
                     clearable = False, # permite remover o valor (acho importante manter false para evitar problemas)
                     )
        ], style = {'width': '33%',
                            'display': 'inline-block'})
    ]),
    # Grafico de dispersão
    html.Div([
        # Titulo do gráfico de dispersão
        html.H3(id='titulo-scatter',
                style={'textAlign': 'center',
                       'fontFamily' : "Roboto",
                       'paddingTop': 15
                      }
               ),
        # O gráfico de dispersão
        dcc.Graph(id = 'scatter-plot')
         ], style = {'paddingLeft': '10%',
                     'padingRight': '10%',
                     'width': '80%',
                     'display': 'inline-block'}
    ),
    # Grafico de barras
    html.Div([
        # Titulo do gráfico de dispersão
        html.H3(id = 'titulo-barplot',
               style = {'textAlign': 'center',
                        'fontFamily': 'Roboto',
                        'paddingTop': 10
                       }
               ),
        # Gráfico de barras
        dcc.Graph(id = 'bar-plot')
    ], style = {'paddingLeft': '10%',
                     'padingRight': '10%',
                     'width': '80%',
                     'display': 'inline-block'}
    ),
    # Grafico de pizza
    html.Div([
        #Titulo de gráfico de rosquinha
        html.H3(id = 'titulo-pie',
               style = {'textAlign': 'center',
                        'fontFamily': 'Roboto',
                        'paddingTop': 10
                       }
               ),
        # Gráfico de rosquinha
        dcc.Graph(id = 'pie-plot')
    ], style = {'paddingLeft': '10%',
                     'padingRight': '10%',
                     'width': '80%',
                     'display': 'inline-block'}
    ),
    # Referencia
    html.Div([
        html.Label(["Fonte: ",
                html.A('queimadas.dgi.inpe.br',
                       href='http://queimadas.dgi.inpe.br/queimadas/portal-static/estatisticas_estados/'),
                        ". Acesso em 27/01/2021"
                   ]),
        html.Label([
            html.P(["Desenvolvido por Anderson Canteli (andersonmdcanteli@gmail.com)"])
        ])
            ], style={'textAlign': 'center',
                       'fontFamily' : "Roboto",
                       'paddingTop': 15
                      }
    )


])


# Grafico de rosquinha
@app.callback(Output('pie-plot', 'figure'),
              [Input('biome-picker', 'value')])
def update_pie_plot(selected_biome):

    df_aux = df[df['Bioma'] == selected_biome] # data frame filtrado baseado no selected_biome
    df_aux.reset_index(drop=True, inplace=True) # resetando o indice para facilitar a vida

    traco = [go.Pie(
                labels = df_aux['Ano'], # adicionando os labels das fatias de pizza
                values = df_aux['Total'], # adicionando o tamanho das fatais de pizza
                insidetextorientation='radial', # mudando a orientação do texto dentro das fatias
                hole=.3, # transformando a pizza em uma rosquinha
                )
            ]
    return {
            'data': traco,
            'layout': go.Layout(
                    annotations=[dict(text = 'Total', # Colocando o que será inserido dentro do buraco
                                      x = .5, # posição de x do centro do buraco da rosquinha
                                      y = 0.5, # posição de y do centro do buraco da rosquinha
                                      font_size = 24, # tamanho da fonte
                                      font_family = "Roboto", # alterando a fonte do texto
                                      showarrow = False, # removendo a seta que vem por padrão inserida
                                     )],
                  )
            }

# Titulo do gráfico de rosquinha
@app.callback(Output('titulo-pie', 'children'),
             [Input('biome-picker', 'value')])
def update_titulo_pie(selected_biome):
    return "Porcentagem do TOTAL de focos de queimadas por ano durante todo o período no bioma: " + str(selected_biome)



# Grafico de barras
@app.callback(Output('bar-plot', 'figure'),
              [Input('biome-picker', 'value')])
def update_bar_plot(selected_biome):

    df_aux = df[df['Bioma'] == selected_biome] # data frame filtrado baseado no selected_biome
    df_aux.reset_index(drop=True, inplace=True) # resetando o indice para facilitar a vida

    traco = [go.Bar(
            x = df_aux['Ano'], # os dados do eixo x
            y = df_aux['Total'], # dados do eio y
            name = selected_biome, # nome do bioma
            hovertemplate = ['Total de focos de queimadas: ' + i for i in [str(i) for i in (df_aux['Total'])]],
                )
            ]
    return {
            'data': traco,
            'layout': go.Layout(
                              xaxis = dict(title = 'Anos', linecolor='rgba(0,0,0,1)', tickmode = 'array', tickvals = df_aux['Ano'], ticktext = df_aux['Ano']), # adicionando nome eo eixo x, barra (y=0) na cor preta, e fixando o ano abaixo de todas as barras
                              yaxis = dict(title = 'Total de queimadas por ano', linecolor='rgba(0,0,0,1)', tickformat=False), # adicionando nome no eixo y, passando uma linha preta em x = 0, e removendo a formatação padrão dos ticks, para que não apareça o K
                              showlegend=True, # adicionando a legenda
                              hoverlabel=dict(bgcolor="white", # alterando a cor de fundo do hover
                                                font_size=16, # alterando o tamanho da letra no hover
                                                font_family="Roboto") # alterando a fonte do hover

                                )
                }


# Titulo do gráfico de barras
@app.callback(Output('titulo-barplot', 'children'),
             [Input('biome-picker', 'value')])
def update_titulo_barplot(selected_biome):
    return "Número TOTAL focos de queimadas por ano durante todo o período no bioma: " + str(selected_biome)


# Grafico de dispersão
@app.callback(Output('scatter-plot', 'figure'),
              [Input('biome-picker', 'value')])
def update_scatter(selected_biome):

    df_aux = df[df['Bioma'] == selected_biome] # data frame filtrado baseado no selected_biome
    df_aux.reset_index(drop=True, inplace=True) # resetando o indice para facilitar a vida

    tracos = [] # lista vazia para apendar os traços
    for i in range(df_aux.shape[0]):
        tracos.append(go.Scatter(
                            x = df_aux.columns.values[1:13], # os dados do eixo x
                            y = df_aux.loc[i][1:13], # acessando os dados dos meses (dados do eixo y)
                            mode = 'lines+markers', # define o tipo de gráfico, neste caso vai ter linhas e marcadores
                            name = str(df_aux['Ano'][i]), # adiciono o nome do traço
                            hovertemplate = df_aux.columns.values[1:13] + ' de ' + str(df_aux['Ano'][i]) + '<br>nº de focos: '
                            + [str(i) for i in list(df_aux.loc[i][1:13])] , # alterando o template do hover
        ))
    return {
        'data': tracos,
        'layout': go.Layout(
                           showlegend=True, # garante que a legenda será mostrada
                           hovermode = "closest", # garante que o hover irá mostrar os dados do ponto mais próximo a seta do mouse
                           hoverlabel=dict(bgcolor="white", # altera a cor de fundo
                                            font_size=16, # altera o tamanho da fonte
                                            font_family="Roboto"), # altera a fonte de texto
                           xaxis = dict(title = 'Meses', linecolor='rgba(0,0,0,1)'), # Nome do eixo x / adiciona uma linha preta em y=0
                           yaxis = dict(title = 'Número de focos de queimadas', linecolor='rgba(0,0,0,1)'),
                          )
    }

# Titulo do gráfico de dispersão
@app.callback(Output('titulo-scatter', 'children'),
             [Input('biome-picker', 'value')])
def update_titulo_scatter(selected_biome):
    return "Número de focos de queimadas por mês no bioma: " + str(selected_biome)


# Rodando a aplicação através de um servidor
if __name__ == '__main__':
    app.run_server(debug = True, use_reloader = False)