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


import { IChartApi, ISeriesApi, MouseEventParams, SeriesOptionsMap, SeriesType, Time } from 'lightweight-charts';

import { ISeriesDrawing } from '@core/Drawings/common';
import { getPriceFromYCoordinate, getYCoordinateFromPrice } from '@core/Drawings/helpers';
import { CustomPriceAxisView } from '@src/core/Drawings/axis';

import { HorzLinePaneView } from './paneView';

import type { AxisLabel } from '@core/Drawings/types';

export interface HorzLineOptions {
  color: string;
  labelText: string;
  height: number;
  labelBackgroundColor: string;
  labelTextColor: string;
  showLabel: boolean;
}

const defaultOptions: HorzLineOptions = {
  color: 'gray',
  labelText: '',
  height: 3,
  labelBackgroundColor: 'green',
  labelTextColor: 'gray',
  showLabel: false,
};

export class HorzLine implements ISeriesDrawing {
  private _attached = false;

  _chart: IChartApi;
  _series: ISeriesApi<keyof SeriesOptionsMap>;
  _price!: number;
  _paneViews: HorzLinePaneView[];
  _priceAxisViews: CustomPriceAxisView[];
  _options: HorzLineOptions;
  _visible = true;

  constructor(chart: IChartApi, series: ISeriesApi<SeriesType>, options?: Partial<HorzLineOptions>) {
    const horzLineOptions: HorzLineOptions = {
      ...defaultOptions,
      ...options,
    };

    this._chart = chart;
    this._series = series;
    this._options = horzLineOptions;

    chart.subscribeClick(this.startDrawing);

    this._paneViews = [new HorzLinePaneView(this, horzLineOptions)];
    this._priceAxisViews = [
      new CustomPriceAxisView({
        getAxisLabel: (labelKind) => this.getPriceAxisLabel(labelKind),
        labelKind: 'main',
      }),
    ];
  }

  private startDrawing = (param: MouseEventParams<Time>) => {
    if (param.point) {
      const price = getPriceFromYCoordinate(this._series, param.point.y);

      if (price == null) {
        return;
      }

      this._price = price;
      this._series.attachPrimitive(this);
      this._attached = true;
      this.updateAllViews();
    }

    this._chart.unsubscribeClick(this.startDrawing);
  };

  public getPriceAxisLabel(labelKind: string): AxisLabel | null {
    if (labelKind !== 'main') {
      return null;
    }

    if (!this._visible || !this._options.showLabel) {
      return null;
    }

    const coordinate = getYCoordinateFromPrice(this._series, this._price);

    if (coordinate === null) {
      return null;
    }

    return {
      coordinate,
      text: this._options.labelText,
      textColor: this._options.labelTextColor,
      backgroundColor: this._options.labelBackgroundColor,
    };
  }

  public updateAllViews() {
    this._paneViews.forEach((pw) => pw.update());
    this._priceAxisViews.forEach((pw) => pw.update());
  }

  public priceAxisViews() {
    return this._priceAxisViews;
  }

  public paneViews() {
    return this._paneViews;
  }

  public hide(): void {
    this._visible = false;
    this.updateAllViews();
  }

  public show(): void {
    this._visible = true;
    this.updateAllViews();
  }

  public rebind(series: ISeriesApi<SeriesType>): void {
    if (this._attached) {
      this._series.detachPrimitive(this);
    }

    this._series = series;
    this.updateAllViews();

    if (this._attached) {
      this._series.attachPrimitive(this);
    }
  }

  public destroy(): void {
    this._chart.unsubscribeClick(this.startDrawing);

    if (this._attached) {
      this._series.detachPrimitive(this);
      this._attached = false;
    }
  }
}



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

import { BitmapPositionLength } from '@core/Drawings/common';

import type { HorzLineOptions } from './horzLine';

export class HorzLinePaneRenderer implements IPrimitivePaneRenderer {
  private _y: Coordinate | null = null;
  private _options: HorzLineOptions;
  private _visible: boolean;

  constructor(y: Coordinate | null, options: HorzLineOptions, visible: boolean) {
    this._y = y;
    this._options = options;
    this._visible = visible;
  }

  public draw(target: CanvasRenderingTarget2D): void {
    if (!this._visible) {
      return;
    }

    target.useBitmapCoordinateSpace((scope) => {
      if (this._y === null) {
        return;
      }

      const ctx = scope.context;
      const position = getHorizontalLineBitmapPosition(this._y, scope.verticalPixelRatio, this._options.height);

      ctx.fillStyle = this._options.color;
      ctx.fillRect(0, position.position, scope.bitmapSize.width, position.length);
    });
  }
}

function getHorizontalLineBitmapPosition(
  positionMedia: number,
  pixelRatio: number,
  desiredHeightMedia: number,
  heightIsBitmap?: boolean,
): BitmapPositionLength {
  const scaledPosition = Math.round(pixelRatio * positionMedia);
  const lineBitmapHeight = heightIsBitmap ? desiredHeightMedia : Math.round(desiredHeightMedia * pixelRatio);

  const offset = Math.floor(lineBitmapHeight * 0.5);
  const position = scaledPosition - offset;

  return { position, length: lineBitmapHeight };
}



import { Coordinate, IPrimitivePaneRenderer, IPrimitivePaneView } from 'lightweight-charts';

import { getYCoordinateFromPrice } from '@core/Drawings/helpers';

import { HorzLinePaneRenderer } from './paneRenderer';

import type { HorzLine, HorzLineOptions } from './horzLine';

export class HorzLinePaneView implements IPrimitivePaneView {
  _source: HorzLine;
  _y: Coordinate | null = null;
  _options: HorzLineOptions;

  constructor(source: HorzLine, options: HorzLineOptions) {
    this._source = source;
    this._options = options;
  }

  public update(): void {
    this._y = getYCoordinateFromPrice(this._source._series, this._source._price);
  }

  public renderer(): IPrimitivePaneRenderer {
    return new HorzLinePaneRenderer(this._y, this._options, this._source._visible);
  }
}


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

import { BitmapPositionLength } from '@core/Drawings/common';

import type { VertLineOptions } from './vertLine';

export class VertLinePaneRenderer implements IPrimitivePaneRenderer {
  private _x: Coordinate | null = null;
  private _options: VertLineOptions;
  private _visible: boolean;

  constructor(x: Coordinate | null, options: VertLineOptions, visible: boolean) {
    this._x = x;
    this._visible = visible;
    this._options = options;
  }

  public draw(target: CanvasRenderingTarget2D): void {
    if (!this._visible) {
      return;
    }

    target.useBitmapCoordinateSpace((scope) => {
      if (this._x === null) {
        return;
      }

      const ctx = scope.context;
      const position = getVerticalLineBitmapPosition(this._x, scope.horizontalPixelRatio, this._options.width);

      ctx.fillStyle = this._options.color;
      ctx.fillRect(position.position, 0, position.length, scope.bitmapSize.height);
    });
  }
}

function getVerticalLineBitmapPosition(
  positionMedia: number,
  pixelRatio: number,
  desiredWidthMedia: number,
  widthIsBitmap?: boolean,
): BitmapPositionLength {
  const scaledPosition = Math.round(pixelRatio * positionMedia);
  const lineBitmapWidth = widthIsBitmap ? desiredWidthMedia : Math.round(desiredWidthMedia * pixelRatio);

  const offset = Math.floor(lineBitmapWidth * 0.5);
  const position = scaledPosition - offset;

  return { position, length: lineBitmapWidth };
}


import { Coordinate, IPrimitivePaneRenderer, IPrimitivePaneView } from 'lightweight-charts';

import { getXCoordinateFromTime } from '@core/Drawings/helpers';

import { VertLinePaneRenderer } from './paneRenderer';

import type { VertLine, VertLineOptions } from './vertLine';

export class VertLinePaneView implements IPrimitivePaneView {
  private _source: VertLine;
  private _x: Coordinate | null = null;
  private _options: VertLineOptions;

  constructor(source: VertLine, options: VertLineOptions) {
    this._source = source;
    this._options = options;
  }

  public update(): void {
    const { time } = this._source;

    if (!time) {
      this._x = null;
      return;
    }

    this._x = getXCoordinateFromTime(this._source.chart, time);
  }

  public renderer(): IPrimitivePaneRenderer {
    return new VertLinePaneRenderer(this._x, this._options, this._source.visible);
  }
}


import { IChartApi, ISeriesApi, MouseEventParams, SeriesOptionsMap, SeriesType, Time } from 'lightweight-charts';

import { ISeriesDrawing } from '@core/Drawings/common';
import { getTimeFromXCoordinate, getXCoordinateFromTime } from '@core/Drawings/helpers';
import { CustomTimeAxisView } from '@src/core/Drawings/axis';

import { VertLinePaneView } from './paneView';

import type { AxisLabel } from '@core/Drawings/types';

export interface VertLineOptions {
  color: string;
  labelText: string;
  width: number;
  labelBackgroundColor: string;
  labelTextColor: string;
  showLabel: boolean;
}

const defaultOptions: VertLineOptions = {
  color: 'gray',
  labelText: '',
  width: 3,
  labelBackgroundColor: 'green',
  labelTextColor: 'white',
  showLabel: false,
};

export class VertLine implements ISeriesDrawing {
  private _series: ISeriesApi<keyof SeriesOptionsMap>;
  private _paneViews: VertLinePaneView[];
  private _timeAxisViews: CustomTimeAxisView[];
  private _attached = false;
  private _options: VertLineOptions;

  time: Time | null = null;
  chart: IChartApi;
  visible = true;

  constructor(chart: IChartApi, series: ISeriesApi<SeriesType>, options?: Partial<VertLineOptions>) {
    const vertLineOptions: VertLineOptions = {
      ...defaultOptions,
      ...options,
    };

    this.chart = chart;
    this._series = series;
    this._options = vertLineOptions;

    chart.subscribeClick(this.startDrawing);

    this._paneViews = [new VertLinePaneView(this, vertLineOptions)];
    this._timeAxisViews = [
      new CustomTimeAxisView({
        getAxisLabel: (labelKind) => this.getTimeAxisLabel(labelKind),
        labelKind: 'main',
      }),
    ];
  }

  private startDrawing = (param: MouseEventParams<Time>) => {
    if (param.point) {
      const time = getTimeFromXCoordinate(this.chart, param.point.x);

      if (time == null) {
        return;
      }

      this.time = time;
      this._series.attachPrimitive(this);
      this._attached = true;
      this.updateAllViews();
    }

    this.chart.unsubscribeClick(this.startDrawing);
  };

  public getTimeAxisLabel(labelKind: string): AxisLabel | null {
    if (labelKind !== 'main') {
      return null;
    }

    if (!this.visible || !this._options.showLabel || !this.time) {
      return null;
    }

    const coordinate = getXCoordinateFromTime(this.chart, this.time);

    if (coordinate === null) {
      return null;
    }

    return {
      coordinate,
      text: this._options.labelText,
      textColor: this._options.labelTextColor,
      backgroundColor: this._options.labelBackgroundColor,
    };
  }

  public updateAllViews() {
    this._paneViews.forEach((pw) => pw.update());
    this._timeAxisViews.forEach((tw) => tw.update());
  }

  public timeAxisViews() {
    return this._timeAxisViews;
  }

  public paneViews() {
    return this._paneViews;
  }

  public hide(): void {
    this.visible = false;
    this.updateAllViews();
  }

  public show(): void {
    this.visible = true;
    this.updateAllViews();
  }

  public rebind(series: ISeriesApi<SeriesType>): void {
    if (this._attached) {
      this._series.detachPrimitive(this);
    }

    this._series = series;
    this.updateAllViews();

    if (this._attached) {
      this._series.attachPrimitive(this);
    }
  }

  public destroy(): void {
    this.chart.unsubscribeClick(this.startDrawing);

    if (this._attached) {
      this._series.detachPrimitive(this);
      this._attached = false;
    }
  }
}