Avaliação preliminar de funcionalidades do sistema Quarto para publicação de estatísticas da população em área de risco.

Author

Joaquim

Published

January 10, 2022

Introdução

Quarto é um sistema de produção de publicações técnicas e científicas que incorpora ferramentas de formatação de texto com códigos de programação para produção de gráficos e tabelas. É o sucessor do R markdown e segue a linha de outros notebooks reativos, como o Jupyter Notebook. É um sistema de código aberto cujos principais destques são permitir a utilização de diversas linguagens de programação (Python, R, Julia e Observable) e a exportação para diversos formatos de publicação através da utilização do Pandoc.

As suas funcionalidades relativas ao processamento de dados espaciais e tabulares com linguagens de programação e produção de documentos em .pdf ou .doc já são interessantes por facilitar a padronização da análise e publicação de resultados. Mas são sua integração com o Observable e capacidade de produção de documentos html que oferecem as maiores possibilidades de enriquecimento da publicação com conteúdo dinâmico e interativo.

Elementos interativos como listas de seleção, tabelas, e gráficos, etc, produzidos nas linguagens Python, R ou Julia possuem recursos limitados de interatividade em publicações online, necessitando de frameworks como o Dash ou o Shiny para poderem apresentar comportamento realmente dinâmico. Esses frameworks exigem a configuração do servidor de hospedagem com os interpretadores e bibliotecas e também de tempo de processamento para atender requisições dos usuários, dificultando a utilização dessas capacidades na publicação de resultados de pesquisas. O Observable, porém, é um JavaScript melhorado que é interpretado diretamente pelo navegador do usuário, dispensando a utilização do servidor no processamento dos dados. Essa característica, associada à utilização de geoserviços do IBGE, permite desde a publicação enriquecida com conteúdo interativo até a produção de dashboards complexos com dados de diversas fontes sem a necessidade de gastos com servidor e apoio institucional.

Exemplos de funcionalidades

Para avaliar algumas funcionalidades de maior relevância, foram utilizados os dados de Alertas de riscos de desastres emitidos pelo CEMADEN compilados para o Atlas Nacional Digital do Brasil - 2022.

Dados e Bibliotecas

Carrega a tabela com os dados e as bibliotecas utilizadas.

Code
library(tidyverse)
library(readxl)
library(writexl)
library(sf)
library(DT)
library(clock)
library(ggthemes)
library(showtext)
library(RColorBrewer)
library(knitr)

planilha <- read_excel("W:/DGC_ACERVO_CGEO/PROJETOS_EM_ANDAMENTO/Cemaden/BOLSISTAS/Joaquim/AtlasNacional/Alertas/AlertasCemaden2012-2021.xlsx")

Tabelas

Tabela processada em R e apresentada de maneira estática.

Code
Tot_Mun <- planilha %>%
  mutate(Mun = str_to_upper(`Município`)) %>%
  group_by(`Código IBGE`, Mun, `Evento`) %>%
  summarise(Tot_Event = n()) %>%
  mutate(Evento = ifelse(Evento == "Geo/Hidro", "Hidrogeo", Evento)) %>%
  pivot_wider(names_from = Evento, values_from = Tot_Event, values_fill = 0) %>%
  mutate(tot_geo = Geo + Hidrogeo, tot_hidro = Hidro + Hidrogeo) %>%
  select(`Código IBGE`, Mun, tot_geo, tot_hidro)

kable(head(Tot_Mun))
Código IBGE Mun tot_geo tot_hidro
1100189 PIMENTA BUENO 0 1
1100205 PORTO VELHO 4 9
1200104 BRASILÉIA 2 6
1200252 EPITACIOLÂNDIA 0 1
1200328 JORDÃO 0 1
1200401 RIO BRANCO 3 18

Essa tabela pode ser apresentada de maneira dinâmica com a biblioteca DT.

Code
datatable(Tot_Mun)

Ou ainda pelo Observable.

Code
ojs_define(totais = Tot_Mun)
Code
viewof tabexemp = Inputs.table(transpose(totais));

Gráficos

Agregação de dados no R para a produção de gráficos estáticos.

Code
Med_GdReg_data <- planilha %>%
  mutate(dataref = date_group(as.Date(planilha$Data), "month"),
         mes = factor(date_format(dataref, format = "%m"), 
                      labels = c("Jan", "Fev", "Mar", "Abr", "Mai", "Jun", "Jul", "Ago", "Set", "Out", "Nov", "Dez")),
         GdReg = factor(as.numeric(substr(`Código IBGE`, 1, 1)), labels = c("N", "NE", "SE", "S", "CO")),
         geologico = ifelse(Evento %in% c("Geo", "Geo/Hidro"), 1, 0),
         hidrologico = ifelse(Evento %in% c("Hidro", "Geo/Hidro"), 1, 0)) %>%
  group_by(GdReg, mes, dataref) %>%
  summarise(Tot_Event = n(), Tot_geo = sum(geologico), Tot_hidro = sum(hidrologico)) %>%
  ungroup() %>%
  group_by(GdReg, mes) %>%
  summarise(Media_geo = mean(Tot_geo), Media_hidro = mean(Tot_hidro), Media_Event = mean(Tot_Event)) %>%
  # manobra esquisita para arrumar a tabela, rever esse código adiante de pivotagem
  pivot_longer(cols = starts_with("Media_"), names_to = "Tipo", values_to = "Media") %>%
  pivot_wider(names_from = c("mes", "Tipo"), values_from = Media, values_fill = 0) %>%
  pivot_longer(cols = !GdReg, names_to = c("mes", "Tipo"), names_pattern = "(.*)_Media_(.*)", values_to = "Media") %>%
  # até aqui.
  ungroup() %>%
  group_by(Tipo, mes) %>%
  mutate(mes = factor(mes, levels = c("Jan", "Fev", "Mar", "Abr", "Mai", "Jun", "Jul", "Ago", "Set", "Out", "Nov", "Dez")))


Med_Pais_geral <- Med_GdReg_data %>%
  group_by(Tipo, mes) %>%
  summarise(Media = sum(Media)) %>%
  mutate(GdReg = "Brasil") %>%
  select(GdReg, mes, Tipo, Media)

Med_Geral <- Med_GdReg_data %>%
  bind_rows(Med_Pais_geral) %>%
  mutate(GdReg = factor(GdReg, levels = c("Brasil", "N", "NE", "SE", "S", "CO")))

Med_Geral %>% 
  filter(Tipo == "Event") %>%
  mutate(Media = round(Media, 2)) %>%
  pivot_wider(id_cols = GdReg, names_from = mes, values_from = Media) %>%
  datatable()

Configurações de tema e estilo - desenvolvido pro Atlas Nacional Digital 2022.

Code
showtext_auto()
showtext_opts(dpi = 300)
font_add(family = "univers", regular = "C:/Windows/Fonts/univer.TTF")

theme_set(
  theme_igray() + 
    theme(plot.title = element_text(family = "univers", face = "bold", size = 11, hjust = 0.5, vjust = 0.5, lineheight = 1.1, margin = margin(12, 0, 12, 0)),
          legend.position = "bottom",
          legend.title = element_blank(),
          legend.background = element_rect(fill = "white"),
          legend.text = element_text(family = "univers", face = "plain", size = 9, margin = margin(0, 15, 0, 5)),
          legend.key.width = unit(1.5, "cm"),
          plot.background = element_rect(fill = "#d0cece"),
          plot.margin = margin(t = 0, r = 40, b = 0, l = 10 ),
          axis.title = element_text(family = "univers", face = "plain", size = 9),
          axis.text = element_text(family = "univers", face = "plain", size = 7)))

cores <- c("#000000", brewer.pal(5, "Set1"))
names(cores) <- c("Brasil", levels(Med_GdReg_data$GdReg))
colScale <- scale_color_manual(name = "GdReg", labels = c("Brasil", "Norte", "Nordeste", "Sudeste", "Sul", "Centro-Oeste"), values = cores)
lineScale <- scale_linetype_manual(name = "GdReg", labels = c("Brasil", "Norte", "Nordeste", "Sudeste", "Sul", "Centro-Oeste"), values = c(2, 1, 1, 1, 1, 1))

Alertas Hidrológicos.

Code
graf_hidro <- Med_Geral %>%
  filter(Tipo == "hidro") %>%
  ggplot(aes(x = mes, y = Media, colour = GdReg, group = GdReg, linetype = GdReg)) +
  geom_line(stat = "identity", position = "identity", size = 0.8, ) +
  labs(title = "Média mensal de alertas de risco hidrológico,\npor Grandes Regiões - 2012-2021") +
  xlab("Mês") +
  ylab("Alertas") +
  lineScale +
  colScale +
  theme(legend.margin = margin(0, 40, 0, 40))

graf_hidro

Figure 1: Hidrológico

Alertas Geológicos.

Code
graf_geo <- Med_Geral %>%
  filter(Tipo == "geo") %>%
  ggplot(aes(x = mes, y = Media, colour = GdReg, group = GdReg, linetype = GdReg)) +
  geom_line(stat = "identity", position = "identity", size = 0.8, ) +
  labs(title = "Média mensal de alertas de risco geológico,\npor Grandes Regiões - 2012-2021") +
  xlab("Mês") +
  ylab("Alertas") +
  lineScale +
  colScale +
  theme(legend.margin = margin(0, 40, 0, 40))

graf_geo

Figure 2: Geológico

Alertas.

Code
# graf_geral <- Med_Geral %>%
#   filter(Tipo == "Event") %>%
#   arrange(GdReg) %>%
#   ggplot(aes(x = mes, y = Media, colour = GdReg, group = GdReg, linetype = GdReg)) +
#   geom_line(size = 0.5) +
#   labs(title = "Média mensal de alertas de risco geohidrológico,\npor Grandes Regiões - 2012-2021") +
#   xlab("Mês") +
#   ylab("Alertas") +
#   lineScale +
#   colScale +
#   theme(legend.position="none") +
#   facet_wrap(vars(GdReg), nrow = 3)

graf_N <- Med_Geral %>%
  filter(Tipo == "Event" & GdReg == "N") %>%
  ggplot(aes(x = mes, y = Media, colour = GdReg, group = GdReg, linetype = GdReg)) +
  geom_line(size = 0.5) +
  labs(title = "Norte") +
  xlab("Mês") +
  ylab("Alertas") +
  colScale +
  theme(legend.position="none")

graf_NE <- Med_Geral %>%
  filter(Tipo == "Event" & GdReg == "NE") %>%
  ggplot(aes(x = mes, y = Media, colour = GdReg, group = GdReg, linetype = GdReg)) +
  geom_line(size = 0.5) +
  labs(title = "Nordeste") +
  xlab("Mês") +
  ylab("Alertas") +
  colScale +
  theme(legend.position="none")

graf_SE <- Med_Geral %>%
  filter(Tipo == "Event" & GdReg == "SE") %>%
  ggplot(aes(x = mes, y = Media, colour = GdReg, group = GdReg, linetype = GdReg)) +
  geom_line(size = 0.5) +
  labs(title = "Sudeste") +
  xlab("Mês") +
  ylab("Alertas") +
  colScale +
  theme(legend.position="none")

graf_S <- Med_Geral %>%
  filter(Tipo == "Event" & GdReg == "S") %>%
  ggplot(aes(x = mes, y = Media, colour = GdReg, group = GdReg, linetype = GdReg)) +
  geom_line(size = 0.5) +
  labs(title = "Sul") +
  xlab("Mês") +
  ylab("Alertas") +
  colScale +
  theme(legend.position="none")

graf_CO <- Med_Geral %>%
  filter(Tipo == "Event" & GdReg == "CO") %>%
  ggplot(aes(x = mes, y = Media, colour = GdReg, group = GdReg, linetype = GdReg)) +
  geom_line(size = 0.5) +
  labs(title = "Centro-Oeste") +
  xlab("Mês") +
  ylab("Alertas") +
  colScale +
  theme(legend.position="none")

graf_BR <- Med_Geral %>%
  filter(Tipo == "Event" & GdReg == "Brasil") %>%
  ggplot(aes(x = mes, y = Media, colour = GdReg, group = GdReg, linetype = GdReg)) +
  geom_line(size = 0.5) +
  labs(title = "Brasil") +
  xlab("Mês") +
  ylab("Alertas") +
  scale_linetype_manual(values = 2) +
  colScale +
  theme(legend.position="none")

graf_N
graf_NE
graf_SE
graf_S
graf_CO
graf_BR

(a) Norte

(b) Nordeste

(c) Sudeste

(d) Sul

(e) Centro-Oeste

(f) Brasil

Figure 3: Geral

Ainda vou testar as possibilidades de produção de gráficos interativos.

Mapas

primeiro carrega as bibliotecas do Observable. Foram usadas a bertin para produção de cartogramas e Leaflet para a incorporação de mapas com pan e zoom.

Code
bertin = require("bertin@1");
Code
L = require('leaflet@1.7.1');
Code
html` <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"
   integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="
   crossorigin=""/>`
Code
Plot.plot({
  x: {
    label: "% Pop em risco"
  },
  y: {
    label: "Pop abs em risco"
  },
  marks: [
    Plot.dot(MunicUF.data.features.map((d) => d.properties), {x: d => d["morador"] / d["pop2010"] * 100 , y: "morador"}),
    Plot.dot(MunicUF.data.features.filter((d) => d.properties.nommunic == selNomMunic).map((d) => d.properties), {x: d => d["morador"] / d["pop2010"] * 100, y: "morador", fill: "#ff0000"})
  ]
});

Dá para colocar texto e figuras em margem, etc. Usa o bootstrap para formatar o conteúdo html.

Code
viewof selNomUF = Inputs.select(
    listaUF.data.features.map((d) => d.properties.nm_uf), 
    {
        label: "Unidade da Federação"
    }
);

As ferramentas de layout são relativamente simples, mas podem ser trabalhadas com mais detalhe utilizando html e css.

Code
mapauf = bertin.draw({
  params: { background: "#bde1f0", margin: 10 },
  layers: [{
      type: "layer",
      geojson: selMunic,
      fill: "#ff0000",      
      fillOpacity: 1,
      stroke: "#000000",
      strokeWidth: 2,
      symbol: "square",
      symbol_size: 50
    },
    {
      type: "layer",
      geojson: MunicUF.data,
      fill: "#000000",
      fillOpacity: 0.0,
      stroke: "#ff0000",
      strokeWidth: 3,
      symbol: "square",
      symbol_size: 50
    },
    {
      type: "layer",
      geojson: geo_uf.data,
      fill: "#999999",      
      fillOpacity: 0.25,
      stroke: "#777777",
      strokeWidth: 3,
      symbol: "square",
      symbol_size: 50
    },
    {
      type: "tiles",
      opacity: 1,
      style: "openstreetmap",
    }
  ]
});

As camadas utilizadas são obtidas dos geoserviços do IBGE. Para reduzir a transferência de dados, só são requisitados do servidor os dados necessários para construir as listas e mapa mosca das UFs e Municípios e representar as BATERs do município selecionado, através dos campos PropertyName e cql_filter.

Code
urlWFS = "https://geoservicos.ibge.gov.br/geoserver/ows";

listaUF = await wfsRequest(urlWFS, "GetFeature", {
  service: "WFS",
  version: "2.0.0",
  typeNames: "CGMAT:pbqg22_02_Estado_NomUF",
  PropertyName: "cd_uf,nm_uf",
  outputFormat: "application/json"
});

geo_uf = await wfsRequest(urlWFS, "GetFeature", {
  service: "WFS",
  version: "2.0.0",
  typeNames: "CGMAT:pbqg22_02_Estado_NomUF",
  cql_filter: `nm_uf='${selNomUF}'`,
  outputFormat: "application/json"
});

MunicUF = await wfsRequest(urlWFS, "GetFeature", {
  service: "WFS",
  version: "2.0.0",
  typeNames: "CGEO:PARBR2018_Municipios_mapeados",
  cql_filter: `uf='${selUF}'`,
  outputFormat: "application/json"
});

bater = await wfsRequest(urlWFS, "GetFeature", {
  service: "WFS",
  version: "2.0.0",
  typeNames: "CGEO:PARBR2018_BATER_MD",
  cql_filter: `geo_mun='${selCdMunic}'`,
  outputFormat: "application/json"
});

selUF = stripAccents(selNomUF);

selCdMunic = MunicUF.data.features.filter((d) => d.properties.nommunic === selNomMunic)[0].properties.codmunic;

selMunicArray = MunicUF.data.features.filter((d) => d.properties.nommunic == selNomMunic);

selMunic = ({
  "type":"FeatureCollection",
  "features":selMunicArray  
});

Essa tabela pode funcionar como seleção do município…

Code
viewof tabMunic = Inputs.table(MunicUF.data.features.map((d) => d.properties));

… ao invés dessa caixinha de seleção.

Code
viewof selNomMunic = Inputs.select(
    MunicUF.data.features.map((d) => d.properties.nommunic),
    {
        label: "Município"
    }
);
Code
mapaInterativo = {
  let container = DOM.element('div', { style: `width:${width*0.95}px;height:${width/1.7}px` });
 
  yield container;
  
  let map = L.map(container);
  let osmTileLayer = L.tileLayer( 'http://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', {
      attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
  }).addTo( map );

  let limMunic = L.geoJson(selMunic, {
    weight: 4,
    color: '#777777'
  }).addTo(map);
  
  let baterLayer = L.geoJson(bater.data, {
    weight: 5,
    color: '#ff0000',
    onEachFeature: function (feature, layer) {
      let popupTxt = 'Moradores: ' + 
      feature.properties.d004 +
      '<br>Domicílios: ' + feature.properties.d001 +
      '<br>Acurácia: ' + feature.properties.acuracia;
      layer.bindPopup(popupTxt);
    }
  }).addTo(map);
  map.fitBounds(baterLayer.getBounds());
}

Apêndice

Funções do Observable utilizadas no código anterior. Devido à natureza reativa do Observable, a ordem dos códigos do mesmo no documento não tem importância.

Code
async function wfsRequest(
  url,
  operation = "GetCapabilities",
  extraParameters = {}
) {
  const queryParameters = new URLSearchParams({
    request: operation,
    service: "WFS",
    ...extraParameters
  }).toString();

  console.log(queryParameters);

  const response = await fetch(`${url}?${queryParameters}`);
  const responseFormat = response.headers.get("Content-Type");

  // If the WFS server returns Content-Type header containing "json" it will read the data as json, otherwise as text.
  const data = responseFormat.includes("json")
    ? await response.json()
    : await response.text();

  return {
    data: data,
    status: response.status
  };
};

// Maranhão é o único estado com acento no nome na camada de municípios mapeados :P - mantive o "ã" com acento
function stripAccents(str) {
    var reAccents = /[àáâäçèéêëìíîïñòóôõöùúûüýÿÀÁÂÃÄÇÈÉÊËÌÍÎÏÑÒÓÔÕÖÙÚÛÜÝ]/g;
    var replacements = 'aaaaceeeeiiiinooooouuuuyyAAAAACEEEEIIIINOOOOOUUUUY';
    return str.replace(reAccents, function (match) {
        return replacements[reAccents.source.indexOf(match)];
    });
};