Загрузка данных


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);
  });
}