Relação de bilheterias em filmes de ação
Visualização de dados com D3.js e gráfico de dispersão com filmes de grandes bilheterias
Vasculhando as fontes de dados encontrei uma listagem com as maiores bilheterias de filmes de ação esse link
Selecionei as primeiras 200 posições e salvei no arquivo box_office_action_movies.csv
para ser importado e usando o D3.js v7 gerei o seguinte gráfico
Relação entre bilheteria doméstica e internacional das 200 maiores de filmes de ação
Filtragem de filmes
Com interação de passar o ponteiro em cima mostra o nome do filme
Geração do gráfico
Para executar o gráfico criamos uma caixa onde ele vai estar contido, com uma animação de carregando
<div id="d3_wrapper" class="d3_graph">
<div id="loading_graphic"></div>
</div>
Fazendo uso da função getElement
para buscar ou criar os elementos atualizando os valores sem recriar
function getElement(parent, tag, selector = "") {
return parent.select(selector || tag).node()
? parent.select(selector || tag)
: parent.append(tag)
}
Desenhar o gráfico com as movimentações de atualizações e remoção, comentários para cada parte
function drawGraphic(data) {
// Selecionar div para colocar o relatório gráfico dentro
const wrapper = d3.select("#d3_wrapper")
// Caso a caixa não esteja disponível pular o desenho
if (!wrapper.node()) {
return
}
// Margens do gráfico
const margin = { top: 10, right: 10, bottom: 60, left: 60 }
// Dimensão do gráfico (largura e altura)
const width =
wrapper.node().getBoundingClientRect().width - margin.left - margin.right
const height = 600 - margin.top - margin.bottom
// Dimensão do elemento SVG (largura e altura)
const widthWrapper = width + margin.left + margin.right
const heightWrapper = height + margin.top + margin.bottom
// Criação da caixa SVG para contenção do gráfico
const svg = getElement(wrapper, "svg")
.attr("width", widthWrapper)
.attr("height", heightWrapper)
// Criação e posicionamento do gráfico
const graph = getElement(svg, "g")
.attr("class", "graph")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
// Valores máximos dos domínios
const maxDomestic = d3.max(data, (d) => d.domestic)
const maxInternational = d3.max(data, (d) => d.international)
// Definição do eixo X
const xScale = d3.scaleLinear().range([0, width]).domain([0, maxDomestic])
// Definição do eixo Y
const yScale = d3
.scaleLinear()
.range([height, 0])
.domain([0, maxInternational])
// Adicionar linhas de grade no eixo X
getElement(graph, "g", "g.grid.x")
.attr("class", "grid x")
.call(
d3
.axisBottom(xScale)
.ticks(10)
.tickSize(height)
.tickFormat((d) => +d / Math.pow(10, 9) + "B")
)
// Adicionar linhas de grade no eixo Y
getElement(graph, "g", "g.grid.y")
.attr("class", "grid y")
.call(
d3
.axisLeft(yScale)
.ticks(10)
.tickSize(-width)
.tickFormat((d) => +d / Math.pow(10, 9) + "B")
)
// Gerar legenda do eixo X
getElement(graph, "text", "text.bottom")
.attr("class", "bottom")
.style("fill", "var(--texts)")
.style("font-size", "1rem")
.style("text-anchor", "end")
.attr("x", xScale(maxDomestic))
.attr("y", height + margin.bottom / 1.5)
.text("Arrecadação doméstica")
// Gerar legenda do eixo Y
getElement(graph, "text", "text.left")
.attr("class", "left")
.style("fill", "var(--texts)")
.style("font-size", "1rem")
.attr("text-anchor", "end")
.attr("transform", "rotate(-90)")
.attr("y", -margin.left + 20)
.attr("x", 0)
.text("Arrecadação internacional")
// Conteúdo para os círculos de dispersão
const content = getElement(graph, "g", "g.circles").attr("class", "circles")
// Criação da caixa de texto para a integração do mouse
const tooltip = getElement(graph, "text", "text.tooltip")
.attr("class", "tooltip")
.style("text-anchor", "start")
.style("fill", "var(--texts)")
.style("font-size", "1.25rem")
.style("visibility", "hidden")
// Interações com o mouse para os círculos
// Atenção pela redeclaração fora do join para atualizar valores
function takeAction(element) {
element
// Apresentar o texto e atualizar conteúdo
.on("mouseover", function (_, d) {
d3.select(this).transition().duration("50").attr("opacity", "1")
tooltip.style("visibility", "visible").text(d.name)
})
// Posicionar o texto perto do círculo
// Ajustando para baixo e esquerda para encaixar na tela
.on("mousemove", function (_, d) {
const xPos = xScale(d.domestic)
const yPos =
yScale(d.international) +
(d.international < maxInternational * 0.6 ? -20 : 30)
tooltip
.style(
"text-anchor",
d.domestic < maxDomestic * 0.6 ? "start" : "end"
)
.attr("transform", "translate(" + xPos + "," + yPos + ")")
})
// Ocultar o texto
.on("mouseout", function () {
d3.select(this).transition().duration("50").attr("opacity", "0.5")
tooltip.style("visibility", "hidden")
})
}
// Desenhar círculos de dispersão
content
.selectAll("circle")
.data(data)
.join(
// Valores inciais para os círculos
(enter) =>
enter
.append("circle")
.call(takeAction)
.attr("cx", (d) => xScale(d.domestic))
.attr("cy", (d) => yScale(d.international))
.attr("fill", "var(--highlight)")
.attr("stroke", "var(--texts)")
.attr("opacity", "0.5")
.attr("r", 0)
.transition()
.duration(500)
.attr("r", 10),
// Movimentar posição ao atualizar os registros
(update) =>
update
.call(takeAction)
.transition()
.duration(500)
.attr("cx", (d) => xScale(d.domestic))
.attr("cy", (d) => yScale(d.international)),
// Reduzir o círculo antes de ser removida
(exit) => exit.transition().duration(500).attr("r", 0).remove()
)
// Ocultar o ícone de carregar
d3.select("#loading_graphic").style("display", "none")
}
Seleção de dados
Carregamos somente os 200 primeiros registro da fonte de dados original, com os botões para filtrar os valores
<div class="d3_commands">
<button id="top_10">Melhores 10</button>
<button id="top_50">Melhores 50</button>
<button id="top_100">Melhores 100</button>
<button id="reset_data" class="disabled">Todos</button>
<button id="bottom_100">Últimos 100</button>
<button id="bottom_50">Últimos 50</button>
<button id="bottom_10">Últimos 10</button>
</div>
Para cada botão selecionar uma parte dados brutos raw
e atualizar os dados usados no gráfico realizando o desenho novamente
async function onLoad() {
// Busca das informações dos filmes em CSV
const raw = await d3.csv(
"/extras/box_office_action_movies.csv",
// Converter os dados em números
// e colocar o ano no nome do texto
(d) => ({
name: `${d.Movie} (${d.Released})`,
domestic: +d["Domestic Box Office"].replace("$", "").split(",").join(""),
international: +d["International Box Office"]
.replace("$", "")
.split(",")
.join(""),
})
)
function enable_button(element) {
d3.select("#top_10").attr("class", "")
d3.select("#top_50").attr("class", "")
d3.select("#top_100").attr("class", "")
d3.select("#reset_data").attr("class", "")
d3.select("#bottom_100").attr("class", "")
d3.select("#bottom_50").attr("class", "")
d3.select("#bottom_10").attr("class", "")
d3.select(element).attr("class", "disabled")
}
function filterData(start, end) {
return function () {
enable_button(this)
// Aplicar filtros das informações
drawGraphic(raw.slice(start, end))
}
}
// Selecionar as 10 primeiras
d3.select("#top_10").on("click", filterData(0, 10))
// Selecionar as 20 primeiras
d3.select("#top_50").on("click", filterData(0, 50))
// Selecionar as 50 primeiras
d3.select("#top_100").on("click", filterData(0, 100))
// Apresentar todos os registros
d3.select("#reset_data").on("click", filterData(0, 200))
// Selecionar as 50 últimos
d3.select("#bottom_100").on("click", filterData(100, 200))
// Selecionar as 20 últimos
d3.select("#bottom_50").on("click", filterData(150, 200))
// Selecionar as 10 últimos
d3.select("#bottom_10").on("click", filterData(190, 200))
// Desenhar o gráfico no momento do carregamento
drawGraphic(raw)
}
Primeiro desenho
Executar o primeiro desenho com o load da página, caso o documento não carregue completamente, garantimos o desenho depois de 2 segundos
// Chamada da função para desenhar gráfico
window.addEventListener("load", onLoad)
// Caso o evento load não seja acionado rodar depois de 2 segundos
setTimeout(() => document.querySelector("#d3_wrapper svg") || onLoad(), 2000)
Temos nosso gráfico de dispersão em tela
Referências
Kaggle - All Time Worldwide Box Office for Action Movies
Versões das bibliotecas utilizadas
d3: "7.8.4"