extraído de climatempo
Gráfico com Seletor
Seção com gráfico de linhas e um seletor para trocar a métrica exibida, com um painel opcional ao lado (por exemplo, um resumo com números).
Variações
Playground
Props
Geradas dos tipos TypeScript do componente; a descrição vem dos comentários /** */.
<GraficoComSeletor>
Seção com um gráfico de linhas e um seletor para trocar o que ele mostra (receita, pedidos, ticket médio...), com um painel opcional ao lado. Trocar de opção muda a escala, mas não a altura.
| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
| tituloobrigatória | string | — | |
| opcoesobrigatória | OpcaoDoGrafico[] | — | |
| rotuloDoSeletor | string | "Mostrar" | |
| lateral | string | number | bigint | boolean | ReactElement<unknown, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | Promise<AwaitedReactNode> | — | Conteúdo ao lado do gráfico em telas largas (embaixo nas estreitas). |
| altura | number | 300 |
Código
stories.tsx tem os exemplos usados no preview e no playground.
grafico-com-seletor/GraficoComSeletor.tsx
"use client";
import { useState, type ReactNode } from "react";
import { GraficoLinhas, type Anotacao, type Serie } from "@/library/grafico-linhas/GraficoLinhas";
import { Secao } from "@/library/secao/Secao";
import { SeletorUnico } from "@/library/seletor-unico/SeletorUnico";
export type OpcaoDoGrafico = {
chave: string;
/** Nome no seletor. */
rotulo: string;
/** Descrição do gráfico para leitores de tela (padrão: o rótulo). */
descricao?: string;
series: Serie[];
unidade?: string;
comSinal?: boolean;
incluirZero?: boolean;
formatarValor?: (valor: number) => string;
anotacoes?: Anotacao[];
/** Explicação mostrada ao lado do título enquanto esta opção está escolhida. */
dica?: string;
};
/**
* Seção com um gráfico de linhas e um seletor para trocar o que ele mostra
* (receita, pedidos, ticket médio...), com um painel opcional ao lado.
* Trocar de opção muda a escala, mas não a altura.
*/
export function GraficoComSeletor({
titulo,
opcoes,
rotuloDoSeletor = "Mostrar",
lateral,
altura = 300,
}: {
titulo: string;
opcoes: OpcaoDoGrafico[];
rotuloDoSeletor?: string;
/** Conteúdo ao lado do gráfico em telas largas (embaixo nas estreitas). */
lateral?: ReactNode;
altura?: number;
}) {
const [chave, setChave] = useState(opcoes[0]?.chave);
const opcao = opcoes.find((o) => o.chave === chave) ?? opcoes[0];
if (!opcao) return null;
return (
<Secao titulo={titulo} dica={opcao.dica}>
<div className="flex flex-col gap-8 lg:flex-row lg:items-start lg:gap-10">
<div className="min-w-0 flex-1">
<GraficoLinhas
rotulo={opcao.descricao ?? opcao.rotulo}
series={opcao.series}
unidade={opcao.unidade}
comSinal={opcao.comSinal}
incluirZero={opcao.incluirZero}
formatarValor={opcao.formatarValor}
anotacoes={opcao.anotacoes}
altura={altura}
acoes={
opcoes.length > 1 ? (
<SeletorUnico
rotulo={rotuloDoSeletor}
opcoes={opcoes.map((o) => ({ valor: o.chave, rotulo: o.rotulo }))}
valor={opcao.chave}
aoEscolher={setChave}
/>
) : null
}
/>
</div>
{lateral}
</div>
</Secao>
);
}
grafico-com-seletor/stories.tsx
import type { Serie } from "@/library/grafico-linhas/GraficoLinhas";
import { defineStories } from "@/registry/stories";
import { GraficoComSeletor, type OpcaoDoGrafico } from "./GraficoComSeletor";
// 30 dias de dados de exemplo, determinísticos
const DIAS = Array.from({ length: 30 }, (_, i) => new Date(Date.UTC(2026, 8, 1 + i)).toISOString().slice(0, 10));
const CANAIS = [
{ nome: "Loja online", peso: 1.4 },
{ nome: "Loja física", peso: 1 },
];
function series(base: number, amplitude: number, casas = 0): Serie[] {
return CANAIS.map((c, n) => ({
nome: c.nome,
cor: `var(--series-${n + 1})`,
pontos: DIAS.map((x, i) => ({
x,
y: Number((c.peso * (base + amplitude * Math.sin(i / 4 + n * 2)) + ((i * 37) % 7)).toFixed(casas)),
})),
}));
}
const OPCOES: OpcaoDoGrafico[] = [
{ chave: "pedidos", rotulo: "Pedidos", series: series(90, 20), unidade: "pedidos", incluirZero: false },
{
chave: "ticket",
rotulo: "Ticket médio",
series: series(160, 25, 2),
formatarValor: (v) => v.toLocaleString("pt-BR", { style: "currency", currency: "BRL" }),
incluirZero: false,
dica: "Receita dividida pelo número de pedidos do dia.",
},
{
chave: "variacao",
rotulo: "Variação vs. meta",
series: series(0, 12, 1),
unidade: "%",
comSinal: true,
dica: "Quanto cada dia ficou acima (+) ou abaixo (−) da meta diária.",
},
];
const RESUMO = [
{ rotulo: "Pedidos", valor: "5.912", descricao: "8% acima de agosto" },
{ rotulo: "Ticket médio", valor: "R$ 182", descricao: "estável" },
{ rotulo: "Dias acima da meta", valor: "19 de 30", descricao: "melhor mês do ano" },
];
/** Coluna de números ao lado do gráfico, só para o exemplo. */
function Resumo() {
return (
<div className="space-y-4">
<h3 className="font-display text-base font-semibold">Resumo do mês</h3>
<ul className="divide-y divide-rule">
{RESUMO.map((item) => (
<li key={item.rotulo} className="py-3 first:pt-0">
<p className="text-xs font-medium text-muted">{item.rotulo}</p>
<p className="font-display text-3xl font-semibold tabular-nums">{item.valor}</p>
<p className="text-sm text-ink-2">{item.descricao}</p>
</li>
))}
</ul>
</div>
);
}
export default defineStories({
args: { titulo: "Desempenho diário", rotuloDoSeletor: "Mostrar", altura: 360, lateral: true, opcoes: 3 },
controls: {
titulo: { type: "text" },
rotuloDoSeletor: { type: "text" },
altura: { type: "number", min: 200, max: 480, step: 20 },
lateral: { type: "boolean", label: "lateral (resumo ao lado)" },
opcoes: { type: "number", label: "opções no seletor", min: 1, max: 3 },
},
render: (a) => (
<div className="w-full">
<GraficoComSeletor
key={a.opcoes}
titulo={a.titulo}
rotuloDoSeletor={a.rotuloDoSeletor}
altura={a.altura}
opcoes={OPCOES.slice(0, a.opcoes)}
lateral={
a.lateral ? (
<Resumo />
) : undefined
}
/>
</div>
),
stories: [
{ name: "Com indicadores ao lado" },
{ name: "Só o gráfico", args: { lateral: false, altura: 300 } },
{ name: "Uma opção (sem seletor)", args: { opcoes: 1, lateral: false } },
],
});
Para usar em outro projeto, copie também
Os componentes de que este depende e o tema. O CSS do tema é um arquivo só: importe depois do Tailwind.
Componentes da biblioteca (diretos e indiretos)
Tema escolhido no cabeçalho (com a ponte para o Tailwind)
Ver o CSS do tema
tema-editorial.css · Editorial
/* Tema "Editorial", gerado no Component Vault.
Importe depois de @import "tailwindcss". Carregue as fontes "Archivo" e "Inter" no projeto. */
@theme inline {
--color-page: var(--page);
--color-surface: var(--surface);
--color-track: var(--track);
--color-grid: var(--grid);
--color-ink: var(--ink);
--color-ink-2: var(--ink-2);
--color-muted: var(--muted);
--color-rule: var(--rule);
--color-rule-strong: var(--rule-strong);
--color-axis: var(--axis);
--color-accent: var(--accent);
--color-success: var(--success);
--color-warning: var(--warning);
--color-critical: var(--critical);
--font-display: var(--fonte-titulo);
--shadow-flutuante: var(--sombra-flutuante);
}
:root {
color-scheme: light dark;
--page: light-dark(#ffffff, #0f0f0e);
--surface: light-dark(#f5f5f5, #1a1a19);
--track: light-dark(#f0f0f0, #262624);
--grid: light-dark(#ededed, #232321);
--ink: light-dark(#111111, #f0efec);
--ink-2: light-dark(#4a4a4a, #c3c2b7);
--muted: light-dark(#666666, #a3a29a);
--rule: light-dark(#e3e3e3, #2c2c2a);
--rule-strong: light-dark(#111111, #f0efec);
--axis: light-dark(#9a9a9a, #6b6a64);
--accent: light-dark(#1a56db, #7aa2f7);
--success: light-dark(#13865c, #3fc48f);
--warning: #fab219;
--critical: #d03b3b;
--series-1: light-dark(#2a78d6, #3987e5);
--series-2: light-dark(#eb6834, #d95926);
--series-3: light-dark(#1baf7a, #199e70);
--series-4: light-dark(#eda100, #c98500);
--series-5: light-dark(#e87ba4, #d55181);
--series-6: #008300;
--series-7: light-dark(#6250d6, #9085e9);
--series-8: light-dark(#e34948, #e66767);
--fonte-titulo: "Archivo", system-ui, -apple-system, "Segoe UI", sans-serif;
--fonte-texto: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
--spacing: 0.25rem;
--text-xs: 0.8rem;
--text-sm: 0.8944rem;
--text-base: 1rem;
--text-lg: 1.118rem;
--text-xl: 1.25rem;
--text-2xl: 1.5625rem;
--text-3xl: 1.9531rem;
--text-4xl: 2.4414rem;
--text-5xl: 3.0518rem;
--text-6xl: 3.8147rem;
--text-7xl: 4.7684rem;
--radius-xs: 2px;
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--radius-xl: 12px;
--radius-2xl: 16px;
--radius-3xl: 24px;
--radius-4xl: 32px;
--sombra-flutuante: 0 8px 24px -6px rgb(0 0 0 / 0.14), 0 2px 6px -2px rgb(0 0 0 / 0.08);
color: var(--ink);
font-family: var(--fonte-texto);
}
Na vitrine, a ponte com o Tailwind fica em src/themes/tokens.css:
src/themes/tokens.css
/* Ponte entre os tokens do tema e o Tailwind.
Os componentes da biblioteca usam classes comuns do Tailwind (p-5, text-sm,
rounded-xl...) e as cores semânticas abaixo (bg-page, text-ink...). Dentro de
[data-theme="vault"], o tema redefine as variáveis que essas classes leem:
--spacing, --text-*, --radius-* e as cores. Os valores vêm do tema escolhido
na vitrine (src/themes/presets.ts) e são gerados por themeCss(). */
@theme inline {
--color-page: var(--page);
--color-surface: var(--surface);
--color-ink: var(--ink);
--color-ink-2: var(--ink-2);
--color-muted: var(--muted);
--color-rule: var(--rule);
--color-rule-strong: var(--rule-strong);
--color-track: var(--track);
--color-grid: var(--grid);
--color-axis: var(--axis);
--color-accent: var(--accent);
--color-success: var(--success);
--color-warning: var(--warning);
--color-critical: var(--critical);
--font-display: var(--fonte-titulo);
--shadow-flutuante: var(--sombra-flutuante);
}