Um gráfico de barras é uma representação visual de um conjunto de dados categóricos onde uma barra é um mapeamento direto de uma categoria e cujo tamanho (a altura das barras verticais) é proporcional aos valores representar.
Se um eixo tem uma escala linear (para coincidir com o tamanho das barras), a posição das barras em relação ao outro eixo (as categorias) geralmente não importa muito e elas simplesmente ocupam o espaço de maneira uniforme.
Neste artigo, vamos cobrir como construir uma biblioteca de gráfico de barras usando componentes da web.
Criação de unidades de segmento em uma biblioteca de gráfico de barras
Para primeiro calcular as proporções de uma barra, precisamos de uma função simples para projetar um valor contra um segmento de uma unidade que representa o domínio de valores possíveis que queremos exibir:
const createScale=({domainMin, domainMax})=> (valor)=> (valor-domainMin)/(domainMax-domainMin);
Por exemplo, se um segmento de uma unidade vai de 0 a 100, o valor 50 estará bem no meio do segmento, enquanto 25 estará no trimestre.
const scale=createScale ({domainMin: 0, domainMax: 100}); escala (50)//> 0,5 escala (25)//> 0,25
O que você deseja que a unidade do segmento seja fisicamente é com você (900px, 4cm, etc). Também precisamos cuidar dos valores fora do intervalo definido pelo domínio (ou seja, os valores que você não pode caber no segmento).
Normalmente, se o valor for maior, ele é superior no final do segmento, enquanto se for menor, a proporção relativa será simplesmente nula.
//um utilitário para compor funções juntas const compose=(... fns)=> (arg)=> fns.reduceRight ((acc, cfn)=> cfn (acc), arg); const maiorOrEqual=(min)=> (valor)=> Math.max (min, valor); const lowerOrEqual=(max)=> (value)=> Math.min (max, value); const createProjection=({domainMin, domainMax})=> compose ( lowerOrEqual (1), maiorOrEqual (0), createScale ({ domainMin, domainMax }) ); //exemplo projeto const=createProjection ({domainMin: 0, domainMax: 100}); projeto (50);//> 0,5"unidade" projeto (120);//> 1"unidade" projeto (-40);//> unidade de 0"
O que são componentes da web?
Componentes da web é um conjunto de três tecnologias que fornecem ao desenvolvedor a capacidade de criar controles de IU compartilháveis como elementos DOM regulares:
- Elementos personalizados fornecem uma API de baixo nível para criar novos elementos HTML
- Shadow DOM nos permitirá encapsular uma subárvore DOM privada e ocultá-la do resto do documento
- Modelos HTML (
) ajuda com o design da subárvore e como ela se encaixa em outro DOM árvores
Você não precisa usar todos eles juntos para criar um componente da web. As pessoas costumam confundir componentes da web com DOM de sombra, mas você pode criar um elemento personalizado sem DOM de sombra.
Criação de um componente de barra com elementos personalizados
O poder dos elementos personalizados reside no fato de que são elementos HTML válidos que você pode usar de forma declarativa por meio de HTML ou programaticamente com a mesma API de qualquer elemento HTML (atributos, eventos, seletores, etc.) p>
Para criar um elemento personalizado, você precisa de uma classe que estenda a classe base do elemento HTML. Em seguida, você tem acesso a alguns ciclos de vida e métodos de gancho:
export classe Bar extends HTMLElement { static getnoteAttributes () { return ['size']; } obter tamanho () { return Number (this.getAttribute ('size')); } definir tamanho (valor) { this.setAttribute ('tamanho', valor); } //o valor absoluto mapeado para a barra Obter valor() { return Number (this.getAttribute ('value')); } definir valor (val) { this.setAttribute ('valor', val); } attributeChangedCallback () { this.style.setProperty ('-bar-size', `$ {this.size}%`); } } customElements.define ('app-bar', Bar);
Normalmente, você define a API declarativa por meio de atributos HTML ( size
, no nosso caso) junto com o acesso programático por meio de getters e setters. Elementos personalizados oferecem algum tipo de vínculos reativos (como você pode encontrar em estruturas Javascript de IU comuns), expondo atributos observáveis por meio do getter estático observedAttributes
e do retorno de chamada reativo attributeChangedCallback
.
Em nosso caso, sempre que o atributo size
muda, atualizamos a propriedade de estilo do componente --bar-size
, que é um variável CSS que podemos usar para definir as proporções das barras.
Idealmente, os acessadores devem refletir sobre os atributos e, portanto, usar apenas tipos de dados simples (strings, números, booleanos) porque você não sabe como o consumidor usará o componente (com atributos, programaticamente, etc.).
Finalmente, você precisa registrar o elemento personalizado em um registro global para que o navegador saiba como lidar com o novo elemento HTML que encontrar no DOM.
Agora você pode soltar a tag app-bar
em um documento HTML. Como qualquer elemento HTML, você pode associar um estilo a ele com uma folha de estilo CSS. No nosso caso, podemos, por exemplo, aproveitar a variável CSS reativa --bar-size
para gerenciar as alturas das barras.
Você encontrará um exemplo de execução com a seguinte Code Pen ou stackblitz (para uma amostra mais organizada). Além das alturas das barras, adicionamos algumas animações e alguns aprimoramentos para provar nosso ponto. Os elementos personalizados são antes de todos os elementos HTML, o que os torna muito expressivos com tecnologias padrão da web, como CSS e HTML.
Criação da área do gráfico de barras
Na seção anterior, conseguimos criar algo próximo a um gráfico de barras real, graças a um componente da web simples e uma folha de estilo. No entanto, se parte do estilo aplicado for personalizado, grande parte dele faz parte dos requisitos funcionais de qualquer gráfico de barras:
- A proporção das alturas das barras
- A forma como as barras de categorias ocupam o espaço (uniformemente para evitar viés visual)
Portanto, precisamos encapsular essa parte em nosso componente para tornar seu uso menos tedioso e repetitivo para o consumidor. Digite o shadow DOM .
Shadow DOM permite que o componente da web crie sua própria árvore DOM isolada do resto do documento. Isso significa que você pode definir a estrutura interna sem que os outros elementos saibam disso, como uma caixa preta.
Da mesma forma, você pode definir regras de estilo privadas e com escopo específico para as partes internas. Vamos ver como funciona o seguinte exemplo:
import {createProjection} de'./util.js'; modelo const=document.createElement ('modelo'); ///linguagem=css estilo const=` :hospedeiro{ display: grade; largura: 100%; altura: 100%; } : host ([oculto]) { Mostrar nenhum; } # bar-area { alinhar itens: flex-end; display: flex; justify-content: space-around; } :: slotted (app-bar) { flex-grow: 1; altura: var (-tamanho da barra, 0%); fundo: salmão;//cor padrão que pode ser substituída pelo consumidor } `; template.innerHTML=``; export class BarChart extends HTMLElement { static getnoteAttributes () { return ['domainmin','domainmax']; } get domainMin () { retornar this.hasAttribute ('domainmin')? Número (this.getAttribute ('domainmin')): Math.min (... [... this.querySelectorAll ('app-bar')]. Map (b=> b.value)); } set domainMin (val) { this.setAttribute ('domainmin', val); } obter domainMax () { retornar this.hasAttribute ('domainmax')? Número (this.getAttribute ('domainmax')): Math.max (... [... this.querySelectorAll ('app-bar')]. Map (b=> b.value)); } set domainMax (val) { this.setAttribute ('domainmax', val); } attributeChangedCallback (... args) { this.update (); } construtor () { super(); this.attachShadow ({modo:'aberto'}); this.shadowRoot.appendChild (template.content.cloneNode (true)); } update () { const project=createProjection ({domainMin: this.domainMin, domainMax: this.domainMax}); const bars=this.querySelectorAll ('app-bar'); para (barra constante de barras) { bar.size=projeto (bar.value); } } connectedCallback () { this.shadowRoot.querySelector ('slot') .addEventListener ('slotchange', ()=> this.update ()); } } customElements.define ('app-bar-chart', BarChart);
Existem algumas coisas novas acontecendo aqui. Primeiro, criamos um elemento template
com uma árvore DOM, que será usada como a árvore privada do documento graças ao shadow DOM anexado (construtor cf).
Observe que este modelo possui um slot slot elemento, que é essencialmente uma lacuna que o consumidor do componente pode preencher com outros elementos HTML. Nesse caso, esses elementos não pertencem ao DOM sombra do componente da web e permanecem no escopo superior. Ainda assim, eles assumirão sua posição conforme definido pelo layout DOM de sombra.
Também usamos um novo método de ciclo de vida, nomeando connectedCallback
. Esta função é executada sempre que o componente é montado em um documento. Registramos um ouvinte de evento que pedirá ao nosso componente para renderizar novamente sempre que o conteúdo com slot (barras) mudar.
Temos um estilo de escopo que nos permite implementar e encapsular os requisitos funcionais do gráfico de barras (o que antes era obtido por meio de uma folha de estilo global). O pseudo elemento : host
se refere ao nó raiz do componente da web, enquanto :: slotted
permite que o componente defina algum estilo padrão nos elementos”recebidos”(as barras, em nosso caso).
Os elementos personalizados têm por padrão a propriedade display
definida como inline
; aqui, nós sobrescrevemos o padrão com uma grade
. Mas, por causa das regras de especificidade CSS, precisamos lidar com o caso em que o componente tem o atributo oculto
.
Da mesma forma, o cálculo das alturas projetadas agora faz parte dos componentes internos. Como antes, o componente tem atributos/propriedades reativas, então sempre que o intervalo de domínio definido muda, as proporções das barras também mudam.
Agora podemos combinar nossos dois componentes da web para criar gráficos de barras em HTML. Embora permaneça amplamente personalizável, o consumidor não tem mais o fardo de lidar com o cálculo das alturas das barras nem com sua renderização.
Você notará que há um contrato implícito entre os dois componentes: o atributo size
do app-bar
deve ser gerenciado pelo app-bar componente-chart
.
Tecnicamente, o consumidor poderia quebrar o comportamento interferindo com a variável css --bar-size
(vazamento de encapsulamento), mas esta compensação nos dá uma grande flexibilidade ao mesmo tempo.
Você encontrará no código a seguir ( Stackblitz ) um exemplo mais avançado onde você também pode definir o orientações das barras.
Definindo os eixos do gráfico de barras
Até agora, o componente permite que o leitor compreenda rapidamente as proporções relativas das categorias.
No entanto, sem nenhum eixo, ainda é difícil mapear essas proporções para valores absolutos e dar um rótulo ou uma categoria a uma determinada barra.
Eixo das categorias
Afirmamos anteriormente que as posições das barras não são muito significativas e precisam apenas ocupar o espaço de maneira uniforme. Os rótulos de categoria seguirão a mesma lógica.
Primeiro, precisamos alterar o modelo da área da barra para adicionar um slot para o eixo e adicionar algum estilo para manter o layout consistente. CSS grid
torna isso mais fácil:
//bar-chart.js template.innerHTML=` `
Agora, o gráfico de barras tem dois slots nomeados distintos. Precisamos então especificar em qual slot os elementos filhos serão inseridos. Para as barras, nós os posicionamos na seção bar-area
. Adicionamos o atributo slot
nas barras com um valor bar-area
.
Adicionamos este comportamento como padrão em nosso componente de barra:
//bar.js export class Bar extends HTMLElement { /*... */ connectedCallback () { if (! this.hasAttribute ('slot')) { this.setAttribute ('slot','área de barra'); } } }
Dentro de connectedCallback
, adicionamos condicionalmente o atributo mencionado anteriormente. Observe que, com as propriedades padrão, geralmente é uma boa prática dar precedência aos atributos especificados pelo usuário (daí a condição) porque você não sabe como o consumidor usará ou estenderá seu componente.
Vamos agora criar um eixo de categoria e um componente de rótulo, que será um par de componentes simples sem lógica com estilo básico para reforçar o layout:
//label.js modelo const=document.createElement ('modelo'); ///linguagem=css estilo const=` :hospedeiro{ display: flex; } : host ([oculto]) { Mostrar nenhum; } # label-text { flex-grow: 1; alinhamento de texto: centro; } : host (: último filho) # tick-after { Mostrar nenhum; } : host (: primeiro filho) # tick-before { Mostrar nenhum; } `; template.innerHTML=` `; export class Label extends HTMLElement { construtor () { super(); this.attachShadow ({modo:'aberto'}); this.shadowRoot.appendChild (template.content.cloneNode (true)); } } customElements.define ('app-label', Label); //category-axis.js modelo const=document.createElement ('modelo'); ///linguagem=css estilo const=` :hospedeiro{ display: flex; border-top: 1px cinza sólido; } : host ([oculto]) { Mostrar nenhum; } :: slotted (app-label) { flex-grow: 1; } app-label:: part (tick) { largura: 1px; altura: 5px; fundo: cinza; } `; template.innerHTML=``; export class CategoryAxis extends HTMLElement { construtor () { super(); this.attachShadow ({modo:'aberto'}); this.shadowRoot.appendChild (template.content.cloneNode (true)); } connectedCallback () { if (! this.hasAttribute ('slot')) { this.setAttribute ('slot','eixo inferior'); } } } customElements.define ('eixo da categoria do aplicativo', CategoryAxis);
Agora você pode adicionar esses componentes ao documento HTML:
cat-1 cat-2 cat-3 cat-4 cat-5
Não há nada de novo aqui, exceto por um ponto: o modelo de etiqueta tem dois elementos com o atributo part
. Isso permite que você personalize partes específicas do DOM sombra, embora normalmente não sejam acessíveis de fora do componente.
Você pode vê-lo em ação na seguinte caneta de código ( Stackblitz ).
Eixo da escala linear
Para o eixo linear, usaremos principalmente uma combinação das técnicas que vimos até agora, mas também apresentaremos um novo conceito: eventos personalizados .
Como fizemos anteriormente para o componente do gráfico de barras, o componente do eixo linear irá expor uma API declarativa para definir os valores do intervalo do domínio e a lacuna entre dois tiques consecutivos.
Na verdade, faz sentido deixar esse componente conduzir o intervalo do domínio, mas, ao mesmo tempo, não queremos adicionar um acoplamento entre as barras e o eixo.
Em vez disso, usaremos o componente do gráfico de barras pai como um mediador entre eles para que sempre que o eixo veja uma mudança de domínio, ele notifique o gráfico de barras para renderizar novamente as barras.
Podemos alcançar esse padrão com eventos personalizados:
//linear-axis.js //... export class LinearAxis extends HTMLElement { static getnoteAttributes () { return ['domainmin','domainmax','gap']; } //... attributeChangedCallback () { const {domainMin, domainMax, gap}=isso; if (domainMin!==void 0 && domainMax!==void 0 && gap) { this.update (); this.dispatchEvent (novo CustomEvent ('domínio', { bolhas: verdade, composto: verdadeiro, detalhe: { domainMax, domainMin, Gap=Vão } })); } } }
Além de solicitar uma atualização, o componente emite um CustomEvent, passando o detalhe dos valores do domínio. Passamos dois sinalizadores bubbles
e composite
para garantir que o evento suba na hierarquia da árvore e possa sair dos limites da árvore de sombra.
Então, no componente do gráfico de barras:
//bar-chart.js //... class BarChar extends HTMLElement { //... connectedCallback () { this.addEventListener ('domínio', ev=> { const {detalhe}=ev; const {domainMin, domainMax}=detalhe; //os setters irão acionar a atualização das barras this.domainMin=domainMin; this.domainMax=domainMax; ev.stopPropagation (); }); } }
Simplesmente registramos no evento personalizado uma chamada para uma atualização nas barras usando os configuradores de propriedades como antes. Decidimos interromper a propagação do evento porque, neste caso, usamos o evento apenas para implementar o padrão do mediador.
Como de costume, você pode dar uma olhada no codepen ou o stackblitz se você estiver interessado nos detalhes.
Conclusão
Agora temos todos os blocos de construção básicos para construir um gráfico de barras de forma declarativa. No entanto, você nem sempre terá os dados disponíveis no momento em que escreve o código, mas sim carregados dinamicamente mais tarde. Isso realmente não importa-a chave é transformar seus dados na árvore DOM correspondente.
Com bibliotecas como React, Vue.js e outras, é um progresso bastante direto. Lembre-se de que a integração de componentes da web em qualquer aplicativo da web é trivial, pois eles são, antes de tudo, elementos HTML regulares.
Outro benefício de usar componentes da web é a capacidade de personalizar os gráficos e lidar com muitos casos de uso diferentes com uma pequena quantidade de código.
Embora as bibliotecas de gráficos geralmente sejam enormes e precisem expor muitas configurações para oferecer alguma flexibilidade, os componentes da web permitem que você simplesmente use um pouco de CSS e Javascript para criar sua biblioteca de gráfico de barras.
Obrigado por ler!
A postagem Construir uma biblioteca de gráfico de barras com a web componentes apareceu primeiro em LogRocket Blog .