class: center, middle, inverse # Visualização de informação na web com D3.js .author[Fábio Sikansi] .author[Samuel Fadel] vis.icmc.usp.br 18 de Agosto de 2015 .left-column.column-6.stretch-4[![](figs/logo-semcomp-inverse.svg)] .right-column.column-6.stretch-8[![](figs/logo-icmc-inverse.svg)] --- class: middle,center # Big Data ## O que é Big Data? --- class: middle,center .center.stretch-10[![...](figs/bigdata.jpg)] --- class: middle,center .center.stretch-10[![...](figs/infographic.jpg)] --- # Introdução ### O que é visualização de informação? .center.italic["Visualização de dados que não tem forma ou cor inerente."] - Grafos - Tabelas de bancos de dados - Música - Texto - .italic[Software] - etc. .center.stretch-12[ ![Visualização de informação](figs/teaser_small.png) ] --- # Introdução ### Por que visualização de informação? - Reconhecimento de padrões é natural à visão .center.stretch-10[ ![Gestalt](figs/gestalt.gif) ] --- # Introdução ### Por que visualização de informação? - 40% das fibras nervosas conectadas ao cérebro estão ligadas à retina -- - 50% dos tecidos neurais estão direta ou indiretamente ligados à visão -- ### Objetivo Representar dados visualmente, comunicando - Padrões - Tendências - Anomalias --- # Como comunicar efetivamente? -- ### Escolha bem a representação .center.stretch-12[ ![Representações alternativas de grafos](figs/grafos.svg) ] --- # Como comunicar efetivamente? ### Visão não é perfeita - Tanto com formas... .center.stretch-12[ ![Ilusão de forma](figs/ilusao.svg) ] --- # Como comunicar efetivamente? ### Visão não é perfeita - ... quanto com cores .center.stretch-6[ ![Ilusão de cor](figs/gradiente1.svg) ] --- # Como comunicar efetivamente? ### Visão não é perfeita - ... quanto com cores .center.stretch-6[ ![Ilusão de cor](figs/gradiente2.svg) ] --- # Como comunicar efetivamente? ### _Guidelines_ .relative[ - Casamento entre _natureza_ dos dados e _representação_ visual - Quando duas ou mais ferramentas podem desempenhar a mesma tarefa, escolha a que permite o trabalho mais valioso por unidade de tempo - Adote novas soluções apenas quando o retorno for maior do que o custo de se aprender - Representações devem levar em conta as capacidades sensoriais humanas, destacando elementos importantes e acelerando a percepção de padrões - Simbologia gráfica deve ser padronizada dentro e entre aplicações .center.big.overlay[ ] ] --- # Como comunicar efetivamente? ### _Guidelines_ .relative[ .fadeOut[ - Casamento entre _natureza_ dos dados e _representação_ visual - Quando duas ou mais ferramentas podem desempenhar a mesma tarefa, escolha a que permite o trabalho mais valioso por unidade de tempo - Adote novas soluções apenas quando o retorno for maior do que o custo de se aprender - Representações devem levar em conta as capacidades sensoriais humanas, destacando elementos importantes e acelerando a percepção de padrões - Simbologia gráfica deve ser padronizada dentro e entre aplicações ] .center.big.overlay[ .italic["Information Visualization: Perception for Design"] Collin Ware ] ] --- class: center, middle # Como comunicar? --- # A biblioteca D3.js .left-column.column-8[ - Biblioteca .italic[javascript] para a visualização de informação - Criada por Mike Bostock - Publicada no IEEE Vis 2011 ] .right-column.column-4.padding-20.center[ ![Logo D3](figs/d3.svg) ] .column-12.center.big.padding-300[ http://d3js.org/ ] --- class: center, middle # O que é o D3? -- ## .crimson[Não é uma biblioteca para gráficos] ### (embora você possa fazer excelentes gráficos com ela) --- class: center, middle # O que é o D3? ## .crimson[Não é uma biblioteca para mapas] ### (embora você possa fazer excelentes mapas com ela) --- class: center, middle ## .crimson[Não é uma biblioteca para desenhar em SVG] -- ### (embora você com certeza vá desenhar em um SVG com ela!) #### O D3 pode ser usado também para manipular HTML e Canvas --- class: center, middle # O D3 É -- .center[.big[Dados <i class="fa fa-long-arrow-right crimson"></i>] ![Logo D3](figs/d3.svg) .big[<i class="fa fa-long-arrow-right crimson"></i> Documentos]] -- # É uma biblioteca para que você construa uma aplicação em que os .crimson[dados entram puros] e saem em uma .crimson[representação visual] --- class: center, middle .stretch-10[![...](figs/nytimesvis.png)] --- class: center, middle .stretch-10[![...](figs/nytimesvis2.jpg)] --- class: center, middle .stretch-12[![...](figs/nytimesvis3.jpg)] --- # O minicurso ### Atividades - Abordagem prática - Foco no D3.js - 2 visualizações - Gráficos - Mapas -- .center[ # Nada de HTML e CSS... ] --- # ...ou quase nada ## HTML básico e área de desenho .center.stretch-12[![Template](figs/template.jpg)] --- # HTML e CSS ### HTML ```xml <div class="vis"> </div> ``` ### CSS ```css .vis { margin-top: 10px; margin-left: 0.5%; width: 99%; } ``` --- class: center, middle # Primeiros passos --- class: middle,center # Download [http://d3.fabiosikansi.com/TemplateD3.zip](http://d3.fabiosikansi.com/TemplateD3.zip) --- # Um pouco de JavaScript ## Funções - Funções são como outros tipos básicos (e.g. `int`, `char`) - Definindo `foo` de maneiras equivalentes: ```javascript function foo(i) { return i + 1; } ``` ```javascript // Uma função anônima que agora tem nome 'foo' var foo = function(i) { return i + 1; }; ``` - A chamada é a mesma para as duas declarações: ```javascript console.log(foo(1)); // 2 ``` --- # Um pouco de JavaScript ## _Callback_ - Uma função a ser chamada no futuro - Ao fim de operações assíncronas - Operar dentro de outro contexto (exemplo abaixo) ```javascript function forEach(array, callback) { for (var i = 0; i < array.length; i++) { callback(array[i]); } } function myPrint(element) { console.log(element); } var values = [ 1, 2, 3, 4, 5 ]; // array literal forEach(values, myPrint); // 1 2 3 4 5 ``` --- # Um pouco de JavaScript ## _Callback_ + função anônima - Código mais "enxuto" para casos mais simples ```javascript function forEach(array, callback) { for (var i = 0; i < array.length; i++) { callback(array[i]); } } forEach(values, function(element) { console.log(element); }); // 1 2 3 4 5 ``` --- # Mais um pouco de JavaScript ## Objetos - JSON (JavaScript Object Notation) ```javascript var obj = { // Atributos "val": 0, "name": "foo", // Métodos "show": function() { console.log(this.name + ": " + this.val); }, "setValue": function(val) { this.val = val; }, "setName": function(name) { this.name = name; } }; obj.show(); // "foo: 0" obj.setValue(1); obj.setName("bar"); obj.show(); // "bar: 1" ``` --- # Mais um pouco de JavaScript ## Objetos - Como deixar mais "enxuto"? ```javascript obj.setValue(1) .setName("bar") .show(); // "bar: 1" ``` - *Convenção:* métodos retornam o próprio objeto quando alteram estado ```javascript "setValue": function(val) { this.val = val; * return this; }, "setName": function(name) { this.name = name; * return this; }, ``` --- name: barchart # Mapeando dados - Suponha o seguinte conjunto de dados ```javascript var dataset = [ 5, 3, 7, 11, 3, 4, 8, 8 ]; ``` - Como mostrar esses dados em um gráfico de barras? --- # Como o D3.js "desenha" na tela? - Ele não desenha! - A essência do D3.js: gerar documentos a partir de dados .center[ ## D3: Data-Driven Documents ] --- # Como o D3.js "desenha" na tela? - SVG (Scalable Vector Graphics) .left-column.column-6[ ```xml <?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg viewBox="0 0 35 35" xmlns="http://www.w3.org/2000/svg"> <circle cx="25" cy="15" r="10" style="fill: #d13e2e;"/> <rect x="0" y="15" width="30" height="15" style="fill: #2d6ac2;"/> </svg> ``` ] .right-column.column-6.stretch-8.center[ ![](figs/example.svg) ] --- # Como o D3.js "desenha" na tela? - SVG (Scalable Vector Graphics) .left-column.column-6[ ```xml <?xml version="1.0" encoding="UTF-8" standalone="no"?> *<svg viewBox="0 0 35 35" xmlns="http://www.w3.org/2000/svg"> <circle cx="25" cy="15" r="10" style="fill: #d13e2e;"/> <rect x="0" y="15" width="30" height="15" style="fill: #2d6ac2;"/> </svg> ``` ] .right-column.column-6.stretch-8.center[ ![](figs/example-viewbox.svg) ] ??? - Correspondência entre viewport e viewbox - Sistema de coordenadas onde - *y* aumenta pra cima - *x* aumenta para a esquerda --- # Como o D3.js "desenha" na tela? - A API do D3 permite manipular elementos para gerar um documento .left-column.column-6[ ```javascript svg.append("circle") .attr("cx", 25) .attr("cy", 15) .attr("r", 10) .style("fill", "#d13e2e"); svg.append("rect") .attr("x", 0) .attr("y", 15) .attr("width", 30) .attr("height", 15) .style("fill", "#2d6ac2"); ``` ] .right-column.column-6.stretch-8.center[ ![](figs/example.svg) ] --- # Como o D3.js "desenha" na tela? - Podemos também usar CSS para definir a aparência dos elementos .left-column.column-6[ ```css .blue { fill: #d13e2e; } .red { fill: #2d6ac2; } ``` ```javascript svg.append("circle") .attr("cx", 25) .attr("cy", 15) .attr("r", 10) * .attr("class", "blue"); svg.append("rect") .attr("x", 0) .attr("y", 15) .attr("width", 30) .attr("height", 15) * .style("class", "red"); ``` ] .right-column.column-6.stretch-8.center[ ![](figs/example.svg) ] --- # Mapeando dados - Agora sabemos como desenhar um retângulo ```javascript svg.append("rect") ``` - Gráfico de barras: - Um retângulo para cada elemento - A altura do retângulo representa o tamanho do número - O D3.js já possui mecanismos para facilitar tudo isso --- # Seleções - Seletores como os de CSS - `d3.select()`: o primeiro elemento de um seletor ```javascript var svg = d3.select("svg"); // Primeiro SVG que encontrar ``` - `d3.selectAll()`: todos os elementos ```javascript var allBars = d3.selectAll("svg rect"); // Todos os rects dentro de todos os SVGs var bars = svg.selectAll("rect"); // Todos os rects dentro de uma seleção ``` --- # Associação de dados - Opera sobre seleções ```javascript // associa cada elemento de 'dataset' a um elemento da seleção svg.selectAll("rect").data(dataset); ``` - Como criar/remover/atualizar elementos com base nos dados? - `.enter()` - `.update()` - `.exit()` --- # Associação de dados - `.enter()` ```javascript // Seleção especial que contém elementos AINDA NÃO CRIADOS var newBars = svg.selectAll("rect") .data(dataset) .enter(); ``` - Lembre-se: para criar elementos usamos `.append()` ```javascript // Cria um rect para cada dado que ainda não possui um rect associado *var newBars = svg.selectAll("rect") .data(dataset) .enter() * .append("rect"); ``` - Como fazer os atributos serem baseados nos dados? (cadê o *data-driven*?) --- # Associação de dados - `.enter()` - Cadê o *data-driven*? - `.attr(atrib, valor)` define o valor de um atributo - Se `valor` é uma função, o D3 a chama passando o dado e o índice do dado ```javascript svg.selectAll("rect") .data(dataset) .enter() .append("rect") .attr("x", function(d, i) { return i * 5; }) .attr("y", function(d) { d * 5; }) .attr("width", 5) .attr("height", function(d) { d * 5; }); ``` --- # Associação de dados - `.enter()` - Cadê o *data-driven*? - `.attr(atrib, valor)` define o valor de um atributo - Se `valor` é uma função, o D3 a chama passando o dado e o índice do dado ```javascript svg.selectAll("rect") .data(dataset) .enter() .append("rect") * .attr("x", function(d, i) { return i * 5; }) .attr("y", function(d) { d * 5; }) .attr("width", 5) .attr("height", function(d) { d * 5; }); ``` --- # Associação de dados - `.enter()` - Cadê o *data-driven*? - `.attr(atrib, valor)` define o valor de um atributo - Se `valor` é uma função, o D3 a chama passando o dado e o índice do dado ```javascript svg.selectAll("rect") .data(dataset) .enter() .append("rect") .attr("x", function(d, i) { return i * 5; }) * .attr("y", function(d) { d * 5; }) .attr("width", 5) * .attr("height", function(d) { d * 5; }); ``` --- # Associação de dados - `.enter()` - Cadê o *data-driven*? - `.attr(atrib, valor)` define o valor de um atributo - Se `valor` é uma função, o D3 a chama passando o dado e o índice do dado ```javascript svg.selectAll("rect") .data(dataset) .enter() .append("rect") .attr("x", function(d, i) { return i * 5; }) .attr("y", function(d) { d * 5; }) * .attr("width", 5) .attr("height", function(d) { d * 5; }); ``` --- # Associação de dados ## Outras seleções especiais - `.update()` ```javascript svg.selectAll("rect") .data(dataset) .update() // atualizar usando .attr() ``` - `.exit()` ```javascript // Remove do documento todos os elementos que perderam associação svg.selectAll("rect") .data(dataset) .exit() .remove(); ``` --- # Mapeando dados ## Atividade - Mostrar o seguinte conjunto de dados em um gráfico de barras: ```javascript var dataset = [ 5, 3, 7, 11, 3, 4, 8, 8 ]; ``` --- # D3 vs jQuery .left-column.column-6.stretch-10[ .center[ ## D3 ] ```javascript d3.select('#foo') .style('background', '#000') .on('click', function() {}) .append('div'); ``` ```javascript d3.json('foo.json', function(err, data) { }); ``` ] .right-column.column-6.stretch-10[ .center[ ## jQuery ] ```javascript $('#foo') .css('background', '#000') .on('click',function() {}) .append($('<div></div>')); ``` ```javascript $.getJSON('foo.json', function(data) { }); ``` ] --- # D3 vs jQuery - Insira 10 itens na lista `$foo`: #### jQuery: "faça isso!" ```javascript var $foo = $('#foo'); for (var i = 0; i < 10; i++) { $foo.append($('<li></li>').text(i).on('click', function() { alert(i); })); } ``` #### D3: "eu quero isso!" ```javascript d3.select('#foo').selectAll('li') .data(d3.range(10)) .enter() .append('li') .text(function(d) { return d; }) .on('click', function(d) { alert(d); }); ``` --- class: center, middle ## Atividade # Análise de conflitos entre países nos anos 1400 a 2000 --- class: center, middle ## Para começar # algumas perguntas... --- # Perguntas - Qual guerra matou mais pessoas proporcionalmente ao número de habitantes? - Desde o século 14 até hoje, qual o século de maior "paz" na Terra? - Desde o século 14 até hoje, a Terra está mais pacífica ou mais belicista? -- .center[ ## É possível responder todas essas perguntas em um gráfico? ] --- # Dados #### Conflict Catalog *"Violent Conflicts 1400 A.D. to the Present in Different Regions of the World"* .right[ Centre for Global Economic History ] O conjunto de dados tem registros de 3708 conflitos com: - Participantes - Datas de início e fim - Fatalidades --- # Preparando os dados - Conjuntos de dados geralmente precisam ser preparados - No nosso caso, dados faltantes em alguns atributos - Não temos o ano de início, fim e fatalidades totais em todos registros - Usaremos só os registros que possuem essas informações (1205 registros) --- # Preparando a área de desenho ```javascript var aspect = 580.0 / 1280.0; $(document).on("ready", function () { var width = $(".vis").width(), height = width * aspect; var svg = d3.select(".vis").append("svg") .attr("width", width) .attr("height", height); * d3.json("conflicts-filtered.json", vis); }); ``` --- # *Callback* para requisição dos dados ```javascript function vis(error, data) { if (error) { console.log(error); return; } // ... } ``` --- # Preparando a área de desenho (2) ```javascript var PADDING = 50; var svg = $(".vis > svg"); var visWidth = svg.width() - 2*PADDING, visHeight = svg.height() - 2*PADDING; svg = d3.select(".vis > svg").append("g") .attr("transform", "translate(" + PADDING + ", " + PADDING + ")"); ``` --- # Escalas - No nosso gráfico, .big.center[(ano, fatalidades) <i class="fa fa-long-arrow-right crimson"></i> <i class="fa fa-magic"></i> <i class="fa fa-long-arrow-right crimson"></i> (x, y)] - Escalas são funções que mapeiam um intervalo em outro - O D3 oferece vários tipos de escalas (lineares, logarítmicas, etc.) ```javascript // Para mapear os anos 1400 a 2015 nos pixels 0 a visWidth var x = d3.scale.linear() .range([0, visWidth]) .domain([1400, 2015]); // x(1400) retorna 0 e x(2015) retorna 'visWidth' ``` - y? -- ```javascript var y = d3.scale.log().range([visHeight, 0]).domain([10.0, 100000000.0]); ``` --- # Uma primeira versão ```javascript // Podemos também usar escalas para outros atributos gráficos! var r = d3.scale.linear().range([2.5, 40]).domain(y.domain()); svg.selectAll("circle") .data(data) .enter() .append("circle") .attr("cx", function(d) { return x(d.StartYear); }) .attr("cy", function(d) { return y(d.TotalFatalities); }) .attr("r", function(d) { return r(d.TotalFatalities); }) .attr("class", "conflict"); ``` ```css .vis circle.conflict { fill: #eeaaaa; stroke: #882222; stroke-width: 1; opacity: 0.7; } ``` --- # Uma primeira versão .stretch-12[ ![](figs/atividade_conflitos.png) ] --- # Eixos - Não conseguimos comunicar o que os dados mapeados representam - Eixos servem para mostrar os valores que as escalas mapeiam ```javascript var xAxis = d3.svg.axis() .scale(x) .tickSize(-visHeight), yAxis = d3.svg.axis() .scale(y) .tickSize(-visWidth) .orient("left"); svg.append("g") .attr("class", "x axis") .attr("transform", "translate(0, " + visHeight + ")") .call(xAxis); svg.append("g") .attr("class", "y axis") .call(yAxis); ``` --- # Eixos ```css .axis { shape-rendering: crispEdges; } .axis text { font: 14px sans-serif; } .axis path, .axis line { fill: none; stroke: #222; } .axis .tick line { stroke: #dfdfdf; } ``` -- .center[ **Ainda não está ideal!** ] --- # Combinando duas visualizações - Vamos embutir mais informação na visualização existente - Resposta para a pergunta .center.big[Qual o número médio de fatalidades a cada 15 anos?] --- # Agregando dados por período ```javascript function aggregateYears(data, attr, range) { var agg = []; for (var year = range.start; year <= range.end; year += range.stride) { var dataSlice = data.filter(function(d) { return d.StartYear >= year && d.StartYear < year + range.stride; }); if (dataSlice.length == 0) { continue; } var datum = { "Year": year, "TotalFatalities": d3.mean(dataSlice, function(d) { return d[attr]; }) }; agg.push(datum); } return agg; } ``` --- # Adicionando ao gráfico existente ```javascript var avgGroup = svg.append("g"); var fatalitiesAgg = aggregateYears(data, "TotalFatalities", { start: 1400, end: 2015, stride: 15 }); var line = d3.svg.line() .interpolate("monotone") .x(function(d) { return x(d.Year + 7.5); }) .y(function(d) { return y(d.TotalFatalities); }); avgGroup.append("path") .attr("class", "line") .attr("d", line(fatalitiesAgg)); ``` ```css path.line { fill: none; stroke: #777777; stroke-width: 1.5; } ``` --- # Combinando visualizações - O novo elemento pode trazer informações a mais - Precisaremos de mais dados: população total por período ```javascript d3.csv("world-population.csv", function(error, populationData) { * var population = d3.map(populationData, function(d) { return +d.Year; }); for (var i = 0; i < fatalitiesAgg.length; i++) { fatalitiesAgg[i].Population = * +population.get(fatalitiesAgg[i].Year).Population; } // o resto do código continua o mesmo, exceto pelo posicionamento em y: var yProp = d3.scale.linear().range([visHeight, 0]).domain([0, 1]); var line = d3.svg.line() // ... .y(function(d) { return yProp(d.TotalFatalities * 1000 / d.Population); }); }); ``` --- # O que mais melhorar? - Legenda - Texto dos eixos - Interação (*tooltip*) --- class: center, middle # FIM (desta atividade) --- class: center, middle # Visualizando Mapas .stretch-10[![Mapa utilizando D3](figs/map.svg)] --- # Um pouco de cartografia ## Formatos Shapefile/GeoJSON/TopoJSON - Representação de informação cartográfica em um objeto ```json { "type": "Feature", "geometry": { "type": "Point", "coordinates": [125.6, 10.1] }, "properties": { "name": "Dinagat Islands" } } ``` --- # Como obter o arquivo JSON de uma região específica? .padding-100[## [Let’s Make a Map](http://bost.ocks.org/mike/map/)] .padding-100[## [Interactive Map with d3.js](http://www.tnoda.com/blog/2013-12-07)] --- # Brasil .center.stretch-10[![Mapa do Brasil](figs/brasil.svg)] --- # Mais detalhadamente... ### Extraindo objetos (ruas, avenidas, rodovias, etc) .center.big[[OpenStreetMap](http://www.openstreetmap.org/)] .center.padding-50[[OpenStreetMap Wiki](http://wiki.openstreetmap.org/)] --- # Formato OSM .column-6.left-column[ ### Arquivos OSM.PBF de todos os continentes [http://download.geofabrik.de/](http://download.geofabrik.de/) ] .column-6.right-column[ .center.stretch-12[![Arquivos](figs/pbf.jpg)] ] --- # Filtrar informações ### Osmosis filter file format [http://wiki.openstreetmap.org/wiki/Osmosis/Polygon_Filter_File_Format](http://wiki.openstreetmap.org/wiki/Osmosis/Polygon_Filter_File_Format) -- # Converter para JSON ### GDAL ([http://trac.osgeo.org/gdal/wiki/DownloadingGdalBinaries](http://trac.osgeo.org/gdal/wiki/DownloadingGdalBinaries)) --- # Onde usar isso: .big.center[[http://leafletjs.com/](http://leafletjs.com/)] .big.center[[Google Maps + D3](http://bl.ocks.org/mbostock/899711)] .big.center[[Leaflet + D3](http://bost.ocks.org/mike/leaflet/)] --- class: center,middle # Voltando ao minicurso... --- class: center,middle ### Atividade # Construir um mapa do Brasil interativo com o resultado da apuração de uma eleição --- class: center, middle .stretch-12[![...](figs/atividade_mapa.jpg)] --- # Desafios - Carregar os dados da apuração a partir de uma URL - Ler e desenhar um mapa na tela com o D3 - Atualizar os dados em intervalos regulares - Animar as modificações ### Template da atividade ```terminal index.html /css/template.css /js/code.js /json/states_bra.topo.json ``` --- #TopoJSON Brasil ```javascript { "type": "FeatureCollection", "features": [ { "type": "Feature", "properties": { "adm1_code": "BRA-1294", ..., "name": "Goiás", ..., "type": "Estado",..., "postal": "GO"..., "latitude": -15.863000, "longitude": -49.578600, ...}, "geometry": { "type": "Polygon", "coordinates": [ [ [ -46.233529019348708, -12.796634616794123 ], ...] ] } }, ``` --- # Template: index.hmtl ```html <li><a href="#" onclick="loadMapData(0)">Apurado</a></li> <li><a href="#" onclick="loadMapData(1)">Candidato 1</a></li> <li><a href="#" onclick="loadMapData(2)">Candidato 2</a></li> <li><a href="#" onclick="loadMapData(3)">Candidato 3</a></li> ``` ```html <div class="vis"> <div class="tooltipBox"> <div id="statePopover" class="popover bottom"> <div class="arrow"></div> <h3 class="popover-title">State</h3> <div class="popover-content"> <p>Data</p> </div> </div> </div> </div> <div class="chart"> </div> ``` ??? - O SVG do mapa será inserido dentro da div .vis - O SVG do gráfico será inserido dentro da div .chart --- # Template: template.css ```css #states { fill: black; stroke: white; } #states path { opacity: 0.8; } #states path:hover { opacity: 1.0; } .chart { position: absolute; right: 20px; bottom: 10px; width: 500px; height: 600px; } ``` --- # Template: template.css ```css .axis text { font: 14px sans-serif; fill: white; font-weight: bold; } .axis path, .axis line { fill: none; stroke: none; shape-rendering: crispEdges; } .x.axis path { stroke: white; } .barValue { font: 18px sans-serif; fill: white; font-weight: bold; } ``` --- # Template: code.js ```javascript var aspectHeight = 580; var width = $(".vis").width(), aspect = aspectHeight / 1280, height = width * aspect; var mapSVG = d3.select(".vis").append("svg") .attr("preserveAspectRatio", "xMinYMin meet") .attr("viewBox", "0 0 1280 " + aspectHeight) .attr("width", width) .attr("height", height); ``` --- # Template: code.js Atualizando os dados do mapa ```javascript function loadMapData (profile) { currentProfile = profile; requestMapUpdates++; $.ajax({ url: "http://d3.fabiosikansi.com/election.php?request=" + requestMapUpdates + "&callback=?", jsonp: "callback", dataType: "jsonp", data: { format: "json" }, success: function( response ) { if (requestMapUpdates == response.requestNumber) { updateMapData(response,profile); } } }); } $(document).on("ready", function () { loadMapData(currentProfile); setInterval (function () { loadMapData(currentProfile); },30*1000); }); ??? Comentar o motivo do requestMapUpdate ``` --- # Template: code.js Funções que vamos implementar ```javascript function updateMapData (data,profile) {} function loadTooltipData (state) {} function fillTooltip (data) {} ``` ??? Discutir a variavel profile - Determina qual coloração será carregada no mapa - 0 --- # Para começar .left-column.column-6.padding-100[ - Desenhar o mapa na tela - Desenhar a barra de votos apurados - Desenhar o gráfico do resultado - Criar as interações do usuário ] .right-column.column-6[.stretch-12.padding-50[ ![...](figs/atividade_mapa.jpg) ] ] --- # Passo 1: # Criando o SVG para o mapa -- ```javascript var aspectHeight = 580; var width = $(".vis").width(), aspect = aspectHeight / 1280, height = width * aspect; var mapSVG = d3.select(".vis").append("svg") .attr("preserveAspectRatio", "xMinYMin meet") .attr("viewBox", "0 0 1280 " + aspectHeight) .attr("width", width) .attr("height", height); ``` ??? ```javascript var mapSVG = d3.select(".vis").append("svg") .attr("preserveAspectRatio", "xMinYMin meet") .attr("viewBox", "0 0 1280 " + aspectHeight) .attr("width", width) .attr("height", height); ``` --- ## Como converter dados geográficos para informação na tela do computador?? ```javascript "geometry": { "type": "Polygon", "coordinates": [ [ [ -46.233529019348708, -12.796634616794123 ], ...] ] } ``` .big.center[(lat,lng)<i class="fa fa-globe darkblue"></i> <i class="fa fa-long-arrow-right crimson"></i> <i class="fa fa-magic"></i> <i class="fa fa-long-arrow-right crimson"></i> <i class="fa fa-object-group darkblue"></i>(x,y)] -- ```javascript var projection = d3.geo.mercator() .scale(750) .translate([1100, 100]); var path = d3.geo.path() .projection(projection); ``` [Mais informações](https://github.com/mbostock/d3/wiki/Geo-Projections) --- #### d3.geo .center.stretch-8[![D3 Geo](figs/d3.geo.jpg)] [Outro exemplo](http://bl.ocks.org/mbostock/raw/3795040/) --- # Lendo o arquivo TopoJson ```javascript d3.json("json/states_bra.topo.json", function(error, bra) { mapSVG.append("g") .attr("id", "states") .selectAll("path") .data(topojson.feature(bra, bra.objects.states_bra).features) .enter() .append("path") .attr("id", function(d) { return d.id; }) .attr("class", "state") .attr("data-state", function(d) { return d.properties.name; }) .attr("d", path); }); ``` --- # Lendo o arquivo TopoJson ```javascript *d3.json("json/states_bra.topo.json", function(error, bra) { mapSVG.append("g") .attr("id", "states") .selectAll("path") .data(topojson.feature(bra, bra.objects.states_bra).features) .enter() .append("path") .attr("id", function(d) { return d.id; }) .attr("class", "state") .attr("data-state", function(d) { return d.properties.name; }) .attr("d", path); }); ``` --- # Lendo o arquivo TopoJson ```javascript d3.json("json/states_bra.topo.json", function(error, bra) { * mapSVG.append("g") * .attr("id", "states") * .selectAll("path") .data(topojson.feature(bra, bra.objects.states_bra).features) .enter() .append("path") .attr("id", function(d) { return d.id; }) .attr("class", "state") .attr("data-state", function(d) { return d.properties.name; }) .attr("d", path); }); ``` --- # Lendo o arquivo TopoJson ```javascript d3.json("json/states_bra.topo.json", function(error, bra) { mapSVG.append("g") .attr("id", "states") .selectAll("path") * .data(topojson.feature(bra, bra.objects.states_bra).features) * .enter() .append("path") .attr("id", function(d) { return d.id; }) .attr("class", "state") .attr("data-state", function(d) { return d.properties.name; }) .attr("d", path); }); ``` --- # Lendo o arquivo TopoJson ```javascript d3.json("json/states_bra.topo.json", function(error, bra) { mapSVG.append("g") .attr("id", "states") .selectAll("path") .data(topojson.feature(bra, bra.objects.states_bra).features) .enter() * .append("path") * .attr("id", function(d) { return d.id; }) * .attr("class", "state") * .attr("data-state", function(d) { return d.properties.name; }) .attr("d", path); }); ``` --- # Lendo o arquivo TopoJson ```javascript d3.json("json/states_bra.topo.json", function(error, bra) { mapSVG.append("g") .attr("id", "states") .selectAll("path") .data(topojson.feature(bra, bra.objects.states_bra).features) .enter() .append("path") .attr("id", function(d) { return d.id; }) .attr("class", "state") .attr("data-state", function(d) { return d.properties.name; }) * .attr("d", path); }); ``` --- class: center,middle # Resultado --- .left-column.column-10[ # Barra de Apuração - 2 textos - "Apurado" - "XX.XX %" - 2 Retângulos - Barra "cheia" - Barra "carregado" ] .right-column.column-2.stretch-8.center[ ![Barra de Apuração](figs/read_bar.png) ] -- ```javascript mapSVG.append("g") .append("rect") .attr("x",10) .attr("y",2) .attr("width",2) .attr("height",aspectHeight-10) .style("fill","#2c3e50"); mapSVG.append("g") .append("text") .attr("x",20) .attr("y",15) .style("fill","#ecf0f1") .text("Apurado"); ``` ??? Dicas para os outros elementos - É necessário ter uma classe, uma vez que eles serão selecionados e terão seus valores alterados - O valor inicial é zero - A apuração é dada em porcentagem, então é necessário uma escala ```javascript mapSVG.append("g") .append("rect") .attr("class","readCount") .attr("x",10) .attr("y",2) .attr("width",2) .attr("height",5) .style("fill","#ecf0f1"); mapSVG.append("g") .append("text") .attr("class","readText") .attr("x",20) .attr("y",35) .style("fill","#ecf0f1") .text("0%"); //Escala da apuração var countScale = d3.scale.linear() .range([0, aspectHeight-10]) .domain([0,100]); ``` --- .right-column.column-2.stretch-8.center[ ![Barra de Apuração](figs/read_bar.png) ] # Barra de Apuração ```javascript mapSVG.append("g") .append("rect") .attr("class","readCount") .attr("x",10) .attr("y",2) .attr("width",2) .attr("height",5) .style("fill","#ecf0f1"); ``` -- ```javascript mapSVG.append("g") .append("text") .attr("class","readText") .attr("x",20) .attr("y",35) .style("fill","#ecf0f1") .text("0%"); ``` -- ```javascript var countScale = d3.scale.linear() .range([0, aspectHeight-10]) .domain([0,100]); ``` --- # Gráfico do Resultado Geral - Lembrete: O gráfico será inserido em outra div (.chart) - No estágio inicial não temos nenhuma informação sobre os candidatos - Como desenhar as barras? -- ### Inicializando o gráfico .left-column.column-6[ ```javascript var chartWidth = 500, chartHeight = 600, chartMargin = 40; var chartSVG = d3.select(".chart") .append("svg") .attr("id","chart") .attr("width",chartWidth) .attr("height",chartHeight); ``` ] .right-column.column-6.stretch-10.center[ ![Gráfico](figs/chart.png) ] --- # Escalas e eixos - Eixo X: No momento sem informação - Eixo Y: Porcentagem: [0,100] - O eixo Y não será desenhado -- ### Escala do Eixo Y .left-column.column-6[ ```javascript var y = d3.scale.linear() .range([0,chartHeight]) .domain([0,100]); ``` ] .right-column.column-6.stretch-10.center[ ![Gráfico](figs/chart.png) ] -- .left-column.column-6[ ### Eixo X: Aguardar ```javascript var x = null,xAxis = null; ``` ] --- class: middle,center # Algo a mais? -- ### Escalas de cores --- # Escalas de cores ### Escala de cor com o D3 ```javascript d3.scale.linear().domain([0, 50, 100]).range(["red", "white", "green"]); ``` -- ## Uma escala para cada candidato ```javascript var colorScale = []; colorScale[0] = d3.scale.linear().domain([0, 50]).range(["#99d8c9", "#005824"]); colorScale[1] = d3.scale.linear().domain([0, 50]).range(["#9ecae1", "#084594"]); colorScale[2] = d3.scale.linear().domain([0, 50]).range(["#fc9272", "#99000d"]); colorScale[3] = d3.scale.linear().domain([0, 50]).range(["#bdbdbd", "#252525"]); var colors = ["#016c59","#084594","#99000d","#252525"]; ``` --- class: middle,center # Hora de trabalhar com os dados -- ### Os dados da eleição são fornecidos por uma simples "API" [http://d3.fabiosikansi.com/election.php?request=10](http://d3.fabiosikansi.com/election.php?request=10) --- class: middle # Relembrando nosso template ```javascript $(document).on("ready", function () { loadMapData(currentProfile); setInterval (function () { loadMapData(currentProfile); },30*1000); }); ``` --- # Entendendo a função loadMapData ```javascript function loadMapData (profile) { currentProfile = profile; requestMapUpdates++; $.ajax({ url: "http://d3.fabiosikansi.com/election.php?request=" + requestMapUpdates + "&callback=?", jsonp: "callback", dataType: "jsonp", data: { format: "json" }, success: function( response ) { if (requestMapUpdates == response.requestNumber) { updateMapData(response,profile); } } }); } ``` --- .center[### http://d3.fabiosikansi.com/election.php?request=10] ### Formato: JSON ```javascript ({"requestNumber":10,"time":"14:28", "states":[ {"key":612,"state":"RS","c1":932,"c2":224,"c3":658,"read":1814,"total":16422}, {"key":614,"state":"SC","c1":1424,"c2":122,"c3":702,"read":2248,"total":19178}, {"key":613,"state":"PR","c1":1512,"c2":279,"c3":315,"read":2106,"total":19959}, {"key":625,"state":"ES","c1":948,"c2":228,"c3":336,"read":1512,"total":15153}, ...], "country":{ "votes":[ {"votes":21536,"name":"Candidato 1"}, {"votes":28451,"name":"Candidato 2"}, {"votes":29729,"name":"Candidato 3"}], "read":79716, "total":680510}}); ``` ??? Discutir o formato, o que fazer com os os estados e o país --- # Implementando a função updateMapData(response,profile) ### Quais ações devem ser executadas? - Atualizar tempo no menu superior - Atualizar a barra de apuração - Atualizar a informação mostrada no mapa - Atualizar o gráfico -- ### Atualizar tempo no menu superior ```html <li><a href="#">Atualizado em: <strong id="time"></strong></a></li> ``` -- ```javascript $("#time").html(data.time); ``` --- # Atualizar a barra de apuração ```javascript "country":{ "votes":[...], "read":79716, "total":680510} ``` - Acessar os dados por data.country - Modificar o retângulo .readCount e o texto .readText - Não esquecer da escala: ```javascript var countScale = d3.scale.linear().range([0, aspectHeight-10]).domain([0,100]); ``` ??? Aspectos a serem discutidos: - Calcular em porcentagem a quantidade de votos apurados - Quais parâmetros devem ser atualizados em cada objeto - Como determinar a posição do texto em X -- ```javascript mapSVG.select(".readCount") .attr("height",countScale(100*data.country.read/data.country.total)); ``` -- ```javascript mapSVG.select(".readText") .text((100*data.country.read/data.country.total).toFixed(2) + "%") .attr("y",...); ``` --- class: middle ```javascript mapSVG.select(".readText") .text((100*data.country.read/data.country.total).toFixed(2) + "%") .attr("y",function () { var y = countScale(100*data.country.read/data.country.total); return (y > 35) ? y : 35; }); ``` --- # Um pouco de animação ```javascript mapSVG.select(".readCount") * .transition() * .duration(1000) .attr("height",countScale(100*data.country.read/data.country.total)); mapSVG.select(".readText") .text((100*data.country.read/data.country.total).toFixed(2) + "%") * .transition() * .duration(1000) .attr("y",function () { var y = countScale(100*data.country.read/data.country.total); return (y > 35) ? y : 35; }); ``` ### Veja o resultado ??? - Discutir como é feito a atualização --- class: middle,center # Vamos para o mapa --- # Atualizando a coloração de cada estado ### Relembrando os dados ```javascript "states":[ {"key":612,"state":"RS","c1":932,"c2":224,"c3":658,"read":1814,"total":16422}, {"key":614,"state":"SC","c1":1424,"c2":122,"c3":702,"read":2248,"total":19178}, {"key":613,"state":"PR","c1":1512,"c2":279,"c3":315,"read":2106,"total":19959}, {"key":625,"state":"ES","c1":948,"c2":228,"c3":336,"read":1512,"total":15153}, ...], ``` ??? - Mostrar código do mapa e explicar seletor "#BRA-" + state.key - Detalhar a chave de cada estado - Discutir a opção "profile" e como isso deve ser trabalhado na função que define a coloração - Discutir o retorno de votos de cada candidato, o lido e o total -- ```javascript data.states.forEach(function (state) { d3.select("#BRA-" + state.key).style('fill', function () { return "black" }); }); ``` --- # Utilizando as escalas de cores ```javascript data.states.forEach(function (state) { d3.select("#BRA-" + state.key).style('fill', function () { if (profile == 1) return colorScale[0](100*state.c1/state.total); if (profile == 2) return colorScale[1](100*state.c2/state.total); if (profile == 3) return colorScale[2](100*state.c3/state.total); * if (profile == 0) { * ... * } }); }); ``` ??? - Profile = 0 significa que deve ser exibido a cor do candidato com melhor desempenho no estado --- # Utilizando as escalas de cores ```javascript data.states.forEach(function (state) { d3.select("#BRA-" + state.key).style('fill', function () { if (profile == 1) return colorScale[0](100*state.c1/state.total); if (profile == 2) return colorScale[1](100*state.c2/state.total); if (profile == 3) return colorScale[2](100*state.c3/state.total); if (profile == 0) { var max = 0,color; if (state.c1 > state.c2) { max = state.c1; color = 0; } else { max = state.c2; color = 1; } if (state.c3 > max) { max = state.c3; color = 2; } return colorScale[color](100*max/state.total); } }); }); ``` ??? - Discutir como isso poderia ser melhorado - API é flexível a diferentes candidatos --- class: middle,center # Confira o resultado --- class: middle,center # Agora vamos ao gráfico de barras com o resultado nacional ### Lembrando que não inicializamos as informações do eixo X ```javascript var x = null,xAxis = null; ``` --- # Inicializando o Eixo X ### Código deve ser executado apenas na primeira vez que a função for chamada - Solução "ad-hoc" ```javascript if (init == 0) { init = 1; //Código para inicialização } ``` --- # Escalas e eixos: Eixo X .big.center["Rótulo" <i class="fa fa-user darkblue"></i> <i class="fa fa-long-arrow-right crimson"></i> <i class="fa fa-magic"></i> <i class="fa fa-long-arrow-right crimson"></i> <i class="fa fa-bar-chart darkblue"></i>(x,0)] -- #### Escala ```javascript x = d3.scale.ordinal() .rangeRoundBands([0, chartWidth], 0.1, 0.1) .domain(data.country.votes.map(function(d) { return d.name; })); ``` ??? - Explicar o método rangeRoundBands - Construir o gráfico na lousa - Explicar diferença entre a função xAxis e a .append("g") -- #### Desenho ```javascript xAxis = d3.svg.axis() .scale(x) .outerTickSize(0) .orient("bottom"); chartSVG.append("g") .attr("class", "x axis") .attr("transform", "translate(0," + (chartHeight-chartMargin) + ")") .call(xAxis); ``` --- # Barras e números #### Barra ```javascript chartSVG.selectAll(".bar") .data(data.country.votes) .enter().append("rect") .attr("class", "bar") * .attr("x", function(d) { return x(d.name); }) * .attr("width", x.rangeBand()) * .attr("height", 0) * .attr("y", chartHeight - chartMargin) .style("text-anchor","middle") .style("fill",function (d,k) { return colors[k]; }); ``` -- #### Texto ```javascript chartSVG.selectAll(".barValue") .data(data.country.votes) .enter().append("text") .attr("class","barValue") .text("0.00%") * .attr("x", function(d) { return x(d.name) + x.rangeBand()/2; }) * .attr("y", function() { return chartHeight - chartMargin - 10; }) .style("text-anchor","middle"); ``` --- class: middle,center ## No momento os gráficos estão inicializados ### Porém falta atualizar o valor de cada candidato --- ## Atualizado o valor de cada barra ```javascript chartSVG.selectAll(".bar") * .data(data.country.votes) .attr("height", function(d) { return y(100*d.votes/data.country.read); }) .attr("y", function(d) { return chartHeight - chartMargin - y(100*d.votes/data.country.read); }); ``` ??? - Lembrar da escala em Y -- ## Atualizando o valor do texto ```javascript chartSVG.selectAll(".barValue").data(data.country.votes) .text(function (d) { return (100*d.votes/data.country.read).toFixed(2) + "%"; }) .attr("y", function(d) { return chartHeight - chartMargin - y(100*d.votes/data.country.read) - 10; }); ``` ??? Importante: fora do IF init == 0 --- # Um pouco mais de animação ```javascript chartSVG.selectAll(".bar") .data(data.country.votes) * .transition() * .duration(1000) .attr("height", function(d) { return y(100*d.votes/data.country.read); }) .attr("y", function(d) { return chartHeight - chartMargin - y(100*d.votes/data.country.read); }); ``` ```javascript chartSVG.selectAll(".barValue").data(data.country.votes) * .transition() * .duration(1000) .text(function (d) { return (100*d.votes/data.country.read).toFixed(2) + "%"; }) .attr("y", function(d) { return chartHeight - chartMargin - y(100*d.votes/data.country.read) - 10; }); ``` --- class: middle,center # Confira o resultado -- ## Acabou? -- # Interação com o mouse no mapa --- # Adicionando listeners no mapa .left-column.column-6[ ```javascript function startTooltip (state) {} function moveTooltip (coordinates) {} function closeTooltip () {} function loadTooltipData (state) {} ``` ] .right-column.column-6.stretch-10.center[ ![Tooltip](figs/tooltip.png) ] -- # Vamos desenvolver ```javascript function fillTooltip (data) {} ``` ??? Estas funções estão no template pois não funções para manipulação no HTML, e não para construção da visualização --- # Entendendo as funções ## startToolTip (state) - Deve ser chamada quando o mouse entra sobre o estado ```javascript function startTooltip (state) { if (state == "BRA-612" || state == "BRA-613" || state == "BRA-614") { $("#statePopover").removeClass('bottom'); $("#statePopover").addClass('top'); } else { if ($("#statePopover").hasClass('top')) { $("#statePopover").removeClass('top'); $("#statePopover").addClass('bottom'); } } $("#statePopover").attr('data-current-state',state); $("#statePopover").css('display','block'); } ``` --- # Entendendo as funções ## moveTooltip (coordinates) - Deve ser chamada quando o mouse se move sobre o estado, atualiza a posição do tooltip ```javascript function moveTooltip (coordinates) { if ($("#statePopover").hasClass('bottom')) { $("#statePopover").css('left',(coordinates[0] - 148) + "px"); $("#statePopover").css('top',(coordinates[1] - 50) + "px"); } else { $("#statePopover").css('left',(coordinates[0] - 148) + "px"); $("#statePopover").css('top',(coordinates[1] - 250) + "px"); } } ``` --- # Entendendo as funções ## startToolTip (state) - Deve ser chamada quando o mouse sai do mapa, escondendo o tooltip ```javascript function closeTooltip () { $("#statePopover").css('display','none'); } ``` --- # Entendendo as funções ## loadTooltipData (state) - Deve ser chamada quando o mouse entra no mapa, para fazer a requisição para obter os dados do estado ```javascript function loadTooltipData (state) { $("#statePopover .popover-title").html("Carregando..."); $("#statePopover .popover-content").html(""); requestUpdates++; $.ajax({ url: "http://d3.fabiosikansi.com/election.php?request=" + requestUpdates + "&state=" + state + "&callback=?", jsonp: "callback", dataType: "jsonp", data: { format: "json" }, success: function( response ) { if (requestUpdates == response.requestNumber) fillTooltip(response); } }); } ``` --- # Adicionando os listeners no mapa ### Voltando ao que criamos anteriormente... ```javascript d3.json("json/states_bra.topo.json", function(error, bra) { mapSVG.append("g") .attr("id", "states") .selectAll("path") .data(topojson.feature(bra, bra.objects.states_bra).features) .enter() .append("path") .attr("id", function(d) { return d.id; }) .attr("class", "state") .attr("data-state", function(d) { return d.properties.name; }) .attr("d", path); }); ``` --- # Adicionando os listeners no mapa ### Voltando ao que criamos anteriormente... ```javascript d3.json("json/states_bra.topo.json", function(error, bra) { mapSVG.append("g") .attr("id", "states") .selectAll("path") .data(topojson.feature(bra, bra.objects.states_bra).features) .enter() .append("path") .attr("id", function(d) { return d.id; }) .attr("class", "state") .attr("data-state", function(d) { return d.properties.name; }) .attr("d", path) * .on("mouseenter", function(d) { * startTooltip(d.id); loadTooltipData(d.id); }) * .on("mouseout", function() { closeTooltip(); }) * .on("mousemove", function() { * moveTooltip(d3.mouse(d3.select('body').node())); * }); }); ``` --- ## Como preencher o Tooltip com os dados? ### Novamente, primeiro vamos olhar os dados ```javascript {"time":"16:54", "requestNumber":0, "votes":[ {"name":"Candidato 1","votes":46.782771926165}, {"name":"Candidato 2","votes":3.5770153300657}, {"name":"Candidato 3","votes":21.993951402649}, {"name":"Apurado","votes":72.35373865888} ], "name":"Santa Catarina" } ``` ??? Note que a informação já está com porcentagem --- ## Como preencher o Tooltip com os dados? ### Agora vamos olhar o html do tooltip ```html <div class="tooltipBox"> <div id="statePopover" class="popover bottom"> <div class="arrow"></div> <h3 class="popover-title">State</h3> * <div class="popover-content"> * <p>Data</p> * </div> </div> </div> ``` --- ## Como preencher o Tooltip com os dados? ### Vamos inserir um svg no tooltip ```javascript function fillTooltip (data) { $("#statePopover .popover-title").html(data.name); * var svg = d3.select("#statePopover .popover-content").append('svg') .attr("width", 250) .attr("height", 140); } ``` -- .left-column.column-6[ ### Informação de cada candidato: - Nome - Porcentagem de votos - Barra de porcentagem ] .right-column.column-6.stretch-8.center[ ![Tooltip](figs/tooltip.png) ] ??? Comparar com a tarefa para construir a barra de apuração --- ## Agrupando as informações por candidato ```javascript function fillTooltip (data) { $("#statePopover .popover-title").html(data.name); * var svg = d3.select("#statePopover .popover-content").append('svg') .attr("width", 250) .attr("height", 140); var candidatos = svg.selectAll('g') * .data(data.votes) * .enter() * .append('g'); } ``` -- ### Qual é o conteúdo da variável candidatos? --- ## Adicionando o texto ```javascript candidatos.append('text') .attr('x',0) * .attr('y',function (d,k) { return (k*35) + 10; }) .style('font-size','10px') * .text(function (d) { return d.name; }); ``` -- ## Adicionando a barra de votos ```javascript candidatos.append('rect') .attr('height',15) .attr('width',function (d) { return (200*d.votes/100); }) * .attr('x',0) * .attr('y',function (d,k) { return (k*35) + 15; }) .style('fill',function (d,k) { return colors[k]; }); ``` ??? Poderia ter usado uma escala para definir o comprimento --- ## Adicionando o número de votos ```javascript candidatos.append('text') .attr('x',250) * .attr('y',function (d,k) { return (k*35) + 25; }) .style('font-size','10px') .style('font-weight','bold') .style('text-anchor','end') .style('fill','black') .text(function (d) { return d.votes.toFixed(2) + "%"; }); ``` --- class: center,middle # FIM (desta atividade) ## Perguntas??? --- class: center,middle # Desafio --- .center.stretch-12.padding-100[![...](figs/desafio.png)] -- [https://live2.zoomdata.com/zoomdata/visualization#53f22849e4b08f9d5f15360a-522655b0e4b00f4f3af30f12](https://live2.zoomdata.com/zoomdata/visualization#53f22849e4b08f9d5f15360a-522655b0e4b00f4f3af30f12) ??? ### Zoom --- class: center,middle # Algumas dicas para encerrar ## .crimson[Programe para os seus dados, não para a sua representação] ## .crimson[SEMPRE escreva códigos reusáveis] ## .crimson[angular.js] --- class: center, middle, inverse # Obrigado! ### Visualização de informação na web com D3.js .author[Fábio Sikansi] .author[Samuel Fadel] fabio.sikansi@gmail.com samuelfadel@gmail.com .left-column.column-6.stretch-4[![](figs/logo-semcomp-inverse.svg)] .right-column.column-6.stretch-8[![](figs/logo-icmc-inverse.svg)]