Загрузка данных
import type {
LaidOutPriceAxisLabel,
MeasuredPriceAxisLabel,
PriceAxisLabelsLayoutOptions,
} from './types';
type StackAlignment = 'start' | 'center' | 'end';
const DEFAULT_GAP = 3;
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 createLabel(
label: MeasuredPriceAxisLabel,
coordinate: number,
): LaidOutPriceAxisLabel {
return {
...label,
coordinate,
};
}
function getStackHeight(
labels: readonly MeasuredPriceAxisLabel[],
gap: number,
): number {
const labelsHeight = labels.reduce(
(height, label) => height + label.height,
0,
);
return labelsHeight + gap * Math.max(0, labels.length - 1);
}
function placeStack(
labels: readonly MeasuredPriceAxisLabel[],
top: number,
gap: number,
): LaidOutPriceAxisLabel[] {
let currentTop = top;
return labels.map((label) => {
const coordinate = currentTop + label.height / 2;
currentTop += label.height + gap;
return createLabel(label, coordinate);
});
}
function placeOverflowingStack(
labels: readonly MeasuredPriceAxisLabel[],
minCoordinate: number,
maxCoordinate: number,
gap: number,
alignment: StackAlignment,
): LaidOutPriceAxisLabel[] {
const stackHeight = getStackHeight(labels, gap);
const availableHeight = Math.max(0, maxCoordinate - minCoordinate);
let top = minCoordinate;
if (alignment === 'end') {
top = maxCoordinate - stackHeight;
}
if (alignment === 'center') {
top = minCoordinate + (availableHeight - stackHeight) / 2;
}
return placeStack(labels, top, gap);
}
function layoutRange(
labels: readonly MeasuredPriceAxisLabel[],
minCoordinate: number,
maxCoordinate: number,
gap: number,
overflowAlignment: StackAlignment,
): LaidOutPriceAxisLabel[] {
const sortedLabels = [...labels].sort(compareLabels);
if (sortedLabels.length === 0) {
return [];
}
const availableHeight = Math.max(0, maxCoordinate - minCoordinate);
const stackHeight = getStackHeight(sortedLabels, gap);
if (stackHeight > availableHeight) {
return placeOverflowingStack(
sortedLabels,
minCoordinate,
maxCoordinate,
gap,
overflowAlignment,
);
}
const result = sortedLabels.map((label) => {
const minimum = minCoordinate + label.height / 2;
const maximum = maxCoordinate - label.height / 2;
const coordinate = Math.min(
Math.max(label.desiredCoordinate, minimum),
maximum,
);
return createLabel(label, coordinate);
});
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 validLabels = labels.filter((label) =>
Number.isFinite(label.desiredCoordinate),
);
if (
options.reservedCoordinate === undefined ||
options.reservedHeight === undefined
) {
return layoutRange(
validLabels,
0,
axisHeight,
gap,
'center',
);
}
const reservedCoordinate = Math.min(
Math.max(options.reservedCoordinate, 0),
axisHeight,
);
const reservedHeight = Math.max(
0,
options.reservedHeight,
);
const reservedTop =
reservedCoordinate - reservedHeight / 2;
const reservedBottom =
reservedCoordinate + reservedHeight / 2;
const labelsAbove = validLabels.filter(
(label) =>
label.desiredCoordinate <= reservedCoordinate,
);
const labelsBelow = validLabels.filter(
(label) =>
label.desiredCoordinate > reservedCoordinate,
);
const result = [
...layoutRange(
labelsAbove,
0,
reservedTop - gap,
gap,
'end',
),
...layoutRange(
labelsBelow,
reservedBottom + gap,
axisHeight,
gap,
'start',
),
];
return result.sort((left, right) => {
if (left.priority !== right.priority) {
return left.priority - right.priority;
}
return left.id.localeCompare(right.id);
});
}