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


import { CanvasRenderingTarget2D } from 'fancy-canvas';
import { IPrimitivePaneRenderer } from 'lightweight-charts';

import { getThemeStore } from '@src/theme';

import { Diapson, DiapsonRenderData } from './diapson';

const UI = {
  lineWidth: 1,
  handleRadius: 6,
  handleBorderWidth: 2,
  labelPadding: 4,
  labelLineHeight: 12,
  labelFontSize: 10,
  labelRadius: 2,
  labelBottomOffset: 8,
  arrowSize: 7,
};

export class DiapsonPaneRenderer implements IPrimitivePaneRenderer {
  private readonly diapson: Diapson;

  constructor(diapson: Diapson) {
    this.diapson = diapson;
  }

  public draw(target: CanvasRenderingTarget2D): void {
    const data = this.diapson.getRenderData();

    if (!data) {
      return;
    }

    const { colors } = getThemeStore();

    target.useBitmapCoordinateSpace(({ context, horizontalPixelRatio, verticalPixelRatio }) => {
      const left = data.left * horizontalPixelRatio;
      const right = data.right * horizontalPixelRatio;
      const top = data.top * verticalPixelRatio;
      const bottom = data.bottom * verticalPixelRatio;
      const width = Math.max(right - left, 0);
      const height = Math.max(bottom - top, 0);
      const lineWidth = UI.lineWidth * Math.max(horizontalPixelRatio, verticalPixelRatio);
      const arrowSize = UI.arrowSize * Math.max(horizontalPixelRatio, verticalPixelRatio);

      context.save();

      if (data.showFill) {
        context.fillStyle = data.fillColor;
        context.fillRect(left, top, width, height);
      }

      context.strokeStyle = data.borderColor;
      context.lineWidth = lineWidth;

      if (data.rangeMode === 'date') {
        drawVerticalBoundary(context, left, top, bottom);
        drawVerticalBoundary(context, right, top, bottom);

        drawHorizontalArrow(
          context,
          left,
          right,
          (top + bottom) / 2,
          arrowSize,
          data.startPoint.x <= data.endPoint.x ? 'right' : 'left',
        );
      } else {
        drawHorizontalBoundary(context, top, left, right);
        drawHorizontalBoundary(context, bottom, left, right);

        drawVerticalArrow(
          context,
          (left + right) / 2,
          top,
          bottom,
          arrowSize,
          data.startPoint.y <= data.endPoint.y ? 'down' : 'up',
        );
      }

      if (data.showHandles) {
        drawHandle(
          context,
          data.startPoint.x * horizontalPixelRatio,
          data.startPoint.y * verticalPixelRatio,
          horizontalPixelRatio,
          verticalPixelRatio,
          colors.chartBackground,
          colors.chartLineColor,
        );

        drawHandle(
          context,
          data.endPoint.x * horizontalPixelRatio,
          data.endPoint.y * verticalPixelRatio,
          horizontalPixelRatio,
          verticalPixelRatio,
          colors.chartBackground,
          colors.chartLineColor,
        );
      }

      if (data.labelLines.length > 0) {
        drawLabel(
          context,
          data,
          horizontalPixelRatio,
          verticalPixelRatio,
          data.infoBackgroundColor,
          data.infoTextColor,
        );
      }

      context.restore();
    });
  }
}

function drawVerticalBoundary(context: CanvasRenderingContext2D, x: number, top: number, bottom: number): void {
  context.beginPath();
  context.moveTo(x, top);
  context.lineTo(x, bottom);
  context.stroke();
}

function drawHorizontalBoundary(context: CanvasRenderingContext2D, y: number, left: number, right: number): void {
  context.beginPath();
  context.moveTo(left, y);
  context.lineTo(right, y);
  context.stroke();
}

function drawHorizontalArrow(
  context: CanvasRenderingContext2D,
  left: number,
  right: number,
  y: number,
  arrowSize: number,
  direction: 'left' | 'right',
): void {
  context.beginPath();
  context.moveTo(left, y);
  context.lineTo(right, y);
  context.stroke();

  if (direction === 'right') {
    context.beginPath();
    context.moveTo(right, y);
    context.lineTo(right - arrowSize, y - arrowSize);
    context.moveTo(right, y);
    context.lineTo(right - arrowSize, y + arrowSize);
    context.stroke();

    return;
  }

  context.beginPath();
  context.moveTo(left, y);
  context.lineTo(left + arrowSize, y - arrowSize);
  context.moveTo(left, y);
  context.lineTo(left + arrowSize, y + arrowSize);
  context.stroke();
}

function drawVerticalArrow(
  context: CanvasRenderingContext2D,
  x: number,
  top: number,
  bottom: number,
  arrowSize: number,
  direction: 'up' | 'down',
): void {
  context.beginPath();
  context.moveTo(x, top);
  context.lineTo(x, bottom);
  context.stroke();

  if (direction === 'down') {
    context.beginPath();
    context.moveTo(x, bottom);
    context.lineTo(x - arrowSize, bottom - arrowSize);
    context.moveTo(x, bottom);
    context.lineTo(x + arrowSize, bottom - arrowSize);
    context.stroke();

    return;
  }

  context.beginPath();
  context.moveTo(x, top);
  context.lineTo(x - arrowSize, top + arrowSize);
  context.moveTo(x, top);
  context.lineTo(x + arrowSize, top + arrowSize);
  context.stroke();
}

function drawHandle(
  context: CanvasRenderingContext2D,
  x: number,
  y: number,
  horizontalPixelRatio: number,
  verticalPixelRatio: number,
  fillStyle: string,
  strokeStyle: string,
): void {
  const radius = UI.handleRadius * Math.max(horizontalPixelRatio, verticalPixelRatio);
  const lineWidth = UI.handleBorderWidth * Math.max(horizontalPixelRatio, verticalPixelRatio);

  context.save();
  context.fillStyle = fillStyle;
  context.strokeStyle = strokeStyle;
  context.lineWidth = lineWidth;

  context.beginPath();
  context.arc(x, y, radius, 0, Math.PI * 2);
  context.fill();
  context.stroke();

  context.restore();
}

function drawLabel(
  context: CanvasRenderingContext2D,
  data: DiapsonRenderData,
  horizontalPixelRatio: number,
  verticalPixelRatio: number,
  backgroundColor: string,
  textColor: string,
): void {
  const fontSize = UI.labelFontSize * Math.max(horizontalPixelRatio, verticalPixelRatio);
  const padding = UI.labelPadding * Math.max(horizontalPixelRatio, verticalPixelRatio);
  const lineHeight = UI.labelLineHeight * verticalPixelRatio;
  const radius = UI.labelRadius * Math.max(horizontalPixelRatio, verticalPixelRatio);
  const bottomOffset = UI.labelBottomOffset * verticalPixelRatio;

  const paneLeft = data.left * horizontalPixelRatio;
  const paneRight = data.right * horizontalPixelRatio;
  const paneBottom = data.bottom * verticalPixelRatio;

  context.save();
  context.font = `${fontSize}px sans-serif`;

  const maxTextWidth = data.labelLines.reduce((maxWidth, line) => {
    return Math.max(maxWidth, context.measureText(line).width);
  }, 0);

  const labelWidth = maxTextWidth + padding * 2;
  const labelHeight = data.labelLines.length * lineHeight + padding * 2;
  const rangeCenterX = (paneLeft + paneRight) / 2;
  const maxLeft = Math.max(4, context.canvas.width - labelWidth - 4);
  const maxTop = Math.max(4, context.canvas.height - labelHeight - 4);

  const boxLeft = clampNumber(rangeCenterX - labelWidth / 2, 4, maxLeft);
  const boxTop = clampNumber(paneBottom + bottomOffset, 4, maxTop);

  context.fillStyle = backgroundColor;
  fillRoundedRect(context, boxLeft, boxTop, labelWidth, labelHeight, radius);

  context.fillStyle = textColor;
  context.textAlign = 'center';
  context.textBaseline = 'middle';

  data.labelLines.forEach((line, index) => {
    const lineY = boxTop + padding + lineHeight * index + lineHeight / 2;
    context.fillText(line, boxLeft + labelWidth / 2, lineY);
  });

  context.restore();
}

function fillRoundedRect(
  context: CanvasRenderingContext2D,
  x: number,
  y: number,
  width: number,
  height: number,
  radius: number,
): void {
  context.beginPath();
  context.moveTo(x + radius, y);
  context.lineTo(x + width - radius, y);
  context.quadraticCurveTo(x + width, y, x + width, y + radius);
  context.lineTo(x + width, y + height - radius);
  context.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
  context.lineTo(x + radius, y + height);
  context.quadraticCurveTo(x, y + height, x, y + height - radius);
  context.lineTo(x, y + radius);
  context.quadraticCurveTo(x, y, x + radius, y);
  context.closePath();
  context.fill();
}

function clampNumber(value: number, min: number, max: number): number {
  if (max < min) {
    return min;
  }

  return Math.max(min, Math.min(value, max));
}