@mui/x-charts
Version:
The community edition of MUI X Charts components.
237 lines (234 loc) • 8.69 kB
JavaScript
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.useChartVoronoi = void 0;
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
var React = _interopRequireWildcard(require("react"));
var _useEnhancedEffect = _interopRequireDefault(require("@mui/utils/useEnhancedEffect"));
var _useEventCallback = _interopRequireDefault(require("@mui/utils/useEventCallback"));
var _d3Delaunay = require("@mui/x-charts-vendor/d3-delaunay");
var _useScale = require("../../../../hooks/useScale");
var _getSVGPoint = require("../../../getSVGPoint");
var _useSelector = require("../../../store/useSelector");
var _useChartCartesianAxis = require("../useChartCartesianAxis");
var _useChartSeries = require("../../corePlugins/useChartSeries/useChartSeries.selectors");
var _useChartDimensions = require("../../corePlugins/useChartDimensions");
const useChartVoronoi = ({
svgRef,
params,
store,
instance
}) => {
const {
disableVoronoi,
voronoiMaxRadius,
onItemClick
} = params;
const drawingArea = (0, _useSelector.useSelector)(store, _useChartDimensions.selectorChartDrawingArea);
const {
axis: xAxis,
axisIds: xAxisIds
} = (0, _useSelector.useSelector)(store, _useChartCartesianAxis.selectorChartXAxis);
const {
axis: yAxis,
axisIds: yAxisIds
} = (0, _useSelector.useSelector)(store, _useChartCartesianAxis.selectorChartYAxis);
const zoomIsInteracting = (0, _useSelector.useSelector)(store, _useChartCartesianAxis.selectorChartZoomIsInteracting);
const {
series,
seriesOrder
} = (0, _useSelector.useSelector)(store, _useChartSeries.selectorChartSeriesProcessed)?.scatter ?? {};
const voronoiRef = React.useRef({});
const delauneyRef = React.useRef(undefined);
const lastFind = React.useRef(undefined);
const defaultXAxisId = xAxisIds[0];
const defaultYAxisId = yAxisIds[0];
(0, _useEnhancedEffect.default)(() => {
store.update(prev => prev.voronoi.isVoronoiEnabled === !disableVoronoi ? prev : (0, _extends2.default)({}, prev, {
voronoi: {
isVoronoiEnabled: !disableVoronoi
}
}));
}, [store, disableVoronoi]);
(0, _useEnhancedEffect.default)(() => {
// This effect generate and store the Delaunay object that's used to map coordinate to closest point.
if (zoomIsInteracting || seriesOrder === undefined || series === undefined || disableVoronoi) {
// If there is no scatter chart series
return;
}
voronoiRef.current = {};
let points = [];
seriesOrder.forEach(seriesId => {
const {
data,
xAxisId,
yAxisId
} = series[seriesId];
const xScale = xAxis[xAxisId ?? defaultXAxisId].scale;
const yScale = yAxis[yAxisId ?? defaultYAxisId].scale;
const getXPosition = (0, _useScale.getValueToPositionMapper)(xScale);
const getYPosition = (0, _useScale.getValueToPositionMapper)(yScale);
const seriesPoints = data.flatMap(({
x,
y
}) => {
const pointX = getXPosition(x);
const pointY = getYPosition(y);
if (!instance.isPointInside(pointX, pointY)) {
// If the point is not displayed we move them to a trash coordinate.
// This avoids managing index mapping before/after filtering.
// The trash point is far enough such that any point in the drawing area will be closer to the mouse than the trash coordinate.
return [-drawingArea.width, -drawingArea.height];
}
return [pointX, pointY];
});
voronoiRef.current[seriesId] = {
seriesId,
startIndex: points.length,
endIndex: points.length + seriesPoints.length
};
points = points.concat(seriesPoints);
});
delauneyRef.current = new _d3Delaunay.Delaunay(points);
lastFind.current = undefined;
}, [zoomIsInteracting, defaultXAxisId, defaultYAxisId, series, seriesOrder, xAxis, yAxis, drawingArea, instance, disableVoronoi]);
React.useEffect(() => {
if (svgRef.current === null || disableVoronoi) {
return undefined;
}
const element = svgRef.current;
function getClosestPoint(event) {
// Get mouse coordinate in global SVG space
const svgPoint = (0, _getSVGPoint.getSVGPoint)(element, event);
if (!instance.isPointInside(svgPoint.x, svgPoint.y)) {
lastFind.current = undefined;
return 'outside-chart';
}
if (!delauneyRef.current) {
return 'no-point-found';
}
const closestPointIndex = delauneyRef.current.find(svgPoint.x, svgPoint.y, lastFind.current);
if (closestPointIndex === undefined) {
return 'no-point-found';
}
lastFind.current = closestPointIndex;
const closestSeries = Object.values(voronoiRef.current).find(value => {
return 2 * closestPointIndex >= value.startIndex && 2 * closestPointIndex < value.endIndex;
});
if (closestSeries === undefined) {
return 'no-point-found';
}
const dataIndex = (2 * closestPointIndex - voronoiRef.current[closestSeries.seriesId].startIndex) / 2;
if (voronoiMaxRadius !== undefined) {
const pointX = delauneyRef.current.points[2 * closestPointIndex];
const pointY = delauneyRef.current.points[2 * closestPointIndex + 1];
const dist2 = (pointX - svgPoint.x) ** 2 + (pointY - svgPoint.y) ** 2;
if (dist2 > voronoiMaxRadius ** 2) {
// The closest point is too far to be considered.
return 'outside-voronoi-max-radius';
}
}
return {
seriesId: closestSeries.seriesId,
dataIndex
};
}
const handleMouseLeave = () => {
instance.cleanInteraction?.();
instance.clearHighlight?.();
};
const handleMouseMove = event => {
const closestPoint = getClosestPoint(event);
if (closestPoint === 'outside-chart') {
instance.cleanInteraction?.();
instance.clearHighlight?.();
return;
}
if (closestPoint === 'outside-voronoi-max-radius' || closestPoint === 'no-point-found') {
instance.removeItemInteraction?.();
instance.clearHighlight?.();
return;
}
const {
seriesId,
dataIndex
} = closestPoint;
instance.setItemInteraction?.({
type: 'scatter',
seriesId,
dataIndex
});
instance.setHighlight?.({
seriesId,
dataIndex
});
};
const handleMouseClick = event => {
if (!onItemClick) {
return;
}
const closestPoint = getClosestPoint(event);
if (typeof closestPoint === 'string') {
// No point fond for any reason
return;
}
const {
seriesId,
dataIndex
} = closestPoint;
onItemClick(event, {
type: 'scatter',
seriesId,
dataIndex
});
};
element.addEventListener('pointerleave', handleMouseLeave);
element.addEventListener('pointermove', handleMouseMove);
element.addEventListener('click', handleMouseClick);
return () => {
element.removeEventListener('pointerleave', handleMouseLeave);
element.removeEventListener('pointermove', handleMouseMove);
element.removeEventListener('click', handleMouseClick);
};
}, [svgRef, yAxis, xAxis, voronoiMaxRadius, onItemClick, disableVoronoi, drawingArea, instance]);
// Instance implementation
const enableVoronoiCallback = (0, _useEventCallback.default)(() => {
store.update(prev => (0, _extends2.default)({}, prev, {
voronoi: (0, _extends2.default)({}, prev.voronoi, {
isVoronoiEnabled: true
})
}));
});
const disableVoronoiCallback = (0, _useEventCallback.default)(() => {
store.update(prev => (0, _extends2.default)({}, prev, {
voronoi: (0, _extends2.default)({}, prev.voronoi, {
isVoronoiEnabled: false
})
}));
});
return {
instance: {
enableVoronoi: enableVoronoiCallback,
disableVoronoi: disableVoronoiCallback
}
};
};
exports.useChartVoronoi = useChartVoronoi;
useChartVoronoi.getDefaultizedParams = ({
params
}) => (0, _extends2.default)({}, params, {
disableVoronoi: params.disableVoronoi ?? !params.series.some(item => item.type === 'scatter')
});
useChartVoronoi.getInitialState = params => ({
voronoi: {
isVoronoiEnabled: !params.disableVoronoi
}
});
useChartVoronoi.params = {
disableVoronoi: true,
voronoiMaxRadius: true,
onItemClick: true
};
;