in packages/charts/src/chart_types/heatmap/state/utils/axis.ts [77:211]
export function getXAxisSize(
isCategoricalScale: boolean,
style: HeatmapStyle['xAxisLabel'],
formatter: HeatmapSpec['xAxisLabelFormatter'],
labels: (string | number)[],
textMeasure: TextMeasure,
containerWidth: number,
surroundingSpace: [left: number, right: number],
): Size & { right: number; left: number; tickCadence: number; minRotation: Radian } {
if (!style.visible) {
return {
height: 0,
width: Math.max(containerWidth - surroundingSpace[0] - surroundingSpace[1], 0),
left: surroundingSpace[0],
right: surroundingSpace[1],
tickCadence: NaN,
minRotation: 0,
};
}
const isRotated = style.rotation !== 0;
const normalizedScale = scaleBand<NonNullable<PrimitiveValue>>().domain(labels).range([0, 1]);
const alignment = isRotated ? 'right' : isCategoricalScale ? 'center' : 'left';
const alignmentOffset = isCategoricalScale ? normalizedScale.bandwidth() / 2 : 0;
const scale = (d: NonNullable<PrimitiveValue>) => (normalizedScale(d) ?? 0) + alignmentOffset;
// use positive angle from 0 to 90 only
const rotationRad = degToRad(style.rotation);
const measuredLabels = labels.map((label) => ({
...textMeasure(formatter(label), style, style.fontSize),
label,
}));
// don't filter ticks if categorical scale or with rotated labels
if (isCategoricalScale || isRotated) {
const maxLabelBBox = measuredLabels.reduce(
(acc, curr) => {
return {
height: Math.max(acc.height, curr.height),
width: Math.max(acc.width, curr.width),
};
},
{ height: 0, width: 0 },
);
const compressedScale = computeCompressedScale(
style,
scale,
measuredLabels,
containerWidth,
surroundingSpace,
alignment,
rotationRad,
);
const scaleStep = compressedScale.width / labels.length;
// this optimal rotation is computed on a suboptimal compressed scale, it can be further enhanced with a monotonic hill climber
const optimalRotation =
scaleStep > maxLabelBBox.width ? 0 : Math.asin(Math.min(maxLabelBBox.height / scaleStep, 1));
// if the current requested rotation is not at least bigger then the optimal one, recalculate the compression
// using the optimal one forcing the rotation to be without overlaps
const { width, height, left, right, minRotation } = {
...(rotationRad !== 0 && optimalRotation > rotationRad
? computeCompressedScale(
style,
scale,
measuredLabels,
containerWidth,
surroundingSpace,
alignment,
optimalRotation,
)
: compressedScale),
minRotation: isRotated ? Math.max(optimalRotation, rotationRad) : 0,
};
const validCompression = isFiniteNumber(width);
return {
height: validCompression ? height : 0,
width: validCompression ? width : Math.max(containerWidth - surroundingSpace[0] - surroundingSpace[1], 0),
left: validCompression ? left : surroundingSpace[0],
right: validCompression ? right : surroundingSpace[1],
tickCadence: validCompression ? 1 : NaN,
minRotation,
};
}
// TODO refactor and move to monotonic hill climber and no mutations
// reduce the tick cadence on time scale to avoid overlaps and overflows
let tickCadence = 1;
let dimension = computeCompressedScale(
style,
scale,
measuredLabels,
containerWidth,
surroundingSpace,
alignment,
rotationRad,
);
for (let i = 1; i < measuredLabels.length; i++) {
if ((!dimension.overlaps && !dimension.overflow.right) || !isFiniteNumber(dimension.width)) {
break;
}
dimension = computeCompressedScale(
style,
scale,
measuredLabels.filter((_, index) => index % (i + 1) === 0),
containerWidth,
surroundingSpace,
alignment,
rotationRad,
);
tickCadence++;
}
// hide the axis because there is no space for labels
if (!isFiniteNumber(dimension.width)) {
return {
// hide the whole axis
height: 0,
width: Math.max(containerWidth - surroundingSpace[0] - surroundingSpace[1], 0),
left: surroundingSpace[0],
right: surroundingSpace[1],
// hide all ticks
tickCadence: NaN,
minRotation: rotationRad,
};
}
return {
...dimension,
tickCadence,
minRotation: rotationRad,
};
}