Chart components enable tooltips by default. Use their tooltipprop to configure fields or supply a custom renderer. Stream Frames expose tooltipContent for custom content. The shared wrapper measures the tooltip, wraps content to fit the plot width and keeps its position within the plot when its size permits.
When you set an axis formatter on a chart — valueFormaton ordinal charts (BarChart, StackedBarChart, GroupedBarChart, DotPlot, SwarmPlot, SwimlaneChart) or xFormat/yFormat on XY charts (LineChart, AreaChart, Scatterplot, BubbleChart, etc.) — the same formatter is applied to the default tooltip. One function, both places, so a bar chart that reads "$450k" on its axis also reads "$450k" on hover.
valueFormat applies to both axis and tooltip
import { BarChart } from "semiotic" const frameProps = { /* --- Data --- */ data: barData, /* --- Process --- */ valueAccessor: "sales", categoryAccessor: "category", /* --- Customize --- */ showGrid: true, valueLabel: "Sales", valueFormat: d => `${(d / 1000).toFixed(1)}k` } export default () => { return <BarChart {...frameProps} /> }
Precedence
The cascade only drives the default tooltip. Passingtooltip explicitly takes over:
tooltip={false} — no tooltip is rendered.tooltip={customFn}, custom multi-mode content, or {Tooltip({...})}/{MultiLineTooltip({...})} — your content fully replaces the default. Axis formatters do notapply automatically; re-pass valueFormat/xFormat inside your tooltip if you want them.tooltip="multi" (or tooltip={{ mode: "multi" }}) on LineChart, AreaChart, and StackedAreaChart uses xFormatfor the header and yFormat for each series value. MultiAxisLineChart uses xFormat and each series' format. Formatters receive unrounded values, so yFormat={v => `${Number(v).toFixed(1)}°`}can show one decimal, or a higher precision when needed. Without a formatter, decimal values use a compact six-significant-digit default.- Default tooltip active — the chart's
valueFormator xFormat/yFormat is applied to the matching field. - A few charts format internally (Histogram, FunnelChart, LikertChart, GaugeChart) and don't participate in the cascade — customize via the
tooltip prop if needed.
Supplementing or overriding
To keep the default tooltip but add a custom format for one field, pass a function and call the formatter yourself:
JSX
const money = d => `${(d / 1000).toFixed(1)}k` <BarChart data={data} categoryAccessor="category" valueAccessor="sales" valueFormat={money} // → axis + default tooltip /> // Or override the tooltip entirely — cascade is bypassed, so // re-apply the formatter explicitly: <BarChart data={data} categoryAccessor="category" valueAccessor="sales" valueFormat={money} // → axis only (tooltip is custom) tooltip={MultiLineTooltip({ title: "category", fields: [ { key: "sales", label: "Sales", format: money }, { key: "profit", label: "Profit", format: money }, ] })} />
With Charts
Chart components enable hover tooltips by default. You can customize the tooltip content using the tooltip prop with theTooltip or MultiLineTooltip utilities:
Callback data and intentional omission
Custom and network chart tooltip callbacks receive the authored node or edge: read d.label, for example. Handle both record types when edges are interactive. FrametooltipContent callbacks receive a hover wrapper; callunwrapDatum(hover) once, imported from semiotic/utils, to recover authored data. Avoid guessing nested d.data paths. Legacy realtime charts retain their documented callback signatures.
Set tooltip={false} to omit all tooltips, or returnnull directly from the callback to omit one datum. Undefined, booleans, empty or whitespace-only strings, and empty arrays or fragments also suppress the background. Numeric zero remains visible. Keep enableHoverenabled if observations still need it. Use TooltipRoot from your chart family entry for custom chrome. ThePipeline Explorer demonstrates separate card and connection tooltips alongside an inspector.
Verify custom tooltips by hovering actual marks and interactive edges: check expected labels and values, a single readable box, placement after zoom, pan and resize, and dismissal on leave. A rendered chart or an inspector does not establish that hover behavior works.
Chrome ownership through wrapper components
Declare consumer-owned chrome directly withtooltip={{ content: renderer, chrome: "none" }}. The renderer receives authored data and can return nested components; Semiotic adds positioning without adding a background, padding, or shadow. This also works with { mode: "multi", content, chrome: "none" }. Use chrome: "default" to request Semiotic's surface explicitly.
JSX
<Scatterplot data={data} xAccessor="x" yAccessor="y" tooltip={{ content: d => <MyTooltip label={d.name} />, chrome: "none" }} /> // Apply the policy to custom tooltips across a dashboard. <ThemeProvider theme={{ tooltip: { chrome: "none" } }}> <Scatterplot data={data} xAccessor="x" yAccessor="y" tooltip={d => <MyTooltip label={d.name} />} /> </ThemeProvider>
Chart configs override the theme. Field tooltip configs also acceptchrome; raw frame callbacks inherit the theme policy. Existing marker APIs remain supported.
Use markTooltipChrome(renderer) when a custom renderer supplies its own background, padding and shadow. Pass that renderer to tooltip, multi-mode content, or a frame's tooltipContent. Ownership applies to every non-empty result, even through unmarked wrapper components. Existing component markers, TooltipRoot, inline backgrounds and data-semiotic-tooltip-chrome still work.
JSX
import { Scatterplot, TooltipRoot, markTooltipChrome } from "semiotic/xy" function ChartTooltip({ label }) { return <TooltipRoot>{label}</TooltipRoot> } const MyTooltip = props => <ChartTooltip {...props} /> const renderTooltip = markTooltipChrome(d => <MyTooltip label={d.name} />) <Scatterplot data={data} xAccessor="x" yAccessor="y" tooltip={renderTooltip} />
Leave plain renderers such as d => d.name unmarked so Semiotic supplies its default surface. hasOwnTooltipChromeaccepts a renderer or its returned element. Detection inspects explicit metadata and the immediate element; it does not render components or inspect computed CSS. Marking a renderer preserves its identity and does not call it.
Wrapping libraries can import hasTooltipContent from their chart family entry or semiotic/utils and check the consumer's result before adding an element. The helper checks arrays and fragments recursively; arbitrary elements remain opaque, including components that later return null.
JSX
import { TooltipRoot, hasTooltipContent, hasOwnTooltipChrome, markTooltipChrome } from "semiotic/xy" function withTooltipSurface(content) { return markTooltipChrome(d => { const result = content(d) if (!hasTooltipContent(result)) return null if (hasOwnTooltipChrome(content) || hasOwnTooltipChrome(result)) return result return <TooltipRoot>{result}</TooltipRoot> }) }
Smart defaults for custom & network charts
When a chart can't declare tooltip fields — aNetworkCustomChart layout, a recipe like the Mermaid or lineage DAG, anything "weird" — the default tooltip is a concise summary of every property in object order. It picks a meaningfultitle (a name/label/title field, falling back to id), then atype (type/kind/category/shape…), then avalue, then the rest — skipping positional and internal bookkeeping (x/y/layer/row/depth, _-prefixed keys, nested objects). So a Mermaid node shows its name andtype: decision rather than a bare id. The same heuristic backs the generic MultiLineTooltip default (and the XY/ordinal custom-layout fallbacks) — anywhere a tooltip would otherwise dump every field, it now leads with a title and orders the rest by role. To steer it, give your datum a name (or label) and atype field; to override entirely, pass atooltip function. The same heuristic is exposed as the puresmartTooltipEntries(datum) helper (fromsemiotic).
Default Hover Tooltip
import { LineChart } from "semiotic" const frameProps = { /* --- Data --- */ data: [ { month: 1, revenue: 12000 }, { month: 2, revenue: 18000 }, { month: 3, revenue: 14000 }, // ...more data points ], /* --- Process --- */ xAccessor: "month", yAccessor: "revenue", /* --- Customize --- */ xLabel: "Month", yLabel: "Revenue ($)" } export default () => { return <LineChart {...frameProps} /> }
Use MultiLineTooltip with a Chart's tooltipprop for formatted multi-field tooltips:
JSX
import { BarChart, MultiLineTooltip } from "semiotic" <BarChart data={productData} categoryAccessor="category" valueAccessor="sales" tooltip={MultiLineTooltip({ title: "category", fields: [ { key: "sales", label: "Sales", format: v => `${v}` }, { key: "profit", label: "Profit", format: v => `${v}` }, { key: "units", label: "Units Sold" } ] })} />
With Frames
Import Tooltip from Semiotic and pass it totooltipContent along withhoverAnnotation={true}:
import { StreamXYFrame } from "semiotic" import { StreamXYFrame, Tooltip } from "semiotic" const frameProps = { /* --- Data --- */ data: [ { x: 10, y: 20, category: "A", value: 100 }, { x: 25, y: 35, category: "B", value: 150 }, // ...more points ], chartType: "scatter", /* --- Size --- */ margin: { top: 30, bottom: 60, left: 60, right: 20 }, /* --- Process --- */ xAccessor: "x", yAccessor: "y", /* --- Customize --- */ pointStyle: d => ({ fill: colorHash[d.category], r: 5 }), showAxes: true, xLabel: "X Value", yLabel: "Y Value", /* --- Interact --- */ enableHover: true, /* --- Annotate --- */ tooltipContent: hover => Tooltip({ title: "category" })(hover.data || hover) } export default () => { return <StreamXYFrame {...frameProps} /> }
MultiLineTooltip displays multiple data fields with labels and optional formatting:
import { StreamOrdinalFrame } from "semiotic" import { StreamOrdinalFrame, MultiLineTooltip } from "semiotic" const frameProps = { /* --- Data --- */ data: [ { category: "Product A", sales: 450, profit: 120, units: 230 }, { category: "Product B", sales: 380, profit: 95, units: 190 }, { category: "Product C", sales: 520, profit: 145, units: 260 }, { category: "Product D", sales: 290, profit: 75, units: 145 }, { category: "Product E", sales: 610, profit: 180, units: 305 } ], chartType: "bar", /* --- Size --- */ margin: { top: 20, bottom: 80, left: 60, right: 20 }, /* --- Process --- */ oAccessor: "category", rAccessor: "sales", /* --- Customize --- */ pieceStyle: () => ({ fill: "#6366f1", stroke: "white" }), showAxes: true, /* --- Interact --- */ enableHover: true, /* --- Annotate --- */ tooltipContent: hover => { const d = hover.data || hover return MultiLineTooltip({ title: "category", fields: [ { key: "sales", label: "Sales", format: v => `${v}` }, { key: "profit", label: "Profit", format: v => `${v}` }, { key: "units", label: "Units Sold" } ] })(d) } } export default () => { return <StreamOrdinalFrame {...frameProps} /> }
For complete control over tooltip rendering, pass a custom function totooltipContent. The function receives the Stream Frame's HoverData wrapper —{ data, x, y, ... } — where data is the raw datum the user pushed or passed. Read fields offd.data directly:
JSX
<StreamXYFrame data={data} chartType="scatter" enableHover={true} tooltipContent={d => { const datum = d.data return ( <div style={{ background: "var(--surface-1)", border: "1px solid #ccc", padding: "8px 12px", borderRadius: 4, boxShadow: "0 2px 8px rgba(0,0,0,0.15)" }}> <strong>{datum.category}</strong> <div>X: {datum.x}</div> <div>Y: {datum.y}</div> <div>Value: {datum.value.toLocaleString()}</div> </div> ) }} xAccessor="x" yAccessor="y" />
Higher-level HOC props like tooltip (onBarChart, LineChart, etc.) auto-unwrap the wrapper for you, so the function there receives the datum directly —tooltip={d => d.category}. The rawtooltipContent form on Stream Frames is the unwrapped path for callers that need the full hover context.
Advanced hoverAnnotation
The hoverAnnotation prop can accept an array of annotation types for richer hover behavior. This lets you combine tooltips with guide lines and point highlights:
Crosshair Tooltip with Guide Lines
import { StreamXYFrame } from "semiotic" const frameProps = { /* --- Data --- */ data: [{ x: 10, y: 20, category: "A", value: 100 }, { x: 25, y: 35, category: "B", value: 150 }, ... ], chartType: "scatter", /* --- Size --- */ margin: { top: 30, bottom: 60, left: 60, right: 20 }, /* --- Process --- */ xAccessor: "x", yAccessor: "y", /* --- Customize --- */ pointStyle: d => ({ fill: colorHash[d.category], r: 5 }), showAxes: true, xLabel: "X Value", yLabel: "Y Value", /* --- Interact --- */ hoverAnnotation: [ { type: "x", disable: ["connector", "note"] }, { type: "y", disable: ["connector", "note"] }, { type: "frame-hover" } ] } export default () => { return <StreamXYFrame {...frameProps} /> }
Configuration
A factory function that returns a tooltip renderer for a single value:
| Prop | Type | Required | Default | Description |
|---|
title | string | function | — | auto | Field name or function to display as the tooltip header. |
format | function | — | — | Format function for the displayed value. |
style | object | — | — | Custom CSS styles for the tooltip container. |
className | string | — | — | Custom CSS class for the tooltip container. |
JSX
import { Tooltip } from "semiotic" // Basic usage Tooltip({ title: "name" }) // With formatting Tooltip({ title: "category", format: v => v.toUpperCase() }) // With custom styles Tooltip({ title: "name", style: { background: "#1e293b", color: "white" } })
A factory function that returns a tooltip renderer for multiple fields:
| Prop | Type | Required | Default | Description |
|---|
title | string | function | — | — | Header field name or function. |
fields | array | Yes | — | Array of field names (strings) or objects: { key, label, format }. |
showLabels | boolean | — | true | Whether to show field labels. |
separator | string | — | ": " | Separator string between label and value. |
style | object | — | — | Custom CSS styles for the tooltip container. |
className | string | — | — | Custom CSS class for the tooltip container. |
JSX
import { MultiLineTooltip } from "semiotic" // String fields (field name = label) MultiLineTooltip({ title: "product", fields: ["revenue", "units", "category"] }) // Object fields with formatting MultiLineTooltip({ title: "product", fields: [ { key: "revenue", label: "Revenue", format: v => `${v.toLocaleString()}` }, { key: "margin", label: "Margin", format: v => `${(v * 100).toFixed(1)}%` }, { key: "units", label: "Units" } ] })
hoverAnnotation Options
The hoverAnnotation prop accepts several forms:
JSX
// Boolean: default frame-hover tooltip hoverAnnotation={true} // Array: multiple annotation types on hover hoverAnnotation={[ { type: "frame-hover" }, // Tooltip { type: "x", disable: ["connector", "note"] }, // Vertical guide { type: "y", disable: ["connector", "note"] }, // Horizontal guide { type: "highlight", style: { strokeWidth: 5 } }, // Highlight mark { type: "vertical-points", threshold: 0.1 }, // Show nearby points { type: "desaturation-layer", style: { fill: "white", opacity: 0.5 } } ]} // For StreamOrdinalFrame, use pieceHoverAnnotation for individual pieces // vs hoverAnnotation for entire columns <StreamOrdinalFrame pieceHoverAnnotation={true} /> <StreamOrdinalFrame hoverAnnotation={true} /> // column-level hover
Tooltips are positioned automatically near the hovered data point. They render in an HTML layer above the SVG visualization, so they can contain any HTML content. For point-based data,StreamXYFrame uses Voronoi tesselation to determine the nearest data point on hover, providing smooth and responsive tooltip behavior even when points are small.
The tooltip sits to the right of and below the pointer, and flips to the other side when it would run past the plot edge. The wrapper (.stream-frame-tooltip) reports its placement in data attributes: data-placement is "pending" until the tooltip has been measured and "placed" after, anddata-flip-x / data-flip-y are"true" while it sits left of or above the pointer.
To ease the tooltip between samples, put a CSS transition onleft and top. Only moves along the same side animate. The first placement and every flip set an inlinetransition: none, so the tooltip doesn't glide in from the pointer or swing across it.
CSS
.stream-frame-tooltip { transition: left 120ms ease-out, top 120ms ease-out; }
- Annotations — the full annotation system that powers tooltips
- Interaction — highlighting, cross-highlighting, and custom click/hover behaviors
- StreamXYFrame — point, line, and area tooltips with Voronoi hover
- StreamOrdinalFrame — piece-level and column-level hover annotations
- LineChart — simplified tooltip via the
tooltip prop