Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

The reliable way to scrape values that appear on hover is to capture the chart library’s data or hover event—not to OCR a screenshot. Identify whether the page uses Plotly, Highcharts, or another library; read its point objects or event payload; and use real browser pointer movement only when no structured interface is exposed. This preserves raw values and works even when the chart is drawn on a canvas.

Start with the chart, not the tooltip

A tooltip is presentation output. It may round decimals, rename fields, combine several series, or disappear as soon as the pointer moves. The chart usually has a more stable source: a JavaScript data array, a point object, a network response, or a documented hover event.

  1. Open developer tools and inspect script names, DOM classes, global variables, and network requests.
  2. Look for library markers such as js-plotly-plot, Plotly, Highcharts, highcharts-chart, or a chart-specific canvas element.
  3. Prefer the library object or the request that supplied the data. Use rendered tooltip text only as a last resort.
  4. Record the chart state that matters: date range, filters, selected series, locale, timezone, and whether data loads after scrolling.

Do not bypass authentication, bot protection, rate limits, robots guidance, terms, or data-licensing restrictions. A chart that is visible to a visitor is not automatically licensed for redistribution.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Scrape Plotly hover values with plotly_hover

Plotly exposes a documented plotly_hover event on the graph div. The event contains a points array for the point under the cursor, along with fields such as the x and y values, series name, point number, and pixel coordinates. Registering this listener is normally more accurate than scraping a tooltip node.

Capture every hovered point in the browser console

const gd = document.querySelector('.js-plotly-plot');
if (!gd) throw new Error('Plotly graph div not found');

gd.on('plotly_hover', (event) => {
  const rows = event.points.map((p) => ({
    curveNumber: p.curveNumber,
    pointNumber: p.pointNumber,
    series: p.data?.name ?? null,
    x: p.x ?? null,
    y: p.y ?? null,
    text: p.text ?? null,
    customdata: p.customdata ?? null,
    bbox: p.bbox ?? null
  }));
  console.log(JSON.stringify(rows));
});

Move the pointer over a plotted mark. The logged object is the extraction record. Keep customdata when the page uses it for IDs or dimensions that are not displayed in the tooltip. The bbox information can help map an event back to a screen location when you are collecting observations with a browser driver.

Read Plotly’s loaded data without hovering

If the graph exposes its data array, you can serialize all loaded points directly. This avoids missing points that are too close together to target with a pointer.

const gd = document.querySelector('.js-plotly-plot');
const rows = gd.data.flatMap((trace, curveNumber) => {
  const xs = trace.x || [];
  const ys = trace.y || [];
  return ys.map((y, pointNumber) => ({
    curveNumber,
    pointNumber,
    series: trace.name ?? null,
    x: xs[pointNumber] ?? pointNumber,
    y,
    text: trace.text?.[pointNumber] ?? trace.text ?? null,
    customdata: trace.customdata?.[pointNumber] ?? null
  }));
});
console.table(rows);

This method covers the data already present in the page. It will not magically include points held on the server but not yet requested, points removed by a filter, or values generated only after an interaction. Check network requests and chart controls before concluding that a series is incomplete.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Plotly details that change your output

  • Several traces can share one x position. Depending on the hover mode, one event may contain multiple entries in points.
  • Formatted labels can differ from raw values. Store x, y, and customdata before using display text.
  • Dates and numbers may be JavaScript objects or strings. Normalize them with an explicit timezone and decimal policy in your pipeline.
  • Responsive charts move their pixel bounding boxes after resize. Recalculate coordinates after viewport changes.

Extract Highcharts series and point objects

Highcharts associates tooltips with points in a series, and its mouse-tracking system controls point tooltips and click events. When the chart instance is exposed, reading chart.series and each series’ points is more stable than selecting tooltip markup.

Dump loaded Highcharts points

const chart = (window.Highcharts?.charts || []).find(Boolean);
if (!chart) throw new Error('Highcharts instance not found');

const rows = chart.series.flatMap((series, seriesIndex) =>
  series.points.map((point, pointIndex) => ({
    seriesIndex,
    pointIndex,
    series: series.name,
    id: point.id ?? null,
    x: point.x,
    y: point.y,
    category: point.category ?? null,
    name: point.name ?? null,
    custom: point.custom ?? null
  }))
);
console.table(rows);

Some pages keep the chart instance in an application variable rather than Highcharts.charts. Inspect the page’s own initialization code or set a breakpoint where Highcharts.chart is called. A chart can also contain data grouped or reduced for performance; compare the point count with the visible range and the network response.

When the tooltip itself is the only exposed value

Highcharts normally renders an SVG tooltip. With the useHTML option, tooltip content can instead be HTML. After moving the pointer, inspect the resulting SVG or HTML nodes and read their text. Shared tooltips can return one value per series for a single x position; split tooltips can render separate labels; formatter functions can change both labels and ordering. Treat the tooltip as a view, not as a schema.

const tooltip = document.querySelector(
  '.highcharts-tooltip, .highcharts-label, [class*="highcharts-tooltip"]'
);
console.log(tooltip?.textContent?.trim() || 'No visible tooltip');

Selectors vary by theme and version, so use this only after confirming the node in the inspector. If the chart is SVG, point elements may have coordinates but not their numeric values; the point object remains preferable.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use a real browser when no data object is available

Canvas pixels do not expose point-level text in the DOM. If the site does not provide a usable chart object or data request, drive a browser, move the pointer over the chart, wait for a tooltip, and save each observation with its coordinates. The following Python example uses Playwright and scans a grid; adjust the URL and tooltip selectors after inspecting the page.

import asyncio
import json
from playwright.async_api import async_playwright

URL = "https://your-site.example/chart"
CHART = ".chart, canvas, .js-plotly-plot, .highcharts-container"
TOOLTIP_SELECTORS = [
    ".plotly-tooltip",
    ".highcharts-tooltip",
    "[role='tooltip']",
    ".tooltip"
]

async def read_visible_tooltip(page):
    for selector in TOOLTIP_SELECTORS:
        locator = page.locator(selector).first
        if await locator.is_visible():
            text = (await locator.inner_text()).strip()
            if text:
                return text
    return None

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        page = await browser.new_page(viewport={"width": 1440, "height": 1000})
        await page.goto(URL, wait_until="networkidle", timeout=90000)
        chart = page.locator(CHART).first
        await chart.wait_for(state="visible", timeout=30000)
        box = await chart.bounding_box()
        if not box:
            raise RuntimeError("Chart has no visible bounding box")

        seen = {}
        columns, rows = 80, 30
        for row in range(rows):
            y = box["y"] + box["height"] * (row + 0.5) / rows
            for col in range(columns):
                x = box["x"] + box["width"] * (col + 0.5) / columns
                await page.mouse.move(x, y)
                await page.wait_for_timeout(40)
                text = await read_visible_tooltip(page)
                if text:
                    key = (round(x, 1), round(y, 1), text)
                    seen[key] = {"x": x, "y": y, "text": text}

        print(json.dumps(list(seen.values()), ensure_ascii=False, indent=2))
        await browser.close()

asyncio.run(main())

Grid scanning is a fallback, not a guarantee that every point was hit. Dense lines, overlapping series, logarithmic axes, touch-oriented charts, and tooltips that require a precise marker can all produce misses. A library-specific routine that obtains point coordinates is more complete. Save the page URL, capture time, viewport, filters, and locale with the result so another run is comparable.

Make pointer collection less noisy

  • Move slowly near likely marks and wait for the tooltip’s DOM node instead of sleeping a fixed interval.
  • Scroll the chart into view and disable animations when the page supports it.
  • Deduplicate by a stable key such as series plus x value, not by tooltip text alone.
  • Capture a screenshot only for audit evidence; do not use OCR when the page exposes structured values.

Choose the method by stability and coverage

Method Stability Coverage Precision Maintenance
Plotly event or gd.data High while the page keeps its Plotly structure SVG and canvas Plotly charts Raw values and custom data Low
Highcharts series and point objects High when the chart instance is reachable SVG and charts rendered with Highcharts Raw point fields Low to medium
Rendered SVG/HTML tooltip Medium; themes and formatters change markup Charts whose tooltip is in the DOM Formatted labels, possibly rounded Medium
Pointer automation on canvas Lowest; coordinates and timing are sensitive Any chart a real browser can interact with Displayed tooltip text High

Use the highest row available. A successful screenshot does not prove that the value was captured; validation must compare extracted rows with visible axes, a second representation, or a sample of known points.

Validate, normalize, and store the results

Check that the tooltip belongs to the intended point

Revisit a sample of points and verify the series name, x label, y value, and coordinate. Shared tooltips can make a cursor appear to report one series while actually returning several. Keep the complete event or point record until validation is finished.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Preserve numeric meaning

Do not parse “1.2k” as 1.2. Prefer raw numeric fields; if only labels exist, store the original string and a separately documented normalized value. Record decimal separators, currency symbols, units, and the chart’s timezone.

Handle changing pages

For live charts, pin a capture timestamp and repeat the extraction under the same filters. For lazy-loaded charts, scroll or trigger the documented range control before reading the final series. If a network response contains the authoritative dataset, archive its request parameters and response version subject to the site’s rules.

Troubleshooting common failures

“No Plotly graph div found”

The chart may be inside an iframe, rendered after a delay, or use a different library. Wait for the frame, inspect all frames, and search for Plotly in loaded scripts. Do not assume a canvas means Plotly or that a class name is permanent.

The event fires but points is empty

You may be listening to the wrong element, hovering a non-data region, or using a chart configured without point interaction. Confirm that the listener is attached to the graph div and move over an actual mark. Check hover mode and trace visibility.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Highcharts is present but series is empty

Data may be loaded asynchronously, the selected range may contain no points, or the page may expose a wrapper rather than the chart instance. Wait for the series to populate and inspect network responses or the application’s chart reference.

The tooltip appears visually but selectors return nothing

It may be rendered in an iframe, shadow DOM, SVG text, or a transient portal. Inspect the tooltip while it is visible, query the correct frame or shadow root, and prefer the chart API if available.

Canvas scanning returns duplicates or misses points

Increase sampling near marks, wait for the tooltip to settle, and deduplicate by series and x value. For dense or overlapping charts, recover the underlying data request or library object; pixel coordinates alone cannot reconstruct hidden values.

Headless mode behaves differently

Use the same viewport, device scale, timezone, locale, authentication state, and consent choices as a normal visitor. Some sites defer data until a real interaction, so move the pointer and scroll deliberately rather than relying only on networkidle.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup: ScreenshotNeo

ScreenshotNeo is useful when you need a repeatable visual record of a chart rather than a custom scraper. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be disabled. Only clean shots are billed: bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and the response identifies the result with X-Page-Verdict and X-Billed headers. It does not replace extracting Plotly or Highcharts point data, but it gives you a consistent image or PDF for checking what a visitor saw.

One GET request returns a PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation for all options, including waits, cookies, custom JavaScript, full-page capture, element selectors, and PDF settings.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://your-site.example/chart -o chart.webp

Python

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://your-site.example/chart"}, timeout=90)
r.raise_for_status()
open("chart.webp", "wb").write(r.content)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://your-site.example/chart' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('chart.webp', Buffer.from(await res.arrayBuffer()));

ScreenshotNeo also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account to try it.

Performance, reliability, and cost decisions

Reading a loaded chart object is usually faster and cheaper than launching a browser for every point. Reuse one browser context when automation is unavoidable, wait for a specific chart or tooltip selector, and cap retries for pages that repeatedly time out. Cache results only when the chart’s data freshness allows it. For large jobs, extract the underlying request once and process its structured response rather than moving a pointer over thousands of marks.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Keep failures separate from valid zero values and empty series. Log HTTP status, page verdict, authentication state, chart dimensions, and extraction method. A timeout or bot check is an operational failure, not a data point. If you use ScreenshotNeo for visual evidence, its response headers distinguish billed clean captures from non-billed failures and cache hits.

FAQ

Can I scrape a chart that has no visible tooltip?

Yes, if its JavaScript object or network response contains the points. A tooltip is optional presentation; it is not required for extraction.

How do I keep a reproducible historical record?

Store the raw event or point objects, page state, capture time, timezone, viewport, and a visual capture for audit. Re-run a small validation sample whenever the publisher changes the chart.

What should I do when values are loaded only after login?

Use an authorized session and the site’s permitted API or browser flow. Do not evade access controls or share credentials with an extraction service.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Frequently Asked Questions

Can I scrape a chart that has no visible tooltip?

Yes. Read the chart’s JavaScript object or the network response that supplies its points; a tooltip is only a presentation layer.

How do I keep a reproducible historical record?

Save raw point or event data together with page state, capture time, timezone, viewport, and a validation screenshot.

What if the chart requires login?

Use an authorized session and any permitted API or browser flow. Do not bypass access controls or expose credentials.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.