@mui/x-charts
Version:
The community edition of MUI X Charts components.
268 lines (260 loc) • 9.51 kB
JavaScript
;
'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]);
}