ToDo:
- Refatorar padrões em VerticalLinePlus.tsx
- common.ts está meio bobo. Não serve pra muita coisa, além de ter um nome ridículo.
Novos plugins lightweight-charts-plugins
Arquivos antigos
- Plugins:
- packages/ui/lightweight-charts-plugins/src/lib/plugins/highlight-bar.ts
- packages/ui/lightweight-charts-plugins/src/lib/plugins/vertical-line.ts
- Páginas de teste:
- packages/ui/lightweight-charts-plugins/src/pages/HighlightBar.tsx
- packages/ui/lightweight-charts-plugins/src/pages/VerticalLine.tsx
Novos arquivos
- packages/ui/lightweight-charts-plugins/src/pages/VerticalLinePlus.tsx
- packages/ui/lightweight-charts-plugins/src/lib/plugins/vertical-line-plus/common.ts
- packages/ui/lightweight-charts-plugins/src/lib/plugins/vertical-line-plus/vertical-line.ts
- packages/ui/lightweight-charts-plugins/src/lib/plugins/vertical-line-plus/highlight-click.ts
- packages/ui/lightweight-charts-plugins/src/lib/plugins/vertical-line-plus/highlight-crosshair.ts
VerticalLinePlus.tsx
Beleza — vou explicar o arquivo VerticalLinePlus.tsx de cima para baixo, destacando como a página usa os três plugins novos no mesmo chart. Quando quiser, me diga "continue" que sigo para a próxima seção.
Imports e contexto do chart
- Importa React hooks:
useEffect,useRef,useState. - Importa
LineSerieseTimedo lightweight-charts. - Usa o hook
useCandlestickChartdo projeto para criar o chart e a série de candles dentro docontainerRef. - Importa
sampleCandles(dados) e os três plugins novos:VertLinePlus(linhas verticais fixas por tempo)HighlightCrosshairPrimitive(highlight seguindo o mouse)HighlightClickPrimitive(highlight fixo no último clique)
Por que LineSeries invisível? É a série "hospedeira" para as linhas em tempo fixo — esse padrão isola a primitiva de linhas verticais da série de candles, sem poluir a renderização principal.
Componente e estados/refs principais
Logo no início do componente:
containerRef: ref do container do chart.{ chartRef, seriesRef } = useCandlestickChart(...): o hook cria um chart e retorna:chartRef.current: instância do chartseriesRef.current: série de candles (onde anexamos os highlights)
clickedIdx: estado para mostrar na UI qual candle está selecionado pelo highlight-click (ex.: "Candle: 12 / 71").- Refs para instâncias e handlers:
highlightClickRef: guarda a instância do Highlight Click para controle programático (mover via botões).highlightCrosshairRef: guarda a instância do Highlight Crosshair.clickHandlerRef: guarda a função que assinachart.subscribeClick(para desinscrever corretamente).lineSeriesRef: guarda a sérieLineinvisível (hospedeira das linhas fixas).vertLinesRef: guarda o array das instânciasVertLinePlus.
- Toggles (checkboxes):
showVertLines,showCrosshairHighlight,showClickHighlight— todos começamtruepara ligar tudo por padrão e permitir ligar/desligar dinamicamente.
Até aqui, tudo pronto para controlar o attach/detach de cada plugin independentemente.
Se quiser, diga "continue" e eu explico o primeiro efeito: como as "Vertical Lines (fixas)" são criadas e destruídas conforme o checkbox.
Perfeito — seguindo na mesma toada, vou explicar os efeitos (useEffect) e depois a parte do JSX com os checkboxes, o container do gráfico e os botões de navegação do highlight-click.
Efeito 1: Vertical Lines (fixas)
-
Estados/refs envolvidos:
showVertLinescontrola se as linhas fixas devem estar ativas.lineSeriesRefguarda a sérieLineinvisível que "hospeda" as linhas.vertLinesRefguarda o array de instânciasVertLinePlus.
-
Quando
showVertLinesmuda para true:- Se não existir, cria uma
LineSeriesinvisível:lineSeriesRef.current = chart.addSeries(LineSeries, { visible: false }). - Se ainda não houver linhas, instancia 3
VertLinePlusem tempos espaçados:- índices: 1/5, 1/2, 4/5 do comprimento de
sampleCandles - cores/estilos: red solid, blue dashed [8,4], green dashed [2,2]
- labels no time axis: L1, L2, L3 (com
showLabel: true)
- índices: 1/5, 1/2, 4/5 do comprimento de
- As instâncias são anexadas automaticamente à série no construtor (seguindo o padrão do plugin original de vertical line).
- Se não existir, cria uma
-
Quando
showVertLinesmuda para false:- Chama
destroy()em todas as instâncias deVertLinePluse zera o array. - Remove a
lineSeriesinvisível do chart e zera o ref.
- Chama
-
Cleanup do efeito (ao desmontar a página):
- Garante
destroy()das linhas restantes eremoveSeries()na série invisível.
- Garante
Pontos-chave:
- A série invisível isola o desenho das linhas verticais da série de candles.
destroy()limpa as views internas;removeSeries()retira a série do chart.
Efeito 2: Highlight Crosshair
-
Estados/refs:
showCrosshairHighlightcontrola o attach/detach.highlightCrosshairRefguarda a instância.
-
Quando
showCrosshairHighlightvira true:- Cria
new HighlightCrosshairPrimitive({ color: 'rgba(200,200,200,0.30)' }). - Faz
attachPrimitivena série de candles e guarda no ref.
- Cria
-
Quando
showCrosshairHighlightvira false:- Faz
detachPrimitivee zera o ref.
- Faz
-
Cleanup do efeito:
- Garante
detachPrimitivese ainda estiver anexado.
- Garante
Pontos-chave:
- O plugin
highlight-crosshair(no attach) oculta a linha vertical padrão do crosshair para evitar "duas linhas". - Ele também monitora
visible rangepara manter a largura (bar spacing) correta mesmo sem movimento do mouse.
Efeito 3: Highlight Click + subscription de clique
-
Estados/refs:
showClickHighlightcontrola o attach/detach.highlightClickRefguarda a instância.clickHandlerRefguarda o handler inscrito emchart.subscribeClick.clickedIdxmantém o índice atual para mostrar "Candle: x / n" e para navegação programática.
-
Quando
showClickHighlightvira true:- Se ainda não existir, instancia
new HighlightClickPrimitive({ color: 'rgba(80,160,255,0.35)' }). attachPrimitivena série de candles.- Inicializa a UI e o plugin com o primeiro candle:
setClickedIdx(0)einst.setLogical(0), resultando em "Candle: 1 / N" desde o início.
- Assina
chart.subscribeClick(handler)para atualizarclickedIdxquando o usuário clicar.
- Se ainda não existir, instancia
-
Quando
showClickHighlightvira false:- Desfaz
subscribeClickchamandounsubscribeClick(handler). - Faz
detachPrimitivee zera o ref.
- Desfaz
-
Cleanup do efeito:
- Sempre garante
unsubscribeClick(se assinado) edetachPrimitive.
- Sempre garante
Pontos-chave:
setLogical()é uma API opcional que adicionei no plugin para permitir que a UI mova o destaque em passos (via botões).- O plugin por si só também escuta
visible rangepara manter o alinhamento ao fazer zoom/pan.
Função moveHighlight(delta)
- Depende de
clickedIdxe do tamanho desampleCandles. - Faz clamp do novo índice:
0 <= newIdx <= (n-1). - Atualiza a UI com
setClickedIdx(newIdx)e chamahighlightClickRef.current?.setLogical(newIdx)para mover a primitiva. - Os botões de navegação ficam desabilitados quando:
- O highlight-click está desligado (checkbox desmarcado),
clickedIdxénull,- Está nos limites do array.
JSX (layout/controles)
- Cabeçalho: botão de voltar, título "Vertical Line Plus", descrição breve e lista do que está sendo testado.
- Checkboxes:
- "Vertical Lines (fixas)"
- "Highlight Crosshair"
- "Highlight Click"
- Eles alteram
showVertLines,showCrosshairHighlighteshowClickHighlight, respectivamente, acionando os efeitos que fazem attach/detach.
- Container do gráfico:
ref={containerRef}com dimensões e borda — é onde o hookuseCandlestickChartmonta o chart e a série de candles.
- Barra de navegação do highlight-click:
- Fica abaixo do gráfico.
- Botões
<e>para navegação manual do candle selecionado no modo click. - Texto "Candle: x / n" exibe o índice humano (x =
clickedIdx + 1) e o total de candles.
Se quiser, avançamos para revisar os códigos dos plugins, sugerindo a ordem: vertical-line-plus/highlight-click.ts (pois se integra com os botões), depois vertical-line-plus/highlight-crosshair.ts, vertical-line-plus/vertical-line.ts, e por fim vertical-line-plus/common.ts (helpers).
Vou fazer um overview rápido do common.ts e como ele é usado pelos três plugins "plus", para você já ter insumos de documentação.
common.ts
Arquivo: common.ts
- É um módulo de utilidades compartilhadas para overlays verticais (highlights preenchidos e linha vertical "stroked").
- Centraliza:
- Conversões de coordenadas (logical/time → x no canvas).
- Cálculo de bar spacing com base no range visível.
- Normalizações de dash pattern e line width.
- Tipos/dados comuns usados pelos renderers e views.
Tipos exportados
VerticalFilledData- Shape dos dados que os highlights preenchidos usam durante o draw:
x: number— coordenada X em pixels (chart space).visible: boolean— se deve desenhar.color: string— cor do fill.barSpacing: number— largura base do retângulo (antes do pixel ratio).
- Shape dos dados que os highlights preenchidos usam durante o draw:
VerticalStrokeOptions- Opções de estilo para uma linha vertical com stroke:
colorwidth?lineStyle?: 'solid' | 'dashed'dashPattern?: number[]
- Opções de estilo para uma linha vertical com stroke:
ZOrderWithDefault- Conveniência para
zOrderse você quiser padronizar'bottom' | 'normal' | 'top'(ainda não usada diretamente nos arquivos plus, mas pronta).
- Conveniência para
Funções utilitárias
-
computeBarSpacing(chart: IChartApi): number- Calcula um bar spacing aproximado:
ts.width() / (to - from)do range lógico visível. - Fallback para 6 e clamp para ≥ 1 px. Isso garante que o highlight sempre tenha pelo menos 1 pixel de largura, mesmo em zooms extremos.
- Calcula um bar spacing aproximado:
-
logicalToX(chart, logical): number | null- Converte índice lógico para coordenada X usando
timeScale().logicalToCoordinate. - Retorna
nullquando fora da tela.
- Converte índice lógico para coordenada X usando
-
timeToX(chart, time): number | null- Converte
Timepara coordenada X usandotimeScale().timeToCoordinate. - Retorna
nullquando fora da tela.
- Converte
-
coordVisible(x: number | null): { x: number; visible: boolean }- Helper para montar
{ x, visible }rapidamente com base emxser nulo ou não.
- Helper para montar
-
normalizeDashPattern(opts)- Se
lineStyle === 'dashed', usadashPatternfornecido ou[5,5]como padrão. - Caso contrário, retorna
[](linha contínua).
- Se
-
normalizeLineWidth(width?)- Retorna ao menos 1px (antes de multiplicar pelo pixel ratio).
- Default 2px.
-
truthy(v)- Type guard simples para checagens booleanas de valores possivelmente undefined/null.
Como os plugins usam isso
-
highlight-crosshair.tsehighlight-click.ts- Usam
VerticalFilledDatacomo estado interno do renderer/view. - Chamam
computeBarSpacing(chart)para atualizar a largura do retângulo a cada evento (mouse move, visible range change). - Mantêm a renderização consistente em HiDPI (o pixel ratio é aplicado dentro do canvas via
useBitmapCoordinateSpaceepositionsLine).
- Usam
-
vertical-line.ts(VertLinePlus)- Usa
normalizeDashPatternenormalizeLineWidthpara configurar ostrokeno canvas. - A conversão
time → xé feita no próprio view (timeToCoordinate); poderia usartimeToXdaqui se quiséssemos unificar ainda mais.
- Usa
Observações/contexto
- Este common.ts já resolve a maioria dos pontos comuns sem impor um "base class" com lifecycle; os plugins mantêm seus próprios attach/detach e subscriptions.
- Se quiser, futuramente podemos evoluir para um "mini base primitive" contendo: attach/detach padronizado, subscribe/unsubscribe helpers e um "requestUpdate pipeline" compartilhado. Por ora, ficou simples e explícito em cada plugin.
Se você disser "continue", sigo com o highlight-click.ts: contrato, lifecycle, eventos, e como ele se conecta aos botões da página de teste.
Plugins
Primitives
Series primitives
Series primitives are attached to a specific series and can render on the main pane, price and time scales. For implementation details, refer to the Series Primitives article.
Use the attachPrimitive method to add a primitive to the chart and attach it to the series.
class MyCustomPrimitive {
/* Class implementing the ISeriesPrimitive interface */
}
// Create an instantiated series primitive
const myCustomPrimitive = new MyCustomPrimitive();
const chart = createChart(document.getElementById('container'));
const lineSeries = chart.addSeries(LineSeries);
const data = [
{ time: 1642425322, value: 123 },
/* ... more data */
];
lineSeries.setData(data);
// Attach the primitive to the series
lineSeries.attachPrimitive(myCustomPrimitive);
Pane primitives
Pane primitives are attached to a chart pane rather than a specific series. You can use them to create chart-wide annotations and features like watermarks. For implementation details, refer to the Pane Primitives article.
⚠️ CAUTION
Note that pane primitives cannot render on the price or time scale.
Use the attachPrimitive method to add a primitive to the chart and attach it to the pane.
class MyCustomPanePrimitive {
/* Class implementing the IPanePrimitive interface */
}
// Create an instantiated pane primitive
const myCustomPanePrimitive = new MyCustomPanePrimitive();
const chart = createChart(document.getElementById('container'));
// Get the main pane
const mainPane = chart.panes()[0];
// Attach the primitive to the pane
mainPane.attachPrimitive(myCustomPanePrimitive);
ChatGPT
Ótima ideia. Antes de mergulhar nos arquivos, aqui vai um guia rápido e prático sobre "primitives" no lightweight-charts — o suficiente para entender claramente o que estamos fazendo nos plugins.
O que são "primitives"
- Uma primitive é um "plugin de renderização" que desenha por cima do gráfico, sincronizada com o time scale e outros elementos.
- Ela implementa a interface
ISeriesPrimitive<Time>e fornece "views" para o mecanismo de renderização do chart:paneViews()— views que desenham dentro do painel (o "canvas" principal do gráfico).timeAxisViews()— views que desenham no eixo do tempo (por exemplo, labels).priceAxisViews()— views que desenham no eixo de preço (não usamos aqui).
Em resumo: a primitive é o "dono" da lógica; as views são os "olhos" que calculam coordenadas e entregam um renderer para desenhar no canvas.
Onde anexar (attach)
- Você anexa uma primitive em uma série específica usando:
series.attachPrimitive(primitive)- E remove com
series.detachPrimitive(primitive)
- Por que anexar à série?
- O pipeline de render do lightweight-charts renderiza por série. Ao anexar à série de candles, seu overlay fica sincronizado com ela.
- Se você quer um overlay desvinculado dos candles (como linhas em tempos fixos com label), é comum anexar a uma série "invisível" dedicada (padrão que usamos para vertical lines).
Dica prática:
- Highlights sobre candles (crosshair/click): anexe à série de candles.
- Linhas fixas com label: anexe a uma
LineSeriesinvisível, isolando as views e labels dessa primitive.
Ciclo de vida: attach/detach
Sua primitive define dois métodos opcionais:
-
attached(param: SeriesAttachedParameter<Time>):- Chamado quando a série faz
attachPrimitive. - Recebe
paramcomcharte aserieshospedeira. - Bom lugar para: guardar referências, assinar eventos (crosshair move, click, visible range change), ajustar opções (como esconder a linha vertical do crosshair), fazer uma primeira computação e chamar
requestUpdate().
- Chamado quando a série faz
-
detached():- Chamado quando a série faz
detachPrimitive. - Bom lugar para: desinscrever de todos os eventos e limpar refs internas.
- Chamado quando a série faz
Nos nossos plugins:
- Highlight Crosshair/Click usam
attached/detachedpara assinar/unassinar eventos do chart/timeScale. - VertLinePlus não precisa de eventos, mas ainda segue o pipeline de views. Ele é anexado no construtor (mesmo padrão do seu
VertLineoriginal).
Views e Renderers
-
IPrimitivePaneView(view de painel):update(): calcula e guarda coords (ex.:x = timeToCoordinateoux = logicalToCoordinate).renderer(): retorna um objetoIPrimitivePaneRendererque tem o métododraw(target).
-
IPrimitivePaneRenderer:draw(target: CanvasRenderingTarget2D):- Use
target.useBitmapCoordinateSpace(scope => { ... })— isso fornece:scope.context(Canvas 2D)scope.horizontalPixelRatioescope.bitmapSize— para HiDPI (retina).
- Desenho típico:
- Calcular posição X (em pixels "chart space").
- Ajustar para pixel ratio (
x * scope.horizontalPixelRatio) ou usar helpers (positionsLine). - Desenhar retângulo ou linha vertical.
- Sempre limpe efeitos do contexto (ex.:
ctx.setLineDash([])depois de stroked dashed).
- Use
-
ISeriesPrimitiveAxisView(opcional):- Usado para desenhar labels no eixo.
- Métodos importantes:
visible(),tickVisible(),coordinate(),text(),textColor(),backColor().
Nos nossos plugins:
- Highlights (crosshair/click) fornecem apenas
paneViews. - VertLinePlus fornece
paneViewsetimeAxisViews(para labels "L1/L2/L3").
Atualização e re-render
- Quando algo muda (ex.: mouse move, zoom/pan, clique), você:
- Atualiza o estado interno (ex.:
{ x, visible, barSpacing }para highlight). - Chama
requestUpdate()(viaparam.requestUpdate()guardado emattached) — isso agenda um re-render. - As views serão chamadas para
update()e, em seguida, o renderer chamarádraw().
- Atualiza o estado interno (ex.:
Dica:
- Em highlight-click, guardamos o
logicalclicado e recomputamos a coordenadaxquando o range visível muda. - Em highlight-crosshair, recomputamos
barSpacingemvisible range changepara manter a largura da barra correta, mesmo com o mouse parado.
Coordenadas: logical vs time
- logical: índice lógico do candle (0, 1, 2, ...)
- Converte para X com
timeScale.logicalToCoordinate(logical). - Crosshair e clicks te entregam
param.logical.
- Converte para X com
- time:
Time(timestamp/bar) usado para coordenadas em séries de tempo fixo.- Converte para X com
timeScale.timeToCoordinate(time). - Linhas verticais fixas (VertLinePlus) usam time.
- Converte para X com
Ambos podem resultar em null quando o ponto está fora da viewport — trate como "invisível".
Z-order e HiDPI
paneView.zOrder(): define a camada relativa ("bottom" é comum para ficar atrás de candles/linhas).- HiDPI: sempre multiplique larguras por
scope.horizontalPixelRatio.- Nos highlights, usamos
positionsLine(x, ratio, barSpacing)que centraliza e já trata pixel ratio. - Nas linhas stroked, multiplicamos
lineWidthpelo ratio e limpamossetLineDashno final.
- Nos highlights, usamos
Eventos comuns
- Crosshair move:
chart.subscribeCrosshairMove(handler) - Click:
chart.subscribeClick(handler) - Mudança de escala (zoom/pan):
chart.timeScale().subscribeVisibleLogicalRangeChange(handler)
Sempre desinscreva no detached():
unsubscribeCrosshairMove,unsubscribeClick,timeScale().unsubscribeVisibleLogicalRangeChange.
Onde cada coisa entra nos nossos plugins
-
Highlight Crosshair:
- Attach: oculta a linha vertical do crosshair padrão (
chart.applyOptions({ crosshair.vertLine.visible: false })), assina mouse move e visible range change. - Update: calcula
xpelo crosshair, atualizabarSpacing,requestUpdate.
- Attach: oculta a linha vertical do crosshair padrão (
-
Highlight Click:
- Attach: assina clique e visible range change.
- Guarda
logicalclicado, recalculaxebarSpacingem zoom/pan. - Tem
setLogical()para controle via UI (botões).
-
VertLinePlus:
- Construtor: anexa à série
Lineinvisível, configurapaneViewsetimeAxisViews. - View: converte
time→xnaupdate(). - Renderer: desenha linha vertical com stroke (solid/dashed), limpando dash.
- Construtor: anexa à série
Isso cobre o "como funciona por baixo dos panos". Se quiser, seguimos para o highlight-click.ts, conectando os pontos com o que a página faz nos botões "< Candle x/n >".