Painel profissional
Indicadores, a grade que pagina no servidor, 27 tipos de gráfico e o fragmento que redesenha um pedaço só.
Os componentes da página anterior montam um relatório. O que faz um painel é o que vem aqui: a faixa de indicadores, o bloco com título discreto, a tabela que pagina sem recarregar a tela, e os gráficos que respondem às perguntas que uma linha e uma barra não respondem.
O indicador#
V.metrica mostra um número. O indicador é o bloco de que um painel é feito: faixa de cor à esquerda, nota embaixo, meta com barra de progresso e uma série miúda ao fundo.
V.indicador("Patrimônio líquido", 1091947.91,
formato := "moeda", cor := "aviso", variacao := 4.2,
nota := "Total em ago/26")
V.indicador("Aportes", 8495.0,
formato := "moeda", cor := "info", alvo := 12000.0,
mini := [6200, 7100, 6800, 9300, 8495])V.indicadores põe vários numa faixa que se ajusta sozinha à largura — quatro cartões numa tela larga, um no celular, sem ninguém escolher o número.
V.indicadores([
{"rotulo": "Receita", "valor": 128400.0, "formato": "moeda"},
{"rotulo": "Clientes", "valor": 2500, "variacao": 8.1},
{"rotulo": "Churn", "valor": 2.4, "formato": "percentual", "cor": "erro"}])Uma chave que não existe é ignorada, e não vira um cartão com o padrão: formatto seria um engano passando calado.
Malha e painel#
V.malha quebra pela largura, e não pelo número de colunas: com minimo := 260, três painéis numa tela larga viram dois numa média e um no celular. O servidor não mede a tela, então quem decide é o CSS.
m := V.malha(3, minimo := 260)
carteira := m.painel("Balanceamento", subtitulo := "6 tipos", icone := "pizza")
carteira.grafico_rosca({"Ações": 303561.0, "Fundos": 377733.0})
fluxo := m.painel("Receitas × despesas", cor := "info")
fluxo.grafico_barras(movimento, x := "mes", y := ["receitas", "despesas"])O título de um painel é pequeno e em caixa alta de propósito: num painel com doze blocos, o que precisa saltar é o dado — não doze títulos competindo com ele.
A barra do topo#
topo := V.barra_superior("Gestão financeira",
subtitulo := "Cliente",
itens := ["Dados", "Carteira", "Painel"],
ativo := "Painel")
ano := topo.escolha("Período", ["2026", "2025"])Ela devolve a área da direita, que é onde os filtros ficam. O seletor é lido ali e devolvido para quem chamou: o programa roda de cima para baixo, e a linha seguinte já usa o ano.
A grade#
A V.frame ordena e filtra no navegador, com o que já está na tela: é instantâneo e mente sobre um conjunto de cem mil linhas, porque só as que vieram participam. A grade ordena, filtra e pagina no servidor, sobre o conjunto inteiro.
escolhidas := V.grade(pedidos,
colunas := [
V.coluna("cliente", "Cliente", largura := 240),
V.coluna("total", "Total", tipo := "moeda"),
V.coluna("margem", "Margem", tipo := "barra", maximo := 100),
V.coluna("situacao", "Situação", tipo := "selo")],
destacar := [V.regra("margem", "menor", 10, "erro")],
totais := {"total": "soma"},
selecionar := "varias",
paginar := 25)
given len(escolhidas) bigger 0:
V.exportar_csv(escolhidas, "selecionadas.csv")A paginação vem ligada porque uma listagem sem teto é a forma mais comum de um painel travar: mil linhas viram mil vezes o custo de desenhar, a cada clique. paginar := 0 a desliga.
| Tipo de coluna | O que ele desenha |
|---|---|
texto | o valor como veio |
moeda · numero · percentual · compacto | formatado em pt-BR, alinhado à direita |
data | 2026-09-20 vira 20/09/2026 |
barra · progresso | uma barra dentro da célula, com o valor ao lado |
mini | uma série miúda — a coluna carrega um cluster |
selo | uma etiqueta colorida; a cor sai do valor |
link · imagem · logico | âncora, miniatura e ✓/— |
O formato é aplicado na hora de desenhar, nunca no dado: a coluna continua numérica, e é isso que permite ordenar por valor e somar o rodapé depois de a célula já dizer R$ 1.091.947,91.
O editor#
linhas := V.editor(precos, colunas := [
V.coluna("produto", editavel := no),
V.coluna("preco", tipo := "moeda")])
given V.botao("Salvar"):
cycle linha in linhas:
Banco.executar("UPDATE precos SET preco = ? WHERE produto = ?",
[linha["preco"], linha["produto"]])O que sai é um cluster de vaults novo — o original não é tocado, como em record/with. E o valor volta no tipo da coluna: devolver "12" onde havia 12 faria a soma do rodapé concatenar, e o sintoma seria um total absurdo, não um erro.
Os 27 tipos de gráfico#
| Para responder | O gráfico |
|---|---|
| quanto, ao longo do tempo | grafico_linha, grafico_area, grafico_area_empilhada |
| quanto, por categoria | grafico_barras, grafico_barras_h, grafico_pareto |
| de que isto é feito | grafico_pizza, grafico_rosca, grafico_treemap, grafico_barras_100 |
| duas grandezas de escalas diferentes | grafico_combo com eixo_direito |
| de onde veio a diferença | grafico_cascata |
| quanto sobra em cada etapa | grafico_funil |
| estamos perto da meta? | medidor, grafico_bala |
| quando acontece | mapa_de_calor, grafico_calendario |
| como se distribui | histograma, grafico_caixa |
| um está ligado ao outro? | grafico_dispersao_xy, grafico_bolhas |
| comparar em vários critérios | grafico_radar |
| para onde foi | grafico_sankey, grafico_rede |
| quando cada coisa acontece | grafico_gantt |
| o mercado | grafico_velas |
| onde | grafico_mapa |
| a tendência ao lado de um número | mini_grafico |
g := V.grafico_combo(mes_a_mes, x := "mes",
barras := ["receita", "despesa"],
linhas := ["margem"],
direita := ["margem"])direita põe aquelas séries numa segunda escala. Sem ela, uma margem de 0 a 100 desenhada ao lado de uma receita de milhões vira uma linha colada no zero: o gráfico existe e não mostra nada.
Três decisões que o desenho toma por você#
A barra é ancorada no zero; a linha, não. Numa barra o que significa é o comprimento, e cortar o eixo faz uma barra 3% maior parecer o dobro — o gráfico enganoso clássico. Numa linha o que significa é a posição: forçar o zero num patrimônio que vai de 1,02 a 1,13 milhão desenha uma reta horizontal, e a variação some.
`void` é um vão, não um zero. A linha de um acumulado para no mês que ainda não aconteceu, em vez de despencar. Um gráfico que mostra uma queda de um milhão onde só falta o dado é pior que um gráfico que para: ele afirma um número.
O texto cresce quando o espaço encolhe. O desenho é feito num sistema de 800 unidades e o CSS o encolhe para caber no bloco. Num painel de um terço da tela o fator é 0,45, e um rótulo de 11px chegaria ao olho com 5px. O layout informa a largura, e o desenho compensa.
Fragmento: redesenhar um pedaço#
f := V.fragmento("cotacoes", a_cada := 5)
f.metrica("Dólar", cotacao("USD"))
f.metrica("Euro", cotacao("EUR"))Um clique dentro do fragmento ainda roda o programa inteiro no servidor — isso não muda —, mas a resposta carrega só este pedaço. A diferença aparece onde importa: o filtro da barra lateral não perde o foco, a rolagem da tabela ao lado não volta ao topo, e um gráfico pesado em outro canto não é redesenhado.
A resposta volta a ser a página inteira quando houve falha, quando mais de um campo mudou, ou quando o fragmento saiu da árvore. Responder a página inteira sem precisar custa desempenho; responder um pedaço sem poder custa correção.
Conversa, situação e aviso#
pergunta := V.chat_entrada()
given pergunta is not void:
V.guardar_no_chat("usuario", pergunta)
V.guardar_no_chat("assistente", responder(pergunta))
cycle m in V.historico_de_chat():
V.chat_mensagem(m["quem"], m["conteudo"])passo := V.status("Consultando o banco…")
passo.texto("1.200 linhas")
passo.concluir("Pronto")
V.toast("Salvo", nivel := "sucesso")O V.toast não ocupa espaço no fluxo: é para confirmar o que acabou de acontecer sem empurrar o resto da tela para baixo. O V.esqueleto faz o contrário — ocupa o espaço do que ainda não chegou, para a página não saltar quando o dado chegar.
Conexão, recurso e segredo#
banco := V.conexao("vendas", arquivo := "dados/vendas.db")
linhas := banco.consultar("SELECT * FROM pedidos WHERE mes = ?",
[mes], validade := 300)A conexão é do processo: chamar de novo com o mesmo nome devolve a mesma. Sem isso, cada execução da página abriria outra — o gargalo mais fácil de criar e o mais difícil de notar, porque cada abertura é rápida.
V.cache | V.recurso | |
|---|---|---|
| guarda | o resultado de um cálculo | o objeto em si |
| vence | por validade, ou por teto | nunca, enquanto o processo viver |
| vai a disco | pode | não |
| serve para | consulta, API, agregação | conexão, modelo, cliente |
A diferença não é de tamanho. Um cache com teto solta o menos usado; se a conexão do banco caísse fora por isso, a próxima página abriria outra e o pool do banco acabaria — o bug que faz um painel morrer só depois de uma hora no ar.
chave := V.segredo("OPENAI_API_KEY")
V.vault(V.segredos_mascarados())O ambiente vence o arquivo: é assim que um deploy troca a senha sem reescrever nada. E V.segredos_mascarados mostra só os quatro últimos caracteres — o suficiente para conferir qual chave está configurada sem entregá-la a quem está olhando a tela junto.
Tema e densidade#
V.app("Painel", tema := "meia-noite", densidade := "compacta")Seis temas prontos: claro, escuro, meia-noite, oceano, contraste e papel. Um tema escuro pedido pelo nome vale sempre — deixá-lo no automático faria o tema escolhido deixar de valer numa máquina configurada como clara.
A densidade muda espaçamento e tamanho sem tocar em cor nenhuma. Um painel de operação cabe um terço mais de linha na mesma tela com compacta, e é a diferença entre rolar e não rolar.