UNPKG

@mui/x-charts

Version:

The community edition of MUI X Charts components.

268 lines (260 loc) 9.51 kB
"use strict"; 'use client'; var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default; Object.defineProperty(exports, "__esModule", { value: true }); exports.applyTickSpacing = applyTickSpacing; exports.getTicks = getTicks; exports.useTicks = useTicks; var React = _interopRequireWildcard(require("react")); var _scaleGuards = require("../internals/scaleGuards"); var _isInfinity = require("../internals/isInfinity"); var _timeTicks = require("../utils/timeTicks"); var _dateHelpers = require("../internals/dateHelpers"); var _useChartContext = require("../context/ChartProvider/useChartContext"); const offsetRatio = { start: 0, extremities: 0, end: 1, middle: 0.5 }; function getTickPosition(scale, value, placement) { return scale(value) - (scale.step() - scale.bandwidth()) / 2 + offsetRatio[placement] * scale.step(); } /** * Returns a new domain where each tick is at least {@link tickSpacing}px from the next one. * Assumes tick spacing is greater than 0. * @param domain Domain of the scale. * @param range Range of the scale. * @param tickSpacing Spacing in pixels. */ function applyTickSpacing(domain, range, tickSpacing) { const rangeSpan = Math.abs(range[1] - range[0]); const every = Math.ceil(domain.length / (rangeSpan / tickSpacing)); if (Number.isNaN(every) || every <= 1) { return domain; } return domain.filter((_, index) => index % every === 0); } function getTimeTicks(domain, tickNumber, ticksFrequencies, scale, isInside) { if (ticksFrequencies.length === 0) { return []; } const isReversed = scale.range()[0] > scale.range()[1]; // Indexes are inclusive regarding the entire band. const startIndex = domain.findIndex(value => { return isInside(getTickPosition(scale, value, isReversed ? 'start' : 'end')); }); const endIndex = domain.findLastIndex(value => isInside(getTickPosition(scale, value, isReversed ? 'end' : 'start'))); const start = domain[0]; const end = domain[domain.length - 1]; if (!(start instanceof Date) || !(end instanceof Date)) { return []; } let startFrequencyIndex = 0; for (let i = 0; i < ticksFrequencies.length; i += 1) { if (ticksFrequencies[i].getTickNumber(start, end) !== 0) { startFrequencyIndex = i; break; } } let endFrequencyIndex = startFrequencyIndex; for (let i = startFrequencyIndex; i < ticksFrequencies.length; i += 1) { if (i === ticksFrequencies.length - 1) { // If we reached the end, use the last tick frequency endFrequencyIndex = i; break; } const prevTickCount = ticksFrequencies[i].getTickNumber(start, end); const nextTickCount = ticksFrequencies[i + 1].getTickNumber(start, end); // Smooth ratio between ticks steps: ticksNumber[i]*ticksNumber[i+1] <= targetTickNumber^2 if (nextTickCount > tickNumber || tickNumber / prevTickCount < nextTickCount / tickNumber) { endFrequencyIndex = i; break; } } const ticks = []; for (let tickIndex = Math.max(1, startIndex); tickIndex <= endIndex; tickIndex += 1) { for (let i = startFrequencyIndex; i <= endFrequencyIndex; i += 1) { const prevDate = domain[tickIndex - 1]; const currentDate = domain[tickIndex]; if (prevDate instanceof Date && currentDate instanceof Date && ticksFrequencies[i].isTick(prevDate, currentDate)) { ticks.push({ index: tickIndex, formatter: ticksFrequencies[i].format }); // once we found a matching tick space, we can break the inner loop break; } } } return ticks; } function getTicks(options) { const { scale, tickNumber, valueFormatter, tickInterval, tickPlacement: tickPlacementProp, tickLabelPlacement: tickLabelPlacementProp, tickSpacing, isInside, ordinalTimeTicks } = options; if (ordinalTimeTicks !== undefined && (0, _dateHelpers.isDateData)(scale.domain()) && (0, _scaleGuards.isOrdinalScale)(scale)) { // ordinal scale with spaced ticks. const domain = scale.domain(); if (domain.length === 0 || domain.length === 1) { return []; } const tickPlacement = 'middle'; const ticksIndexes = getTimeTicks(domain, tickNumber, ordinalTimeTicks.map(tickDef => typeof tickDef === 'string' ? _timeTicks.tickFrequencies[tickDef] : tickDef), scale, isInside); return ticksIndexes.map(({ index, formatter }) => { const value = domain[index]; const formattedValue = formatter(value); return { value, formattedValue, offset: getTickPosition(scale, value, tickPlacement), labelOffset: 0 }; }); } const tickPlacement = tickPlacementProp ?? 'extremities'; // Standard ordinal scale: 1 item =1 tick if ((0, _scaleGuards.isOrdinalScale)(scale)) { const domain = scale.domain(); const tickLabelPlacement = tickLabelPlacementProp ?? 'middle'; let filteredDomain = domain; if (typeof tickInterval === 'object' && tickInterval != null) { filteredDomain = tickInterval; } else { if (typeof tickInterval === 'function') { filteredDomain = filteredDomain.filter(tickInterval); } if (tickSpacing !== undefined && tickSpacing > 0) { filteredDomain = applyTickSpacing(filteredDomain, scale.range(), tickSpacing); } } if (filteredDomain.length === 0) { return []; } if (scale.bandwidth() > 0) { // scale type = 'band' const isReversed = scale.range()[0] > scale.range()[1]; // Indexes are inclusive regarding the entire band. const startIndex = filteredDomain.findIndex(value => { return isInside(getTickPosition(scale, value, isReversed ? 'start' : 'end')); }); const endIndex = filteredDomain.findLastIndex(value => isInside(getTickPosition(scale, value, isReversed ? 'end' : 'start'))); return [...filteredDomain.slice(startIndex, endIndex + 1).map(value => { const defaultTickLabel = `${value}`; return { value, formattedValue: valueFormatter?.(value, { location: 'tick', scale, tickNumber, defaultTickLabel }) ?? defaultTickLabel, offset: getTickPosition(scale, value, tickPlacement), labelOffset: tickLabelPlacement === 'tick' ? 0 : scale.step() * (offsetRatio[tickLabelPlacement] - offsetRatio[tickPlacement]) }; }), ...(tickPlacement === 'extremities' && endIndex === domain.length - 1 && isInside(scale.range()[1]) ? [{ formattedValue: undefined, offset: scale.range()[1], labelOffset: 0 }] : [])]; } // scale type = 'point' return filteredDomain.map(value => { const defaultTickLabel = `${value}`; return { value, formattedValue: valueFormatter?.(value, { location: 'tick', scale, tickNumber, defaultTickLabel }) ?? defaultTickLabel, offset: scale(value), labelOffset: 0 }; }); } const domain = scale.domain(); // Skip axis rendering if no data is available // - The domains contains Infinity for continuous scales. if (domain.some(_isInfinity.isInfinity)) { return []; } const tickLabelPlacement = tickLabelPlacementProp; const ticks = typeof tickInterval === 'object' ? tickInterval : getDefaultTicks(scale, tickNumber); // Ticks inside the drawing area const visibleTicks = []; for (let i = 0; i < ticks.length; i += 1) { const value = ticks[i]; const offset = scale(value); if (isInside(offset)) { /* If d3 returns an empty string, it means that a tick should be shown, but its label shouldn't. * This is especially useful in a log scale where we want to show ticks to demonstrate it's a log * scale, but don't want to show labels because they would overlap. * https://github.com/mui/mui-x/issues/18239 */ const defaultTickLabel = scale.tickFormat(tickNumber)(value); visibleTicks.push({ value, formattedValue: valueFormatter?.(value, { location: 'tick', scale, tickNumber, defaultTickLabel }) ?? defaultTickLabel, offset, // Allowing the label to be placed in the middle of a continuous scale is weird. // But it is useful in some cases, like funnel categories with a linear scale. labelOffset: tickLabelPlacement === 'middle' ? scale(ticks[i - 1] ?? 0) - (offset + scale(ticks[i - 1] ?? 0)) / 2 : 0 }); } } return visibleTicks; } function getDefaultTicks(scale, tickNumber) { const domain = scale.domain(); if (domain[0] === domain[1]) { return [domain[0]]; } return scale.ticks(tickNumber); } function useTicks(options) { const { scale, tickNumber, valueFormatter, tickInterval, tickPlacement = 'extremities', tickLabelPlacement, tickSpacing, direction, ordinalTimeTicks } = options; const { instance } = (0, _useChartContext.useChartContext)(); const isInside = direction === 'x' ? instance.isXInside : instance.isYInside; return React.useMemo(() => getTicks({ scale, tickNumber, tickPlacement, tickInterval, tickLabelPlacement, tickSpacing, valueFormatter, isInside, ordinalTimeTicks }), [scale, tickNumber, tickPlacement, tickInterval, tickLabelPlacement, tickSpacing, valueFormatter, isInside, ordinalTimeTicks]); }