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


import { getThemeStore } from '@src/theme';
import { SettingField, SettingsTab, SettingsValues } from '@src/types';

export interface DiapsonStyle {
  borderColor: string;
  fillColor: string;
}

export interface DiapsonTextStyle {
  labelTextColor: string;
  labelBackgroundColor: string;
  fontSize: number;
  isBold: boolean;
  isItalic: boolean;
}

export type DiapsonSettings = SettingsValues & DiapsonStyle & DiapsonTextStyle;

export function createDefaultSettings(): DiapsonSettings {
  const { colors } = getThemeStore();

  return {
    borderColor: colors.diapsonStrokeFill,
    fillColor: colors.diapsonAreaFill,
    labelTextColor: colors.axisRangeTooltipText,
    labelBackgroundColor: colors.axisRangeTooltipFill,
    fontSize: 10,
    isBold: false,
    isItalic: false,
  };
}

export function getDiapsonSettingsTabs(settings: DiapsonSettings): SettingsTab[] {
  const styleFields: SettingField[] = [
    {
      key: 'borderColor',
      label: 'Цвет границы',
      type: 'color',
      defaultValue: settings.borderColor,
    },
    {
      key: 'fillColor',
      label: 'Цвет фона диапазона',
      type: 'color',
      defaultValue: settings.fillColor,
    },
  ];

  const textFields: SettingField[] = [
    {
      key: 'labelTextColor',
      label: 'Цвет текста',
      type: 'color',
      defaultValue: settings.labelTextColor,
    },
    {
      key: 'labelBackgroundColor',
      label: 'Цвет фона',
      type: 'color',
      defaultValue: settings.labelBackgroundColor,
    },
    {
      key: 'fontSize',
      label: 'Размер текста',
      type: 'number',
      defaultValue: settings.fontSize,
      min: 8,
      max: 24,
    },
    {
      key: 'isBold',
      label: 'Жирный текст',
      type: 'boolean',
      defaultValue: settings.isBold,
    },
    {
      key: 'isItalic',
      label: 'Курсив',
      type: 'boolean',
      defaultValue: settings.isItalic,
    },
  ];

  return [
    {
      key: 'style',
      label: 'Стиль',
      fields: styleFields,
    },
    {
      key: 'text',
      label: 'Текст',
      fields: textFields,
    },
  ];
}



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

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

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

import type { DiapsonTextStyle } from './settings';

const UI = {
  lineWidth: 1,
  handleRadius: 6,
  handleBorderWidth: 2,
  labelPadding: 4,
  labelRadius: 2,
  labelBottomOffset: 8,
  arrowSize: 7,
  lineHeightMultiplier: 1.2,
};

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 pixelRatio = Math.max(horizontalPixelRatio, verticalPixelRatio);
      const lineWidth = UI.lineWidth * pixelRatio;
      const arrowSize = UI.arrowSize * pixelRatio;

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

      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 pixelRatio = Math.max(horizontalPixelRatio, verticalPixelRatio);
  const radius = UI.handleRadius * pixelRatio;
  const lineWidth = UI.handleBorderWidth * pixelRatio;

  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,
  textStyle: DiapsonTextStyle,
): void {
  const pixelRatio = Math.max(horizontalPixelRatio, verticalPixelRatio);
  const fontSize = textStyle.fontSize * pixelRatio;
  const padding = UI.labelPadding * pixelRatio;
  const lineHeight = Math.round(textStyle.fontSize * UI.lineHeightMultiplier) * verticalPixelRatio;
  const radius = UI.labelRadius * pixelRatio;
  const bottomOffset = UI.labelBottomOffset * verticalPixelRatio;

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

  context.save();
  context.font = getLabelFont(textStyle, fontSize);

  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 = textStyle.labelBackgroundColor;
  fillRoundedRect(context, boxLeft, boxTop, labelWidth, labelHeight, radius);

  context.fillStyle = textStyle.labelTextColor;
  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 getLabelFont(style: DiapsonTextStyle, fontSize: number): string {
  const italic = style.isItalic ? 'italic ' : '';
  const bold = style.isBold ? '700 ' : '';

  return `${italic}${bold}${fontSize}px Inter, sans-serif`;
}

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