Uma introdução suave ao D3: como criar um gráfico de bolhas reutilizável

Introdução ao D3

Quando comecei a aprender D3, nada fazia sentido para mim. As coisas só ficaram mais claras quando comecei a aprender sobre gráficos reutilizáveis.

Neste artigo, mostrarei como criar um gráfico de bolhas reutilizável e uma introdução suave ao D3 ao longo do caminho. O conjunto de dados que usaremos é composto por histórias publicadas no freeCodeCamp em janeiro de 2017.

Este é o gráfico que você vai construir

Sobre o D3

D3 é uma biblioteca JavaScript para visualização de dados. Dá vida aos dados usando HTML, SVG e CSS.

Geralmente, precisamos reutilizar um gráfico em outro projeto ou compartilhar um gráfico com outros. Para isso, Mike Bostock (o criador do D3) propôs um modelo chamado gráficos reutilizáveis. Usaremos sua abordagem com algumas pequenas modificações, como apresentado por Pablo Navarro Castillo no livro Mastering D3.js.

Estamos usando o D3 versão 4.6.0 aqui.

Gráficos reutilizáveis

Os gráficos que seguem o padrão reutilizável do gráfico têm duas características:

  • Configurabilidade. Queremos modificar a aparência e o comportamento do gráfico sem precisar modificar o próprio código.
  • Capacidade de ser construído de maneira independente. Queremos que cada elemento gráfico associado a um ponto de dados do nosso conjunto de dados seja independente. Isso tem a ver com a maneira como o D3 associa instâncias de dados a elementos DOM. Isso ficará mais claro em um minuto.
“Resumindo: implemente gráficos como fechamento com métodos getter-setter.” - Mike Bostock

O gráfico de bolhas

Você primeiro precisa definir quais elementos do gráfico podem ser personalizados:

  • O tamanho do gráfico
  • O conjunto de dados de entrada

Definindo o tamanho do gráfico

Vamos começar criando uma função para encapsular todas as variáveis ​​do gráfico e definir os valores padrão. Essa estrutura é chamada de fechamento.

// bubble_graph.js
var bubbleChart = function () {
    largura var = 600,
    altura = 400;

    gráfico de funções (seleção) {
        // você vai chegar aqui
    }

    gráfico de retorno;
}

Você deseja criar gráficos de tamanhos diferentes sem precisar alterar o código. Para isso, você criará gráficos da seguinte maneira:

// bubble_graph.html
var gráfico = gráfico de bolhas (). largura (300). altura (200);

Para fazer isso, agora você definirá os acessadores para as variáveis ​​de largura e altura.

// bubble_graph.js
var bubbleChart = function () {
    largura var = 600
    altura = 400;
    gráfico de funções (seleção) {
        // nós vamos chegar aqui
    }

    chart.width = função (valor) {
        if (! argument.length) {retorna largura; }
        largura = valor;

        gráfico de retorno;
    }
    chart.height = função (valor) {
        if (! argument.length) {return height; }
        altura = valor;

        gráfico de retorno;
    }

    gráfico de retorno;
}

Se você chamar bubbleChart () (sem atributos de largura ou altura), o gráfico será criado com os valores padrão de largura e altura que você definiu dentro do fechamento. Se chamado sem argumentos, o método retornará o valor da variável.

// bubble_graph.html
var gráfico = bubbleChart ();
bubbleChart (). width (); // retorna 600

Você pode estar se perguntando por que os métodos retornam a função de gráfico. Este é um padrão JavaScript usado para simplificar o código. Isso se chama encadeamento de métodos. Com esse padrão, você pode criar novos objetos como este:

// bubble_graph.html
var gráfico = gráfico de bolhas (). largura (600). altura (400);

ao invés de:

// bubble_graph.html
var gráfico = bubbleChart ();
chart.setWidth (600);
chart.setHeight (400);

Unindo dados ao nosso gráfico

Agora vamos aprender como associar dados aos elementos do gráfico. Veja como o gráfico está estruturado: a div com o gráfico tem um elemento SVG e cada ponto de dados corresponde a um círculo no gráfico.

// bubble_graph.html, depois que a função bubbleChart () é chamada

      // uma história de dados
      // outra história dos dados
    ...

3 d3.data ()

A função d3.selection.data ([data [, key]]) retorna uma nova seleção que representa um elemento vinculado com êxito aos dados. Para fazer isso, primeiro você precisa carregar os dados do arquivo .csv. Você usará a função d3.csv (url [[, linha], retorno de chamada]).

// bubble_graph.html
d3.csv ('arquivo.csv', função (erro, nossos_dados) {
    var data = nossos_dados; // aqui você pode fazer o que quiser com os dados
}
// medium_january.csv
| título | categoria | corações
| -------------------------------------- | ---------- ---- | -------- |
| Ninguém quer usar software | Desenvolvimento | 2700
| Navegação na Web sem perdas com trilhas | Design | 688
| A ascensão do engenheiro de dados | Ciência de dados | 862

🖍 seleção d3

Você usará as funções d3-select () e data () para passar nossos dados para o gráfico.

As seleções permitem uma poderosa transformação orientada a dados do DOM (Document Object Model): defina atributos, estilos, propriedades, conteúdo em HTML ou texto e muito mais. - documentação D3
// bubble_graph.html
d3.csv ('medium_january.csv', função (erro, nossos_dados) {
    if (erro) {
        console.error ('Erro ao obter ou analisar os dados.');
        erro de lançamento;
    }
    var gráfico = gráfico de bolhas (). largura (600). altura (400);
    d3.select ('# chart'). data (our_data) .call (chart);
 });

Outro seletor importante é o d3.selectAll (). Digamos que você tenha a seguinte estrutura:


    
    
    

d3.select ("body"). selectAll ("div") seleciona todos esses divs para nós.

d3.enter ()

E agora você aprenderá sobre uma importante função D3: d3.enter (). Digamos que você tenha uma etiqueta de corpo vazia e uma matriz com dados. Você deseja percorrer cada elemento da matriz e criar uma nova div para cada elemento. Você pode fazer isso com o seguinte código:



 //esvaziar
----
// script js
var our_data = [1, 2, 3]

var div = d3.select ("corpo")
 .selectAll ("div")
 .data (nossos_dados)
 .entrar()
 .append ("div");

---


    
    
    

Por que você precisa selectAll ("div") se os divs ainda nem existem? Porque no D3, em vez de dizer como fazer algo, dizemos o que queremos.

Nesse caso, você deseja associar cada div a um elemento da matriz. É o que você está dizendo com o selectAll ("div").

var div = d3.select ("corpo")
 .selectAll ("div") // aqui você está dizendo 'ei d3, cada elemento de dados da matriz que vem a seguir será vinculado a uma div'
 .data (nossos_dados)
 .enter (). append ("div");

O enter () retorna a seleção com os dados vinculados ao elemento da matriz. Você finalmente adiciona essa seleção ao DOM com o .append ("div")

3d3.forceSimulation ()

Você precisa de algo para simular a física dos círculos. Para isso, você usará o d3.forceSimulation ([nós]). Você também precisa dizer que tipo de força mudará a posição ou a velocidade dos nós.

No nosso caso, usaremos o d3.forceManyBody ().

// bubble_chart.js
simulação de var = d3.forceSimulation (dados)
 .force ("carga", d3.forceManyBody (). força ([- 50]))
 .force ("x", d3.forceX ())
 .force ("y", d3.forceY ())
 .on ("tick", marcado);

Um valor de força positivo faz com que os nós se atraiam, enquanto um valor de força negativo faz com que eles se repelam.

O efeito de força ()

Porém, não queremos que os nós se espalhem por todo o espaço SVG; portanto, usamos d3.forceX (0) e d3.forceY (0). Isso "arrasta" os círculos para a posição 0. Vá em frente e tente remover isso do código para ver o que acontece.

Quando você atualiza a página, pode ver que os círculos se ajustam até finalmente estabilizarem. A função assinalada () atualiza as posições dos círculos. O d3.forceManyBody () continua atualizando a posição xey de cada nó, e a função ticked () atualiza o DOM com esses valores (os atributos cx e cy).

// bubble_graph.js
função marcada (e) {
    node.attr ("cx", função (d) {return d.x;})
        .attr ("cy", função (d) {return d.y;});
    // 'node' é cada círculo do gráfico de bolhas
 }

Aqui está o código com tudo junto:

simulação de var = d3.forceSimulation (dados)
    .force ("carga", d3.forceManyBody (). força ([- 50]))
    .force ("x", d3.forceX ())
    .force ("y", d3.forceY ())
    .on ("tick", marcado);
função marcada (e) {
    node.attr ("cx", função (d) {return d.x;})
        .attr ("cy", função (d) {return d.y;});
}

Para resumir, tudo o que esta simulação faz é dar a cada círculo uma posição x e y.

d3.scales

Aí vem a parte mais emocionante: realmente adicionando os círculos. Lembra da função enter ()? Você o usará agora. Em nosso gráfico, o raio de cada círculo é proporcional ao número de recomendações de cada história. Para fazer isso, você usará uma escala linear: d3.scaleLinear ()

Para usar escalas, você precisa definir duas coisas:

  • Domínio: os valores mínimo e máximo dos dados de entrada (no nosso caso, o número mínimo e máximo de recomendações). Para obter os valores mínimo e máximo, use as funções d3.min () e d3.max ().
  • Faixa: os valores mínimo e máximo de saída da balança. No nosso caso, queremos o menor raio de tamanho 5 e o maior raio de tamanho 18.
// bubble_graph.js
var scaleRadius = d3.scaleLinear ()
            .domain ([d3.min (dados, função (d) {return + d.views;}),
                    d3.max (dados, função (d) {return + d.views;})])
            .range ([5,18]);

E então você finalmente cria os círculos:

// bubble_graph.js
var node = svg.selectAll ("circle")
   .dados (dados)
   .entrar()
   .append ("círculo")
   .attr ('r', função (d) {return scaleRadius (d.views)})
});

Para colorir os círculos, use uma escala categórica: d3.scaleOrdinal (). Essa escala retorna valores discretos.

Nosso conjunto de dados tem 3 categorias: Design, Desenvolvimento e Ciência de Dados. Você mapeará cada uma dessas categorias para uma cor. O d3.schemeCategory10 fornece uma lista de 10 cores, o que é suficiente para nós.

// bubble_graph.js
var colorCircles = d3.scaleOrdinal (d3.schemeCategory10);

var node = svg.selectAll ("circle")
    .dados (dados)
    .entrar()
    .append ("círculo")
    .attr ('r', função (d) {return scaleRadius (d.views)})
    .style ("preenchimento", função (d) {retorna colorCircles (d.category)});

Você deseja desenhar os círculos no meio do SVG, para mover cada círculo para o meio (metade da largura e metade da altura). Vá em frente e remova isso do código para ver o que acontece.

// bubble_graph.js
var node = svg.selectAll ("circle")
 .dados (dados)
 .entrar()
 .append ("círculo")
 .attr ('r', função (d) {return scaleRadius (d.views)})
 .style ("preenchimento", função (d) {retorna colorCircles (d.category)})
 .attr ('transformar', 'traduzir (' + [largura / 2, altura / 2] + ')');

Agora você adiciona dicas de ferramentas ao gráfico. Eles precisam aparecer sempre que colocamos o mouse sobre os círculos.

var dica de ferramenta = seleção
 .append ("div")
 .style ("posição", "absoluto")
 .style ("visibilidade", "oculto")
 .style ("cor", "branco")
 .style ("preenchimento", "8px")
 .style ("cor de fundo", "# 626D71")
 .style ("raio da borda", "6px")
 .style ("alinhamento de texto", "centro")
 .style ("família de fontes", "monoespaço")
 .style ("largura", "400 px")
 .texto("");
var node = svg.selectAll ("circle")
 .dados (dados)
 .entrar()
 .append ("círculo")
 .attr ('r', função (d) {return scaleRadius (d.views)})
 .style ("preenchimento", função (d) {retorna colorCircles (d.category)})
 .attr ('transformar', 'traduzir (' + [largura / 2, altura / 2] + ')')
 .on ("mouseover", função (d) {
     tooltip.html (d.category + "
" + d.title + "
" + d.views);      retornar tooltip.style ("visible", "visible");})  .on ("mousemove", function () {    return tooltip.style ("top", (d3.event.pageY- 10) + "px"). style ("left", (d3.event.pageX + 10) + "px");})  .on ("mouseout", function () {retornar tooltip.style ("visibilidade", "oculto");});

A ratoeira segue o cursor quando o mouse está se movendo. d3.event.pageX e d3.event.pageY retornam as coordenadas do mouse.

E é isso! Você pode ver o código final aqui.

Você pode jogar com o gráfico de bolhas aqui.

Você achou este artigo útil? Eu tento o meu melhor para escrever um artigo de mergulho profundo todos os meses. Você pode receber um email quando eu publicar um novo.

Alguma pergunta ou sugestão? Deixe-os nos comentários. Obrigado pela leitura!

Agradecimentos especiais a John Carmichael e Alexandre Cisneiros.