@mui/x-charts
Version:
The community edition of MUI X Charts components.
356 lines (340 loc) • 10.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = getItemAtPosition;
var _useChartCartesianAxisRendering = require("../../internals/plugins/featurePlugins/useChartCartesianAxis/useChartCartesianAxisRendering.selectors");
var _seriesSelectorOfType = require("../../internals/seriesSelectorOfType");
var _getAxisValue = require("../../internals/plugins/featurePlugins/useChartCartesianAxis/getAxisValue");
var _scaleGuards = require("../../internals/scaleGuards");
var _getValueToPositionMapper = require("../../hooks/getValueToPositionMapper");
var _curveEvaluation = require("./curveEvaluation");
/**
* For a continuous x-axis, find the two data indices that bracket the pointer's x position.
* For ordinal axes, returns the single matching index (left === right).
* Returns null if the pointer is outside the data range.
*/
function getBracketIndices(xAxis, pointX) {
const {
scale,
data: axisData
} = xAxis;
if (!axisData || axisData.length === 0) {
return null;
}
if ((0, _scaleGuards.isOrdinalScale)(scale)) {
const index = (0, _getAxisValue.getAxisIndex)(xAxis, pointX);
if (index === -1) {
return null;
}
return {
left: index,
right: index
};
}
// For continuous axes, find the two adjacent data points surrounding pointX.
const xValue = scale.invert(pointX);
const xAsNumber = xValue instanceof Date ? xValue.getTime() : xValue;
const getAsNumber = v => v instanceof Date ? v.getTime() : v;
// Find the rightmost index where data[i] <= xValue.
let leftIndex = -1;
for (let i = 0; i < axisData.length; i += 1) {
if (getAsNumber(axisData[i]) <= xAsNumber) {
leftIndex = i;
} else {
break;
}
}
if (leftIndex === -1) {
// Pointer is before the first data point.
return null;
}
if (leftIndex === axisData.length - 1) {
// Pointer is at or after the last data point — check if it's close enough.
return {
left: leftIndex,
right: leftIndex
};
}
return {
left: leftIndex,
right: leftIndex + 1
};
}
/**
* Compute the pixel y0 (baseline) for a given data point,
* replicating the logic from useAreaPlotData.
*/
function getBaselinePixelY(baseline, yScale, stackedY0) {
if (typeof baseline === 'number') {
return yScale(baseline);
}
if (baseline === 'max') {
return yScale.range()[1];
}
if (baseline === 'min') {
return yScale.range()[0];
}
// Default: use the stacked baseline value.
const value = yScale(stackedY0);
if (Number.isNaN(value)) {
return yScale.range()[0];
}
return value;
}
// Collect the pixel-coordinate points for a contiguous (non-null) segment
// of a series that contains the bracket indices.
//
// When connectNulls is true, all non-null points are returned.
// When connectNulls is false, only the contiguous run containing [left, right] is returned.
function collectCurvePoints(data, getPixelX, getPixelY, left, right, connectNulls) {
const points = [];
if (connectNulls) {
// All non-null points form one continuous curve.
for (let i = 0; i < data.length; i += 1) {
if (data[i] != null) {
const y = getPixelY(i);
if (y != null && !Number.isNaN(y)) {
points.push({
x: getPixelX(i),
y
});
}
}
}
return points;
}
// Find the contiguous non-null run containing [left, right].
let start = left;
while (start > 0 && data[start - 1] != null) {
start -= 1;
}
let end = right;
while (end < data.length - 1 && data[end + 1] != null) {
end += 1;
}
for (let i = start; i <= end; i += 1) {
const y = getPixelY(i);
if (y != null && !Number.isNaN(y)) {
points.push({
x: getPixelX(i),
y
});
}
}
return points;
}
/**
* The maximum pixel distance from a line curve at which the line is still
* considered "close enough" to be selected over an area.
*/
const LINE_PROXIMITY_THRESHOLD = 15;
function getItemAtPosition(state, point) {
if (!state.experimentalFeatures?.enablePositionBasedPointerInteraction) {
return undefined;
}
const {
axis: xAxes,
axisIds: xAxisIds
} = (0, _useChartCartesianAxisRendering.selectorChartXAxis)(state);
const {
axis: yAxes,
axisIds: yAxisIds
} = (0, _useChartCartesianAxisRendering.selectorChartYAxis)(state);
const series = (0, _seriesSelectorOfType.selectorAllSeriesOfType)(state, 'line');
if (!series || series.seriesOrder.length === 0) {
return undefined;
}
const defaultXAxisId = xAxisIds[0];
const defaultYAxisId = yAxisIds[0];
// Step 1: Find the closest line (curve) across all series.
let closestDistance = Infinity;
let closestItem;
for (const seriesId of series.seriesOrder) {
const seriesItem = series.series[seriesId];
if (seriesItem.hidden) {
continue;
}
const xAxisId = seriesItem.xAxisId ?? defaultXAxisId;
const yAxisId = seriesItem.yAxisId ?? defaultYAxisId;
const xAxis = xAxes[xAxisId];
const yAxis = yAxes[yAxisId];
const bracket = getBracketIndices(xAxis, point.x);
if (!bracket) {
continue;
}
const {
left,
right
} = bracket;
const {
visibleStackedData,
data,
connectNulls,
curve
} = seriesItem;
const dataIndex = (0, _getAxisValue.getAxisIndex)(xAxis, point.x);
if (dataIndex === -1) {
continue;
}
// For ordinal or pointer exactly on a data point, use the data point directly.
if (left === right) {
const yValue = visibleStackedData[left]?.[1];
if (yValue == null) {
continue;
}
const yPosition = yAxis.scale(yValue);
if (yPosition == null) {
continue;
}
const distance = Math.abs(point.y - yPosition);
if (distance < closestDistance) {
closestDistance = distance;
closestItem = {
type: 'line',
seriesId,
dataIndex
};
}
continue;
}
// Evaluate the actual curve at the pointer's x for precise distance.
const xData = xAxis.data;
if (!xData) {
continue;
}
const xPosition = (0, _getValueToPositionMapper.getValueToPositionMapper)(xAxis.scale);
const getPixelX = idx => xPosition(xData[idx]);
const curvePoints = collectCurvePoints(data, getPixelX, idx => {
const stacked = visibleStackedData[idx];
return stacked ? yAxis.scale(stacked[1]) : null;
}, left, right, connectNulls);
if (curvePoints.length < 2) {
continue;
}
const yPosition = (0, _curveEvaluation.evaluateCurveY)(curvePoints, point.x, curve);
if (yPosition == null) {
continue;
}
const distance = Math.abs(point.y - yPosition);
if (distance < closestDistance) {
closestDistance = distance;
closestItem = {
type: 'line',
seriesId,
dataIndex
};
}
}
// Step 2: If the closest line is within the proximity threshold, pick it.
if (closestItem && closestDistance <= LINE_PROXIMITY_THRESHOLD) {
return closestItem;
}
// Step 3: Check area series — iterate stacking groups in reverse
// so that topmost (last rendered) area is checked first.
const {
stackingGroups
} = series;
for (let g = stackingGroups.length - 1; g >= 0; g -= 1) {
const groupIds = stackingGroups[g].ids;
// Iterate in reverse so the topmost stacked area is checked first.
for (let i = groupIds.length - 1; i >= 0; i -= 1) {
const seriesId = groupIds[i];
const seriesItem = series.series[seriesId];
if (seriesItem.hidden || !seriesItem.area) {
continue;
}
const xAxisId = seriesItem.xAxisId ?? defaultXAxisId;
const yAxisId = seriesItem.yAxisId ?? defaultYAxisId;
const xAxis = xAxes[xAxisId];
const yAxis = yAxes[yAxisId];
if (!xAxis || !yAxis) {
continue;
}
const bracket = getBracketIndices(xAxis, point.x);
if (!bracket) {
continue;
}
const {
left,
right
} = bracket;
const {
visibleStackedData,
data,
connectNulls,
baseline,
curve
} = seriesItem;
// Check for null gaps at bracket points.
const leftIsNull = data[left] == null;
const rightIsNull = data[right] == null;
if (leftIsNull && rightIsNull) {
continue;
}
if ((leftIsNull || rightIsNull) && !connectNulls) {
continue;
}
const xScale = xAxis.scale;
const yScale = yAxis.scale;
const xPosition = (0, _getValueToPositionMapper.getValueToPositionMapper)(xScale);
const xData = xAxis.data;
if (!xData) {
continue;
}
const getPixelX = idx => xPosition(xData[idx]);
if (left === right) {
// Ordinal axis or pointer exactly on a data point.
const stacked = visibleStackedData[left];
if (!stacked) {
continue;
}
const yBottom = getBaselinePixelY(baseline, yScale, stacked[0]);
const yTop = yScale(stacked[1]);
if ([yBottom, yTop].some(v => v == null || Number.isNaN(v))) {
continue;
}
const yMin = Math.min(yBottom, yTop);
const yMax = Math.max(yBottom, yTop);
if (point.y >= yMin && point.y <= yMax) {
return {
type: 'line',
seriesId,
dataIndex: left
};
}
continue;
}
// Build pixel-coordinate points for the top and bottom curves,
// then evaluate them at the pointer's x using the actual d3 curve.
const topPoints = collectCurvePoints(data, getPixelX, idx => {
const stacked = visibleStackedData[idx];
return stacked ? yScale(stacked[1]) : null;
}, left, right, connectNulls);
const bottomPoints = collectCurvePoints(data, getPixelX, idx => {
const stacked = visibleStackedData[idx];
return stacked ? getBaselinePixelY(baseline, yScale, stacked[0]) : null;
}, left, right, connectNulls);
if (topPoints.length < 2 || bottomPoints.length < 2) {
continue;
}
const yTop = (0, _curveEvaluation.evaluateCurveY)(topPoints, point.x, curve);
const yBottom = (0, _curveEvaluation.evaluateCurveY)(bottomPoints, point.x, curve);
if (yTop == null || yBottom == null) {
continue;
}
const yMin = Math.min(yBottom, yTop);
const yMax = Math.max(yBottom, yTop);
if (point.y >= yMin && point.y <= yMax) {
const dataIndex = (0, _getAxisValue.getAxisIndex)(xAxis, point.x);
return {
type: 'line',
seriesId,
dataIndex: dataIndex === -1 ? left : dataIndex
};
}
}
}
// Step 4: No area matched — return the closest line regardless of threshold.
return closestItem;
}