Docs·Charting Library·Core API

Events & Tooltips

Subscribe to crosshair hover events, render your own tooltip UI in the framework of your choice, and read the live data under the cursor without touching the chart canvas.

The chart paints inside its <canvas>, but tooltips render in your DOM. That's a deliberate split — your design system already has tooltips, so we expose the data and let you render however you like. One subscription, one tooltip component, everything theme-aware out of the box.

JavaScript
chart.onTooltip((payload, screenX, screenY) => {
  if (payload === null) {
    // Cursor left the chart — hide your tooltip.
    return
  }
  const data = JSON.parse(payload)
  // Render your own tooltip at (screenX, screenY) with `data`.
})

chart.onTooltip(callback)

Registers a single callback that fires on every hover frame the cursor is over the chart, and once with null when the cursor leaves.

ArgumentTypeNotes
payloadstring | nullJSON-encoded snapshot of every visible layer under the cursor, or null on mouse-leave
screenXnumberCSS pixels relative to the page (use directly for position: fixed tooltips)
screenYnumberCSS pixels relative to the page

Calling onTooltip again replaces the previous callback — there is one slot, not a listener list. To "remove" it without re-registering, register a no-op:

JavaScript
chart.onTooltip(() => {})

Payload shape

The payload is JSON. Parse once per hover frame.

The shape is layered: each visible chart layer (candles, enabled indicators, orderbook heatmap, footprint) contributes its own block to the payload. Layers that are turned off don't appear in the JSON at all — your callback should null-check before reading nested fields.

A typical payload for "candles + RSI + heatmap, cursor sitting on the most recent bar" looks roughly like this (exact field names may evolve — always feature-check):

JavaScript
chart.onTooltip((raw, sx, sy) => {
  if (!raw) return hideTooltip()
  const p = JSON.parse(raw)

  const lines = []
  // Candle block — present when the candle layer is enabled.
  if (p.kline) {
    lines.push(`O ${p.kline.open}  H ${p.kline.high}  L ${p.kline.low}  C ${p.kline.close}`)
  }
  // Indicator block — keyed by the indicator's id.
  if (p.indicators) {
    for (const [id, values] of Object.entries(p.indicators)) {
      lines.push(`${id.toUpperCase()}: ${Object.values(values).map(v => Number(v).toFixed(2)).join(' / ')}`)
    }
  }
  // Heatmap / footprint blocks appear only when the cursor is inside their bands.
  if (p.heatmap)   lines.push(`Depth ${p.heatmap.size?.toFixed(2)} @ ${p.heatmap.price}`)
  if (p.footprint) lines.push(`Δ ${p.footprint.delta}`)

  showTooltip(lines.join('\n'), sx, sy)
})

Treat the payload as untyped JSON and feature-check every block — the engine adds new blocks over time, and renderable layers are user-toggled at runtime.

Positioning tips

The callback gives you viewport-relative pixels (getBoundingClientRect style). The right pattern for a fixed-position tooltip:

JavaScript
function showTooltip(html, sx, sy) {
  const el = tooltipRef.value
  el.innerHTML = html
  el.style.display = 'block'
  el.style.left = `${sx + 12}px`   // 12 px offset from cursor
  el.style.top  = `${sy + 12}px`
  // Flip when near the right / bottom edges:
  const rect = el.getBoundingClientRect()
  if (rect.right > window.innerWidth)   el.style.left = `${sx - rect.width - 12}px`
  if (rect.bottom > window.innerHeight) el.style.top  = `${sy - rect.height - 12}px`
}

Render the tooltip into <body> (or a <Teleport to="body"> in Vue / createPortal(document.body) in React) — putting it inside the canvas's overflow-hidden parent will clip it at the chart edge.

Throttling

You don't need to throttle yourself. The chart only emits a tooltip frame when the data under the cursor actually changes (cursor moved more than 1 px, or a new tick arrived). Calling JSON.parse on every fire is fine — payloads are 50-400 bytes typically.

If you ARE doing something expensive in the callback (DOM diff, framework render, network call to fetch trader profile, etc.), debounce that work, not the callback:

JavaScript
let raf = 0
chart.onTooltip((raw, sx, sy) => {
  cancelAnimationFrame(raf)
  raf = requestAnimationFrame(() => updateExpensiveUi(raw, sx, sy))
})

Crosshair sync across multiple charts

When you have two charts stacked (price on top, an RSI / oscillator pane underneath) you usually want one cursor to drive both. The tooltip callback gives you the data you need to do this from your own code:

JavaScript
priceChart.onTooltip((raw) => {
  if (!raw) {
    hideTooltip()
    hideSyncedCursorOn(rsiPaneEl)
    return
  }
  const { cursorTime, cursorPrice } = JSON.parse(raw)
  // Paint your own thin vertical line on the RSI pane at the
  // same screen X derived from cursorTime.
  drawSyncedCursorOn(rsiPaneEl, cursorTime, cursorPrice)
})

The pattern: parse the cursor time/price out of the upstream chart's tooltip, and draw a thin overlay on the downstream pane in your own DOM. This keeps the contract narrow — only the data crosses the chart boundary, not the rendering.

Drawing-tool events

User-drawn shapes (trendlines, fibs, horizontals…) emit their own events. See Drawing for the events surfaced on user commit / click / delete.

Common pitfalls

Forgetting to handle payload === null. Mouse-leave is delivered as null. If you don't check, your tooltip element keeps showing the last frame's data — sticky tooltip, classic UX bug.

Rendering the tooltip inside the chart container. Most chart containers use overflow: hidden to clip out-of-bounds drawing previews. A tooltip placed inside the container gets clipped at the edges. Use a fixed-position element on <body>.

Recomputing the tooltip body in a framework component on every hover. React / Vue's component reconciler is not free at 60 hover frames per second. Render the tooltip directly into a single DOM element (or a memoised component), not as part of your normal render tree.

Holding the payload reference past the callback. The string is yours — but the moment you parse it into objects, copy the values you need into local state, don't hold the parsed object. The next callback overwrites internal buffers and your held reference may go stale.

Mounting two charts on the same <canvas>.onTooltip is per-instance, but if you createChartBridge twice on the same canvas without destroying the first instance, both will register hover handlers and you'll see double-fired tooltips. Always destroy() the previous instance before re-creating.

Next

  • Drawing — programmatically add trendlines, horizontals, markers, and listen for user-drawn shapes.
  • Indicators — the values that show up under payload.indicators.
  • Performance — how to keep hover smooth on slow laptops.