UNPKG

@mui/x-charts

Version:

The community edition of MUI X Charts components.

221 lines (210 loc) 9.18 kB
'use client'; import _extends from "@babel/runtime/helpers/esm/extends"; import * as React from 'react'; import { warnOnce } from '@mui/x-internals/warning'; import { selectorChartDrawingArea } from "../../corePlugins/useChartDimensions/useChartDimensions.selectors.mjs"; import { defaultizeAxis } from "./defaultizeAxis.mjs"; import { selectorChartsInteractionIsInitialized } from "../useChartInteraction/index.mjs"; import { selectorChartPolarCenter, selectorChartRadiusAxis, selectorChartRotationAxis } from "./useChartPolarAxis.selectors.mjs"; import { getChartPoint } from "../../../getChartPoint.mjs"; import { generatePolar2svg, generateSvg2polar, generateSvg2rotation } from "./coordinateTransformation.mjs"; import { getAxisIndex } from "./getAxisIndex.mjs"; import { selectorChartSeriesProcessed } from "../../corePlugins/useChartSeries/index.mjs"; import { checkHasInteractionPlugin } from "../useChartInteraction/checkHasInteractionPlugin.mjs"; export const useChartPolarAxis = ({ params, store, instance }) => { const { chartsLayerContainerRef } = instance; const { rotationAxis, radiusAxis, dataset } = params; if (process.env.NODE_ENV !== 'production') { const ids = [...(rotationAxis ?? []), ...(radiusAxis ?? [])].filter(axis => axis.id).map(axis => axis.id); const duplicates = new Set(ids.filter((id, index) => ids.indexOf(id) !== index)); if (duplicates.size > 0) { warnOnce([`MUI X Charts: The following axis ids are duplicated: ${Array.from(duplicates).join(', ')}.`, `Please make sure that each axis has a unique id.`].join('\n'), 'error'); } } const drawingArea = store.use(selectorChartDrawingArea); const processedSeries = store.use(selectorChartSeriesProcessed); const center = store.use(selectorChartPolarCenter); const isInteractionEnabled = store.use(selectorChartsInteractionIsInitialized); const { axis: rotationAxisWithScale, axisIds: rotationAxisIds } = store.use(selectorChartRotationAxis); const { axis: radiusAxisWithScale, axisIds: radiusAxisIds } = store.use(selectorChartRadiusAxis); // The effect do not track any value defined synchronously during the 1st render by hooks called after `useChartPolarAxis` // As a consequence, the state generated by the 1st run of this useEffect will always be equal to the initialization one const isFirstRender = React.useRef(true); React.useEffect(() => { if (isFirstRender.current) { isFirstRender.current = false; return; } store.set('polarAxis', _extends({}, store.state.polarAxis, { rotation: defaultizeAxis(rotationAxis, dataset, 'rotation'), radius: defaultizeAxis(radiusAxis, dataset, 'radius') })); }, [drawingArea, rotationAxis, radiusAxis, dataset, store]); const svg2rotation = React.useMemo(() => generateSvg2rotation({ cx: center.cx, cy: center.cy }), [center.cx, center.cy]); const svg2polar = React.useMemo(() => generateSvg2polar({ cx: center.cx, cy: center.cy }), [center.cx, center.cy]); const polar2svg = React.useMemo(() => generatePolar2svg({ cx: center.cx, cy: center.cy }), [center.cx, center.cy]); const usedRotationAxisId = rotationAxisIds[0]; const usedRadiusAxisId = radiusAxisIds[0]; // Use a ref to avoid rerendering on every mousemove event. const mousePosition = React.useRef({ isInChart: false }); const hasInteractionPlugin = checkHasInteractionPlugin(instance); React.useEffect(() => { const element = chartsLayerContainerRef.current; if (!isInteractionEnabled || !hasInteractionPlugin || element === null || params.disableAxisListener) { return () => {}; } // Clean the interaction when the mouse leaves the chart. const moveEndHandler = instance.addInteractionListener('moveEnd', event => { if (!event.detail.activeGestures.pan) { mousePosition.current.isInChart = false; instance.cleanInteraction(); } }); const panEndHandler = instance.addInteractionListener('panEnd', event => { if (!event.detail.activeGestures.move) { mousePosition.current.isInChart = false; instance.cleanInteraction?.(); } }); const pressEndHandler = instance.addInteractionListener('quickPressEnd', event => { if (!event.detail.activeGestures.move && !event.detail.activeGestures.pan) { mousePosition.current.isInChart = false; instance.cleanInteraction?.(); } }); const gestureHandler = event => { const srcEvent = event.detail.srcEvent; // On touch, we want to allow user to interact with the entire svg area in // order to better display the tooltip. if (event.detail.srcEvent.pointerType === 'touch') { const svgRect = element.getBoundingClientRect(); if (srcEvent.clientX < svgRect.left || srcEvent.clientX > svgRect.right || srcEvent.clientY < svgRect.top || srcEvent.clientY > svgRect.bottom) { mousePosition.current.isInChart = false; instance.cleanInteraction?.(); return; } const svgPoint = getChartPoint(element, srcEvent); mousePosition.current.isInChart = true; instance.setPointerCoordinate?.(svgPoint); return; } // On mouse, we want to restrict the interaction to the drawing area and radar circle. const svgPoint = getChartPoint(element, srcEvent); // Test if it's in the drawing area if (!instance.isPointInside(svgPoint.x, svgPoint.y, event.detail.target)) { if (mousePosition.current.isInChart) { instance.cleanInteraction?.(); mousePosition.current.isInChart = false; } return; } // Test if it's in the radar circle const radiusSquare = (center.cx - svgPoint.x) ** 2 + (center.cy - svgPoint.y) ** 2; const maxRadius = radiusAxisWithScale[usedRadiusAxisId].scale.range()[1]; if (radiusSquare > maxRadius ** 2) { if (mousePosition.current.isInChart) { instance.cleanInteraction?.(); mousePosition.current.isInChart = false; } return; } mousePosition.current.isInChart = true; instance.setPointerCoordinate?.(svgPoint); }; const moveHandler = instance.addInteractionListener('move', gestureHandler); const panHandler = instance.addInteractionListener('pan', gestureHandler); const pressHandler = instance.addInteractionListener('quickPress', gestureHandler); return () => { moveHandler.cleanup(); moveEndHandler.cleanup(); panHandler.cleanup(); panEndHandler.cleanup(); pressHandler.cleanup(); pressEndHandler.cleanup(); }; }, [chartsLayerContainerRef, store, center, radiusAxisWithScale, usedRadiusAxisId, rotationAxisWithScale, usedRotationAxisId, instance, params.disableAxisListener, isInteractionEnabled, svg2rotation, hasInteractionPlugin]); React.useEffect(() => { const element = chartsLayerContainerRef.current; const onAxisClick = params.onAxisClick; if (element === null || !onAxisClick) { return () => {}; } const axisClickHandler = instance.addInteractionListener('tap', event => { let dataIndex = null; let isRotationAxis = false; const svgPoint = getChartPoint(element, event.detail.srcEvent); const rotation = generateSvg2rotation(center)(svgPoint.x, svgPoint.y); const rotationIndex = getAxisIndex(rotationAxisWithScale[usedRotationAxisId], rotation); isRotationAxis = rotationIndex !== -1; dataIndex = isRotationAxis ? rotationIndex : null; // radius index is not yet implemented. const USED_AXIS_ID = isRotationAxis ? usedRotationAxisId : usedRadiusAxisId; if (dataIndex == null || dataIndex === -1) { return; } // The .data exist because otherwise the dataIndex would be null or -1. const axisValue = (isRotationAxis ? rotationAxisWithScale : radiusAxisWithScale)[USED_AXIS_ID].data[dataIndex]; const seriesValues = {}; Object.keys(processedSeries).filter(seriesType => seriesType === 'radar').forEach(seriesType => { processedSeries[seriesType]?.seriesOrder.forEach(seriesId => { const seriesItem = processedSeries[seriesType].series[seriesId]; seriesValues[seriesId] = seriesItem.data[dataIndex]; }); }); onAxisClick(event.detail.srcEvent, { dataIndex, axisValue, seriesValues }); }); return () => { axisClickHandler.cleanup(); }; }, [center, instance, params.onAxisClick, processedSeries, radiusAxisWithScale, rotationAxisWithScale, chartsLayerContainerRef, usedRadiusAxisId, usedRotationAxisId]); return { instance: { svg2polar, svg2rotation, polar2svg } }; }; useChartPolarAxis.params = { rotationAxis: true, radiusAxis: true, dataset: true, disableAxisListener: true, onAxisClick: true }; useChartPolarAxis.getInitialState = params => ({ polarAxis: { rotation: defaultizeAxis(params.rotationAxis, params.dataset, 'rotation'), radius: defaultizeAxis(params.radiusAxis, params.dataset, 'radius') } });