Skip to main content

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 LineSeries e Time do lightweight-charts.
  • Usa o hook useCandlestickChart do projeto para criar o chart e a série de candles dentro do containerRef.
  • 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 chart
    • seriesRef.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 assina chart.subscribeClick (para desinscrever corretamente).
    • lineSeriesRef: guarda a série Line invisível (hospedeira das linhas fixas).
    • vertLinesRef: guarda o array das instâncias VertLinePlus.
  • Toggles (checkboxes):
    • showVertLines, showCrosshairHighlight, showClickHighlight — todos começam true para 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:

    • showVertLines controla se as linhas fixas devem estar ativas.
    • lineSeriesRef guarda a série Line invisível que "hospeda" as linhas.
    • vertLinesRef guarda o array de instâncias VertLinePlus.
  • Quando showVertLines muda para true:

    • Se não existir, cria uma LineSeries invisível: lineSeriesRef.current = chart.addSeries(LineSeries, { visible: false }).
    • Se ainda não houver linhas, instancia 3 VertLinePlus em 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)
    • As instâncias são anexadas automaticamente à série no construtor (seguindo o padrão do plugin original de vertical line).
  • Quando showVertLines muda para false:

    • Chama destroy() em todas as instâncias de VertLinePlus e zera o array.
    • Remove a lineSeries invisível do chart e zera o ref.
  • Cleanup do efeito (ao desmontar a página):

    • Garante destroy() das linhas restantes e removeSeries() na série invisível.

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:

    • showCrosshairHighlight controla o attach/detach.
    • highlightCrosshairRef guarda a instância.
  • Quando showCrosshairHighlight vira true:

    • Cria new HighlightCrosshairPrimitive({ color: 'rgba(200,200,200,0.30)' }).
    • Faz attachPrimitive na série de candles e guarda no ref.
  • Quando showCrosshairHighlight vira false:

    • Faz detachPrimitive e zera o ref.
  • Cleanup do efeito:

    • Garante detachPrimitive se ainda estiver anexado.

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 range para manter a largura (bar spacing) correta mesmo sem movimento do mouse.

Efeito 3: Highlight Click + subscription de clique

  • Estados/refs:

    • showClickHighlight controla o attach/detach.
    • highlightClickRef guarda a instância.
    • clickHandlerRef guarda o handler inscrito em chart.subscribeClick.
    • clickedIdx mantém o índice atual para mostrar "Candle: x / n" e para navegação programática.
  • Quando showClickHighlight vira true:

    • Se ainda não existir, instancia new HighlightClickPrimitive({ color: 'rgba(80,160,255,0.35)' }).
    • attachPrimitive na série de candles.
    • Inicializa a UI e o plugin com o primeiro candle:
      • setClickedIdx(0) e inst.setLogical(0), resultando em "Candle: 1 / N" desde o início.
    • Assina chart.subscribeClick(handler) para atualizar clickedIdx quando o usuário clicar.
  • Quando showClickHighlight vira false:

    • Desfaz subscribeClick chamando unsubscribeClick(handler).
    • Faz detachPrimitive e zera o ref.
  • Cleanup do efeito:

    • Sempre garante unsubscribeClick (se assinado) e detachPrimitive.

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 range para manter o alinhamento ao fazer zoom/pan.

Função moveHighlight(delta)

  • Depende de clickedIdx e do tamanho de sampleCandles.
  • Faz clamp do novo índice: 0 <= newIdx <= (n-1).
  • Atualiza a UI com setClickedIdx(newIdx) e chama highlightClickRef.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, showCrosshairHighlight e showClickHighlight, respectivamente, acionando os efeitos que fazem attach/detach.
  • Container do gráfico:
    • ref={containerRef} com dimensões e borda — é onde o hook useCandlestickChart monta 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).
  • VerticalStrokeOptions
    • Opções de estilo para uma linha vertical com stroke:
      • color
      • width?
      • lineStyle?: 'solid' | 'dashed'
      • dashPattern?: number[]
  • ZOrderWithDefault
    • Conveniência para zOrder se você quiser padronizar 'bottom' | 'normal' | 'top' (ainda não usada diretamente nos arquivos plus, mas pronta).

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.
  • logicalToX(chart, logical): number | null

    • Converte índice lógico para coordenada X usando timeScale().logicalToCoordinate.
    • Retorna null quando fora da tela.
  • timeToX(chart, time): number | null

    • Converte Time para coordenada X usando timeScale().timeToCoordinate.
    • Retorna null quando fora da tela.
  • coordVisible(x: number | null): { x: number; visible: boolean }

    • Helper para montar { x, visible } rapidamente com base em x ser nulo ou não.
  • normalizeDashPattern(opts)

    • Se lineStyle === 'dashed', usa dashPattern fornecido ou [5,5] como padrão.
    • Caso contrário, retorna [] (linha contínua).
  • 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.ts e highlight-click.ts

    • Usam VerticalFilledData como 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 useBitmapCoordinateSpace e positionsLine).
  • vertical-line.ts (VertLinePlus)

    • Usa normalizeDashPattern e normalizeLineWidth para configurar o stroke no canvas.
    • A conversão time → x é feita no próprio view (timeToCoordinate); poderia usar timeToX daqui se quiséssemos unificar ainda mais.

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 LineSeries invisí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 param com chart e a series hospedeira.
    • 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().
  • detached():

    • Chamado quando a série faz detachPrimitive.
    • Bom lugar para: desinscrever de todos os eventos e limpar refs internas.

Nos nossos plugins:

  • Highlight Crosshair/Click usam attached/detached para 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 VertLine original).

Views e Renderers

  • IPrimitivePaneView (view de painel):

    • update(): calcula e guarda coords (ex.: x = timeToCoordinate ou x = logicalToCoordinate).
    • renderer(): retorna um objeto IPrimitivePaneRenderer que tem o método draw(target).
  • IPrimitivePaneRenderer:

    • draw(target: CanvasRenderingTarget2D):
      • Use target.useBitmapCoordinateSpace(scope => { ... }) — isso fornece:
        • scope.context (Canvas 2D)
        • scope.horizontalPixelRatio e scope.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).
  • 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 paneViews e timeAxisViews (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() (via param.requestUpdate() guardado em attached) — isso agenda um re-render.
    • As views serão chamadas para update() e, em seguida, o renderer chamará draw().

Dica:

  • Em highlight-click, guardamos o logical clicado e recomputamos a coordenada x quando o range visível muda.
  • Em highlight-crosshair, recomputamos barSpacing em visible range change para 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.
  • 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.

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 lineWidth pelo ratio e limpamos setLineDash no final.

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 x pelo crosshair, atualiza barSpacing, requestUpdate.
  • Highlight Click:

    • Attach: assina clique e visible range change.
    • Guarda logical clicado, recalcula x e barSpacing em zoom/pan.
    • Tem setLogical() para controle via UI (botões).
  • VertLinePlus:

    • Construtor: anexa à série Line invisível, configura paneViews e timeAxisViews.
    • View: converte timex na update().
    • Renderer: desenha linha vertical com stroke (solid/dashed), limpando dash.

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 >".