Загрузка данных
import type {
LaidOutPriceAxisLabel,
MeasuredPriceAxisLabel,
PriceAxisLabelsLayoutOptions,
} from './types';
const DEFAULT_GAP = 2;
function compareLabels(left: MeasuredPriceAxisLabel, right: MeasuredPriceAxisLabel): number {
if (left.desiredCoordinate !== right.desiredCoordinate) {
return left.desiredCoordinate - right.desiredCoordinate;
}
if (left.priority !== right.priority) {
return right.priority - left.priority;
}
return left.id.localeCompare(right.id);
}
function getCoordinate(
desiredCoordinate: number,
height: number,
minCoordinate: number,
maxCoordinate: number,
): number {
const minimum = minCoordinate + height / 2;
const maximum = maxCoordinate - height / 2;
if (maximum < minimum) {
return (minCoordinate + maxCoordinate) / 2;
}
return Math.min(Math.max(desiredCoordinate, minimum), maximum);
}
function distributeLabels(
labels: readonly MeasuredPriceAxisLabel[],
minCoordinate: number,
maxCoordinate: number,
): LaidOutPriceAxisLabel[] {
if (labels.length === 0) {
return [];
}
if (labels.length === 1) {
const label = labels[0];
return [
{
...label,
coordinate: getCoordinate(label.desiredCoordinate, label.height, minCoordinate, maxCoordinate),
},
];
}
const firstCoordinate = minCoordinate + labels[0].height / 2;
const lastCoordinate = maxCoordinate - labels[labels.length - 1].height / 2;
if (lastCoordinate <= firstCoordinate) {
const coordinate = (minCoordinate + maxCoordinate) / 2;
return labels.map((label) => ({
...label,
coordinate,
}));
}
const step = (lastCoordinate - firstCoordinate) / (labels.length - 1);
return labels.map((label, index) => ({
...label,
coordinate: firstCoordinate + step * index,
}));
}
function layoutRange(
labels: readonly MeasuredPriceAxisLabel[],
minCoordinate: number,
maxCoordinate: number,
gap: number,
): LaidOutPriceAxisLabel[] {
const sortedLabels = [...labels].sort(compareLabels);
if (sortedLabels.length === 0) {
return [];
}
const availableHeight = Math.max(0, maxCoordinate - minCoordinate);
const labelsHeight = sortedLabels.reduce((total, label) => total + label.height, 0);
const requiredHeight = labelsHeight + gap * Math.max(0, sortedLabels.length - 1);
if (requiredHeight > availableHeight) {
return distributeLabels(sortedLabels, minCoordinate, maxCoordinate);
}
const result = sortedLabels.map((label) => ({
...label,
coordinate: getCoordinate(label.desiredCoordinate, label.height, minCoordinate, maxCoordinate),
}));
for (let index = 1; index < result.length; index += 1) {
const previousLabel = result[index - 1];
const currentLabel = result[index];
const minimumCoordinate =
previousLabel.coordinate + previousLabel.height / 2 + currentLabel.height / 2 + gap;
currentLabel.coordinate = Math.max(currentLabel.coordinate, minimumCoordinate);
}
const lastLabel = result[result.length - 1];
const maximumLastCoordinate = maxCoordinate - lastLabel.height / 2;
if (lastLabel.coordinate > maximumLastCoordinate) {
const offset = lastLabel.coordinate - maximumLastCoordinate;
result.forEach((label) => {
label.coordinate -= offset;
});
}
for (let index = result.length - 2; index >= 0; index -= 1) {
const currentLabel = result[index];
const nextLabel = result[index + 1];
const maximumCoordinate =
nextLabel.coordinate - nextLabel.height / 2 - currentLabel.height / 2 - gap;
currentLabel.coordinate = Math.min(currentLabel.coordinate, maximumCoordinate);
}
const firstLabel = result[0];
const minimumFirstCoordinate = minCoordinate + firstLabel.height / 2;
if (firstLabel.coordinate < minimumFirstCoordinate) {
const offset = minimumFirstCoordinate - firstLabel.coordinate;
result.forEach((label) => {
label.coordinate += offset;
});
}
return result;
}
export function layoutPriceAxisLabels(
labels: readonly MeasuredPriceAxisLabel[],
axisHeight: number,
options: PriceAxisLabelsLayoutOptions = {},
): LaidOutPriceAxisLabel[] {
if (axisHeight <= 0 || labels.length === 0) {
return [];
}
const gap = options.gap ?? DEFAULT_GAP;
const labelsWithValidCoordinate = labels.filter((label) =>
Number.isFinite(label.desiredCoordinate),
);
if (options.reservedCoordinate === undefined || options.reservedHeight === undefined) {
return layoutRange(labelsWithValidCoordinate, 0, axisHeight, gap);
}
const reservedCoordinate = Math.min(Math.max(options.reservedCoordinate, 0), axisHeight);
const reservedHeight = Math.max(0, options.reservedHeight);
const reservedTop = Math.max(0, reservedCoordinate - reservedHeight / 2);
const reservedBottom = Math.min(axisHeight, reservedCoordinate + reservedHeight / 2);
const labelsAbove = labelsWithValidCoordinate.filter(
(label) => label.desiredCoordinate <= reservedCoordinate,
);
const labelsBelow = labelsWithValidCoordinate.filter(
(label) => label.desiredCoordinate > reservedCoordinate,
);
const result = [
...layoutRange(labelsAbove, 0, Math.max(0, reservedTop - gap), gap),
...layoutRange(labelsBelow, Math.min(axisHeight, reservedBottom + gap), axisHeight, gap),
];
return result.sort((left, right) => {
if (left.priority !== right.priority) {
return left.priority - right.priority;
}
return left.id.localeCompare(right.id);
});
}