Загрузка данных
import type { Anchor, Bounds, ContainerSize, Point, SeriesApi } from './types';
import type { Coordinate, IChartApi, Time } from 'lightweight-charts';
interface SeriesTimeItem {
time: Time;
}
interface TimeCoordinatePoint {
time: number;
coordinate: number;
}
export function getPriceFromYCoordinate(series: SeriesApi, yCoordinate: number): number | null {
return series.coordinateToPrice(yCoordinate as Coordinate);
}
export function getYCoordinateFromPrice(series: SeriesApi, price: number): Coordinate | null {
return series.priceToCoordinate(price);
}
export function getTimeFromXCoordinate(chart: IChartApi, xCoordinate: number, series?: SeriesApi): Time | null {
const directTime = chart.timeScale().coordinateToTime(xCoordinate as Coordinate);
if (directTime !== null) {
return directTime;
}
if (!series) {
return null;
}
return getTimeByCoordinate(chart, series, xCoordinate);
}
export function getXCoordinateFromTime(chart: IChartApi, time: Time, series?: SeriesApi): Coordinate | null {
const directCoordinate = chart.timeScale().timeToCoordinate(time);
if (directCoordinate !== null) {
return directCoordinate;
}
if (!series) {
return null;
}
return getCoordinateByTime(chart, series, time);
}
export function clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(value, max));
}
export function getContainerSize(container: HTMLElement): ContainerSize {
const rect = container.getBoundingClientRect();
return {
width: rect.width,
height: rect.height,
};
}
export function clampPointToContainer(point: Point, container: HTMLElement): Point {
const { width, height } = getContainerSize(container);
return {
x: clamp(point.x, 0, width),
y: clamp(point.y, 0, height),
};
}
export function getPointerPoint(container: HTMLElement, event: PointerEvent): Point {
const rect = container.getBoundingClientRect();
return clampPointToContainer(
{
x: event.clientX - rect.left,
y: event.clientY - rect.top,
},
container,
);
}
export function isNearPoint(point: Point, x: number, y: number, tolerance: number): boolean {
return Math.abs(point.x - x) <= tolerance && Math.abs(point.y - y) <= tolerance;
}
export function isPointInBounds(point: Point, bounds: Bounds, tolerance = 0): boolean {
return (
point.x >= bounds.left - tolerance &&
point.x <= bounds.right + tolerance &&
point.y >= bounds.top - tolerance &&
point.y <= bounds.bottom + tolerance
);
}
export function normalizeBounds(
left: number,
right: number,
top: number,
bottom: number,
container: HTMLElement,
): Bounds {
const { width, height } = getContainerSize(container);
return {
left: clamp(Math.min(left, right), 0, width),
right: clamp(Math.max(left, right), 0, width),
top: clamp(Math.min(top, bottom), 0, height),
bottom: clamp(Math.max(top, bottom), 0, height),
};
}
export function shiftTimeByPixels(chart: IChartApi, time: Time, offsetX: number, series?: SeriesApi): Time | null {
const coordinate = getXCoordinateFromTime(chart, time, series);
if (coordinate === null) {
return null;
}
return getTimeFromXCoordinate(chart, Number(coordinate) + offsetX, series);
}
export function getPriceDelta(series: SeriesApi, fromY: number, toY: number): number {
const fromPrice = getPriceFromYCoordinate(series, fromY);
const toPrice = getPriceFromYCoordinate(series, toY);
if (fromPrice === null || toPrice === null) {
return 0;
}
return toPrice - fromPrice;
}
export function getPriceRangeInContainer(
series: SeriesApi,
container: HTMLElement,
): { min: number; max: number } | null {
const { height } = getContainerSize(container);
if (!height) {
return null;
}
const topPrice = getPriceFromYCoordinate(series, 0);
const bottomPrice = getPriceFromYCoordinate(series, height);
if (topPrice === null || bottomPrice === null) {
return null;
}
return {
min: Math.min(topPrice, bottomPrice),
max: Math.max(topPrice, bottomPrice),
};
}
export function getAnchorFromPoint(chart: IChartApi, series: SeriesApi, point: Point): Anchor | null {
const time = getTimeFromXCoordinate(chart, point.x, series);
const price = getPriceFromYCoordinate(series, point.y);
if (time === null || price === null) {
return null;
}
return {
time,
price,
};
}
function getCoordinateByTime(chart: IChartApi, series: SeriesApi, time: Time): Coordinate | null {
const targetTime = getNumericTime(time);
if (targetTime === null) {
return null;
}
const points = getTimeCoordinatePoints(chart, series);
if (points.length < 2) {
return null;
}
const lastIndex = points.length - 1;
if (targetTime < points[0].time || targetTime > points[lastIndex].time) {
return null;
}
let left = 0;
let right = lastIndex;
while (left <= right) {
const middle = Math.floor((left + right) / 2);
const middleTime = points[middle].time;
if (middleTime === targetTime) {
return points[middle].coordinate as Coordinate;
}
if (middleTime < targetTime) {
left = middle + 1;
} else {
right = middle - 1;
}
}
return interpolateCoordinate(points[right], points[left], targetTime) as Coordinate;
}
function getTimeByCoordinate(chart: IChartApi, series: SeriesApi, coordinate: number): Time | null {
const points = [...getTimeCoordinatePoints(chart, series)].sort((a, b) => a.coordinate - b.coordinate);
if (points.length < 2) {
return null;
}
const lastIndex = points.length - 1;
if (coordinate < points[0].coordinate || coordinate > points[lastIndex].coordinate) {
return null;
}
let left = 0;
let right = lastIndex;
while (left <= right) {
const middle = Math.floor((left + right) / 2);
const middleCoordinate = points[middle].coordinate;
if (middleCoordinate === coordinate) {
return points[middle].time as Time;
}
if (middleCoordinate < coordinate) {
left = middle + 1;
} else {
right = middle - 1;
}
}
return interpolateTime(points[right], points[left], coordinate);
}
function getTimeCoordinatePoints(chart: IChartApi, series: SeriesApi): TimeCoordinatePoint[] {
const points = (series.data() as readonly SeriesTimeItem[]).reduce<TimeCoordinatePoint[]>((result, item) => {
const time = getNumericTime(item.time);
if (time === null) {
return result;
}
const coordinate = chart.timeScale().timeToCoordinate(item.time);
if (coordinate === null) {
return result;
}
result.push({
time,
coordinate: Number(coordinate),
});
return result;
}, []);
return points.sort((a, b) => a.time - b.time);
}
function interpolateCoordinate(
leftPoint: TimeCoordinatePoint,
rightPoint: TimeCoordinatePoint,
targetTime: number,
): number | null {
const timeRange = rightPoint.time - leftPoint.time;
if (timeRange === 0) {
return null;
}
const ratio = (targetTime - leftPoint.time) / timeRange;
return leftPoint.coordinate + (rightPoint.coordinate - leftPoint.coordinate) * ratio;
}
function interpolateTime(
leftPoint: TimeCoordinatePoint,
rightPoint: TimeCoordinatePoint,
coordinate: number,
): Time | null {
const coordinateRange = rightPoint.coordinate - leftPoint.coordinate;
if (coordinateRange === 0) {
return null;
}
const ratio = (coordinate - leftPoint.coordinate) / coordinateRange;
const time = leftPoint.time + (rightPoint.time - leftPoint.time) * ratio;
return Math.round(time) as Time;
}
function getNumericTime(time: Time): number | null {
if (typeof time !== 'number') {
return null;
}
return Number.isFinite(time) ? time : null;
}