import { RealtimeHistogram } from "semiotic"
RealtimeHistogram renders a streaming temporal histogram. Incoming data points are binned by time interval and rendered as bars that scroll across the chart. It wrapsStreamXYFrame withchartType="bar" and supports both simple and stacked (categorical) modes. Edge bins that only partially fall within the visible time window are rendered at proportionally narrower widths.
Quick Start
Create a ref, push data points on an interval, and RealtimeHistogram bins and renders them as bars. ThebinSize prop defines the time interval for aggregation.
Set binAlign="center" when each sample timestamp identifies the center of its time slot. Bins follow a zero-anchored grid and cover[timestamp - binSize/2, timestamp + binSize/2). Automatic extents include the full first and last bins; explicit timeExtentstill clips at the requested bounds. Stacking and brush snapping use the same boundaries.
JSX
import { RealtimeHistogram } from "semiotic" import { useRef, useEffect } from "react" function StreamingBars() { const chartRef = useRef() const indexRef = useRef(0) useEffect(() => { const id = setInterval(() => { chartRef.current?.push({ time: indexRef.current++, value: Math.floor(Math.random() * 39) + 1 }) }, 30) return () => clearInterval(id) }, []) return ( <RealtimeHistogram ref={chartRef} binSize={20} fill="#007bff" windowSize={200} showAxes={true} /> ) }
Examples
Horizontal grid lines and column hover
Hover either color in a column: both stacked segments stay highlighted while the other time bins dim. Horizontal grid lines make their totals easier to compare. Move off the chart to restore every column.
Skip to data tableThe 30–40 interval has the most requests. Hover either segment to highlight its entire column.
TSX
import React from "react" import { TemporalHistogram } from "semiotic/realtime" const data = [ { time: 5, value: 6, status: "Completed" }, { time: 5, value: 2, status: "Retried" }, { time: 15, value: 8, status: "Completed" }, { time: 15, value: 4, status: "Retried" }, { time: 25, value: 7, status: "Completed" }, { time: 25, value: 3, status: "Retried" }, { time: 35, value: 10, status: "Completed" }, { time: 35, value: 4, status: "Retried" }, ] export default function TemporalHistogramHoverExample() { return ( <TemporalHistogram data={data} binSize={10} categoryAccessor="status" colors={{ Completed: "#2563eb", Retried: "#d97706" }} responsiveWidth height={340} margin={{ top: 48, right: 20, bottom: 80, left: 52 }} timeExtent={[0, 40]} valueExtent={[0, 16]} tickFormatTime={String} gap={8} background="transparent" showGrid axes={[ { orient: "bottom", label: "Time", grid: false }, { orient: "left", label: "Requests", gridStyle: { stroke: "#94a3b8", strokeOpacity: 0.45 }, }, ]} hoverHighlight legendPosition="bottom" title="Requests per time bin" description="Completed and retried requests stacked in four time bins." summary="The 30–40 interval has the most requests. Hover either segment to highlight its entire column." /> ) }
showGrid enables the grid; grid: false on the bottom axis removes vertical lines. hoverHighlight matches the whole time bin, including all its stacked categories. These props work on both TemporalHistogram and RealtimeHistogram.
Set tooltip="multi" to list every stacked category in the hovered time bin, from bottom to top, with the bin total. The tooltip responds anywhere in the column, including the space above a short stack and the gap between bars; an empty bin shows nothing.
Skip to data tableHover anywhere in a column, including the space above its stack, to compare every status in that bin.
TSX
import React from "react" import { TemporalHistogram } from "semiotic/realtime" const data = [ { time: 5, value: 6, status: "Completed" }, { time: 5, value: 2, status: "Retried" }, { time: 15, value: 8, status: "Completed" }, { time: 15, value: 4, status: "Retried" }, { time: 25, value: 7, status: "Completed" }, { time: 25, value: 3, status: "Retried" }, { time: 35, value: 10, status: "Completed" }, { time: 35, value: 4, status: "Retried" }, ] export default function TemporalHistogramMultiTooltipExample() { return ( <TemporalHistogram data={data} binSize={10} categoryAccessor="status" colors={{ Completed: "#2563eb", Retried: "#d97706" }} responsiveWidth height={340} margin={{ top: 48, right: 20, bottom: 80, left: 52 }} timeExtent={[0, 40]} valueExtent={[0, 16]} tickFormatTime={String} gap={8} background="transparent" tooltip="multi" hoverHighlight legendPosition="bottom" title="Requests per time bin" description="Completed and retried requests stacked in four time bins." summary="Hover anywhere in a column, including the space above its stack, to compare every status in that bin." /> ) }
Custom tooltip={{ mode: "multi", content }} content receivesallSeries rows (group, value,color, datum) and the bin's range andcategories; getSourceRows(datum) returns the bin's authored rows.
Value-banded fills
valueBands colors each bar by the value ranges it crosses. Here a bar is solid up to the soft limit, amber-hatched up to the hard limit, and red-hatched above it. Bands are ordered byupTo, and a last band without upTo covers every higher value. A band's fill is a color or aHatchFill.
TSX
import React from "react" import { TemporalHistogram } from "semiotic/realtime" // Concurrent jobs per minute against a soft limit of 5 and a hard limit of 10. const data = [3, 6, 4, 9, 12, 7, 5, 11, 8, 2].map((value, minute) => ({ time: minute * 60_000, value, })) const overSoftLimit = { type: "hatch", background: "#fef3c7", stroke: "#b45309", spacing: 5, angle: -45 } as const const overHardLimit = { type: "hatch", background: "#fee2e2", stroke: "#b91c1c", spacing: 5, angle: -45 } as const export default function TemporalHistogramValueBandsExample() { return ( <TemporalHistogram data={data} binSize={60_000} timeExtent={[0, 600_000]} valueExtent={[0, 14]} tickFormatTime={(ms) => `${ms / 60_000}m`} valueBands={[ { upTo: 5, fill: "#2563eb" }, { upTo: 10, fill: overSoftLimit }, { fill: overHardLimit }, ]} annotations={[ { type: "y-threshold", value: 5, label: "Soft limit", color: "#b45309" }, { type: "y-threshold", value: 10, label: "Hard limit", color: "#b91c1c" }, ]} gap={4} responsiveWidth height={300} background="transparent" title="Concurrent jobs per minute" description="Each bar is solid up to the soft limit, amber-hatched up to the hard limit, and red-hatched above it." summary="Minutes 4 and 7 run past the hard limit of 10 jobs." /> ) }
Each bar is still one mark with its bin's datum, so hover, selection,hoverHighlight, and tooltips treat it as a single bin. Values no band covers keep the bar's own fill. Stacked bins (categoryAccessor) ignore valueBands.
Stacked Bars by Category
Use categoryAccessor and colors to stack bars by category within each bin. The stack order follows the key order of the colors object.
JSX
const categories = ["errors", "warnings", "info"] useEffect(() => { const id = setInterval(() => { const cat = categories[indexRef.current % 3] chartRef.current?.push({ time: Math.floor(indexRef.current++ / 3), value: Math.floor(Math.random() * 10) + 1, category: cat }) }, 30) return () => clearInterval(id) }, []) <RealtimeHistogram ref={chartRef} binSize={20} categoryAccessor="category" colors={{ errors: "#dc3545", warnings: "#fd7e14", info: "#007bff" }} windowSize={200} />
Custom Bar Styling
Control the appearance with fill, stroke, and gap to create distinct visual styles.
JSX
useEffect(() => { const id = setInterval(() => { const i = indexRef.current++ chartRef.current?.push({ time: i, value: Math.floor(Math.abs(Math.sin(i * 0.04)) * 50) + 5 }) }, 30) return () => clearInterval(id) }, []) <RealtimeHistogram ref={chartRef} binSize={20} fill="#28a745" stroke="#1e7e34" gap={2} windowSize={200} />
Brushable Selection
Static Data with Brush
Toggle between static data and streaming push API. In static mode, 100 pre-generated points are rendered with brush="x". The selected time range is displayed below the chart.
JSX
<RealtimeHistogram data={staticData} binSize={10} fill="#007bff" brush="x" onBrush={(extent) => console.log(extent)} />
Streaming Brush with Bin Snapping
In streaming mode, the brush tracks with the data. When selected bins scroll off the left edge, the brush shrinks. When all selected bins are evicted, the brush clears automatically. Usesnap: "bin" to snap to bin boundaries on mouse-up.
Drag on the chart to brush. The selection tracks and shrinks as data scrolls.
JSX
<RealtimeHistogram ref={chartRef} binSize={20} fill="#6f42c1" brush={{ dimension: "x", snap: "bin" }} onBrush={(extent) => setBrushExtent(extent)} />
Cross-Chart Brush Filtering
Wrap both charts in <LinkedCharts>. The histogram writes its brush extent to the "timeRange" selection vialinkedBrush. A child component reads it withuseFilteredData. The base layer always renders all data as a thin faded line; when a brush is active, a bold gradient-filled area for the filtered subset is overlaid on top.
Histogram (brush to filter)
Line Chart (filtered by brush)
JSX
import { LinkedCharts, RealtimeHistogram, LineChart, AreaChart, useFilteredData } from "semiotic" // Consumer component — must be inside <LinkedCharts> function FilteredOverlay({ allData, width }) { const filtered = useFilteredData(allData, "timeRange") const hasBrush = filtered.length < allData.length return ( <div style={{ position: "relative" }}> {/* Base: always-faded thin line, stable across brush on/off */} <LineChart data={allData} xAccessor="time" yAccessor="value" color="#c4cdd8" lineWidth={1} width={width} height={200} frameProps={{ xExtent, yExtent, showAxes: true }} /> {/* Overlay: bold gradient-filled area for brushed range. background: "transparent" keeps the base layer visible. */} {hasBrush && filtered.length > 1 && ( <div style={{ position: "absolute", top: 0, left: 0, pointerEvents: "none" }}> <AreaChart data={filtered} xAccessor="time" yAccessor="value" color="#007bff" gradientFill={{ stops: [ { offset: 0, opacity: 0.8 }, { offset: 1, opacity: 0.05 }, ] }} areaOpacity={0.35} showLine lineWidth={2} width={width} height={200} frameProps={{ xExtent, yExtent, showAxes: false, background: "transparent" }} /> </div> )} </div> ) } // Dashboard <LinkedCharts> <RealtimeHistogram ref={histRef} binSize={20} brush={{ dimension: "x", snap: "bin" }} linkedBrush="timeRange" /> <FilteredOverlay allData={allData} width={600} /> </LinkedCharts>
Stacked Histogram → Multi-Line Filtering
A stacked histogram with three categories (errors,warnings, info) drives a line chart that splits into three colored lines. The base layer always renders all categories with a faded colorScheme; when a brush is active, a bold overlay using the full-opacity scheme is drawn on top. A fixed margin on both charts keeps them aligned.
Stacked Histogram (brush to filter)
Per-Category Lines (filtered by brush)
JSX
import { LinkedCharts, RealtimeHistogram, LineChart, useFilteredData } from "semiotic" const COLORS = { errors: "#dc3545", warnings: "#fd7e14", info: "#007bff" } const SCHEME = Object.values(COLORS) const SCHEME_FADED = [ "rgba(220, 53, 69, 0.25)", "rgba(253, 126, 20, 0.25)", "rgba(0, 123, 255, 0.25)", ] const MARGIN = { top: 10, right: 110, bottom: 40, left: 50 } function FilteredMultiLineOverlay({ allData, width }) { const filtered = useFilteredData(allData, "catBrush") const hasBrush = filtered.length < allData.length return ( <div style={{ position: "relative" }}> {/* Base: always-faded colored lines + axes + legend */} <LineChart data={allData} xAccessor="time" yAccessor="value" lineBy="category" colorBy="category" colorScheme={SCHEME_FADED} lineWidth={1} width={width} height={220} margin={MARGIN} showLegend showGrid frameProps={{ xExtent, yExtent, showAxes: true }} /> {/* Overlay: bold colored lines for brushed range. background: "transparent" keeps the base layer visible. */} {hasBrush && filtered.length > 1 && ( <div style={{ position: "absolute", top: 0, left: 0, pointerEvents: "none" }}> <LineChart data={filtered} xAccessor="time" yAccessor="value" lineBy="category" colorBy="category" colorScheme={SCHEME} lineWidth={3} width={width} height={220} margin={MARGIN} showLegend={false} frameProps={{ xExtent, yExtent, showAxes: false, background: "transparent" }} /> </div> )} </div> ) } <LinkedCharts> <RealtimeHistogram ref={histRef} binSize={20} categoryAccessor="category" colors={COLORS} brush={{ dimension: "x", snap: "bin" }} linkedBrush="catBrush" /> <FilteredMultiLineOverlay allData={allData} width={600} /> </LinkedCharts>
Linked hover and mirrored histograms
Hover a category bar or line to highlight its series. Hover a time bin to highlight the source observations on the line. The two histograms below share the same zero baseline: North grows upward and South grows downward.
Hover a category bar, a line, or a time bin to highlight matching observations.
Use linkedHover={{ name: "detail", mode: "field", fields: ["time", "category"] }}on the histogram and selection={{ name: "detail", unselectedOpacity: 0.12 }}on the line inside LinkedCharts. Set showPointson the line to distinguish matching observations. Bins publish authored source field values, including every time key when several rows share a bin. Fields directly on a bin (binStart, binEnd,total, and stacked category) take precedence. Moving off a mark clears the hover selection.
With selection, a bin also matches a selected time anywhere inside[binStart, binEnd) on its time field (the stringtimeAccessor, else time), even when no row carries that exact time, so hovering a linked line chart highlights the bin it falls in. Selected times may be numbers, Dates, or date strings. To find those bins outside the chart (for example to open a tooltip on the matched bin), pass each bin throughhistogramBinSelectionDatum(timeField) from semiotic/realtimeor semiotic/utils before testing it withuseSelection().predicate.
For application events, onHover receives a hover object whosedata contains those bin fields, or null on exit.onObservation provides the standard hover and hover-end observation events. A field join selects matching rows; usemode: "x-position" with xField for a linked crosshair.
A stacked segment's datum also carries categories: every non-zero category in its bin, bottom to top, as { category, value } entries.getSourceRows(datum) from semiotic/realtime orsemiotic/utils returns the authored rows behind a bin, a segment, or a categories entry, and accepts the hover object as well. Tooltip content, observations, and tap-to-lock linked hover keep those rows.
JSX
import { LinkedCharts } from "semiotic/ai" import { LineChart } from "semiotic/xy" import { TemporalHistogram } from "semiotic/realtime" <LinkedCharts> <LineChart data={rows} xAccessor="time" yAccessor="value" lineBy="category" colorBy="category" showPoints responsiveWidth selection={{ name: "detail", unselectedOpacity: 0.12 }} /> <TemporalHistogram data={rows} binSize={10} responsiveWidth categoryAccessor="category" linkedHover={{ name: "detail", mode: "field", fields: ["time", "category"] }} onHover={hover => console.log(hover?.data)} /> </LinkedCharts>
For a shared time crosshair, use mode: "x-position" andxField: "time" on both charts. Tables can read and set the same position with useLinkedCrosshair("time") fromsemiotic/ai, semiotic/xy, or semiotic/realtime. The hook returns { position, setPosition }; position is{ xValue, sourceId, locked? } | null. Set{ xValue: timestamp, locked: true } to lock from a table, or null to clear. Click and Escape update the same lock.
TSX
import { useState } from "react" import { LinkedCharts, type CrosshairPosition } from "semiotic/ai" import { LineChart } from "semiotic/xy" import { TemporalHistogram } from "semiotic/realtime" function HealthCharts({ rows }: { rows: { time: number; value: number }[] }) { const [position, setPosition] = useState<CrosshairPosition | null>(null) const linkedHover = { name: "time", mode: "x-position" as const, xField: "time" } return ( <LinkedCharts crosshair={{ name: "time", position, onPositionChange: setPosition }}> <LineChart data={rows} xAccessor="time" yAccessor="value" linkedHover={linkedHover} /> <TemporalHistogram data={rows} binSize={10} binAlign="center" linkedHover={linkedHover} /> <HealthTable position={position} onPositionChange={setPosition} /> </LinkedCharts> ) }
Passing position makes the named crosshair controlled:null hides it and undefined uses internal state.onPositionChange reports hover, leave, click, Escape, and table requests; accepting the callback value updates controlled charts. Crosshair names are scoped to their LinkedCharts parent.
For mirrored charts, use identical timeExtent,valueExtent, binSize, and left/right margins. Hide the upper time axis with showTimeAxis={false}; its bottom margin defaults to zero. Set the lower chart's top margin to zero anddirection="down". Keep the value extent ascending on both halves: Semiotic reverses the lower domain. Both halves must have the same plot height (allow extra outer height for the lower time axis).
JSX
const shared = { binSize: 10, timeExtent: [0, 30], valueExtent: [0, 12], responsiveWidth: true, showLegend: false } <> <TemporalHistogram {...shared} data={north} height={100} showTimeAxis={false} margin={{ left: 48, right: 20, top: 0 }} /> <TemporalHistogram {...shared} data={south} height={128} direction="down" margin={{ left: 48, right: 20, top: 0, bottom: 28 }} /> </>
responsiveWidth and responsiveHeight measure the container before browser paint and follow later resizes. A responsive height needs a parent with a definite height. showValueAxis={false}removes the default left margin. Explicit margins remain authoritative. For axis placement and formatting, use the shared XY axesconfig, for example axes={[{ orient: "top" }, { orient: "left", visible: false }]}. This overrides the histogram visibility conveniences. Other XY charts accept the same config through frameProps.axes.
Props
| Prop | Type | Required | Default | Description |
|---|
binAlign | "start" | "center" | — | "start" | Centered bins surround zero-anchored grid timestamps by binSize/2 on each side. Automatic extents include whole edge bins; explicit timeExtent clips them. |
binSize | number | Yes | — | Time interval for binning data points into bars. Points within the same bin are aggregated. |
data | array | — | [] | Controlled data array. Each object should contain fields matched by timeAccessor and valueAccessor. |
timeAccessor | string | function | — | "time" | Field name or function to access the time value from each data point. |
valueAccessor | string | function | — | "value" | Field name or function to access the numeric value from each data point. |
size | [number, number] | — | [500, 300] | Chart dimensions as [width, height]. |
margin | object | — | — | Chart margins: { top, right, bottom, left }. |
arrowOfTime | "left" | "right" | — | "right" | Direction that time flows across the chart. |
windowMode | "sliding" | "growing" | — | "sliding" | Data retention strategy. "sliding" discards old points beyond windowSize. |
windowSize | number | — | 200 | Ring buffer capacity when using sliding window mode. |
timeExtent | [number, number] | — | — | Fixed time domain. Defaults to auto-fit. |
valueExtent | [number, number] | — | — | Fixed value domain. Defaults to auto-fit. |
extentPadding | number | — | — | Padding factor applied to auto-computed extents. |
categoryAccessor | string | function | — | — | Category accessor for stacked bars. When provided, bars are stacked by category within each bin. |
colors | object | — | — | Category-to-color map for stacked bars. Keys also determine stack order. |
fill | string | — | — | Bar fill color, or fallback for categories missing from colors. |
stroke | string | — | — | Bar stroke (outline) color. |
strokeWidth | number | — | — | Bar stroke width. |
cursor | CSS cursor | — | — | Presentation-only cursor for bars, such as "pointer". It does not add click or keyboard behavior and is also inherited by TemporalHistogram. |
gap | number | — | — | Gap between bars in pixels. |
valueBands | Array<{ upTo?: number; fill: string | HatchFill }> | — | — | Split each unstacked bar's fill at value edges. A last band without upTo covers every higher value; values no band covers keep the bar fill. Stacked bins ignore it. |
responsiveWidth | boolean | — | — | Fit the container width before paint and on resize. |
responsiveHeight | boolean | — | — | Fit a parent with a definite height. |
showTimeAxis | boolean | — | true | Hide the time axis with false; removes its default margin. |
showValueAxis | boolean | — | true | Hide the value axis with false; removes its default margin. |
axes | array | — | — | Shared XY axes configuration. Its entries override the visibility conveniences; an orientation it leaves out still follows showTimeAxis / showValueAxis. |
direction | "up" | "down" | — | "up" | Reverse the value domain for mirrored histograms, including push mode. |
linkedHover | boolean | string | object | — | — | Publish a named hover selection using bin fields or authored source-row fields. |
selection | object | — | — | Consume a named selection and dim unmatched bins. |
showAxes | boolean | — | true | Show axis baselines, ticks, and labels. |
showGrid | boolean | — | false | Show grid lines. Set grid: false on the bottom axis for horizontal lines only. |
hoverHighlight | boolean | — | false | Dim other time bins on hover, keeping the whole stacked column highlighted. Also supported by TemporalHistogram; no category accessor is required. |
background | string | — | — | Background fill color for the chart area. |
enableHover | boolean | object | — | — | Enable hover annotations on bars. |
tooltip | boolean | "multi" | object | function | — | — | Tooltip config. "multi" or { mode: "multi", content? } lists every stacked category in the hovered bin. |
tooltipContent | function | — | — | Custom tooltip render function. Receives hover data. |
onHover | function | — | — | Callback fired on hover. Receives hover data or null. |
annotations | array | — | — | Array of annotation objects rendered on the chart. |
svgAnnotationRules | function | — | — | Custom SVG annotation render function. |
tickFormatTime | function | — | — | Custom formatter for time axis tick labels; the default tooltip formats its bin range with it too (epoch-millisecond bins otherwise show a UTC date and time). |
tickFormatValue | function | — | — | Custom formatter for value axis tick labels. |
className | string | — | — | CSS class name for the chart container. |
brush | boolean | "x" | object | — | — | Brush configuration. `true` defaults to `{ dimension: "x", snap: "bin" }`. Object form accepts `dimension` ("x"|"y"|"xy") and `snap` ("continuous"|"bin"). |
onBrush | function | — | — | Callback when brush selection changes. Receives `{ x: [min, max], y: [min, max] }` or `null` when cleared. |
linkedBrush | string | object | — | — | Linked brush for cross-chart coordination via LinkedCharts. String shorthand sets the selection name. |
When to Use the Frame
Use StreamXYFrame directly for custom bin aggregation, mixed chart types, or advanced annotation logic.RealtimeHistogram delegates to a configured StreamXYFrame.
Chart (simple)
JSX
import { RealtimeHistogram } from "semiotic" <RealtimeHistogram ref={chartRef} data={eventStream} binSize={5000} timeAccessor="time" valueAccessor="count" fill="#007bff" gap={2} enableHover />
Frame (full control)
JSX
import { StreamXYFrame } from "semiotic" <StreamXYFrame ref={frameRef} chartType="bar" data={eventStream} binSize={5000} timeAccessor="time" valueAccessor="count" barStyle={{ fill: "#007bff", gap: 2 }} hoverAnnotation={true} showAxes={true} size={[500, 300]} />
The categoryAccessor and colors props on RealtimeHistogram map directly to categoryAccessor andbarColors on StreamXYFrame for stacked bar support.