Measuring a Region
The measure drawing tool reports what happened over the region it spans, not just the distance between its two anchors. Drag one on the chart, or create it in code:
const { id, stats } = chart.measureRange('2024-01-01', '2024-03-01')
stats.change.percent
stats.volatility.annualized
stats.drawdown.max
A measurement is a measure drawing. It renders, it can be selected and dragged, it stays anchored to its bars through zoom and pan, and it round-trips through getState()/setState() with the other drawings. There is no separate state key for measurements.
The API
| Method | Returns |
|---|---|
measureRange(from, to, opts?) | { id, stats }, or null |
getMeasurements() | [{ id, from, to, selection, stats }] |
getMeasurement(id) | One of the above |
clearMeasurement(id?) | The number removed. Omit id to clear all |
clearMeasurement() never touches other drawing types, so clearing every measurement leaves your trend lines and zones alone.
opts takes the drawing's own style (color, upColor, downColor, fillOpacity, showLabel, locked, meta) plus by, for how a bare number endpoint is read. Statistics conventions are chart-level, in analysis, and deliberately not per-measurement: a restored measurement cannot then disagree with the chart it was restored into.
Two changes, deliberately
A measurement reports its change twice under different names, because they answer different questions:
| Field | Is |
|---|---|
stats.change | The instrument's close-to-close move over the spanned bars. Every other statistic is consistent with this one |
selection | The delta between the two anchors the user dragged. This is what the box's height shows |
They agree when the anchors sit on the closes. They diverge when someone drags from a low to a high, which is the right number for a swing and the wrong number for a return. Both are reported so neither reading has to be inferred.
To pull hand-drawn anchors onto real bar values:
analysis: { measure: { snap: 'close' } } // true, or 'open' | 'high' | 'low' | 'close'
Reshaping a measurement
Selecting a measure box puts a drag handle on each of its two anchors. Dragging one changes what the box covers from that end and the statistics recompute over the bars it now spans. Dragging the body translates it with the span intact, which is deliberate: that is how a measured move gets projected onto a later breakout.
A drawing with locked: true shows no handles. The same handles are on every two-anchor drawing, so trend lines, rays and fibs reshape the same way.
The analysis panel
An on-chart readout of the region's statistics. It is automatic by default: it appears while a measurement exists and hides when the last one is cleared.
chart.showAnalysisPanel({ position: 'top-right' })
chart.hideAnalysisPanel()
chart.isAnalysisPanelVisible()
Configure it at construction:
analysis: {
panel: {
position: 'top-right', // or top-left, bottom-left, bottom-right
metrics: ['change', 'duration', 'high', 'low', 'volatility', 'drawdown'],
formatters: { price: (v) => v.toFixed(2), percent: (v) => `${v.toFixed(2)}%` },
title: 'Selection',
placeholder: 'Drag the measure tool across the chart',
},
}
Available metrics: change, selection, duration, high, low, average, volume, volatility, annualized, drawdown, recovery. Every row carries a data-metric="<key>" attribute, so a specific row can be restyled without depending on its position.
For headless use, compute the numbers yourself and render your own UI:
analysis: { panel: false }
Replacing the on-chart label
The label drawn on the measure box comes from the same statistics engine. Replace it wholesale:
analysis: {
measure: {
label: (stats, { selection, drawing }) =>
[`${stats.change.percent.toFixed(2)}%`, `${stats.bars} bars`],
},
}
Return a string or an array of strings, one per line.
Events
chart.on('rangeMeasured', ({ id, from, to, selection, stats, source }) => {
// source: 'drag' | 'api' | 'coreRuler'
})
chart.on('measurementRemoved', ({ id }) => {})
rangeMeasured fires once per settled change. One drag produces one event, not one per frame, and a plain zoom or pan produces none. For per-frame updates during a gesture, see rangeChanging.
Interop with the ApexCharts measure ruler
ApexCharts has its own measure ruler, as an opt-in bundle. If you load it, ApexStock cooperates rather than competing:
import 'apexcharts/features/measure'
const chart = new ApexStock(el, {
chart: { measure: { enabled: true } },
// ...
})
ApexStock supplies chart.measure.label from the same statistics engine and re-emits the core measured event as rangeMeasured with source: 'coreRuler', so both gestures produce one consistent readout. id is null for a core-ruler reading.
ApexStock keeps its own measure drawing as the primary gesture. The core ruler's pins live on the chart instance rather than in ApexStock's state, so they do not serialize, and its numbers are geometric only.
See also
- Range Statistics and Drawdown for the same numbers without a drawing
- Drawing Tools for the rest of the drawing layer
- State Persistence for how measurements are saved