Загрузка данных
import { LaidOutPriceAxisLabel, PRICE_AXIS_LABEL_HEIGHT, PriceAxisLabel, PriceAxisLabelsLayoutOptions } from './types';
const DEFAULT_LABEL_GAP = 2;
const DEFAULT_DUPLICATE_TOLERANCE = 1;
function compareLabels(left: PriceAxisLabel, right: PriceAxisLabel): number {
if (left.desiredCoordinate !== right.desiredCoordinate) {
return left.desiredCoordinate - right.desiredCoordinate;
}
const priorityDiff = (right.priority ?? 0) - (left.priority ?? 0);
if (priorityDiff !== 0) {
return priorityDiff;
}
return left.id.localeCompare(right.id);
}
function isDuplicate(left: PriceAxisLabel, right: PriceAxisLabel, tolerance: number): boolean {
return (
left.text === right.text &&
left.symbol === right.symbol &&
Math.abs(left.desiredCoordinate - right.desiredCoordinate) <= tolerance
);
}
function deduplicateLabels(labels: readonly PriceAxisLabel[], tolerance: number): PriceAxisLabel[] {
const sortedLabels = [...labels].sort((left, right) => {
const priorityDiff = (right.priority ?? 0) - (left.priority ?? 0);
if (priorityDiff !== 0) {
return priorityDiff;
}
return left.id.localeCompare(right.id);
});
const result: PriceAxisLabel[] = [];
sortedLabels.forEach((label) => {
const duplicateExists = result.some((currentLabel) => isDuplicate(currentLabel, label, tolerance));
if (!duplicateExists) {
result.push(label);
}
});
return result;
}
function createLaidOutLabel(label: PriceAxisLabel, coordinate: number): LaidOutPriceAxisLabel {
return {
...label,
coordinate,
height: label.height ?? PRICE_AXIS_LABEL_HEIGHT,
};
}
function distributeLabelsInsideSmallAxis(
labels: readonly PriceAxisLabel[],
axisHeight: number,
): LaidOutPriceAxisLabel[] {
if (labels.length === 0) {
return [];
}
if (labels.length === 1) {
const label = labels[0];
const height = label.height ?? PRICE_AXIS_LABEL_HEIGHT;
const minCoordinate = height / 2;
const maxCoordinate = Math.max(minCoordinate, axisHeight - height / 2);
return [createLaidOutLabel(label, Math.min(Math.max(label.desiredCoordinate, minCoordinate), maxCoordinate))];
}
const firstHeight = labels[0].height ?? PRICE_AXIS_LABEL_HEIGHT;
const lastHeight = labels[labels.length - 1].height ?? PRICE_AXIS_LABEL_HEIGHT;
const minCoordinate = firstHeight / 2;
const maxCoordinate = Math.max(minCoordinate, axisHeight - lastHeight / 2);
const step = Math.max(0, (maxCoordinate - minCoordinate) / (labels.length - 1));
return labels.map((label, index) => createLaidOutLabel(label, minCoordinate + step * index));
}
function resolveCollisionGroup(
labels: readonly PriceAxisLabel[],
axisHeight: number,
gap: number,
): LaidOutPriceAxisLabel[] {
const sortedLabels = [...labels].sort(compareLabels);
if (sortedLabels.length === 0) {
return [];
}
const totalLabelsHeight = sortedLabels.reduce((total, label) => total + (label.height ?? PRICE_AXIS_LABEL_HEIGHT), 0);
if (totalLabelsHeight > axisHeight) {
return distributeLabelsInsideSmallAxis(sortedLabels, axisHeight);
}
const maxAvailableGap =
sortedLabels.length > 1 ? Math.max(0, (axisHeight - totalLabelsHeight) / (sortedLabels.length - 1)) : gap;
const effectiveGap = Math.min(gap, maxAvailableGap);
const resolvedLabels = sortedLabels.map((label) => {
const height = label.height ?? PRICE_AXIS_LABEL_HEIGHT;
const minCoordinate = height / 2;
const maxCoordinate = Math.max(minCoordinate, axisHeight - height / 2);
return createLaidOutLabel(label, Math.min(Math.max(label.desiredCoordinate, minCoordinate), maxCoordinate));
});
for (let index = 1; index < resolvedLabels.length; index += 1) {
const previousLabel = resolvedLabels[index - 1];
const currentLabel = resolvedLabels[index];
const minCoordinate = previousLabel.coordinate + previousLabel.height / 2 + currentLabel.height / 2 + effectiveGap;
if (currentLabel.coordinate < minCoordinate) {
currentLabel.coordinate = minCoordinate;
}
}
const lastLabel = resolvedLabels[resolvedLabels.length - 1];
const lastMaxCoordinate = axisHeight - lastLabel.height / 2;
if (lastLabel.coordinate > lastMaxCoordinate) {
const offset = lastLabel.coordinate - lastMaxCoordinate;
resolvedLabels.forEach((label) => {
label.coordinate -= offset;
});
}
for (let index = resolvedLabels.length - 2; index >= 0; index -= 1) {
const currentLabel = resolvedLabels[index];
const nextLabel = resolvedLabels[index + 1];
const maxCoordinate = nextLabel.coordinate - nextLabel.height / 2 - currentLabel.height / 2 - effectiveGap;
if (currentLabel.coordinate > maxCoordinate) {
currentLabel.coordinate = maxCoordinate;
}
}
const firstLabel = resolvedLabels[0];
const firstMinCoordinate = firstLabel.height / 2;
if (firstLabel.coordinate < firstMinCoordinate) {
const offset = firstMinCoordinate - firstLabel.coordinate;
resolvedLabels.forEach((label) => {
label.coordinate += offset;
});
}
return resolvedLabels;
}
export function layoutPriceAxisLabels(
labels: readonly PriceAxisLabel[],
axisHeight: number,
options: PriceAxisLabelsLayoutOptions = {},
): LaidOutPriceAxisLabel[] {
if (axisHeight <= 0 || labels.length === 0) {
return [];
}
const gap = options.gap ?? DEFAULT_LABEL_GAP;
const duplicateTolerance = options.duplicateTolerance ?? DEFAULT_DUPLICATE_TOLERANCE;
const labelsByCollisionGroup = new Map<string, PriceAxisLabel[]>();
labels.forEach((label) => {
if (!Number.isFinite(label.desiredCoordinate)) {
return;
}
const group = labelsByCollisionGroup.get(label.collisionGroup);
if (group) {
group.push(label);
return;
}
labelsByCollisionGroup.set(label.collisionGroup, [label]);
});
const result: LaidOutPriceAxisLabel[] = [];
labelsByCollisionGroup.forEach((groupLabels) => {
const uniqueLabels = deduplicateLabels(groupLabels, duplicateTolerance);
result.push(...resolveCollisionGroup(uniqueLabels, axisHeight, gap));
});
return result.sort((left, right) => {
const priorityDiff = (left.priority ?? 0) - (right.priority ?? 0);
if (priorityDiff !== 0) {
return priorityDiff;
}
return left.id.localeCompare(right.id);
});
}