Загрузка данных
export type PriceAxisLabelStyle = 'filled' | 'outlined';
export interface PriceAxisLabel {
id: string;
desiredCoordinate: number;
text: string;
color: string;
style: PriceAxisLabelStyle;
priority: number;
}
export interface MeasuredPriceAxisLabel extends PriceAxisLabel {
width: number;
height: number;
}
export interface LaidOutPriceAxisLabel extends MeasuredPriceAxisLabel {
coordinate: number;
}
export interface PriceAxisLabelsLayoutOptions {
gap?: number;
reservedCoordinate?: number;
reservedHeight?: number;
}
import type {
LaidOutPriceAxisLabel,
MeasuredPriceAxisLabel,
PriceAxisLabelsLayoutOptions,
} from './types';
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 layoutRange(
labels: readonly MeasuredPriceAxisLabel[],
minCoordinate: number,
maxCoordinate: number,
gap: number,
overflowAlignment: 'start' | 'center' | 'end',
): LaidOutPriceAxisLabel[] {
const sortedLabels = [...labels].sort(compareLabels);
if (sortedLabels.length === 0) {
return [];
}
const availableHeight = Math.max(0, maxCoordinate - minCoordinate);
const stackHeight =
sortedLabels.reduce((height, label) => height + label.height, 0) +
gap * Math.max(0, sortedLabels.length - 1);
if (stackHeight > availableHeight) {
let top = minCoordinate;
if (overflowAlignment === 'end') {
top = maxCoordinate - stackHeight;
} else if (overflowAlignment === 'center') {
top = minCoordinate + (availableHeight - stackHeight) / 2;
}
return sortedLabels.map((label) => {
const coordinate = top + label.height / 2;
top += label.height + gap;
return {
...label,
coordinate,
};
});
}
const result = sortedLabels.map((label) => ({
...label,
coordinate: Math.min(
Math.max(label.desiredCoordinate, minCoordinate + label.height / 2),
maxCoordinate - label.height / 2,
),
}));
for (let index = 1; index < result.length; index += 1) {
const previousLabel = result[index - 1];
const currentLabel = result[index];
currentLabel.coordinate = Math.max(
currentLabel.coordinate,
previousLabel.coordinate + previousLabel.height / 2 + currentLabel.height / 2 + gap,
);
}
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];
currentLabel.coordinate = Math.min(
currentLabel.coordinate,
nextLabel.coordinate - nextLabel.height / 2 - currentLabel.height / 2 - gap,
);
}
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 = DEFAULT_GAP,
reservedCoordinate,
reservedHeight,
} = options;
if (reservedCoordinate === undefined || reservedHeight === undefined) {
return layoutRange(labels, 0, axisHeight, gap, 'center');
}
const coordinate = Math.min(Math.max(reservedCoordinate, 0), axisHeight);
const height = Math.max(0, reservedHeight);
const reservedTop = Math.max(0, coordinate - height / 2);
const reservedBottom = Math.min(axisHeight, coordinate + height / 2);
const labelsAbove = labels.filter((label) => label.desiredCoordinate <= coordinate);
const labelsBelow = labels.filter((label) => label.desiredCoordinate > coordinate);
return [
...layoutRange(labelsAbove, 0, Math.max(0, reservedTop - gap), gap, 'end'),
...layoutRange(labelsBelow, Math.min(axisHeight, reservedBottom + gap), axisHeight, gap, 'start'),
].sort((left, right) => {
if (left.priority !== right.priority) {
return left.priority - right.priority;
}
return left.id.localeCompare(right.id);
});
}