16865 lines
802 KiB
JavaScript
16865 lines
802 KiB
JavaScript
/*!
|
|
* @license
|
|
* TradingView Lightweight Charts™ v5.2.0
|
|
* Copyright (c) 2026 TradingView, Inc.
|
|
* Licensed under Apache License 2.0 https://www.apache.org/licenses/LICENSE-2.0
|
|
*/
|
|
(function () {
|
|
'use strict';
|
|
|
|
const customStyleDefaults$1 = {
|
|
color: '#2196f3',
|
|
};
|
|
const seriesOptionsDefaults = {
|
|
title: '',
|
|
visible: true,
|
|
hitTestTolerance: 3,
|
|
lastValueVisible: true,
|
|
priceLineVisible: true,
|
|
priceLineSource: 0 /* PriceLineSource.LastBar */,
|
|
priceLineWidth: 1,
|
|
priceLineColor: '',
|
|
priceLineStyle: 2 /* LineStyle.Dashed */,
|
|
baseLineVisible: true,
|
|
baseLineWidth: 1,
|
|
baseLineColor: '#B2B5BE',
|
|
baseLineStyle: 0 /* LineStyle.Solid */,
|
|
priceFormat: {
|
|
type: 'price',
|
|
precision: 2,
|
|
minMove: 0.01,
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Represents the possible line types.
|
|
*/
|
|
var LineType;
|
|
(function (LineType) {
|
|
/**
|
|
* A line.
|
|
*/
|
|
LineType[LineType["Simple"] = 0] = "Simple";
|
|
/**
|
|
* A stepped line.
|
|
*/
|
|
LineType[LineType["WithSteps"] = 1] = "WithSteps";
|
|
/**
|
|
* A curved line.
|
|
*/
|
|
LineType[LineType["Curved"] = 2] = "Curved";
|
|
})(LineType || (LineType = {}));
|
|
/**
|
|
* Represents the possible line styles.
|
|
*/
|
|
var LineStyle;
|
|
(function (LineStyle) {
|
|
/**
|
|
* A solid line.
|
|
*/
|
|
LineStyle[LineStyle["Solid"] = 0] = "Solid";
|
|
/**
|
|
* A dotted line.
|
|
*/
|
|
LineStyle[LineStyle["Dotted"] = 1] = "Dotted";
|
|
/**
|
|
* A dashed line.
|
|
*/
|
|
LineStyle[LineStyle["Dashed"] = 2] = "Dashed";
|
|
/**
|
|
* A dashed line with bigger dashes.
|
|
*/
|
|
LineStyle[LineStyle["LargeDashed"] = 3] = "LargeDashed";
|
|
/**
|
|
* A dotted line with more space between dots.
|
|
*/
|
|
LineStyle[LineStyle["SparseDotted"] = 4] = "SparseDotted";
|
|
})(LineStyle || (LineStyle = {}));
|
|
function getDashPattern(style, lineWidth) {
|
|
switch (style) {
|
|
case 0 /* LineStyle.Solid */: return [];
|
|
case 1 /* LineStyle.Dotted */: return [lineWidth, lineWidth];
|
|
case 2 /* LineStyle.Dashed */: return [2 * lineWidth, 2 * lineWidth];
|
|
case 3 /* LineStyle.LargeDashed */: return [6 * lineWidth, 6 * lineWidth];
|
|
case 4 /* LineStyle.SparseDotted */: return [lineWidth, 4 * lineWidth];
|
|
default: return [];
|
|
}
|
|
}
|
|
function getDashPatternLength(dashPattern) {
|
|
return dashPattern.reduce((sum, val) => sum + val, 0);
|
|
}
|
|
function setLineStyle(ctx, style) {
|
|
const dashPattern = getDashPattern(style, ctx.lineWidth);
|
|
ctx.setLineDash(dashPattern);
|
|
return dashPattern;
|
|
}
|
|
function drawHorizontalLine(ctx, y, left, right) {
|
|
ctx.beginPath();
|
|
const correction = (ctx.lineWidth % 2) ? 0.5 : 0;
|
|
ctx.moveTo(left, y + correction);
|
|
ctx.lineTo(right, y + correction);
|
|
ctx.stroke();
|
|
}
|
|
function drawVerticalLine(ctx, x, top, bottom) {
|
|
ctx.beginPath();
|
|
const correction = (ctx.lineWidth % 2) ? 0.5 : 0;
|
|
ctx.moveTo(x + correction, top);
|
|
ctx.lineTo(x + correction, bottom);
|
|
ctx.stroke();
|
|
}
|
|
function strokeInPixel(ctx, drawFunction) {
|
|
ctx.save();
|
|
if (ctx.lineWidth % 2) {
|
|
ctx.translate(0.5, 0.5);
|
|
}
|
|
drawFunction();
|
|
ctx.restore();
|
|
}
|
|
|
|
/**
|
|
* Checks an assertion. Throws if the assertion is failed.
|
|
*
|
|
* @param condition - Result of the assertion evaluation
|
|
* @param message - Text to include in the exception message
|
|
*/
|
|
function assert(condition, message) {
|
|
if (!condition) {
|
|
throw new Error('Assertion failed' + (message ? ': ' + message : ''));
|
|
}
|
|
}
|
|
function ensureDefined(value) {
|
|
if (value === undefined) {
|
|
throw new Error('Value is undefined');
|
|
}
|
|
return value;
|
|
}
|
|
function ensureNotNull(value) {
|
|
if (value === null) {
|
|
throw new Error('Value is null');
|
|
}
|
|
return value;
|
|
}
|
|
function ensure(value) {
|
|
return ensureNotNull(ensureDefined(value));
|
|
}
|
|
/**
|
|
* Compile time check for never
|
|
*/
|
|
function ensureNever(value) { }
|
|
|
|
class Delegate {
|
|
constructor() {
|
|
this._private__listeners = [];
|
|
}
|
|
_internal_subscribe(callback, linkedObject, singleshot) {
|
|
const listener = {
|
|
_internal_callback: callback,
|
|
_internal_linkedObject: linkedObject,
|
|
_internal_singleshot: singleshot === true,
|
|
};
|
|
this._private__listeners.push(listener);
|
|
}
|
|
_internal_unsubscribe(callback) {
|
|
const index = this._private__listeners.findIndex((listener) => callback === listener._internal_callback);
|
|
if (index > -1) {
|
|
this._private__listeners.splice(index, 1);
|
|
}
|
|
}
|
|
_internal_unsubscribeAll(linkedObject) {
|
|
this._private__listeners = this._private__listeners.filter((listener) => listener._internal_linkedObject !== linkedObject);
|
|
}
|
|
_internal_fire(param1, param2, param3) {
|
|
const listenersSnapshot = [...this._private__listeners];
|
|
this._private__listeners = this._private__listeners.filter((listener) => !listener._internal_singleshot);
|
|
listenersSnapshot.forEach((listener) => listener._internal_callback(param1, param2, param3));
|
|
}
|
|
_internal_hasListeners() {
|
|
return this._private__listeners.length > 0;
|
|
}
|
|
_internal_destroy() {
|
|
this._private__listeners = [];
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
function merge(dst, ...sources) {
|
|
for (const src of sources) {
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
for (const i in src) {
|
|
if (src[i] === undefined ||
|
|
!Object.prototype.hasOwnProperty.call(src, i) ||
|
|
['__proto__', 'constructor', 'prototype'].includes(i)) {
|
|
continue;
|
|
}
|
|
if ('object' !== typeof src[i] || dst[i] === undefined || Array.isArray(src[i])) {
|
|
dst[i] = src[i];
|
|
}
|
|
else {
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
merge(dst[i], src[i]);
|
|
}
|
|
}
|
|
}
|
|
return dst;
|
|
}
|
|
function isNumber(value) {
|
|
return (typeof value === 'number') && (isFinite(value));
|
|
}
|
|
function isInteger(value) {
|
|
return (typeof value === 'number') && ((value % 1) === 0);
|
|
}
|
|
function isString(value) {
|
|
return typeof value === 'string';
|
|
}
|
|
function isBoolean(value) {
|
|
return typeof value === 'boolean';
|
|
}
|
|
function clone(object) {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const o = object;
|
|
if (!o || 'object' !== typeof o) {
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
return o;
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
let c;
|
|
if (Array.isArray(o)) {
|
|
c = [];
|
|
}
|
|
else {
|
|
c = {};
|
|
}
|
|
let p;
|
|
let v;
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
for (p in o) {
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call,no-prototype-builtins
|
|
if (o.hasOwnProperty(p)) {
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
v = o[p];
|
|
if (v && 'object' === typeof v) {
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
c[p] = clone(v);
|
|
}
|
|
else {
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
c[p] = v;
|
|
}
|
|
}
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
return c;
|
|
}
|
|
function notNull(t) {
|
|
return t !== null;
|
|
}
|
|
function undefinedIfNull(t) {
|
|
return (t === null) ? undefined : t;
|
|
}
|
|
|
|
/**
|
|
* Default font family.
|
|
* Must be used to generate font string when font is not specified.
|
|
*/
|
|
const defaultFontFamily = `-apple-system, BlinkMacSystemFont, 'Trebuchet MS', Roboto, Ubuntu, sans-serif`;
|
|
/**
|
|
* Generates a font string, which can be used to set in canvas' font property.
|
|
* If no family provided, {@link defaultFontFamily} will be used.
|
|
*
|
|
* @param size - Font size in pixels.
|
|
* @param family - Optional font family.
|
|
* @param style - Optional font style.
|
|
* @returns The font string.
|
|
*/
|
|
function makeFont(size, family, style) {
|
|
if (style !== undefined) {
|
|
style = `${style} `;
|
|
}
|
|
else {
|
|
style = '';
|
|
}
|
|
if (family === undefined) {
|
|
family = defaultFontFamily;
|
|
}
|
|
return `${style}${size}px ${family}`;
|
|
}
|
|
|
|
class PriceAxisRendererOptionsProvider {
|
|
constructor(chartModel) {
|
|
this._private__rendererOptions = {
|
|
_internal_borderSize: 1 /* RendererConstants.BorderSize */,
|
|
_internal_tickLength: 5 /* RendererConstants.TickLength */,
|
|
_internal_fontSize: NaN,
|
|
_internal_font: '',
|
|
_internal_fontFamily: '',
|
|
_internal_color: '',
|
|
_internal_paneBackgroundColor: '',
|
|
_internal_paddingBottom: 0,
|
|
_internal_paddingInner: 0,
|
|
_internal_paddingOuter: 0,
|
|
_internal_paddingTop: 0,
|
|
_internal_baselineOffset: 0,
|
|
};
|
|
this._private__chartModel = chartModel;
|
|
}
|
|
_internal_options() {
|
|
const rendererOptions = this._private__rendererOptions;
|
|
const currentFontSize = this._private__fontSize();
|
|
const currentFontFamily = this._private__fontFamily();
|
|
if (rendererOptions._internal_fontSize !== currentFontSize || rendererOptions._internal_fontFamily !== currentFontFamily) {
|
|
rendererOptions._internal_fontSize = currentFontSize;
|
|
rendererOptions._internal_fontFamily = currentFontFamily;
|
|
rendererOptions._internal_font = makeFont(currentFontSize, currentFontFamily);
|
|
rendererOptions._internal_paddingTop = 2.5 / 12 * currentFontSize; // 2.5 px for 12px font
|
|
rendererOptions._internal_paddingBottom = rendererOptions._internal_paddingTop;
|
|
rendererOptions._internal_paddingInner = currentFontSize / 12 * rendererOptions._internal_tickLength;
|
|
rendererOptions._internal_paddingOuter = currentFontSize / 12 * rendererOptions._internal_tickLength;
|
|
rendererOptions._internal_baselineOffset = 0;
|
|
}
|
|
rendererOptions._internal_color = this._private__textColor();
|
|
rendererOptions._internal_paneBackgroundColor = this._private__paneBackgroundColor();
|
|
return this._private__rendererOptions;
|
|
}
|
|
_private__textColor() {
|
|
return this._private__chartModel._internal_options()['layout'].textColor;
|
|
}
|
|
_private__paneBackgroundColor() {
|
|
return this._private__chartModel._internal_backgroundTopColor();
|
|
}
|
|
_private__fontSize() {
|
|
return this._private__chartModel._internal_options()['layout'].fontSize;
|
|
}
|
|
_private__fontFamily() {
|
|
return this._private__chartModel._internal_options()['layout'].fontFamily;
|
|
}
|
|
}
|
|
|
|
function normalizeRgbComponent(component) {
|
|
if (component < 0) {
|
|
return 0;
|
|
}
|
|
if (component > 255) {
|
|
return 255;
|
|
}
|
|
// NaN values are treated as 0
|
|
return (Math.round(component) || 0);
|
|
}
|
|
function normalizeAlphaComponent(component) {
|
|
if (component <= 0 || component > 1) {
|
|
return Math.min(Math.max(component, 0), 1);
|
|
}
|
|
// limit the precision of all numbers to at most 4 digits in fractional part
|
|
return (Math.round(component * 10000) / 10000);
|
|
}
|
|
function rgbaToGrayscale(rgbValue) {
|
|
// Originally, the NTSC RGB to YUV formula
|
|
// perfected by @eugene-korobko's black magic
|
|
const redComponentGrayscaleWeight = 0.199;
|
|
const greenComponentGrayscaleWeight = 0.687;
|
|
const blueComponentGrayscaleWeight = 0.114;
|
|
return (redComponentGrayscaleWeight * rgbValue[0] +
|
|
greenComponentGrayscaleWeight * rgbValue[1] +
|
|
blueComponentGrayscaleWeight * rgbValue[2]);
|
|
}
|
|
/**
|
|
* For colors which fall within the sRGB space, the browser can
|
|
* be used to convert the color string into a rgb /rgba string.
|
|
*
|
|
* For other colors, it will be returned as specified (i.e. for
|
|
* newer formats like display-p3)
|
|
*
|
|
* See: https://www.w3.org/TR/css-color-4/#serializing-sRGB-values
|
|
*/
|
|
function getRgbStringViaBrowser(color) {
|
|
const element = document.createElement('div');
|
|
element.style.display = 'none';
|
|
// We append to the body as it is the most reliable way to get a color reading
|
|
// appending to the chart container or similar element can result in the following
|
|
// getComputedStyle returning empty strings on each check.
|
|
document.body.appendChild(element);
|
|
element.style.color = color;
|
|
const computed = window.getComputedStyle(element).color;
|
|
document.body.removeChild(element);
|
|
return computed;
|
|
}
|
|
class ColorParser {
|
|
constructor(customParsers, initialCache) {
|
|
this._private__rgbaCache = new Map();
|
|
this._private__customParsers = customParsers;
|
|
if (initialCache) {
|
|
this._private__rgbaCache = initialCache;
|
|
}
|
|
}
|
|
/**
|
|
* We fallback to RGBA here since supporting alpha transformations
|
|
* on wider color gamuts would currently be a lot of extra code
|
|
* for very little benefit due to actual usage.
|
|
*/
|
|
_internal_applyAlpha(color, alpha) {
|
|
// special case optimization
|
|
if (color === 'transparent') {
|
|
return color;
|
|
}
|
|
const originRgba = this._private__parseColor(color);
|
|
const originAlpha = originRgba[3];
|
|
return `rgba(${originRgba[0]}, ${originRgba[1]}, ${originRgba[2]}, ${alpha * originAlpha})`;
|
|
}
|
|
_internal_generateContrastColors(background) {
|
|
const rgba = this._private__parseColor(background);
|
|
return {
|
|
_internal_background: `rgb(${rgba[0]}, ${rgba[1]}, ${rgba[2]})`, // no alpha
|
|
_internal_foreground: rgbaToGrayscale(rgba) > 160 ? 'black' : 'white',
|
|
};
|
|
}
|
|
_internal_colorStringToGrayscale(background) {
|
|
return rgbaToGrayscale(this._private__parseColor(background));
|
|
}
|
|
_internal_gradientColorAtPercent(topColor, bottomColor, percent) {
|
|
const [topR, topG, topB, topA] = this._private__parseColor(topColor);
|
|
const [bottomR, bottomG, bottomB, bottomA] = this._private__parseColor(bottomColor);
|
|
const resultRgba = [
|
|
normalizeRgbComponent((topR + percent * (bottomR - topR))),
|
|
normalizeRgbComponent((topG + percent * (bottomG - topG))),
|
|
normalizeRgbComponent((topB + percent * (bottomB - topB))),
|
|
normalizeAlphaComponent((topA + percent * (bottomA - topA))),
|
|
];
|
|
return `rgba(${resultRgba[0]}, ${resultRgba[1]}, ${resultRgba[2]}, ${resultRgba[3]})`;
|
|
}
|
|
_private__parseColor(color) {
|
|
const cached = this._private__rgbaCache.get(color);
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
const computed = getRgbStringViaBrowser(color);
|
|
const match = computed.match(/^rgba?\s*\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d*\.?\d+))?\)$/);
|
|
if (!match) {
|
|
if (this._private__customParsers.length) {
|
|
for (const parser of this._private__customParsers) {
|
|
const result = parser(color);
|
|
if (result) {
|
|
this._private__rgbaCache.set(color, result);
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
throw new Error(`Failed to parse color: ${color}`);
|
|
}
|
|
const rgba = [
|
|
parseInt(match[1], 10),
|
|
parseInt(match[2], 10),
|
|
parseInt(match[3], 10),
|
|
(match[4] ? parseFloat(match[4]) : 1),
|
|
];
|
|
this._private__rgbaCache.set(color, rgba);
|
|
return rgba;
|
|
}
|
|
}
|
|
|
|
class CompositeRenderer {
|
|
constructor() {
|
|
this._private__renderers = [];
|
|
}
|
|
_internal_setRenderers(renderers) {
|
|
this._private__renderers = renderers;
|
|
}
|
|
_internal_draw(target, isHovered, hitTestData) {
|
|
this._private__renderers.forEach((r) => {
|
|
r._internal_draw(target, isHovered, hitTestData);
|
|
});
|
|
}
|
|
}
|
|
|
|
class BitmapCoordinatesPaneRenderer {
|
|
_internal_draw(target, isHovered, hitTestData) {
|
|
target.useBitmapCoordinateSpace((scope) => this._internal__drawImpl(scope, isHovered, hitTestData));
|
|
}
|
|
}
|
|
|
|
class PaneRendererMarks extends BitmapCoordinatesPaneRenderer {
|
|
constructor() {
|
|
super(...arguments);
|
|
this._internal__data = null;
|
|
}
|
|
_internal_setData(data) {
|
|
this._internal__data = data;
|
|
}
|
|
_internal__drawImpl({ context: ctx, horizontalPixelRatio, verticalPixelRatio }) {
|
|
if (this._internal__data === null || this._internal__data._internal_visibleRange === null) {
|
|
return;
|
|
}
|
|
const visibleRange = this._internal__data._internal_visibleRange;
|
|
const data = this._internal__data;
|
|
const tickWidth = Math.max(1, Math.floor(horizontalPixelRatio));
|
|
const correction = (tickWidth % 2) / 2;
|
|
const draw = (radiusMedia) => {
|
|
ctx.beginPath();
|
|
for (let i = visibleRange.to - 1; i >= visibleRange.from; --i) {
|
|
const point = data._internal_items[i];
|
|
const centerX = Math.round(point._internal_x * horizontalPixelRatio) + correction; // correct x coordinate only
|
|
const centerY = point._internal_y * verticalPixelRatio;
|
|
const radius = radiusMedia * verticalPixelRatio + correction;
|
|
ctx.moveTo(centerX, centerY);
|
|
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
|
}
|
|
ctx.fill();
|
|
};
|
|
if (data._internal_lineWidth > 0) {
|
|
ctx.fillStyle = data._internal_backColor;
|
|
draw(data._internal_radius + data._internal_lineWidth);
|
|
}
|
|
ctx.fillStyle = data._internal_lineColor;
|
|
draw(data._internal_radius);
|
|
}
|
|
}
|
|
|
|
function createEmptyMarkerData() {
|
|
return {
|
|
_internal_items: [{
|
|
_internal_x: 0,
|
|
_internal_y: 0,
|
|
_internal_time: 0,
|
|
_internal_price: 0,
|
|
}],
|
|
_internal_lineColor: '',
|
|
_internal_backColor: '',
|
|
_internal_radius: 0,
|
|
_internal_lineWidth: 0,
|
|
_internal_visibleRange: null,
|
|
};
|
|
}
|
|
const rangeForSinglePoint = { from: 0, to: 1 };
|
|
class CrosshairMarksPaneView {
|
|
constructor(chartModel, crosshair, pane) {
|
|
this._private__compositeRenderer = new CompositeRenderer();
|
|
this._private__markersRenderers = [];
|
|
this._private__markersData = [];
|
|
this._private__invalidated = true;
|
|
this._private__chartModel = chartModel;
|
|
this._private__crosshair = crosshair;
|
|
this._private__pane = pane;
|
|
this._private__compositeRenderer._internal_setRenderers(this._private__markersRenderers);
|
|
}
|
|
_internal_update(updateType) {
|
|
this._private__createMarkerRenderersIfNeeded();
|
|
this._private__invalidated = true;
|
|
}
|
|
_internal_renderer() {
|
|
if (this._private__invalidated) {
|
|
this._private__updateImpl();
|
|
this._private__invalidated = false;
|
|
}
|
|
return this._private__compositeRenderer;
|
|
}
|
|
_private__createMarkerRenderersIfNeeded() {
|
|
const serieses = this._private__pane._internal_orderedSources();
|
|
if (serieses.length !== this._private__markersRenderers.length) {
|
|
this._private__markersData = serieses.map(createEmptyMarkerData);
|
|
this._private__markersRenderers = this._private__markersData.map((data) => {
|
|
const res = new PaneRendererMarks();
|
|
res._internal_setData(data);
|
|
return res;
|
|
});
|
|
this._private__compositeRenderer._internal_setRenderers(this._private__markersRenderers);
|
|
}
|
|
}
|
|
_private__updateImpl() {
|
|
const forceHidden = this._private__crosshair._internal_options().mode === 2 /* CrosshairMode.Hidden */ || !this._private__crosshair._internal_visible();
|
|
const serieses = this._private__pane._internal_orderedSeries();
|
|
const timePointIndex = this._private__crosshair._internal_appliedIndex();
|
|
const timeScale = this._private__chartModel._internal_timeScale();
|
|
this._private__createMarkerRenderersIfNeeded();
|
|
serieses.forEach((s, index) => {
|
|
const data = this._private__markersData[index];
|
|
const seriesData = s._internal_markerDataAtIndex(timePointIndex);
|
|
const firstValue = s._internal_firstValue();
|
|
if (forceHidden || seriesData === null || !s._internal_visible() || firstValue === null) {
|
|
data._internal_visibleRange = null;
|
|
return;
|
|
}
|
|
data._internal_lineColor = seriesData._internal_backgroundColor;
|
|
data._internal_radius = seriesData._internal_radius;
|
|
data._internal_lineWidth = seriesData._internal_borderWidth;
|
|
data._internal_items[0]._internal_price = seriesData._internal_price;
|
|
data._internal_items[0]._internal_y = s._internal_priceScale()._internal_priceToCoordinate(seriesData._internal_price, firstValue._internal_value);
|
|
data._internal_backColor = seriesData._internal_borderColor ?? this._private__chartModel._internal_backgroundColorAtYPercentFromTop(data._internal_items[0]._internal_y / s._internal_priceScale()._internal_height());
|
|
data._internal_items[0]._internal_time = timePointIndex;
|
|
data._internal_items[0]._internal_x = timeScale._internal_indexToCoordinate(timePointIndex);
|
|
data._internal_visibleRange = rangeForSinglePoint;
|
|
});
|
|
}
|
|
}
|
|
|
|
class CrosshairRenderer extends BitmapCoordinatesPaneRenderer {
|
|
constructor(data) {
|
|
super();
|
|
this._private__data = data;
|
|
}
|
|
_internal__drawImpl({ context: ctx, bitmapSize, horizontalPixelRatio, verticalPixelRatio }) {
|
|
if (this._private__data === null) {
|
|
return;
|
|
}
|
|
const vertLinesVisible = this._private__data._internal_vertLine._internal_visible;
|
|
const horzLinesVisible = this._private__data._internal_horzLine._internal_visible;
|
|
if (!vertLinesVisible && !horzLinesVisible) {
|
|
return;
|
|
}
|
|
const x = Math.round(this._private__data._internal_x * horizontalPixelRatio);
|
|
const y = Math.round(this._private__data._internal_y * verticalPixelRatio);
|
|
ctx.lineCap = 'butt';
|
|
if (vertLinesVisible && x >= 0) {
|
|
ctx.lineWidth = Math.floor(this._private__data._internal_vertLine._internal_lineWidth * horizontalPixelRatio);
|
|
ctx.strokeStyle = this._private__data._internal_vertLine._internal_color;
|
|
ctx.fillStyle = this._private__data._internal_vertLine._internal_color;
|
|
setLineStyle(ctx, this._private__data._internal_vertLine._internal_lineStyle);
|
|
drawVerticalLine(ctx, x, 0, bitmapSize.height);
|
|
}
|
|
if (horzLinesVisible && y >= 0) {
|
|
ctx.lineWidth = Math.floor(this._private__data._internal_horzLine._internal_lineWidth * verticalPixelRatio);
|
|
ctx.strokeStyle = this._private__data._internal_horzLine._internal_color;
|
|
ctx.fillStyle = this._private__data._internal_horzLine._internal_color;
|
|
setLineStyle(ctx, this._private__data._internal_horzLine._internal_lineStyle);
|
|
drawHorizontalLine(ctx, y, 0, bitmapSize.width);
|
|
}
|
|
}
|
|
}
|
|
|
|
class CrosshairPaneView {
|
|
constructor(source, pane) {
|
|
this._private__invalidated = true;
|
|
this._private__rendererData = {
|
|
_internal_vertLine: {
|
|
_internal_lineWidth: 1,
|
|
_internal_lineStyle: 0,
|
|
_internal_color: '',
|
|
_internal_visible: false,
|
|
},
|
|
_internal_horzLine: {
|
|
_internal_lineWidth: 1,
|
|
_internal_lineStyle: 0,
|
|
_internal_color: '',
|
|
_internal_visible: false,
|
|
},
|
|
_internal_x: 0,
|
|
_internal_y: 0,
|
|
};
|
|
this._private__renderer = new CrosshairRenderer(this._private__rendererData);
|
|
this._private__source = source;
|
|
this._private__pane = pane;
|
|
}
|
|
_internal_update() {
|
|
this._private__invalidated = true;
|
|
}
|
|
_internal_renderer(pane) {
|
|
if (this._private__invalidated) {
|
|
this._private__updateImpl();
|
|
this._private__invalidated = false;
|
|
}
|
|
return this._private__renderer;
|
|
}
|
|
_private__updateImpl() {
|
|
const visible = this._private__source._internal_visible();
|
|
const crosshairOptions = this._private__pane._internal_model()._internal_options().crosshair;
|
|
const data = this._private__rendererData;
|
|
if (crosshairOptions.mode === 2 /* CrosshairMode.Hidden */) {
|
|
data._internal_horzLine._internal_visible = false;
|
|
data._internal_vertLine._internal_visible = false;
|
|
return;
|
|
}
|
|
data._internal_horzLine._internal_visible = visible && this._private__source._internal_horzLineVisible(this._private__pane);
|
|
data._internal_vertLine._internal_visible = visible && this._private__source._internal_vertLineVisible();
|
|
data._internal_horzLine._internal_lineWidth = crosshairOptions.horzLine.width;
|
|
data._internal_horzLine._internal_lineStyle = crosshairOptions.horzLine.style;
|
|
data._internal_horzLine._internal_color = crosshairOptions.horzLine.color;
|
|
data._internal_vertLine._internal_lineWidth = crosshairOptions.vertLine.width;
|
|
data._internal_vertLine._internal_lineStyle = crosshairOptions.vertLine.style;
|
|
data._internal_vertLine._internal_color = crosshairOptions.vertLine.color;
|
|
data._internal_x = this._private__source._internal_appliedX();
|
|
data._internal_y = this._private__source._internal_appliedY();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fills rectangle's inner border (so, all the filled area is limited by the [x, x + width]*[y, y + height] region)
|
|
* ```
|
|
* (x, y)
|
|
* O***********************|*****
|
|
* | border | ^
|
|
* | ***************** | |
|
|
* | | | | |
|
|
* | b | | b | h
|
|
* | o | | o | e
|
|
* | r | | r | i
|
|
* | d | | d | g
|
|
* | e | | e | h
|
|
* | r | | r | t
|
|
* | | | | |
|
|
* | ***************** | |
|
|
* | border | v
|
|
* |***********************|*****
|
|
* | |
|
|
* |<------- width ------->|
|
|
* ```
|
|
*
|
|
* @param ctx - Context to draw on
|
|
* @param x - Left side of the target rectangle
|
|
* @param y - Top side of the target rectangle
|
|
* @param width - Width of the target rectangle
|
|
* @param height - Height of the target rectangle
|
|
* @param borderWidth - Width of border to fill, must be less than width and height of the target rectangle
|
|
*/
|
|
function fillRectInnerBorder(ctx, x, y, width, height, borderWidth) {
|
|
// horizontal (top and bottom) edges
|
|
ctx.fillRect(x + borderWidth, y, width - borderWidth * 2, borderWidth);
|
|
ctx.fillRect(x + borderWidth, y + height - borderWidth, width - borderWidth * 2, borderWidth);
|
|
// vertical (left and right) edges
|
|
ctx.fillRect(x, y, borderWidth, height);
|
|
ctx.fillRect(x + width - borderWidth, y, borderWidth, height);
|
|
}
|
|
function clearRect(ctx, x, y, w, h, clearColor) {
|
|
ctx.save();
|
|
ctx.globalCompositeOperation = 'copy';
|
|
ctx.fillStyle = clearColor;
|
|
ctx.fillRect(x, y, w, h);
|
|
ctx.restore();
|
|
}
|
|
function changeBorderRadius(borderRadius, offset) {
|
|
return borderRadius.map((x) => x === 0 ? x : x + offset);
|
|
}
|
|
function drawRoundRect(
|
|
// eslint:disable-next-line:max-params
|
|
ctx, x, y, w, h, radii) {
|
|
/**
|
|
* As of May 2023, all of the major browsers now support ctx.roundRect() so we should
|
|
* be able to switch to the native version soon.
|
|
*/
|
|
ctx.beginPath();
|
|
if (ctx.roundRect) {
|
|
ctx.roundRect(x, y, w, h, radii);
|
|
return;
|
|
}
|
|
/*
|
|
* Deprecate the rest in v5.
|
|
*/
|
|
ctx.lineTo(x + w - radii[1], y);
|
|
if (radii[1] !== 0) {
|
|
ctx.arcTo(x + w, y, x + w, y + radii[1], radii[1]);
|
|
}
|
|
ctx.lineTo(x + w, y + h - radii[2]);
|
|
if (radii[2] !== 0) {
|
|
ctx.arcTo(x + w, y + h, x + w - radii[2], y + h, radii[2]);
|
|
}
|
|
ctx.lineTo(x + radii[3], y + h);
|
|
if (radii[3] !== 0) {
|
|
ctx.arcTo(x, y + h, x, y + h - radii[3], radii[3]);
|
|
}
|
|
ctx.lineTo(x, y + radii[0]);
|
|
if (radii[0] !== 0) {
|
|
ctx.arcTo(x, y, x + radii[0], y, radii[0]);
|
|
}
|
|
}
|
|
/**
|
|
* Draws a rounded rect with a border.
|
|
*
|
|
* This function assumes that the colors will be solid, without
|
|
* any alpha. (This allows us to fix a rendering artefact.)
|
|
*
|
|
* @param outerBorderRadius - The radius of the border (outer edge)
|
|
*/
|
|
// eslint-disable-next-line max-params
|
|
function drawRoundRectWithBorder(ctx, left, top, width, height, backgroundColor, borderWidth = 0, outerBorderRadius = [0, 0, 0, 0], borderColor = '') {
|
|
ctx.save();
|
|
if (!borderWidth || !borderColor || borderColor === backgroundColor) {
|
|
drawRoundRect(ctx, left, top, width, height, outerBorderRadius);
|
|
ctx.fillStyle = backgroundColor;
|
|
ctx.fill();
|
|
ctx.restore();
|
|
return;
|
|
}
|
|
const halfBorderWidth = borderWidth / 2;
|
|
const radii = changeBorderRadius(outerBorderRadius, -halfBorderWidth);
|
|
drawRoundRect(ctx, left + halfBorderWidth, top + halfBorderWidth, width - borderWidth, height - borderWidth, radii);
|
|
if (backgroundColor !== 'transparent') {
|
|
ctx.fillStyle = backgroundColor;
|
|
ctx.fill();
|
|
}
|
|
if (borderColor !== 'transparent') {
|
|
ctx.lineWidth = borderWidth;
|
|
ctx.strokeStyle = borderColor;
|
|
ctx.closePath();
|
|
ctx.stroke();
|
|
}
|
|
ctx.restore();
|
|
}
|
|
// eslint-disable-next-line max-params
|
|
function clearRectWithGradient(ctx, x, y, w, h, topColor, bottomColor) {
|
|
ctx.save();
|
|
ctx.globalCompositeOperation = 'copy';
|
|
const gradient = ctx.createLinearGradient(0, 0, 0, h);
|
|
gradient.addColorStop(0, topColor);
|
|
gradient.addColorStop(1, bottomColor);
|
|
ctx.fillStyle = gradient;
|
|
ctx.fillRect(x, y, w, h);
|
|
ctx.restore();
|
|
}
|
|
|
|
class PriceAxisViewRenderer {
|
|
constructor(data, commonData) {
|
|
this._internal_setData(data, commonData);
|
|
}
|
|
_internal_setData(data, commonData) {
|
|
this._private__data = data;
|
|
this._private__commonData = commonData;
|
|
}
|
|
_internal_height(rendererOptions, useSecondLine) {
|
|
if (!this._private__data._internal_visible) {
|
|
return 0;
|
|
}
|
|
return rendererOptions._internal_fontSize + rendererOptions._internal_paddingTop + rendererOptions._internal_paddingBottom;
|
|
}
|
|
_internal_draw(target, rendererOptions, textWidthCache, align) {
|
|
if (!this._private__data._internal_visible || this._private__data._internal_text.length === 0) {
|
|
return;
|
|
}
|
|
const textColor = this._private__data._internal_color;
|
|
const backgroundColor = this._private__commonData._internal_background;
|
|
const geometry = target.useBitmapCoordinateSpace((scope) => {
|
|
const ctx = scope.context;
|
|
ctx.font = rendererOptions._internal_font;
|
|
const geom = this._private__calculateGeometry(scope, rendererOptions, textWidthCache, align);
|
|
const gb = geom._internal_bitmap;
|
|
/*
|
|
draw label. backgroundColor will always be a solid color (no alpha) [see generateContrastColors in color.ts].
|
|
Therefore we can draw the rounded label using simplified code (drawRoundRectWithBorder) that doesn't need to ensure the background and the border don't overlap.
|
|
*/
|
|
if (geom._internal_alignRight) {
|
|
drawRoundRectWithBorder(ctx, gb._internal_xOutside, gb._internal_yTop, gb._internal_totalWidth, gb._internal_totalHeight, backgroundColor, gb._internal_horzBorder, [gb._internal_radius, 0, 0, gb._internal_radius], backgroundColor);
|
|
}
|
|
else {
|
|
drawRoundRectWithBorder(ctx, gb._internal_xInside, gb._internal_yTop, gb._internal_totalWidth, gb._internal_totalHeight, backgroundColor, gb._internal_horzBorder, [0, gb._internal_radius, gb._internal_radius, 0], backgroundColor);
|
|
}
|
|
// draw tick
|
|
if (this._private__data._internal_tickVisible) {
|
|
ctx.fillStyle = textColor;
|
|
ctx.fillRect(gb._internal_xInside, gb._internal_yMid, gb._internal_xTick - gb._internal_xInside, gb._internal_tickHeight);
|
|
}
|
|
// draw separator
|
|
if (this._private__data._internal_borderVisible) {
|
|
ctx.fillStyle = rendererOptions._internal_paneBackgroundColor;
|
|
ctx.fillRect(geom._internal_alignRight ? gb._internal_right - gb._internal_horzBorder : 0, gb._internal_yTop, gb._internal_horzBorder, gb._internal_yBottom - gb._internal_yTop);
|
|
}
|
|
return geom;
|
|
});
|
|
target.useMediaCoordinateSpace(({ context: ctx }) => {
|
|
const gm = geometry._internal_media;
|
|
ctx.font = rendererOptions._internal_font;
|
|
ctx.textAlign = geometry._internal_alignRight ? 'right' : 'left';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillStyle = textColor;
|
|
ctx.fillText(this._private__data._internal_text, gm._internal_xText, (gm._internal_yTop + gm._internal_yBottom) / 2 + gm._internal_textMidCorrection);
|
|
});
|
|
}
|
|
_private__calculateGeometry(scope, rendererOptions, textWidthCache, align) {
|
|
const { context: ctx, bitmapSize, mediaSize, horizontalPixelRatio, verticalPixelRatio } = scope;
|
|
const tickSize = (this._private__data._internal_tickVisible || !this._private__data._internal_moveTextToInvisibleTick) ? rendererOptions._internal_tickLength : 0;
|
|
const horzBorder = this._private__data._internal_separatorVisible ? rendererOptions._internal_borderSize : 0;
|
|
const paddingTop = rendererOptions._internal_paddingTop + this._private__commonData._internal_additionalPaddingTop;
|
|
const paddingBottom = rendererOptions._internal_paddingBottom + this._private__commonData._internal_additionalPaddingBottom;
|
|
const paddingInner = rendererOptions._internal_paddingInner;
|
|
const paddingOuter = rendererOptions._internal_paddingOuter;
|
|
const text = this._private__data._internal_text;
|
|
const actualTextHeight = rendererOptions._internal_fontSize;
|
|
const textMidCorrection = textWidthCache._internal_yMidCorrection(ctx, text);
|
|
const textWidth = Math.ceil(textWidthCache._internal_measureText(ctx, text));
|
|
const totalHeight = actualTextHeight + paddingTop + paddingBottom;
|
|
const totalWidth = rendererOptions._internal_borderSize + paddingInner + paddingOuter + textWidth + tickSize;
|
|
const tickHeightBitmap = Math.max(1, Math.floor(verticalPixelRatio));
|
|
let totalHeightBitmap = Math.round(totalHeight * verticalPixelRatio);
|
|
if (totalHeightBitmap % 2 !== tickHeightBitmap % 2) {
|
|
totalHeightBitmap += 1;
|
|
}
|
|
const horzBorderBitmap = horzBorder > 0 ? Math.max(1, Math.floor(horzBorder * horizontalPixelRatio)) : 0;
|
|
const totalWidthBitmap = Math.round(totalWidth * horizontalPixelRatio);
|
|
// tick overlaps scale border
|
|
const tickSizeBitmap = Math.round(tickSize * horizontalPixelRatio);
|
|
const yMid = this._private__commonData._internal_fixedCoordinate ?? this._private__commonData._internal_renderCoordinate ?? this._private__commonData._internal_coordinate;
|
|
const yMidBitmap = Math.round(yMid * verticalPixelRatio) - Math.floor(verticalPixelRatio * 0.5);
|
|
const yTopBitmap = Math.floor(yMidBitmap + tickHeightBitmap / 2 - totalHeightBitmap / 2);
|
|
const yBottomBitmap = yTopBitmap + totalHeightBitmap;
|
|
const alignRight = align === 'right';
|
|
const xInside = alignRight ? mediaSize.width - horzBorder : horzBorder;
|
|
const xInsideBitmap = alignRight ? bitmapSize.width - horzBorderBitmap : horzBorderBitmap;
|
|
let xOutsideBitmap;
|
|
let xTickBitmap;
|
|
let xText;
|
|
if (alignRight) {
|
|
// 2 1
|
|
//
|
|
// 6 5
|
|
//
|
|
// 3 4
|
|
xOutsideBitmap = xInsideBitmap - totalWidthBitmap;
|
|
xTickBitmap = xInsideBitmap - tickSizeBitmap;
|
|
xText = xInside - tickSize - paddingInner - horzBorder;
|
|
}
|
|
else {
|
|
// 1 2
|
|
//
|
|
// 6 5
|
|
//
|
|
// 4 3
|
|
xOutsideBitmap = xInsideBitmap + totalWidthBitmap;
|
|
xTickBitmap = xInsideBitmap + tickSizeBitmap;
|
|
xText = xInside + tickSize + paddingInner;
|
|
}
|
|
return {
|
|
_internal_alignRight: alignRight,
|
|
_internal_bitmap: {
|
|
_internal_yTop: yTopBitmap,
|
|
_internal_yMid: yMidBitmap,
|
|
_internal_yBottom: yBottomBitmap,
|
|
_internal_totalWidth: totalWidthBitmap,
|
|
_internal_totalHeight: totalHeightBitmap,
|
|
// TODO: it is better to have different horizontal and vertical radii
|
|
_internal_radius: 2 * horizontalPixelRatio,
|
|
_internal_horzBorder: horzBorderBitmap,
|
|
_internal_xOutside: xOutsideBitmap,
|
|
_internal_xInside: xInsideBitmap,
|
|
_internal_xTick: xTickBitmap,
|
|
_internal_tickHeight: tickHeightBitmap,
|
|
_internal_right: bitmapSize.width,
|
|
},
|
|
_internal_media: {
|
|
_internal_yTop: yTopBitmap / verticalPixelRatio,
|
|
_internal_yBottom: yBottomBitmap / verticalPixelRatio,
|
|
_internal_xText: xText,
|
|
_internal_textMidCorrection: textMidCorrection,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
class PriceAxisView {
|
|
constructor(ctor) {
|
|
this._private__commonRendererData = {
|
|
_internal_coordinate: 0,
|
|
_internal_background: '#000',
|
|
_internal_additionalPaddingBottom: 0,
|
|
_internal_additionalPaddingTop: 0,
|
|
};
|
|
this._private__axisRendererData = {
|
|
_internal_text: '',
|
|
_internal_visible: false,
|
|
_internal_tickVisible: true,
|
|
_internal_moveTextToInvisibleTick: false,
|
|
_internal_borderColor: '',
|
|
_internal_color: '#FFF',
|
|
_internal_borderVisible: false,
|
|
_internal_separatorVisible: false,
|
|
};
|
|
this._private__paneRendererData = {
|
|
_internal_text: '',
|
|
_internal_visible: false,
|
|
_internal_tickVisible: false,
|
|
_internal_moveTextToInvisibleTick: true,
|
|
_internal_borderColor: '',
|
|
_internal_color: '#FFF',
|
|
_internal_borderVisible: true,
|
|
_internal_separatorVisible: true,
|
|
};
|
|
this._private__invalidated = true;
|
|
this._private__axisRenderer = new (ctor || PriceAxisViewRenderer)(this._private__axisRendererData, this._private__commonRendererData);
|
|
this._private__paneRenderer = new (ctor || PriceAxisViewRenderer)(this._private__paneRendererData, this._private__commonRendererData);
|
|
}
|
|
_internal_text() {
|
|
this._private__updateRendererDataIfNeeded();
|
|
return this._private__axisRendererData._internal_text;
|
|
}
|
|
_internal_coordinate() {
|
|
this._private__updateRendererDataIfNeeded();
|
|
return this._private__commonRendererData._internal_coordinate;
|
|
}
|
|
_internal_update() {
|
|
this._private__invalidated = true;
|
|
}
|
|
_internal_height(rendererOptions, useSecondLine = false) {
|
|
return Math.max(this._private__axisRenderer._internal_height(rendererOptions, useSecondLine), this._private__paneRenderer._internal_height(rendererOptions, useSecondLine));
|
|
}
|
|
_internal_getFixedCoordinate() {
|
|
return this._private__commonRendererData._internal_fixedCoordinate ?? null;
|
|
}
|
|
_internal_getRenderCoordinate() {
|
|
return this._private__commonRendererData._internal_fixedCoordinate ?? this._private__commonRendererData._internal_renderCoordinate ?? this._internal_coordinate();
|
|
}
|
|
_internal_setRenderCoordinate(value) {
|
|
this._private__commonRendererData._internal_renderCoordinate = value ?? undefined;
|
|
}
|
|
_internal_isVisible() {
|
|
this._private__updateRendererDataIfNeeded();
|
|
return this._private__axisRendererData._internal_visible || this._private__paneRendererData._internal_visible;
|
|
}
|
|
_internal_isAxisLabelVisible() {
|
|
this._private__updateRendererDataIfNeeded();
|
|
return this._private__axisRendererData._internal_visible;
|
|
}
|
|
_internal_renderer(priceScale) {
|
|
this._private__updateRendererDataIfNeeded();
|
|
// force update tickVisible state from price scale options
|
|
// because we don't have and we can't have price axis in other methods
|
|
// (like paneRenderer or any other who call _updateRendererDataIfNeeded)
|
|
this._private__axisRendererData._internal_tickVisible = this._private__axisRendererData._internal_tickVisible && priceScale._internal_options().ticksVisible;
|
|
this._private__paneRendererData._internal_tickVisible = this._private__paneRendererData._internal_tickVisible && priceScale._internal_options().ticksVisible;
|
|
this._private__axisRenderer._internal_setData(this._private__axisRendererData, this._private__commonRendererData);
|
|
this._private__paneRenderer._internal_setData(this._private__paneRendererData, this._private__commonRendererData);
|
|
return this._private__axisRenderer;
|
|
}
|
|
_internal_paneRenderer() {
|
|
this._private__updateRendererDataIfNeeded();
|
|
this._private__axisRenderer._internal_setData(this._private__axisRendererData, this._private__commonRendererData);
|
|
this._private__paneRenderer._internal_setData(this._private__paneRendererData, this._private__commonRendererData);
|
|
return this._private__paneRenderer;
|
|
}
|
|
_private__updateRendererDataIfNeeded() {
|
|
if (this._private__invalidated) {
|
|
this._private__axisRendererData._internal_tickVisible = true;
|
|
this._private__paneRendererData._internal_tickVisible = false;
|
|
this._internal__updateRendererData(this._private__axisRendererData, this._private__paneRendererData, this._private__commonRendererData);
|
|
}
|
|
}
|
|
}
|
|
|
|
class CrosshairPriceAxisView extends PriceAxisView {
|
|
constructor(source, priceScale, valueProvider) {
|
|
super();
|
|
this._private__source = source;
|
|
this._private__priceScale = priceScale;
|
|
this._private__valueProvider = valueProvider;
|
|
}
|
|
_internal__updateRendererData(axisRendererData, paneRendererData, commonRendererData) {
|
|
axisRendererData._internal_visible = false;
|
|
if (this._private__source._internal_options().mode === 2 /* CrosshairMode.Hidden */) {
|
|
return;
|
|
}
|
|
const options = this._private__source._internal_options().horzLine;
|
|
if (!options.labelVisible) {
|
|
return;
|
|
}
|
|
const firstValue = this._private__priceScale._internal_firstValue();
|
|
if (!this._private__source._internal_visible() || this._private__priceScale._internal_isEmpty() || (firstValue === null)) {
|
|
return;
|
|
}
|
|
const colors = this._private__priceScale._internal_colorParser()._internal_generateContrastColors(options.labelBackgroundColor);
|
|
commonRendererData._internal_background = colors._internal_background;
|
|
axisRendererData._internal_color = colors._internal_foreground;
|
|
const additionalPadding = 2 / 12 * this._private__priceScale._internal_fontSize();
|
|
commonRendererData._internal_additionalPaddingTop = additionalPadding;
|
|
commonRendererData._internal_additionalPaddingBottom = additionalPadding;
|
|
const value = this._private__valueProvider(this._private__priceScale);
|
|
commonRendererData._internal_coordinate = value._internal_coordinate;
|
|
axisRendererData._internal_text = this._private__priceScale._internal_formatPrice(value._internal_price, firstValue);
|
|
axisRendererData._internal_visible = true;
|
|
}
|
|
}
|
|
|
|
const optimizationReplacementRe = /[1-9]/g;
|
|
const radius$1 = 2;
|
|
class TimeAxisViewRenderer {
|
|
constructor() {
|
|
this._private__data = null;
|
|
}
|
|
_internal_setData(data) {
|
|
this._private__data = data;
|
|
}
|
|
_internal_draw(target, rendererOptions) {
|
|
if (this._private__data === null || this._private__data._internal_visible === false || this._private__data._internal_text.length === 0) {
|
|
return;
|
|
}
|
|
const textWidth = target.useMediaCoordinateSpace(({ context: ctx }) => {
|
|
ctx.font = rendererOptions._internal_font;
|
|
return Math.round(rendererOptions._internal_widthCache._internal_measureText(ctx, ensureNotNull(this._private__data)._internal_text, optimizationReplacementRe));
|
|
});
|
|
if (textWidth <= 0) {
|
|
return;
|
|
}
|
|
const horzMargin = rendererOptions._internal_paddingHorizontal;
|
|
const labelWidth = textWidth + 2 * horzMargin;
|
|
const labelWidthHalf = labelWidth / 2;
|
|
const timeScaleWidth = this._private__data._internal_width;
|
|
let coordinate = this._private__data._internal_coordinate;
|
|
let x1 = Math.floor(coordinate - labelWidthHalf) + 0.5;
|
|
if (x1 < 0) {
|
|
coordinate = coordinate + Math.abs(0 - x1);
|
|
x1 = Math.floor(coordinate - labelWidthHalf) + 0.5;
|
|
}
|
|
else if (x1 + labelWidth > timeScaleWidth) {
|
|
coordinate = coordinate - Math.abs(timeScaleWidth - (x1 + labelWidth));
|
|
x1 = Math.floor(coordinate - labelWidthHalf) + 0.5;
|
|
}
|
|
const x2 = x1 + labelWidth;
|
|
const y1 = 0;
|
|
const y2 = Math.ceil(y1 +
|
|
rendererOptions._internal_borderSize +
|
|
rendererOptions._internal_tickLength +
|
|
rendererOptions._internal_paddingTop +
|
|
rendererOptions._internal_fontSize +
|
|
rendererOptions._internal_paddingBottom);
|
|
target.useBitmapCoordinateSpace(({ context: ctx, horizontalPixelRatio, verticalPixelRatio }) => {
|
|
const data = ensureNotNull(this._private__data);
|
|
ctx.fillStyle = data._internal_background;
|
|
const x1scaled = Math.round(x1 * horizontalPixelRatio);
|
|
const y1scaled = Math.round(y1 * verticalPixelRatio);
|
|
const x2scaled = Math.round(x2 * horizontalPixelRatio);
|
|
const y2scaled = Math.round(y2 * verticalPixelRatio);
|
|
const radiusScaled = Math.round(radius$1 * horizontalPixelRatio);
|
|
ctx.beginPath();
|
|
ctx.moveTo(x1scaled, y1scaled);
|
|
ctx.lineTo(x1scaled, y2scaled - radiusScaled);
|
|
ctx.arcTo(x1scaled, y2scaled, x1scaled + radiusScaled, y2scaled, radiusScaled);
|
|
ctx.lineTo(x2scaled - radiusScaled, y2scaled);
|
|
ctx.arcTo(x2scaled, y2scaled, x2scaled, y2scaled - radiusScaled, radiusScaled);
|
|
ctx.lineTo(x2scaled, y1scaled);
|
|
ctx.fill();
|
|
if (data._internal_tickVisible) {
|
|
const tickX = Math.round(data._internal_coordinate * horizontalPixelRatio);
|
|
const tickTop = y1scaled;
|
|
const tickBottom = Math.round((tickTop + rendererOptions._internal_tickLength) * verticalPixelRatio);
|
|
ctx.fillStyle = data._internal_color;
|
|
const tickWidth = Math.max(1, Math.floor(horizontalPixelRatio));
|
|
const tickOffset = Math.floor(horizontalPixelRatio * 0.5);
|
|
ctx.fillRect(tickX - tickOffset, tickTop, tickWidth, tickBottom - tickTop);
|
|
}
|
|
});
|
|
target.useMediaCoordinateSpace(({ context: ctx }) => {
|
|
const data = ensureNotNull(this._private__data);
|
|
const yText = y1 +
|
|
rendererOptions._internal_borderSize +
|
|
rendererOptions._internal_tickLength +
|
|
rendererOptions._internal_paddingTop +
|
|
rendererOptions._internal_fontSize / 2;
|
|
ctx.font = rendererOptions._internal_font;
|
|
ctx.textAlign = 'left';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillStyle = data._internal_color;
|
|
const textYCorrection = rendererOptions._internal_widthCache._internal_yMidCorrection(ctx, 'Apr0');
|
|
ctx.translate(x1 + horzMargin, yText + textYCorrection);
|
|
ctx.fillText(data._internal_text, 0, 0);
|
|
});
|
|
}
|
|
}
|
|
|
|
class CrosshairTimeAxisView {
|
|
constructor(crosshair, model, valueProvider) {
|
|
this._private__invalidated = true;
|
|
this._private__renderer = new TimeAxisViewRenderer();
|
|
this._private__rendererData = {
|
|
_internal_visible: false,
|
|
_internal_background: '#4c525e',
|
|
_internal_color: 'white',
|
|
_internal_text: '',
|
|
_internal_width: 0,
|
|
_internal_coordinate: NaN,
|
|
_internal_tickVisible: true,
|
|
};
|
|
this._private__crosshair = crosshair;
|
|
this._private__model = model;
|
|
this._private__valueProvider = valueProvider;
|
|
}
|
|
_internal_update() {
|
|
this._private__invalidated = true;
|
|
}
|
|
_internal_renderer() {
|
|
if (this._private__invalidated) {
|
|
this._private__updateImpl();
|
|
this._private__invalidated = false;
|
|
}
|
|
this._private__renderer._internal_setData(this._private__rendererData);
|
|
return this._private__renderer;
|
|
}
|
|
_private__updateImpl() {
|
|
const data = this._private__rendererData;
|
|
data._internal_visible = false;
|
|
if (this._private__crosshair._internal_options().mode === 2 /* CrosshairMode.Hidden */) {
|
|
return;
|
|
}
|
|
const options = this._private__crosshair._internal_options().vertLine;
|
|
if (!options.labelVisible) {
|
|
return;
|
|
}
|
|
const timeScale = this._private__model._internal_timeScale();
|
|
if (timeScale._internal_isEmpty()) {
|
|
return;
|
|
}
|
|
data._internal_width = timeScale._internal_width();
|
|
const value = this._private__valueProvider();
|
|
if (value === null) {
|
|
return;
|
|
}
|
|
data._internal_coordinate = value._internal_coordinate;
|
|
const currentTime = timeScale._internal_indexToTimeScalePoint(this._private__crosshair._internal_appliedIndex());
|
|
data._internal_text = timeScale._internal_formatDateTime(ensureNotNull(currentTime));
|
|
data._internal_visible = true;
|
|
const colors = this._private__model._internal_colorParser()._internal_generateContrastColors(options.labelBackgroundColor);
|
|
data._internal_background = colors._internal_background;
|
|
data._internal_color = colors._internal_foreground;
|
|
data._internal_tickVisible = timeScale._internal_options().ticksVisible;
|
|
}
|
|
}
|
|
|
|
class DataSource {
|
|
constructor() {
|
|
this._internal__priceScale = null;
|
|
this._private__zorder = 0;
|
|
}
|
|
_internal_zorder() {
|
|
return this._private__zorder;
|
|
}
|
|
_internal_setZorder(zorder) {
|
|
this._private__zorder = zorder;
|
|
}
|
|
_internal_priceScale() {
|
|
return this._internal__priceScale;
|
|
}
|
|
_internal_setPriceScale(priceScale) {
|
|
this._internal__priceScale = priceScale;
|
|
}
|
|
_internal_labelPaneViews(pane) {
|
|
return [];
|
|
}
|
|
_internal_timeAxisViews() {
|
|
return [];
|
|
}
|
|
_internal_visible() {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Represents the crosshair mode.
|
|
*/
|
|
var CrosshairMode;
|
|
(function (CrosshairMode) {
|
|
/**
|
|
* This mode allows crosshair to move freely on the chart.
|
|
*/
|
|
CrosshairMode[CrosshairMode["Normal"] = 0] = "Normal";
|
|
/**
|
|
* This mode sticks crosshair's horizontal line to the price value of a single-value series or to the close price of OHLC-based series.
|
|
*/
|
|
CrosshairMode[CrosshairMode["Magnet"] = 1] = "Magnet";
|
|
/**
|
|
* This mode disables rendering of the crosshair.
|
|
*/
|
|
CrosshairMode[CrosshairMode["Hidden"] = 2] = "Hidden";
|
|
/**
|
|
* This mode sticks crosshair's horizontal line to the price value of a single-value series or to the open/high/low/close price of OHLC-based series.
|
|
*/
|
|
CrosshairMode[CrosshairMode["MagnetOHLC"] = 3] = "MagnetOHLC";
|
|
})(CrosshairMode || (CrosshairMode = {}));
|
|
class Crosshair extends DataSource {
|
|
constructor(model, options) {
|
|
super();
|
|
this._private__pane = null;
|
|
this._private__price = NaN;
|
|
this._private__index = 0;
|
|
this._private__visible = false; // initially the crosshair should not be visible, until the user interacts.
|
|
this._private__priceAxisViews = new Map();
|
|
this._private__subscribed = false;
|
|
this._private__crosshairPaneViewCache = new WeakMap();
|
|
this._private__markersPaneViewCache = new WeakMap();
|
|
this._private__x = NaN;
|
|
this._private__y = NaN;
|
|
this._private__originX = NaN;
|
|
this._private__originY = NaN;
|
|
this._private__model = model;
|
|
this._private__options = options;
|
|
const valuePriceProvider = (rawPriceProvider, rawCoordinateProvider) => {
|
|
return (priceScale) => {
|
|
const coordinate = rawCoordinateProvider();
|
|
const rawPrice = rawPriceProvider();
|
|
if (priceScale === ensureNotNull(this._private__pane)._internal_defaultPriceScale()) {
|
|
// price must be defined
|
|
return { _internal_price: rawPrice, _internal_coordinate: coordinate };
|
|
}
|
|
else {
|
|
// always convert from coordinate
|
|
const firstValue = ensureNotNull(priceScale._internal_firstValue());
|
|
const price = priceScale._internal_coordinateToPrice(coordinate, firstValue);
|
|
return { _internal_price: price, _internal_coordinate: coordinate };
|
|
}
|
|
};
|
|
};
|
|
const valueTimeProvider = (rawIndexProvider, rawCoordinateProvider) => {
|
|
return () => {
|
|
const time = this._private__model._internal_timeScale()._internal_indexToTime(rawIndexProvider());
|
|
const coordinate = rawCoordinateProvider();
|
|
if (!time || !Number.isFinite(coordinate)) {
|
|
return null;
|
|
}
|
|
return {
|
|
_internal_time: time,
|
|
_internal_coordinate: coordinate,
|
|
};
|
|
};
|
|
};
|
|
// for current position always return both price and coordinate
|
|
this._private__currentPosPriceProvider = valuePriceProvider(() => this._private__price, () => this._private__y);
|
|
const currentPosTimeProvider = valueTimeProvider(() => this._private__index, () => this._internal_appliedX());
|
|
this._private__timeAxisView = new CrosshairTimeAxisView(this, model, currentPosTimeProvider);
|
|
}
|
|
_internal_options() {
|
|
return this._private__options;
|
|
}
|
|
_internal_saveOriginCoord(x, y) {
|
|
this._private__originX = x;
|
|
this._private__originY = y;
|
|
}
|
|
_internal_clearOriginCoord() {
|
|
this._private__originX = NaN;
|
|
this._private__originY = NaN;
|
|
}
|
|
_internal_originCoordX() {
|
|
return this._private__originX;
|
|
}
|
|
_internal_originCoordY() {
|
|
return this._private__originY;
|
|
}
|
|
_internal_setPosition(index, price, pane) {
|
|
if (!this._private__subscribed) {
|
|
this._private__subscribed = true;
|
|
}
|
|
this._private__visible = true;
|
|
this._private__tryToUpdateViews(index, price, pane);
|
|
}
|
|
_internal_appliedIndex() {
|
|
return this._private__index;
|
|
}
|
|
_internal_appliedX() {
|
|
return this._private__x;
|
|
}
|
|
_internal_appliedY() {
|
|
return this._private__y;
|
|
}
|
|
_internal_visible() {
|
|
return this._private__visible;
|
|
}
|
|
_internal_clearPosition() {
|
|
this._private__visible = false;
|
|
this._private__setIndexToLastSeriesBarIndex();
|
|
this._private__price = NaN;
|
|
this._private__x = NaN;
|
|
this._private__y = NaN;
|
|
this._private__pane = null;
|
|
this._internal_clearOriginCoord();
|
|
this._internal_updateAllViews();
|
|
}
|
|
_internal_snapToVisibleSeriesIfNeeded(index) {
|
|
if (!this._private__options.doNotSnapToHiddenSeriesIndices) {
|
|
return index;
|
|
}
|
|
const model = this._private__model;
|
|
const timeScale = model._internal_timeScale();
|
|
let closestLeftIndex = null;
|
|
let closestRightIndex = null;
|
|
for (const series of model._internal_visibleSerieses()) {
|
|
const leftResult = series._internal_bars()._internal_search(index, -1 /* MismatchDirection.NearestLeft */);
|
|
if (leftResult) {
|
|
if (leftResult._internal_index === index) {
|
|
return index; // already snapped
|
|
}
|
|
if (closestLeftIndex === null || leftResult._internal_index > closestLeftIndex) {
|
|
closestLeftIndex = leftResult._internal_index;
|
|
}
|
|
}
|
|
const rightResult = series._internal_bars()._internal_search(index, 1 /* MismatchDirection.NearestRight */);
|
|
if (rightResult) {
|
|
if (rightResult._internal_index === index) {
|
|
return index; // already snapped
|
|
}
|
|
if (closestRightIndex === null || rightResult._internal_index < closestRightIndex) {
|
|
closestRightIndex = rightResult._internal_index;
|
|
}
|
|
}
|
|
}
|
|
const candidates = [closestLeftIndex, closestRightIndex].filter(notNull);
|
|
if (candidates.length === 0) {
|
|
return index;
|
|
}
|
|
const x = timeScale._internal_indexToCoordinate(index);
|
|
const distances = candidates.map((i) => Math.abs(x - timeScale._internal_indexToCoordinate(i)));
|
|
const minDistanceIndex = distances.indexOf(Math.min(...distances));
|
|
return candidates[minDistanceIndex];
|
|
}
|
|
_internal_paneViews(pane) {
|
|
let crosshairPaneView = this._private__crosshairPaneViewCache.get(pane);
|
|
if (!crosshairPaneView) {
|
|
crosshairPaneView = new CrosshairPaneView(this, pane);
|
|
this._private__crosshairPaneViewCache.set(pane, crosshairPaneView);
|
|
}
|
|
let markersPaneView = this._private__markersPaneViewCache.get(pane);
|
|
if (!markersPaneView) {
|
|
markersPaneView = new CrosshairMarksPaneView(this._private__model, this, pane);
|
|
this._private__markersPaneViewCache.set(pane, markersPaneView);
|
|
}
|
|
return [crosshairPaneView, markersPaneView];
|
|
}
|
|
_internal_horzLineVisible(pane) {
|
|
return pane === this._private__pane && this._private__options.horzLine.visible;
|
|
}
|
|
_internal_vertLineVisible() {
|
|
return this._private__options.vertLine.visible;
|
|
}
|
|
_internal_priceAxisViews(pane, priceScale) {
|
|
if (!this._private__visible || this._private__pane !== pane) {
|
|
this._private__priceAxisViews.clear();
|
|
}
|
|
const views = [];
|
|
if (this._private__pane === pane) {
|
|
views.push(this._private__createPriceAxisViewOnDemand(this._private__priceAxisViews, priceScale, this._private__currentPosPriceProvider));
|
|
}
|
|
return views;
|
|
}
|
|
_internal_timeAxisViews() {
|
|
return this._private__visible ? [this._private__timeAxisView] : [];
|
|
}
|
|
_internal_pane() {
|
|
return this._private__pane;
|
|
}
|
|
_internal_updateAllViews() {
|
|
this._private__model._internal_panes().forEach((pane) => {
|
|
this._private__crosshairPaneViewCache.get(pane)?._internal_update();
|
|
this._private__markersPaneViewCache.get(pane)?._internal_update();
|
|
});
|
|
this._private__priceAxisViews.forEach((value) => value._internal_update());
|
|
this._private__timeAxisView._internal_update();
|
|
}
|
|
_private__priceScaleByPane(pane) {
|
|
if (pane && !pane._internal_defaultPriceScale()._internal_isEmpty()) {
|
|
return pane._internal_defaultPriceScale();
|
|
}
|
|
return null;
|
|
}
|
|
_private__tryToUpdateViews(index, price, pane) {
|
|
if (this._private__tryToUpdateData(index, price, pane)) {
|
|
this._internal_updateAllViews();
|
|
}
|
|
}
|
|
_private__tryToUpdateData(newIndex, newPrice, newPane) {
|
|
const oldX = this._private__x;
|
|
const oldY = this._private__y;
|
|
const oldPrice = this._private__price;
|
|
const oldIndex = this._private__index;
|
|
const oldPane = this._private__pane;
|
|
const priceScale = this._private__priceScaleByPane(newPane);
|
|
this._private__index = newIndex;
|
|
this._private__x = isNaN(newIndex) ? NaN : this._private__model._internal_timeScale()._internal_indexToCoordinate(newIndex);
|
|
this._private__pane = newPane;
|
|
const firstValue = priceScale !== null ? priceScale._internal_firstValue() : null;
|
|
if (priceScale !== null && firstValue !== null) {
|
|
this._private__price = newPrice;
|
|
this._private__y = priceScale._internal_priceToCoordinate(newPrice, firstValue);
|
|
}
|
|
else {
|
|
this._private__price = NaN;
|
|
this._private__y = NaN;
|
|
}
|
|
return (oldX !== this._private__x || oldY !== this._private__y || oldIndex !== this._private__index ||
|
|
oldPrice !== this._private__price || oldPane !== this._private__pane);
|
|
}
|
|
_private__setIndexToLastSeriesBarIndex() {
|
|
const lastIndexes = this._private__model._internal_serieses()
|
|
.map((s) => s._internal_bars()._internal_lastIndex())
|
|
.filter(notNull);
|
|
const lastBarIndex = (lastIndexes.length === 0) ? null : Math.max(...lastIndexes);
|
|
this._private__index = lastBarIndex !== null ? lastBarIndex : NaN;
|
|
}
|
|
_private__createPriceAxisViewOnDemand(map, priceScale, valueProvider) {
|
|
let view = map.get(priceScale);
|
|
if (view === undefined) {
|
|
view = new CrosshairPriceAxisView(this, priceScale, valueProvider);
|
|
map.set(priceScale, view);
|
|
}
|
|
return view;
|
|
}
|
|
}
|
|
|
|
function isDefaultPriceScale(priceScaleId) {
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
return priceScaleId === "left" /* DefaultPriceScaleId.Left */ || priceScaleId === "right" /* DefaultPriceScaleId.Right */;
|
|
}
|
|
|
|
function mergePaneInvalidation(beforeValue, newValue) {
|
|
if (beforeValue === undefined) {
|
|
return newValue;
|
|
}
|
|
const level = Math.max(beforeValue._internal_level, newValue._internal_level);
|
|
const autoScale = beforeValue._internal_autoScale || newValue._internal_autoScale;
|
|
return { _internal_level: level, _internal_autoScale: autoScale };
|
|
}
|
|
class InvalidateMask {
|
|
constructor(globalLevel) {
|
|
this._private__invalidatedPanes = new Map();
|
|
this._private__timeScaleInvalidations = [];
|
|
this._private__globalLevel = globalLevel;
|
|
}
|
|
_internal_invalidatePane(paneIndex, invalidation) {
|
|
const prevValue = this._private__invalidatedPanes.get(paneIndex);
|
|
const newValue = mergePaneInvalidation(prevValue, invalidation);
|
|
this._private__invalidatedPanes.set(paneIndex, newValue);
|
|
}
|
|
_internal_fullInvalidation() {
|
|
return this._private__globalLevel;
|
|
}
|
|
_internal_invalidateForPane(paneIndex) {
|
|
const paneInvalidation = this._private__invalidatedPanes.get(paneIndex);
|
|
if (paneInvalidation === undefined) {
|
|
return {
|
|
_internal_level: this._private__globalLevel,
|
|
};
|
|
}
|
|
return {
|
|
_internal_level: Math.max(this._private__globalLevel, paneInvalidation._internal_level),
|
|
_internal_autoScale: paneInvalidation._internal_autoScale,
|
|
};
|
|
}
|
|
_internal_setFitContent() {
|
|
this._internal_stopTimeScaleAnimation();
|
|
// modifies both bar spacing and right offset
|
|
this._private__timeScaleInvalidations = [{ _internal_type: 0 /* TimeScaleInvalidationType.FitContent */ }];
|
|
}
|
|
_internal_applyRange(range) {
|
|
this._internal_stopTimeScaleAnimation();
|
|
// modifies both bar spacing and right offset
|
|
this._private__timeScaleInvalidations = [{ _internal_type: 1 /* TimeScaleInvalidationType.ApplyRange */, _internal_value: range }];
|
|
}
|
|
_internal_setTimeScaleAnimation(animation) {
|
|
this._private__removeTimeScaleAnimation();
|
|
this._private__timeScaleInvalidations.push({ _internal_type: 5 /* TimeScaleInvalidationType.Animation */, _internal_value: animation });
|
|
}
|
|
_internal_stopTimeScaleAnimation() {
|
|
this._private__removeTimeScaleAnimation();
|
|
this._private__timeScaleInvalidations.push({ _internal_type: 6 /* TimeScaleInvalidationType.StopAnimation */ });
|
|
}
|
|
_internal_resetTimeScale() {
|
|
this._internal_stopTimeScaleAnimation();
|
|
// modifies both bar spacing and right offset
|
|
this._private__timeScaleInvalidations = [{ _internal_type: 4 /* TimeScaleInvalidationType.Reset */ }];
|
|
}
|
|
_internal_setBarSpacing(barSpacing) {
|
|
this._internal_stopTimeScaleAnimation();
|
|
this._private__timeScaleInvalidations.push({ _internal_type: 2 /* TimeScaleInvalidationType.ApplyBarSpacing */, _internal_value: barSpacing });
|
|
}
|
|
_internal_setRightOffset(offset) {
|
|
this._internal_stopTimeScaleAnimation();
|
|
this._private__timeScaleInvalidations.push({ _internal_type: 3 /* TimeScaleInvalidationType.ApplyRightOffset */, _internal_value: offset });
|
|
}
|
|
_internal_timeScaleInvalidations() {
|
|
return this._private__timeScaleInvalidations;
|
|
}
|
|
_internal_merge(other) {
|
|
for (const tsInvalidation of other._private__timeScaleInvalidations) {
|
|
this._private__applyTimeScaleInvalidation(tsInvalidation);
|
|
}
|
|
this._private__globalLevel = Math.max(this._private__globalLevel, other._private__globalLevel);
|
|
other._private__invalidatedPanes.forEach((invalidation, index) => {
|
|
this._internal_invalidatePane(index, invalidation);
|
|
});
|
|
}
|
|
static _internal_light() {
|
|
return new InvalidateMask(2 /* InvalidationLevel.Light */);
|
|
}
|
|
static _internal_full() {
|
|
return new InvalidateMask(3 /* InvalidationLevel.Full */);
|
|
}
|
|
_private__applyTimeScaleInvalidation(invalidation) {
|
|
switch (invalidation._internal_type) {
|
|
case 0 /* TimeScaleInvalidationType.FitContent */:
|
|
this._internal_setFitContent();
|
|
break;
|
|
case 1 /* TimeScaleInvalidationType.ApplyRange */:
|
|
this._internal_applyRange(invalidation._internal_value);
|
|
break;
|
|
case 2 /* TimeScaleInvalidationType.ApplyBarSpacing */:
|
|
this._internal_setBarSpacing(invalidation._internal_value);
|
|
break;
|
|
case 3 /* TimeScaleInvalidationType.ApplyRightOffset */:
|
|
this._internal_setRightOffset(invalidation._internal_value);
|
|
break;
|
|
case 4 /* TimeScaleInvalidationType.Reset */:
|
|
this._internal_resetTimeScale();
|
|
break;
|
|
case 5 /* TimeScaleInvalidationType.Animation */:
|
|
this._internal_setTimeScaleAnimation(invalidation._internal_value);
|
|
break;
|
|
case 6 /* TimeScaleInvalidationType.StopAnimation */:
|
|
this._private__removeTimeScaleAnimation();
|
|
}
|
|
}
|
|
_private__removeTimeScaleAnimation() {
|
|
const index = this._private__timeScaleInvalidations.findIndex((inv) => inv._internal_type === 5 /* TimeScaleInvalidationType.Animation */);
|
|
if (index !== -1) {
|
|
this._private__timeScaleInvalidations.splice(index, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
class FormatterBase {
|
|
formatTickmarks(prices) {
|
|
return prices.map((price) => this.format(price));
|
|
}
|
|
}
|
|
|
|
const formatterOptions = {
|
|
_internal_decimalSign: '.'};
|
|
/**
|
|
* @param value - The number of convert.
|
|
* @param length - The length. Must be between 0 and 16 inclusive.
|
|
*/
|
|
function numberToStringWithLeadingZero(value, length) {
|
|
if (!isNumber(value)) {
|
|
return 'n/a';
|
|
}
|
|
if (!isInteger(length)) {
|
|
throw new TypeError('invalid length');
|
|
}
|
|
if (length < 0 || length > 16) {
|
|
throw new TypeError('invalid length');
|
|
}
|
|
if (length === 0) {
|
|
return value.toString();
|
|
}
|
|
const dummyString = '0000000000000000';
|
|
return (dummyString + value.toString()).slice(-length);
|
|
}
|
|
class PriceFormatter extends FormatterBase {
|
|
constructor(priceScale, minMove) {
|
|
super();
|
|
if (!minMove) {
|
|
minMove = 1;
|
|
}
|
|
if (!isNumber(priceScale) || !isInteger(priceScale)) {
|
|
priceScale = 100;
|
|
}
|
|
if (priceScale < 0) {
|
|
throw new TypeError('invalid base');
|
|
}
|
|
this._private__priceScale = priceScale;
|
|
this._private__minMove = minMove;
|
|
this._private__calculateDecimal();
|
|
}
|
|
format(price) {
|
|
// \u2212 is unicode's minus sign https://www.fileformat.info/info/unicode/char/2212/index.htm
|
|
// we should use it because it has the same width as plus sign +
|
|
const sign = price < 0 ? '\u2212' : '';
|
|
price = Math.abs(price);
|
|
return sign + this._private__formatAsDecimal(price);
|
|
}
|
|
_private__calculateDecimal() {
|
|
// check if this._base is power of 10
|
|
// for double fractional _fractionalLength if for the main fractional only
|
|
this._internal__fractionalLength = 0;
|
|
if (this._private__priceScale > 0 && this._private__minMove > 0) {
|
|
let base = this._private__priceScale;
|
|
while (base > 1) {
|
|
base /= 10;
|
|
this._internal__fractionalLength++;
|
|
}
|
|
}
|
|
}
|
|
_private__formatAsDecimal(price) {
|
|
const base = this._private__priceScale / this._private__minMove;
|
|
let intPart = Math.floor(price);
|
|
let fracString = '';
|
|
const fracLength = this._internal__fractionalLength !== undefined ? this._internal__fractionalLength : NaN;
|
|
if (base > 1) {
|
|
let fracPart = +(Math.round(price * base) - intPart * base).toFixed(this._internal__fractionalLength);
|
|
if (fracPart >= base) {
|
|
fracPart -= base;
|
|
intPart += 1;
|
|
}
|
|
fracString = formatterOptions._internal_decimalSign + numberToStringWithLeadingZero(+fracPart.toFixed(this._internal__fractionalLength) * this._private__minMove, fracLength);
|
|
}
|
|
else {
|
|
// should round int part to min move
|
|
intPart = Math.round(intPart * base) / base;
|
|
// if min move > 1, fractional part is always = 0
|
|
if (fracLength > 0) {
|
|
fracString = formatterOptions._internal_decimalSign + numberToStringWithLeadingZero(0, fracLength);
|
|
}
|
|
}
|
|
return intPart.toFixed(0) + fracString;
|
|
}
|
|
}
|
|
|
|
class PercentageFormatter extends PriceFormatter {
|
|
constructor(priceScale = 100) {
|
|
super(priceScale);
|
|
}
|
|
format(price) {
|
|
return `${super.format(price)}%`;
|
|
}
|
|
}
|
|
|
|
class VolumeFormatter extends FormatterBase {
|
|
constructor(precision) {
|
|
super();
|
|
this._private__precision = precision;
|
|
}
|
|
format(vol) {
|
|
let sign = '';
|
|
if (vol < 0) {
|
|
sign = '-';
|
|
vol = -vol;
|
|
}
|
|
if (vol < 995) {
|
|
return sign + this._private__formatNumber(vol);
|
|
}
|
|
else if (vol < 999995) {
|
|
return sign + this._private__formatNumber(vol / 1000) + 'K';
|
|
}
|
|
else if (vol < 999999995) {
|
|
vol = 1000 * Math.round(vol / 1000);
|
|
return sign + this._private__formatNumber(vol / 1000000) + 'M';
|
|
}
|
|
else {
|
|
vol = 1000000 * Math.round(vol / 1000000);
|
|
return sign + this._private__formatNumber(vol / 1000000000) + 'B';
|
|
}
|
|
}
|
|
_private__formatNumber(value) {
|
|
let res;
|
|
const priceScale = Math.pow(10, this._private__precision);
|
|
value = Math.round(value * priceScale) / priceScale;
|
|
if (value >= 1e-15 && value < 1) {
|
|
res = value.toFixed(this._private__precision).replace(/\.?0+$/, ''); // regex removes trailing zeroes
|
|
}
|
|
else {
|
|
res = String(value);
|
|
}
|
|
return res.replace(/(\.[1-9]*)0+$/, (e, p1) => p1);
|
|
}
|
|
}
|
|
|
|
const defaultReplacementRe = /[2-9]/g;
|
|
class TextWidthCache {
|
|
constructor(size = 50) {
|
|
this._private__actualSize = 0;
|
|
this._private__usageTick = 1;
|
|
this._private__oldestTick = 1;
|
|
this._private__tick2Labels = {};
|
|
this._private__cache = new Map();
|
|
this._private__maxSize = size;
|
|
}
|
|
_internal_reset() {
|
|
this._private__actualSize = 0;
|
|
this._private__cache.clear();
|
|
this._private__usageTick = 1;
|
|
this._private__oldestTick = 1;
|
|
this._private__tick2Labels = {};
|
|
}
|
|
_internal_measureText(ctx, text, optimizationReplacementRe) {
|
|
return this._private__getMetrics(ctx, text, optimizationReplacementRe).width;
|
|
}
|
|
_internal_yMidCorrection(ctx, text, optimizationReplacementRe) {
|
|
const metrics = this._private__getMetrics(ctx, text, optimizationReplacementRe);
|
|
// if actualBoundingBoxAscent/actualBoundingBoxDescent are not supported we use 0 as a fallback
|
|
return ((metrics.actualBoundingBoxAscent || 0) - (metrics.actualBoundingBoxDescent || 0)) / 2;
|
|
}
|
|
_private__getMetrics(ctx, text, optimizationReplacementRe) {
|
|
const re = optimizationReplacementRe || defaultReplacementRe;
|
|
const cacheString = String(text).replace(re, '0');
|
|
if (this._private__cache.has(cacheString)) {
|
|
return ensureDefined(this._private__cache.get(cacheString))._internal_metrics;
|
|
}
|
|
if (this._private__actualSize === this._private__maxSize) {
|
|
const oldestValue = this._private__tick2Labels[this._private__oldestTick];
|
|
delete this._private__tick2Labels[this._private__oldestTick];
|
|
this._private__cache.delete(oldestValue);
|
|
this._private__oldestTick++;
|
|
this._private__actualSize--;
|
|
}
|
|
ctx.save();
|
|
ctx.textBaseline = 'middle';
|
|
const metrics = ctx.measureText(cacheString);
|
|
ctx.restore();
|
|
if (metrics.width === 0 && !!text.length) {
|
|
// measureText can return 0 in FF depending on a canvas size, don't cache it
|
|
return metrics;
|
|
}
|
|
this._private__cache.set(cacheString, { _internal_metrics: metrics, _internal_tick: this._private__usageTick });
|
|
this._private__tick2Labels[this._private__usageTick] = cacheString;
|
|
this._private__actualSize++;
|
|
this._private__usageTick++;
|
|
return metrics;
|
|
}
|
|
}
|
|
|
|
class PanePriceAxisViewRenderer {
|
|
constructor(textWidthCache) {
|
|
this._private__priceAxisViewRenderer = null;
|
|
this._private__rendererOptions = null;
|
|
this._private__align = 'right';
|
|
this._private__textWidthCache = textWidthCache;
|
|
}
|
|
_internal_setParams(priceAxisViewRenderer, rendererOptions, align) {
|
|
this._private__priceAxisViewRenderer = priceAxisViewRenderer;
|
|
this._private__rendererOptions = rendererOptions;
|
|
this._private__align = align;
|
|
}
|
|
_internal_draw(target) {
|
|
if (this._private__rendererOptions === null || this._private__priceAxisViewRenderer === null) {
|
|
return;
|
|
}
|
|
this._private__priceAxisViewRenderer._internal_draw(target, this._private__rendererOptions, this._private__textWidthCache, this._private__align);
|
|
}
|
|
}
|
|
class PanePriceAxisView {
|
|
constructor(priceAxisView, dataSource, chartModel) {
|
|
this._private__priceAxisView = priceAxisView;
|
|
this._private__textWidthCache = new TextWidthCache(50); // when should we clear cache?
|
|
this._private__dataSource = dataSource;
|
|
this._private__chartModel = chartModel;
|
|
this._private__fontSize = -1;
|
|
this._private__renderer = new PanePriceAxisViewRenderer(this._private__textWidthCache);
|
|
}
|
|
_internal_renderer() {
|
|
const pane = this._private__chartModel._internal_paneForSource(this._private__dataSource);
|
|
if (pane === null) {
|
|
return null;
|
|
}
|
|
// this price scale will be used to find label placement only (left, right, none)
|
|
const priceScale = pane._internal_isOverlay(this._private__dataSource) ? pane._internal_defaultVisiblePriceScale() : this._private__dataSource._internal_priceScale();
|
|
if (priceScale === null) {
|
|
return null;
|
|
}
|
|
const position = pane._internal_priceScalePosition(priceScale);
|
|
if (position === 'overlay') {
|
|
return null;
|
|
}
|
|
const options = this._private__chartModel._internal_priceAxisRendererOptions();
|
|
if (options._internal_fontSize !== this._private__fontSize) {
|
|
this._private__fontSize = options._internal_fontSize;
|
|
this._private__textWidthCache._internal_reset();
|
|
}
|
|
this._private__renderer._internal_setParams(this._private__priceAxisView._internal_paneRenderer(), options, position);
|
|
return this._private__renderer;
|
|
}
|
|
}
|
|
|
|
class HorizontalLineRenderer extends BitmapCoordinatesPaneRenderer {
|
|
constructor() {
|
|
super(...arguments);
|
|
this._private__data = null;
|
|
}
|
|
_internal_setData(data) {
|
|
this._private__data = data;
|
|
}
|
|
_internal_hitTest(x, y) {
|
|
if (!this._private__data?._internal_visible) {
|
|
return null;
|
|
}
|
|
const { _internal_y: itemY, _internal_lineWidth: lineWidth, _internal_externalId: externalId } = this._private__data;
|
|
// add a fixed area threshold around line (Y + width) for hit test
|
|
if (y >= itemY - lineWidth - 7 /* Constants.HitTestThreshold */ && y <= itemY + lineWidth + 7 /* Constants.HitTestThreshold */) {
|
|
return {
|
|
_internal_hitTestData: this._private__data,
|
|
_internal_distance: Math.abs(y - itemY),
|
|
_internal_priority: 2 /* HitTestPriority.Point */,
|
|
_internal_itemType: 'price-line',
|
|
_internal_externalId: externalId,
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
_internal__drawImpl({ context: ctx, bitmapSize, horizontalPixelRatio, verticalPixelRatio }) {
|
|
if (this._private__data === null) {
|
|
return;
|
|
}
|
|
if (this._private__data._internal_visible === false) {
|
|
return;
|
|
}
|
|
const y = Math.round(this._private__data._internal_y * verticalPixelRatio);
|
|
if (y < 0 || y > bitmapSize.height) {
|
|
return;
|
|
}
|
|
ctx.lineCap = 'butt';
|
|
ctx.strokeStyle = this._private__data._internal_color;
|
|
ctx.lineWidth = Math.floor(this._private__data._internal_lineWidth * horizontalPixelRatio);
|
|
setLineStyle(ctx, this._private__data._internal_lineStyle);
|
|
drawHorizontalLine(ctx, y, 0, bitmapSize.width);
|
|
}
|
|
}
|
|
|
|
class SeriesHorizontalLinePaneView {
|
|
constructor(series) {
|
|
this._internal__lineRendererData = {
|
|
_internal_y: 0,
|
|
_internal_color: 'rgba(0, 0, 0, 0)',
|
|
_internal_lineWidth: 1,
|
|
_internal_lineStyle: 0 /* LineStyle.Solid */,
|
|
_internal_visible: false,
|
|
};
|
|
this._internal__lineRenderer = new HorizontalLineRenderer();
|
|
this._private__invalidated = true;
|
|
this._internal__series = series;
|
|
this._internal__model = series._internal_model();
|
|
this._internal__lineRenderer._internal_setData(this._internal__lineRendererData);
|
|
}
|
|
_internal_update() {
|
|
this._private__invalidated = true;
|
|
}
|
|
_internal_renderer() {
|
|
if (!this._internal__series._internal_visible()) {
|
|
return null;
|
|
}
|
|
if (this._private__invalidated) {
|
|
this._internal__updateImpl();
|
|
this._private__invalidated = false;
|
|
}
|
|
return this._internal__lineRenderer;
|
|
}
|
|
}
|
|
|
|
class SeriesHorizontalBaseLinePaneView extends SeriesHorizontalLinePaneView {
|
|
// eslint-disable-next-line no-useless-constructor
|
|
constructor(series) {
|
|
super(series);
|
|
}
|
|
_internal__updateImpl() {
|
|
this._internal__lineRendererData._internal_visible = false;
|
|
const priceScale = this._internal__series._internal_priceScale();
|
|
const mode = priceScale._internal_mode()._internal_mode;
|
|
if (mode !== 2 /* PriceScaleMode.Percentage */ && mode !== 3 /* PriceScaleMode.IndexedTo100 */) {
|
|
return;
|
|
}
|
|
const seriesOptions = this._internal__series._internal_options();
|
|
if (!seriesOptions.baseLineVisible || !this._internal__series._internal_visible()) {
|
|
return;
|
|
}
|
|
const firstValue = this._internal__series._internal_firstValue();
|
|
if (firstValue === null) {
|
|
return;
|
|
}
|
|
this._internal__lineRendererData._internal_visible = true;
|
|
this._internal__lineRendererData._internal_y = priceScale._internal_priceToCoordinate(firstValue._internal_value, firstValue._internal_value);
|
|
this._internal__lineRendererData._internal_color = seriesOptions.baseLineColor;
|
|
this._internal__lineRendererData._internal_lineWidth = seriesOptions.baseLineWidth;
|
|
this._internal__lineRendererData._internal_lineStyle = seriesOptions.baseLineStyle;
|
|
}
|
|
}
|
|
|
|
class SeriesLastPriceAnimationRenderer extends BitmapCoordinatesPaneRenderer {
|
|
constructor() {
|
|
super(...arguments);
|
|
this._private__data = null;
|
|
}
|
|
_internal_setData(data) {
|
|
this._private__data = data;
|
|
}
|
|
_internal_data() {
|
|
return this._private__data;
|
|
}
|
|
_internal__drawImpl({ context: ctx, horizontalPixelRatio, verticalPixelRatio }) {
|
|
const data = this._private__data;
|
|
if (data === null) {
|
|
return;
|
|
}
|
|
const tickWidth = Math.max(1, Math.floor(horizontalPixelRatio));
|
|
const correction = (tickWidth % 2) / 2;
|
|
const centerX = Math.round(data._internal_center.x * horizontalPixelRatio) + correction; // correct x coordinate only
|
|
const centerY = data._internal_center.y * verticalPixelRatio;
|
|
ctx.fillStyle = data._internal_seriesLineColor;
|
|
ctx.beginPath();
|
|
// TODO: it is better to have different horizontal and vertical radii
|
|
const centerPointRadius = Math.max(2, data._internal_seriesLineWidth * 1.5) * horizontalPixelRatio;
|
|
ctx.arc(centerX, centerY, centerPointRadius, 0, 2 * Math.PI, false);
|
|
ctx.fill();
|
|
ctx.fillStyle = data._internal_fillColor;
|
|
ctx.beginPath();
|
|
ctx.arc(centerX, centerY, data._internal_radius * horizontalPixelRatio, 0, 2 * Math.PI, false);
|
|
ctx.fill();
|
|
ctx.lineWidth = tickWidth;
|
|
ctx.strokeStyle = data._internal_strokeColor;
|
|
ctx.beginPath();
|
|
ctx.arc(centerX, centerY, data._internal_radius * horizontalPixelRatio + tickWidth / 2, 0, 2 * Math.PI, false);
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
|
|
const animationStagesData = [
|
|
{
|
|
_internal_start: 0,
|
|
_internal_end: 0.25 /* Constants.Stage1Period */,
|
|
_internal_startRadius: 4 /* Constants.Stage1StartCircleRadius */,
|
|
_internal_endRadius: 10 /* Constants.Stage1EndCircleRadius */,
|
|
_internal_startFillAlpha: 0.25 /* Constants.Stage1StartFillAlpha */,
|
|
_internal_endFillAlpha: 0 /* Constants.Stage1EndFillAlpha */,
|
|
_internal_startStrokeAlpha: 0.4 /* Constants.Stage1StartStrokeAlpha */,
|
|
_internal_endStrokeAlpha: 0.8 /* Constants.Stage1EndStrokeAlpha */,
|
|
},
|
|
{
|
|
_internal_start: 0.25 /* Constants.Stage1Period */,
|
|
_internal_end: 0.25 /* Constants.Stage1Period */ + 0.275 /* Constants.Stage2Period */,
|
|
_internal_startRadius: 10 /* Constants.Stage2StartCircleRadius */,
|
|
_internal_endRadius: 14 /* Constants.Stage2EndCircleRadius */,
|
|
_internal_startFillAlpha: 0 /* Constants.Stage2StartFillAlpha */,
|
|
_internal_endFillAlpha: 0 /* Constants.Stage2EndFillAlpha */,
|
|
_internal_startStrokeAlpha: 0.8 /* Constants.Stage2StartStrokeAlpha */,
|
|
_internal_endStrokeAlpha: 0 /* Constants.Stage2EndStrokeAlpha */,
|
|
},
|
|
{
|
|
_internal_start: 0.25 /* Constants.Stage1Period */ + 0.275 /* Constants.Stage2Period */,
|
|
_internal_end: 0.25 /* Constants.Stage1Period */ + 0.275 /* Constants.Stage2Period */ + 0.475 /* Constants.Stage3Period */,
|
|
_internal_startRadius: 14 /* Constants.Stage3StartCircleRadius */,
|
|
_internal_endRadius: 14 /* Constants.Stage3EndCircleRadius */,
|
|
_internal_startFillAlpha: 0 /* Constants.Stage3StartFillAlpha */,
|
|
_internal_endFillAlpha: 0 /* Constants.Stage3EndFillAlpha */,
|
|
_internal_startStrokeAlpha: 0 /* Constants.Stage3StartStrokeAlpha */,
|
|
_internal_endStrokeAlpha: 0 /* Constants.Stage3EndStrokeAlpha */,
|
|
},
|
|
];
|
|
function radius(stage, startRadius, endRadius) {
|
|
return startRadius + (endRadius - startRadius) * stage;
|
|
}
|
|
class SeriesLastPriceAnimationPaneView {
|
|
constructor(series) {
|
|
this._private__renderer = new SeriesLastPriceAnimationRenderer();
|
|
this._private__invalidated = true;
|
|
this._private__stageInvalidated = true;
|
|
this._private__startTime = performance.now();
|
|
this._private__endTime = this._private__startTime - 1;
|
|
this._private__series = series;
|
|
}
|
|
_internal_onDataCleared() {
|
|
this._private__endTime = this._private__startTime - 1;
|
|
this._internal_update();
|
|
}
|
|
_internal_onNewRealtimeDataReceived() {
|
|
this._internal_update();
|
|
if (this._private__series._internal_options().lastPriceAnimation === 2 /* LastPriceAnimationMode.OnDataUpdate */) {
|
|
const now = performance.now();
|
|
const timeToAnimationEnd = this._private__endTime - now;
|
|
if (timeToAnimationEnd > 0) {
|
|
if (timeToAnimationEnd < 2600 /* Constants.AnimationPeriod */ / 4) {
|
|
this._private__endTime += 2600 /* Constants.AnimationPeriod */;
|
|
}
|
|
return;
|
|
}
|
|
this._private__startTime = now;
|
|
this._private__endTime = now + 2600 /* Constants.AnimationPeriod */;
|
|
}
|
|
}
|
|
_internal_update() {
|
|
this._private__invalidated = true;
|
|
}
|
|
_internal_invalidateStage() {
|
|
this._private__stageInvalidated = true;
|
|
}
|
|
_internal_visible() {
|
|
// center point is always visible if lastPriceAnimation is not LastPriceAnimationMode.Disabled
|
|
return this._private__series._internal_options().lastPriceAnimation !== 0 /* LastPriceAnimationMode.Disabled */;
|
|
}
|
|
_internal_animationActive() {
|
|
switch (this._private__series._internal_options().lastPriceAnimation) {
|
|
case 0 /* LastPriceAnimationMode.Disabled */:
|
|
return false;
|
|
case 1 /* LastPriceAnimationMode.Continuous */:
|
|
return true;
|
|
case 2 /* LastPriceAnimationMode.OnDataUpdate */:
|
|
return performance.now() <= this._private__endTime;
|
|
}
|
|
}
|
|
_internal_renderer() {
|
|
if (this._private__invalidated) {
|
|
this._private__updateImpl();
|
|
this._private__invalidated = false;
|
|
this._private__stageInvalidated = false;
|
|
}
|
|
else if (this._private__stageInvalidated) {
|
|
this._private__updateRendererDataStage();
|
|
this._private__stageInvalidated = false;
|
|
}
|
|
return this._private__renderer;
|
|
}
|
|
_private__updateImpl() {
|
|
this._private__renderer._internal_setData(null);
|
|
const timeScale = this._private__series._internal_model()._internal_timeScale();
|
|
const visibleRange = timeScale._internal_visibleStrictRange();
|
|
const firstValue = this._private__series._internal_firstValue();
|
|
if (visibleRange === null || firstValue === null) {
|
|
return;
|
|
}
|
|
const lastValue = this._private__series._internal_lastValueData(true);
|
|
if (lastValue._internal_noData || !visibleRange._internal_contains(lastValue._internal_index)) {
|
|
return;
|
|
}
|
|
const lastValuePoint = {
|
|
x: timeScale._internal_indexToCoordinate(lastValue._internal_index),
|
|
y: this._private__series._internal_priceScale()._internal_priceToCoordinate(lastValue._internal_price, firstValue._internal_value),
|
|
};
|
|
const seriesLineColor = lastValue._internal_color;
|
|
const seriesLineWidth = this._private__series._internal_options().lineWidth;
|
|
const data = this._private__animationData(this._private__duration(), seriesLineColor);
|
|
this._private__renderer._internal_setData({
|
|
_internal_seriesLineColor: seriesLineColor,
|
|
_internal_seriesLineWidth: seriesLineWidth,
|
|
_internal_fillColor: data._internal_fillColor,
|
|
_internal_strokeColor: data._internal_strokeColor,
|
|
_internal_radius: data._internal_radius,
|
|
_internal_center: lastValuePoint,
|
|
});
|
|
}
|
|
_private__updateRendererDataStage() {
|
|
const rendererData = this._private__renderer._internal_data();
|
|
if (rendererData !== null) {
|
|
const data = this._private__animationData(this._private__duration(), rendererData._internal_seriesLineColor);
|
|
rendererData._internal_fillColor = data._internal_fillColor;
|
|
rendererData._internal_strokeColor = data._internal_strokeColor;
|
|
rendererData._internal_radius = data._internal_radius;
|
|
}
|
|
}
|
|
_private__duration() {
|
|
return this._internal_animationActive() ? performance.now() - this._private__startTime : 2600 /* Constants.AnimationPeriod */ - 1;
|
|
}
|
|
_private__color(seriesLineColor, stage, startAlpha, endAlpha) {
|
|
const alpha = startAlpha + (endAlpha - startAlpha) * stage;
|
|
return this._private__series._internal_model()._internal_colorParser()._internal_applyAlpha(seriesLineColor, alpha);
|
|
}
|
|
_private__animationData(durationSinceStart, lineColor) {
|
|
const globalStage = (durationSinceStart % 2600 /* Constants.AnimationPeriod */) / 2600 /* Constants.AnimationPeriod */;
|
|
let currentStageData;
|
|
for (const stageData of animationStagesData) {
|
|
if (globalStage >= stageData._internal_start && globalStage <= stageData._internal_end) {
|
|
currentStageData = stageData;
|
|
break;
|
|
}
|
|
}
|
|
assert(currentStageData !== undefined, 'Last price animation internal logic error');
|
|
const subStage = (globalStage - currentStageData._internal_start) / (currentStageData._internal_end - currentStageData._internal_start);
|
|
return {
|
|
_internal_fillColor: this._private__color(lineColor, subStage, currentStageData._internal_startFillAlpha, currentStageData._internal_endFillAlpha),
|
|
_internal_strokeColor: this._private__color(lineColor, subStage, currentStageData._internal_startStrokeAlpha, currentStageData._internal_endStrokeAlpha),
|
|
_internal_radius: radius(subStage, currentStageData._internal_startRadius, currentStageData._internal_endRadius),
|
|
};
|
|
}
|
|
}
|
|
|
|
class SeriesPriceLinePaneView extends SeriesHorizontalLinePaneView {
|
|
// eslint-disable-next-line no-useless-constructor
|
|
constructor(series) {
|
|
super(series);
|
|
}
|
|
_internal__updateImpl() {
|
|
const data = this._internal__lineRendererData;
|
|
data._internal_visible = false;
|
|
const seriesOptions = this._internal__series._internal_options();
|
|
if (!seriesOptions.priceLineVisible || !this._internal__series._internal_visible()) {
|
|
return;
|
|
}
|
|
const lastValueData = this._internal__series._internal_lastValueData(seriesOptions.priceLineSource === 0 /* PriceLineSource.LastBar */);
|
|
if (lastValueData._internal_noData) {
|
|
return;
|
|
}
|
|
data._internal_visible = true;
|
|
data._internal_y = lastValueData._internal_coordinate;
|
|
data._internal_color = this._internal__series._internal_priceLineColor(lastValueData._internal_color);
|
|
data._internal_lineWidth = seriesOptions.priceLineWidth;
|
|
data._internal_lineStyle = seriesOptions.priceLineStyle;
|
|
}
|
|
}
|
|
|
|
class SeriesPriceAxisView extends PriceAxisView {
|
|
constructor(source) {
|
|
super();
|
|
this._private__source = source;
|
|
}
|
|
_internal__updateRendererData(axisRendererData, paneRendererData, commonRendererData) {
|
|
axisRendererData._internal_visible = false;
|
|
paneRendererData._internal_visible = false;
|
|
const source = this._private__source;
|
|
if (!source._internal_visible()) {
|
|
return;
|
|
}
|
|
const seriesOptions = source._internal_options();
|
|
const showSeriesLastValue = seriesOptions.lastValueVisible;
|
|
const showSymbolLabel = source._internal_title() !== '';
|
|
const showPriceAndPercentage = seriesOptions.seriesLastValueMode === 0 /* PriceAxisLastValueMode.LastPriceAndPercentageValue */;
|
|
const lastValueData = source._internal_lastValueData(false);
|
|
if (lastValueData._internal_noData) {
|
|
return;
|
|
}
|
|
if (showSeriesLastValue) {
|
|
axisRendererData._internal_text = this._internal__axisText(lastValueData, showSeriesLastValue, showPriceAndPercentage);
|
|
axisRendererData._internal_visible = axisRendererData._internal_text.length !== 0;
|
|
}
|
|
if (showSymbolLabel || showPriceAndPercentage) {
|
|
paneRendererData._internal_text = this._internal__paneText(lastValueData, showSeriesLastValue, showSymbolLabel, showPriceAndPercentage);
|
|
paneRendererData._internal_visible = paneRendererData._internal_text.length > 0;
|
|
}
|
|
const lastValueColor = source._internal_priceLineColor(lastValueData._internal_color);
|
|
const colors = this._private__source._internal_model()._internal_colorParser()._internal_generateContrastColors(lastValueColor);
|
|
commonRendererData._internal_background = colors._internal_background;
|
|
commonRendererData._internal_coordinate = lastValueData._internal_coordinate;
|
|
paneRendererData._internal_borderColor = source._internal_model()._internal_backgroundColorAtYPercentFromTop(lastValueData._internal_coordinate / source._internal_priceScale()._internal_height());
|
|
axisRendererData._internal_borderColor = lastValueColor;
|
|
axisRendererData._internal_color = colors._internal_foreground;
|
|
paneRendererData._internal_color = colors._internal_foreground;
|
|
}
|
|
_internal__paneText(lastValue, showSeriesLastValue, showSymbolLabel, showPriceAndPercentage) {
|
|
let result = '';
|
|
const title = this._private__source._internal_title();
|
|
if (showSymbolLabel && title.length !== 0) {
|
|
result += `${title} `;
|
|
}
|
|
if (showSeriesLastValue && showPriceAndPercentage) {
|
|
result += this._private__source._internal_priceScale()._internal_isPercentage() ?
|
|
lastValue._internal_formattedPriceAbsolute : lastValue._internal_formattedPricePercentage;
|
|
}
|
|
return result.trim();
|
|
}
|
|
_internal__axisText(lastValueData, showSeriesLastValue, showPriceAndPercentage) {
|
|
if (!showSeriesLastValue) {
|
|
return '';
|
|
}
|
|
if (!showPriceAndPercentage) {
|
|
return lastValueData._internal_text;
|
|
}
|
|
return this._private__source._internal_priceScale()._internal_isPercentage() ?
|
|
lastValueData._internal_formattedPricePercentage : lastValueData._internal_formattedPriceAbsolute;
|
|
}
|
|
}
|
|
|
|
function computeFiniteResult(method, valueOne, valueTwo, fallback) {
|
|
const firstFinite = Number.isFinite(valueOne);
|
|
const secondFinite = Number.isFinite(valueTwo);
|
|
if (firstFinite && secondFinite) {
|
|
return method(valueOne, valueTwo);
|
|
}
|
|
return !firstFinite && !secondFinite ? fallback : (firstFinite ? valueOne : valueTwo);
|
|
}
|
|
class PriceRangeImpl {
|
|
constructor(minValue, maxValue) {
|
|
this._private__minValue = minValue;
|
|
this._private__maxValue = maxValue;
|
|
}
|
|
_internal_equals(pr) {
|
|
if (pr === null) {
|
|
return false;
|
|
}
|
|
return this._private__minValue === pr._private__minValue && this._private__maxValue === pr._private__maxValue;
|
|
}
|
|
_internal_clone() {
|
|
return new PriceRangeImpl(this._private__minValue, this._private__maxValue);
|
|
}
|
|
_internal_minValue() {
|
|
return this._private__minValue;
|
|
}
|
|
_internal_maxValue() {
|
|
return this._private__maxValue;
|
|
}
|
|
_internal_length() {
|
|
return this._private__maxValue - this._private__minValue;
|
|
}
|
|
_internal_isEmpty() {
|
|
return this._private__maxValue === this._private__minValue || Number.isNaN(this._private__maxValue) || Number.isNaN(this._private__minValue);
|
|
}
|
|
_internal_merge(anotherRange) {
|
|
if (anotherRange === null) {
|
|
return this;
|
|
}
|
|
return new PriceRangeImpl(computeFiniteResult(Math.min, this._internal_minValue(), anotherRange._internal_minValue(), -Infinity), computeFiniteResult(Math.max, this._internal_maxValue(), anotherRange._internal_maxValue(), Infinity));
|
|
}
|
|
_internal_scaleAroundCenter(coeff) {
|
|
if (!isNumber(coeff)) {
|
|
return;
|
|
}
|
|
const delta = this._private__maxValue - this._private__minValue;
|
|
if (delta === 0) {
|
|
return;
|
|
}
|
|
const center = (this._private__maxValue + this._private__minValue) * 0.5;
|
|
let maxDelta = this._private__maxValue - center;
|
|
let minDelta = this._private__minValue - center;
|
|
maxDelta *= coeff;
|
|
minDelta *= coeff;
|
|
this._private__maxValue = center + maxDelta;
|
|
this._private__minValue = center + minDelta;
|
|
}
|
|
_internal_shift(delta) {
|
|
if (!isNumber(delta)) {
|
|
return;
|
|
}
|
|
this._private__maxValue += delta;
|
|
this._private__minValue += delta;
|
|
}
|
|
_internal_toRaw() {
|
|
return {
|
|
minValue: this._private__minValue,
|
|
maxValue: this._private__maxValue,
|
|
};
|
|
}
|
|
static _internal_fromRaw(raw) {
|
|
return (raw === null) ? null : new PriceRangeImpl(raw.minValue, raw.maxValue);
|
|
}
|
|
}
|
|
|
|
class AutoscaleInfoImpl {
|
|
constructor(priceRange, margins) {
|
|
this._private__priceRange = priceRange;
|
|
this._private__margins = margins || null;
|
|
}
|
|
_internal_priceRange() {
|
|
return this._private__priceRange;
|
|
}
|
|
_internal_margins() {
|
|
return this._private__margins;
|
|
}
|
|
_internal_toRaw() {
|
|
return {
|
|
priceRange: this._private__priceRange === null ? null : this._private__priceRange._internal_toRaw(),
|
|
margins: this._private__margins || undefined,
|
|
};
|
|
}
|
|
static _internal_fromRaw(raw) {
|
|
return (raw === null) ? null : new AutoscaleInfoImpl(PriceRangeImpl._internal_fromRaw(raw.priceRange), raw.margins);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Power-of-2 conflation levels supported by the system.
|
|
* These represent the number of original data points that get merged into one conflated point.
|
|
*/
|
|
const CONFLATION_LEVELS = [2, 4, 8, 16, 32, 64, 128, 256, 512];
|
|
/**
|
|
* Maximum conflation level supported.
|
|
*/
|
|
const MAX_CONFLATION_LEVEL = 512;
|
|
/**
|
|
* Device pixel ratio threshold for conflation.
|
|
* Conflation happens when barSpacing is less than 1.0 / devicePixelRatio.
|
|
* This ensures we only conflate when we can't physically display the detail.
|
|
*/
|
|
const DPR_CONFLATION_THRESHOLD = 1;
|
|
const CONFLATION_ERROR_MESSAGES = {
|
|
_internal_missingPriceValueBuilder: 'Custom series with conflation reducer must have a priceValueBuilder method',
|
|
};
|
|
|
|
class CustomPriceLinePaneView extends SeriesHorizontalLinePaneView {
|
|
constructor(series, priceLine) {
|
|
super(series);
|
|
this._private__priceLine = priceLine;
|
|
}
|
|
_internal__updateImpl() {
|
|
const data = this._internal__lineRendererData;
|
|
data._internal_visible = false;
|
|
const lineOptions = this._private__priceLine._internal_options();
|
|
if (!this._internal__series._internal_visible() || !lineOptions.lineVisible) {
|
|
return;
|
|
}
|
|
const y = this._private__priceLine._internal_yCoord();
|
|
if (y === null) {
|
|
return;
|
|
}
|
|
data._internal_visible = true;
|
|
data._internal_y = y;
|
|
data._internal_color = lineOptions.color;
|
|
data._internal_lineWidth = lineOptions.lineWidth;
|
|
data._internal_lineStyle = lineOptions.lineStyle;
|
|
data._internal_externalId = this._private__priceLine._internal_options().id;
|
|
}
|
|
}
|
|
|
|
class CustomPriceLinePriceAxisView extends PriceAxisView {
|
|
constructor(series, priceLine) {
|
|
super();
|
|
this._private__series = series;
|
|
this._private__priceLine = priceLine;
|
|
}
|
|
_internal__updateRendererData(axisRendererData, paneRendererData, commonData) {
|
|
axisRendererData._internal_visible = false;
|
|
paneRendererData._internal_visible = false;
|
|
const options = this._private__priceLine._internal_options();
|
|
const labelVisible = options.axisLabelVisible;
|
|
const showPaneLabel = options.title !== '';
|
|
const series = this._private__series;
|
|
if (!labelVisible || !series._internal_visible()) {
|
|
return;
|
|
}
|
|
const y = this._private__priceLine._internal_yCoord();
|
|
if (y === null) {
|
|
return;
|
|
}
|
|
if (showPaneLabel) {
|
|
paneRendererData._internal_text = options.title;
|
|
paneRendererData._internal_visible = true;
|
|
}
|
|
paneRendererData._internal_borderColor = series._internal_model()._internal_backgroundColorAtYPercentFromTop(y / series._internal_priceScale()._internal_height());
|
|
axisRendererData._internal_text = this._private__formatPrice(options.price);
|
|
axisRendererData._internal_visible = true;
|
|
const colors = this._private__series._internal_model()._internal_colorParser()._internal_generateContrastColors(options.axisLabelColor || options.color);
|
|
commonData._internal_background = colors._internal_background;
|
|
const textColor = options.axisLabelTextColor || colors._internal_foreground;
|
|
axisRendererData._internal_color = textColor; // price text
|
|
paneRendererData._internal_color = textColor; // title text
|
|
commonData._internal_coordinate = y;
|
|
}
|
|
_private__formatPrice(price) {
|
|
const firstValue = this._private__series._internal_firstValue();
|
|
if (firstValue === null) {
|
|
return '';
|
|
}
|
|
return this._private__series._internal_priceScale()._internal_formatPrice(price, firstValue._internal_value);
|
|
}
|
|
}
|
|
|
|
class CustomPriceLine {
|
|
constructor(series, options) {
|
|
this._private__series = series;
|
|
this._private__options = options;
|
|
this._private__priceLineView = new CustomPriceLinePaneView(series, this);
|
|
this._private__priceAxisView = new CustomPriceLinePriceAxisView(series, this);
|
|
this._private__panePriceAxisView = new PanePriceAxisView(this._private__priceAxisView, series, series._internal_model());
|
|
}
|
|
_internal_applyOptions(options) {
|
|
merge(this._private__options, options);
|
|
this._internal_update();
|
|
this._private__series._internal_model()._internal_lightUpdate();
|
|
}
|
|
_internal_options() {
|
|
return this._private__options;
|
|
}
|
|
_internal_paneView() {
|
|
return this._private__priceLineView;
|
|
}
|
|
_internal_labelPaneView() {
|
|
return this._private__panePriceAxisView;
|
|
}
|
|
_internal_priceAxisView() {
|
|
return this._private__priceAxisView;
|
|
}
|
|
_internal_update() {
|
|
this._private__priceLineView._internal_update();
|
|
this._private__priceAxisView._internal_update();
|
|
}
|
|
_internal_yCoord() {
|
|
const series = this._private__series;
|
|
const priceScale = series._internal_priceScale();
|
|
const timeScale = series._internal_model()._internal_timeScale();
|
|
if (timeScale._internal_isEmpty() || priceScale._internal_isEmpty()) {
|
|
return null;
|
|
}
|
|
const firstValue = series._internal_firstValue();
|
|
if (firstValue === null) {
|
|
return null;
|
|
}
|
|
return priceScale._internal_priceToCoordinate(this._private__options.price, firstValue._internal_value);
|
|
}
|
|
}
|
|
|
|
class DataConflater {
|
|
constructor() {
|
|
this._private__dataCache = new WeakMap();
|
|
}
|
|
_internal_calculateConflationLevelWithSmoothing(barSpacing, devicePixelRatio, smoothingFactor) {
|
|
const conflationThreshold = (DPR_CONFLATION_THRESHOLD / devicePixelRatio) * smoothingFactor;
|
|
if (barSpacing >= conflationThreshold) {
|
|
return 1;
|
|
}
|
|
// calculate conflation level as power of 2
|
|
const ratio = conflationThreshold / barSpacing;
|
|
const conflationLevel = Math.pow(2, Math.floor(Math.log2(ratio)));
|
|
// ensure we don't exceed maximum conflation level
|
|
return Math.min(conflationLevel, MAX_CONFLATION_LEVEL);
|
|
}
|
|
_internal_conflateByFactor(data, barsToMerge, customReducer, isCustomSeries = false, priceValueBuilder) {
|
|
if (data.length === 0 || barsToMerge <= 1) {
|
|
return data;
|
|
}
|
|
const conflationLevel = this._private__normalizeConflationLevel(barsToMerge);
|
|
if (conflationLevel <= 1) {
|
|
return data;
|
|
}
|
|
const entry = this._private__getValidatedCacheEntry(data);
|
|
let cachedRows = entry._internal_levelResults.get(conflationLevel);
|
|
if (cachedRows !== undefined) {
|
|
return cachedRows;
|
|
}
|
|
cachedRows = this._private__buildRecursively(data, conflationLevel, customReducer, isCustomSeries, priceValueBuilder, entry._internal_levelResults);
|
|
entry._internal_levelResults.set(conflationLevel, cachedRows);
|
|
return cachedRows;
|
|
}
|
|
/**
|
|
* Efficiently update the last conflated chunk when new data arrives.
|
|
* This avoids rebuilding all chunks when just the last data point changes.
|
|
*/
|
|
_internal_updateLastConflatedChunk(originalData, newLastRow, conflationLevel, customReducer, isCustomSeries = false, priceValueBuilder) {
|
|
if (conflationLevel < 1 || originalData.length === 0) {
|
|
return originalData;
|
|
}
|
|
const entry = this._private__getValidatedCacheEntry(originalData);
|
|
const cachedRows = entry._internal_levelResults.get(conflationLevel);
|
|
if (!cachedRows) {
|
|
return this._internal_conflateByFactor(originalData, conflationLevel, customReducer, isCustomSeries, priceValueBuilder);
|
|
}
|
|
const updatedRows = this._private__updateLastChunkInCache(originalData, newLastRow, conflationLevel, cachedRows, isCustomSeries, customReducer, priceValueBuilder);
|
|
entry._internal_levelResults.set(conflationLevel, updatedRows);
|
|
return updatedRows;
|
|
}
|
|
_private__normalizeConflationLevel(barsToMerge) {
|
|
if (barsToMerge <= 2) {
|
|
return 2;
|
|
}
|
|
for (const level of CONFLATION_LEVELS) {
|
|
if (barsToMerge <= level) {
|
|
return level;
|
|
}
|
|
}
|
|
return MAX_CONFLATION_LEVEL;
|
|
}
|
|
_private__getDataVersion(data) {
|
|
if (data.length === 0) {
|
|
return 0;
|
|
}
|
|
// Simple hash based on data length and first/last items
|
|
const first = data[0];
|
|
const last = data[data.length - 1];
|
|
return data.length * 31 + first._internal_index * 17 + last._internal_index * 13;
|
|
}
|
|
/**
|
|
* Build conflation recursively, reusing previous level results.
|
|
*/
|
|
_private__buildRecursively(data, targetLevel, customReducer, isCustomSeries = false, priceValueBuilder, levelResults = new Map()) {
|
|
if (targetLevel === 2) {
|
|
return this._private__buildLevelFromOriginal(data, 2, customReducer, isCustomSeries, priceValueBuilder);
|
|
}
|
|
const prevLevel = targetLevel / 2;
|
|
let prevData = levelResults.get(prevLevel);
|
|
if (!prevData) {
|
|
prevData = this._private__buildRecursively(data, prevLevel, customReducer, isCustomSeries, priceValueBuilder, levelResults);
|
|
levelResults.set(prevLevel, prevData);
|
|
}
|
|
return this._private__buildLevelFromPrevious(prevData, customReducer, isCustomSeries, priceValueBuilder);
|
|
}
|
|
/**
|
|
* Build a conflation level directly from original data (used for level 2).
|
|
*/
|
|
_private__buildLevelFromOriginal(data, level, customReducer, isCustomSeries = false, priceValueBuilder) {
|
|
const chunks = this._private__buildChunksFromData(data, level, customReducer, isCustomSeries, priceValueBuilder);
|
|
return this._private__chunksToSeriesPlotRows(chunks, isCustomSeries);
|
|
}
|
|
/**
|
|
* Build a conflation level from the previous level's result.
|
|
*/
|
|
_private__buildLevelFromPrevious(prevData, customReducer, isCustomSeries = false, priceValueBuilder) {
|
|
// Always merge 2 chunks from the previous level
|
|
const chunks = this._private__buildChunksFromData(prevData, 2, customReducer, isCustomSeries, priceValueBuilder);
|
|
return this._private__chunksToSeriesPlotRows(chunks, isCustomSeries);
|
|
}
|
|
_private__buildChunksFromData(data, mergeFactor, customReducer, isCustomSeries = false, priceValueBuilder) {
|
|
const chunks = [];
|
|
for (let i = 0; i < data.length; i += mergeFactor) {
|
|
const remaining = data.length - i;
|
|
if (remaining >= mergeFactor) {
|
|
const merged = this._private__mergeTwoRows(data[i], data[i + 1], customReducer, isCustomSeries, priceValueBuilder);
|
|
merged._internal_isRemainder = false;
|
|
chunks.push(merged);
|
|
}
|
|
else {
|
|
// remainder of 1 -> fold into previous chunk if possible
|
|
if (chunks.length === 0) {
|
|
chunks.push(this._private__plotRowToChunk(data[i], true));
|
|
}
|
|
else {
|
|
const prev = chunks[chunks.length - 1];
|
|
chunks[chunks.length - 1] = this._private__mergeChunkAndRow(prev, data[i], customReducer, isCustomSeries, priceValueBuilder);
|
|
}
|
|
}
|
|
}
|
|
return chunks;
|
|
}
|
|
_private__sumCount(a, b) {
|
|
return (a ?? 1) + (b ?? 1);
|
|
}
|
|
_private__mergeTwoRows(a, b, customReducer, isCustomSeries = false, priceValueBuilder) {
|
|
if (!isCustomSeries || !customReducer || !priceValueBuilder) {
|
|
const high = a._internal_value[1 /* PlotRowValueIndex.High */] > b._internal_value[1 /* PlotRowValueIndex.High */] ? a._internal_value[1 /* PlotRowValueIndex.High */] : b._internal_value[1 /* PlotRowValueIndex.High */];
|
|
const low = a._internal_value[2 /* PlotRowValueIndex.Low */] < b._internal_value[2 /* PlotRowValueIndex.Low */] ? a._internal_value[2 /* PlotRowValueIndex.Low */] : b._internal_value[2 /* PlotRowValueIndex.Low */];
|
|
return {
|
|
_internal_startIndex: a._internal_index,
|
|
_internal_endIndex: b._internal_index,
|
|
_internal_startTime: a._internal_time,
|
|
_internal_endTime: b._internal_time,
|
|
_internal_open: a._internal_value[0 /* PlotRowValueIndex.Open */],
|
|
_internal_high: high,
|
|
_internal_low: low,
|
|
_internal_close: b._internal_value[3 /* PlotRowValueIndex.Close */],
|
|
_internal_originalDataCount: this._private__sumCount(a._internal_originalDataCount, b._internal_originalDataCount),
|
|
_internal_conflatedData: undefined,
|
|
_internal_isRemainder: false,
|
|
};
|
|
}
|
|
const c1 = this._private__convertToContext(a, priceValueBuilder);
|
|
const c2 = this._private__convertToContext(b, priceValueBuilder);
|
|
const aggregated = customReducer(c1, c2);
|
|
const prices = priceValueBuilder(aggregated);
|
|
const p = prices.length ? prices[prices.length - 1] : 0;
|
|
return {
|
|
_internal_startIndex: a._internal_index,
|
|
_internal_endIndex: b._internal_index,
|
|
_internal_startTime: a._internal_time,
|
|
_internal_endTime: b._internal_time,
|
|
_internal_open: a._internal_value[0 /* PlotRowValueIndex.Open */],
|
|
_internal_high: Math.max(a._internal_value[1 /* PlotRowValueIndex.High */], p),
|
|
_internal_low: Math.min(a._internal_value[2 /* PlotRowValueIndex.Low */], p),
|
|
_internal_close: p,
|
|
_internal_originalDataCount: this._private__sumCount(a._internal_originalDataCount, b._internal_originalDataCount),
|
|
_internal_conflatedData: aggregated,
|
|
_internal_isRemainder: false,
|
|
};
|
|
}
|
|
_private__mergeChunkAndRow(chunk, row, customReducer, isCustomSeries = false, priceValueBuilder) {
|
|
if (!isCustomSeries || !customReducer || !priceValueBuilder) {
|
|
return {
|
|
_internal_startIndex: chunk._internal_startIndex,
|
|
_internal_endIndex: row._internal_index,
|
|
_internal_startTime: chunk._internal_startTime,
|
|
_internal_endTime: row._internal_time,
|
|
_internal_open: chunk._internal_open,
|
|
_internal_high: chunk._internal_high > row._internal_value[1 /* PlotRowValueIndex.High */] ? chunk._internal_high : row._internal_value[1 /* PlotRowValueIndex.High */],
|
|
_internal_low: chunk._internal_low < row._internal_value[2 /* PlotRowValueIndex.Low */] ? chunk._internal_low : row._internal_value[2 /* PlotRowValueIndex.Low */],
|
|
_internal_close: row._internal_value[3 /* PlotRowValueIndex.Close */],
|
|
_internal_originalDataCount: chunk._internal_originalDataCount + (row._internal_originalDataCount ?? 1),
|
|
_internal_conflatedData: chunk._internal_conflatedData,
|
|
_internal_isRemainder: false,
|
|
};
|
|
}
|
|
const prevAgg = chunk._internal_conflatedData;
|
|
const ctx = this._private__convertToContext(row, priceValueBuilder);
|
|
// if prevAgg is missing (e.g single-item remainder chunk)
|
|
// treat the row as the first aggregate seed to avoid calling builder on undefined.
|
|
const prevCtx = prevAgg
|
|
? {
|
|
data: prevAgg,
|
|
index: chunk._internal_startIndex,
|
|
originalTime: chunk._internal_startTime,
|
|
time: chunk._internal_startTime,
|
|
priceValues: priceValueBuilder(prevAgg),
|
|
}
|
|
: null;
|
|
const aggregated = prevCtx ? customReducer(prevCtx, ctx) : (ctx.data);
|
|
const prices = prevCtx ? priceValueBuilder(aggregated) : ctx.priceValues;
|
|
const p = prices.length ? prices[prices.length - 1] : 0;
|
|
return {
|
|
_internal_startIndex: chunk._internal_startIndex,
|
|
_internal_endIndex: row._internal_index,
|
|
_internal_startTime: chunk._internal_startTime,
|
|
_internal_endTime: row._internal_time,
|
|
_internal_open: chunk._internal_open,
|
|
_internal_high: Math.max(chunk._internal_high, p),
|
|
_internal_low: Math.min(chunk._internal_low, p),
|
|
_internal_close: p,
|
|
_internal_originalDataCount: chunk._internal_originalDataCount + (row._internal_originalDataCount ?? 1),
|
|
_internal_conflatedData: aggregated,
|
|
_internal_isRemainder: false,
|
|
};
|
|
}
|
|
// fold [start, end) with override at overrideIndex
|
|
// eslint-disable-next-line max-params
|
|
_private__mergeRangeWithOverride(data, start, end, overrideIndex, overrideRow, customReducer, isCustomSeries = false, priceValueBuilder) {
|
|
const first = (start === overrideIndex) ? overrideRow : data[start];
|
|
if (end - start === 1) {
|
|
return this._private__plotRowToChunk(first, true);
|
|
}
|
|
const second = (start + 1 === overrideIndex) ? overrideRow : data[start + 1];
|
|
let chunk = this._private__mergeTwoRows(first, second, customReducer, isCustomSeries, priceValueBuilder);
|
|
for (let i = start + 2; i < end; i++) {
|
|
const row = (i === overrideIndex) ? overrideRow : data[i];
|
|
chunk = this._private__mergeChunkAndRow(chunk, row, customReducer, isCustomSeries, priceValueBuilder);
|
|
}
|
|
return chunk;
|
|
}
|
|
_private__convertToContext(item, priceValueBuilder) {
|
|
const itemData = item._internal_data ?? {};
|
|
return {
|
|
data: item._internal_data,
|
|
index: item._internal_index,
|
|
originalTime: item._internal_originalTime,
|
|
time: item._internal_time,
|
|
priceValues: priceValueBuilder(itemData),
|
|
};
|
|
}
|
|
_private__chunkToSeriesPlotRow(chunk, isCustomSeries = false) {
|
|
const isCustom = isCustomSeries === true;
|
|
const hasCustomData = !!chunk._internal_conflatedData;
|
|
const base = {
|
|
_internal_index: chunk._internal_startIndex,
|
|
_internal_time: chunk._internal_startTime,
|
|
_internal_originalTime: chunk._internal_startTime,
|
|
_internal_value: [
|
|
isCustom ? chunk._internal_close : chunk._internal_open,
|
|
chunk._internal_high,
|
|
chunk._internal_low,
|
|
chunk._internal_close,
|
|
],
|
|
_internal_originalDataCount: chunk._internal_originalDataCount,
|
|
};
|
|
const data = isCustom
|
|
? (hasCustomData ? chunk._internal_conflatedData : { _internal_time: chunk._internal_startTime })
|
|
: undefined;
|
|
return {
|
|
...base,
|
|
_internal_data: data,
|
|
};
|
|
}
|
|
_private__chunksToSeriesPlotRows(chunks, isCustomSeries = false) {
|
|
return chunks.map((chunk) => this._private__chunkToSeriesPlotRow(chunk, isCustomSeries));
|
|
}
|
|
/**
|
|
* Update only the last chunk in cached conflated data efficiently.
|
|
*/
|
|
// eslint-disable-next-line max-params
|
|
_private__updateLastChunkInCache(originalData, newLastRow, conflationLevel, cachedRows, isCustomSeries = false, customReducer, priceValueBuilder) {
|
|
if (cachedRows.length === 0) {
|
|
return cachedRows;
|
|
}
|
|
const lastOriginalIndex = originalData.length - 1;
|
|
const chunkStartIndex = Math.floor(lastOriginalIndex / conflationLevel) * conflationLevel;
|
|
const chunkEndIndex = Math.min(chunkStartIndex + conflationLevel, originalData.length);
|
|
if (chunkEndIndex - chunkStartIndex < conflationLevel && originalData.length > conflationLevel) {
|
|
// we must allocate a new array here to do a full rebuild.
|
|
const newOriginalData = originalData.slice();
|
|
newOriginalData[newOriginalData.length - 1] = newLastRow;
|
|
return this._internal_conflateByFactor(newOriginalData, conflationLevel, customReducer, isCustomSeries, priceValueBuilder);
|
|
}
|
|
const lastChunkIndex = Math.floor((lastOriginalIndex - 1) / conflationLevel);
|
|
const newChunkIndex = Math.floor(lastOriginalIndex / conflationLevel);
|
|
if (lastChunkIndex === newChunkIndex || cachedRows.length === 1) {
|
|
// Data length is within the same chunk OR it's the only chunk
|
|
const actualEndIndex = Math.min(chunkStartIndex + conflationLevel, originalData.length);
|
|
const count = actualEndIndex - chunkStartIndex;
|
|
if (count <= 0) {
|
|
// This can happen if originalData.length was 0, though we guard at the top.
|
|
return cachedRows;
|
|
}
|
|
const mergedChunk = count === 1
|
|
? this._private__plotRowToChunk((chunkStartIndex === lastOriginalIndex) ? newLastRow : originalData[chunkStartIndex], /* isRemainder*/ true)
|
|
: this._private__mergeRangeWithOverride(originalData, chunkStartIndex, actualEndIndex, lastOriginalIndex, newLastRow, customReducer, isCustomSeries, priceValueBuilder);
|
|
// in-place update of the cached result: avoid allocating a new array
|
|
cachedRows[cachedRows.length - 1] = this._private__chunkToSeriesPlotRow(mergedChunk, isCustomSeries);
|
|
return cachedRows;
|
|
}
|
|
else {
|
|
// update affects chunk structure
|
|
// we must allocate a new array here to do a full rebuild.
|
|
const newOriginalData = originalData.slice();
|
|
newOriginalData[newOriginalData.length - 1] = newLastRow;
|
|
return this._internal_conflateByFactor(newOriginalData, conflationLevel, customReducer, isCustomSeries, priceValueBuilder);
|
|
}
|
|
}
|
|
_private__plotRowToChunk(item, isRemainder = false) {
|
|
const chunk = {
|
|
_internal_startIndex: item._internal_index,
|
|
_internal_endIndex: item._internal_index,
|
|
_internal_startTime: item._internal_time,
|
|
_internal_endTime: item._internal_time,
|
|
_internal_open: item._internal_value[0 /* PlotRowValueIndex.Open */],
|
|
_internal_high: item._internal_value[1 /* PlotRowValueIndex.High */],
|
|
_internal_low: item._internal_value[2 /* PlotRowValueIndex.Low */],
|
|
_internal_close: item._internal_value[3 /* PlotRowValueIndex.Close */],
|
|
_internal_originalDataCount: item._internal_originalDataCount ?? 1,
|
|
_internal_conflatedData: item._internal_data,
|
|
_internal_isRemainder: isRemainder,
|
|
};
|
|
return chunk;
|
|
}
|
|
_private__getValidatedCacheEntry(data) {
|
|
const entry = this._private__ensureCacheEntry(data);
|
|
const dataVersion = this._private__getDataVersion(data);
|
|
if (entry._internal_version !== dataVersion) {
|
|
entry._internal_levelResults.clear();
|
|
entry._internal_version = dataVersion;
|
|
}
|
|
return entry;
|
|
}
|
|
_private__ensureCacheEntry(data) {
|
|
let entry = this._private__dataCache.get(data);
|
|
if (entry === undefined) {
|
|
entry = {
|
|
_internal_version: this._private__getDataVersion(data),
|
|
_internal_levelResults: new Map(),
|
|
};
|
|
this._private__dataCache.set(data, entry);
|
|
}
|
|
return entry;
|
|
}
|
|
}
|
|
|
|
class PriceDataSource extends DataSource {
|
|
constructor(model) {
|
|
super();
|
|
this._private__model = model;
|
|
}
|
|
_internal_model() {
|
|
return this._private__model;
|
|
}
|
|
}
|
|
|
|
const barStyleFnMap = {
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
Bar: (findBar, barStyle, barIndex, precomputedBars) => {
|
|
const upColor = barStyle.upColor;
|
|
const downColor = barStyle.downColor;
|
|
const currentBar = ensureNotNull(findBar(barIndex, precomputedBars));
|
|
const isUp = ensure(currentBar._internal_value[0 /* PlotRowValueIndex.Open */]) <= ensure(currentBar._internal_value[3 /* PlotRowValueIndex.Close */]);
|
|
return {
|
|
_internal_barColor: currentBar._internal_color ?? (isUp ? upColor : downColor),
|
|
};
|
|
},
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
Candlestick: (findBar, candlestickStyle, barIndex, precomputedBars) => {
|
|
const upColor = candlestickStyle.upColor;
|
|
const downColor = candlestickStyle.downColor;
|
|
const borderUpColor = candlestickStyle.borderUpColor;
|
|
const borderDownColor = candlestickStyle.borderDownColor;
|
|
const wickUpColor = candlestickStyle.wickUpColor;
|
|
const wickDownColor = candlestickStyle.wickDownColor;
|
|
const currentBar = ensureNotNull(findBar(barIndex, precomputedBars));
|
|
const isUp = ensure(currentBar._internal_value[0 /* PlotRowValueIndex.Open */]) <= ensure(currentBar._internal_value[3 /* PlotRowValueIndex.Close */]);
|
|
return {
|
|
_internal_barColor: currentBar._internal_color ?? (isUp ? upColor : downColor),
|
|
_internal_barBorderColor: currentBar._internal_borderColor ?? (isUp ? borderUpColor : borderDownColor),
|
|
_internal_barWickColor: currentBar._internal_wickColor ?? (isUp ? wickUpColor : wickDownColor),
|
|
};
|
|
},
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
Custom: (findBar, customStyle, barIndex, precomputedBars) => {
|
|
const currentBar = ensureNotNull(findBar(barIndex, precomputedBars));
|
|
return {
|
|
_internal_barColor: currentBar._internal_color ?? customStyle.color,
|
|
};
|
|
},
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
Area: (findBar, areaStyle, barIndex, precomputedBars) => {
|
|
const currentBar = ensureNotNull(findBar(barIndex, precomputedBars));
|
|
return {
|
|
_internal_barColor: currentBar._internal_lineColor ?? areaStyle.lineColor,
|
|
_internal_lineColor: currentBar._internal_lineColor ?? areaStyle.lineColor,
|
|
_internal_topColor: currentBar._internal_topColor ?? areaStyle.topColor,
|
|
_internal_bottomColor: currentBar._internal_bottomColor ?? areaStyle.bottomColor,
|
|
};
|
|
},
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
Baseline: (findBar, baselineStyle, barIndex, precomputedBars) => {
|
|
const currentBar = ensureNotNull(findBar(barIndex, precomputedBars));
|
|
const isAboveBaseline = currentBar._internal_value[3 /* PlotRowValueIndex.Close */] >= baselineStyle.baseValue.price;
|
|
return {
|
|
_internal_barColor: isAboveBaseline ? baselineStyle.topLineColor : baselineStyle.bottomLineColor,
|
|
_internal_topLineColor: currentBar._internal_topLineColor ?? baselineStyle.topLineColor,
|
|
_internal_bottomLineColor: currentBar._internal_bottomLineColor ?? baselineStyle.bottomLineColor,
|
|
_internal_topFillColor1: currentBar._internal_topFillColor1 ?? baselineStyle.topFillColor1,
|
|
_internal_topFillColor2: currentBar._internal_topFillColor2 ?? baselineStyle.topFillColor2,
|
|
_internal_bottomFillColor1: currentBar._internal_bottomFillColor1 ?? baselineStyle.bottomFillColor1,
|
|
_internal_bottomFillColor2: currentBar._internal_bottomFillColor2 ?? baselineStyle.bottomFillColor2,
|
|
};
|
|
},
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
Line: (findBar, lineStyle, barIndex, precomputedBars) => {
|
|
const currentBar = ensureNotNull(findBar(barIndex, precomputedBars));
|
|
return {
|
|
_internal_barColor: currentBar._internal_color ?? lineStyle.color,
|
|
_internal_lineColor: currentBar._internal_color ?? lineStyle.color,
|
|
};
|
|
},
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
Histogram: (findBar, histogramStyle, barIndex, precomputedBars) => {
|
|
const currentBar = ensureNotNull(findBar(barIndex, precomputedBars));
|
|
return {
|
|
_internal_barColor: currentBar._internal_color ?? histogramStyle.color,
|
|
};
|
|
},
|
|
};
|
|
class SeriesBarColorer {
|
|
constructor(series) {
|
|
this._private__findBar = (barIndex, precomputedBars) => {
|
|
if (precomputedBars !== undefined) {
|
|
return precomputedBars._internal_value;
|
|
}
|
|
return this._private__series._internal_bars()._internal_valueAt(barIndex);
|
|
};
|
|
this._private__series = series;
|
|
this._private__styleGetter = barStyleFnMap[series._internal_seriesType()];
|
|
}
|
|
_internal_barStyle(barIndex, precomputedBars) {
|
|
// precomputedBars: {value: [Array BarValues], previousValue: [Array BarValues] | undefined}
|
|
// Used to avoid binary search if bars are already known
|
|
return this._private__styleGetter(this._private__findBar, this._private__series._internal_options(), barIndex, precomputedBars);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Binary function that accepts two arguments (the first of the type of array elements, and the second is always val), and returns a value convertible to bool.
|
|
* The value returned indicates whether the first argument is considered to go before the second.
|
|
* The function shall not modify any of its arguments.
|
|
*/
|
|
function boundCompare(lower, arr, value, compare, start = 0, to = arr.length) {
|
|
let count = to - start;
|
|
while (0 < count) {
|
|
const count2 = (count >> 1);
|
|
const mid = start + count2;
|
|
if (compare(arr[mid], value) === lower) {
|
|
start = mid + 1;
|
|
count -= count2 + 1;
|
|
}
|
|
else {
|
|
count = count2;
|
|
}
|
|
}
|
|
return start;
|
|
}
|
|
const lowerBound = boundCompare.bind(null, true);
|
|
const upperBound = boundCompare.bind(null, false);
|
|
|
|
/**
|
|
* Search direction if no data found at provided index
|
|
*/
|
|
var MismatchDirection;
|
|
(function (MismatchDirection) {
|
|
/**
|
|
* Search the nearest left item
|
|
*/
|
|
MismatchDirection[MismatchDirection["NearestLeft"] = -1] = "NearestLeft";
|
|
/**
|
|
* Do not search
|
|
*/
|
|
MismatchDirection[MismatchDirection["None"] = 0] = "None";
|
|
/**
|
|
* Search the nearest right item
|
|
*/
|
|
MismatchDirection[MismatchDirection["NearestRight"] = 1] = "NearestRight";
|
|
})(MismatchDirection || (MismatchDirection = {}));
|
|
// TODO: think about changing it dynamically
|
|
const CHUNK_SIZE = 30;
|
|
/**
|
|
* PlotList is an array of plot rows
|
|
* each plot row consists of key (index in timescale) and plot value map
|
|
*/
|
|
class PlotList {
|
|
constructor() {
|
|
this._private__items = [];
|
|
this._private__minMaxCache = new Map();
|
|
this._private__rowSearchCache = new Map();
|
|
this._private__indices = [];
|
|
}
|
|
// @returns Last row
|
|
_internal_last() {
|
|
return this._internal_size() > 0 ? this._private__items[this._private__items.length - 1] : null;
|
|
}
|
|
_internal_firstIndex() {
|
|
return this._internal_size() > 0 ? this._private__indexAt(0) : null;
|
|
}
|
|
_internal_lastIndex() {
|
|
return this._internal_size() > 0 ? this._private__indexAt((this._private__items.length - 1)) : null;
|
|
}
|
|
_internal_size() {
|
|
return this._private__items.length;
|
|
}
|
|
_internal_isEmpty() {
|
|
return this._internal_size() === 0;
|
|
}
|
|
_internal_contains(index) {
|
|
return this._private__search(index, 0 /* MismatchDirection.None */) !== null;
|
|
}
|
|
_internal_valueAt(index) {
|
|
return this._internal_search(index);
|
|
}
|
|
_internal_search(index, searchMode = 0 /* MismatchDirection.None */) {
|
|
const pos = this._private__search(index, searchMode);
|
|
if (pos === null) {
|
|
return null;
|
|
}
|
|
return {
|
|
...this._private__valueAt(pos),
|
|
_internal_index: this._private__indexAt(pos),
|
|
};
|
|
}
|
|
_internal_rows() {
|
|
return this._private__items;
|
|
}
|
|
_internal_minMaxOnRangeCached(start, end, plots) {
|
|
// this code works for single series only
|
|
// could fail after whitespaces implementation
|
|
if (this._internal_isEmpty()) {
|
|
return null;
|
|
}
|
|
let result = null;
|
|
for (const plot of plots) {
|
|
const plotMinMax = this._private__minMaxOnRangeCachedImpl(start, end, plot);
|
|
result = mergeMinMax(result, plotMinMax);
|
|
}
|
|
return result;
|
|
}
|
|
_internal_setData(plotRows) {
|
|
this._private__rowSearchCache.clear();
|
|
this._private__minMaxCache.clear();
|
|
this._private__items = plotRows;
|
|
this._private__indices = plotRows.map((plotRow) => plotRow._internal_index);
|
|
}
|
|
// TimePointIndex values for fulfilled data points
|
|
_internal_indices() {
|
|
return this._private__indices;
|
|
}
|
|
_private__indexAt(offset) {
|
|
return this._private__items[offset]._internal_index;
|
|
}
|
|
_private__valueAt(offset) {
|
|
return this._private__items[offset];
|
|
}
|
|
_private__search(index, searchMode) {
|
|
const exactPos = this._private__bsearch(index);
|
|
if (exactPos === null && searchMode !== 0 /* MismatchDirection.None */) {
|
|
switch (searchMode) {
|
|
case -1 /* MismatchDirection.NearestLeft */:
|
|
return this._private__searchNearestLeft(index);
|
|
case 1 /* MismatchDirection.NearestRight */:
|
|
return this._private__searchNearestRight(index);
|
|
default:
|
|
throw new TypeError('Unknown search mode');
|
|
}
|
|
}
|
|
return exactPos;
|
|
}
|
|
_private__searchNearestLeft(index) {
|
|
let nearestLeftPos = this._private__lowerbound(index);
|
|
if (nearestLeftPos > 0) {
|
|
nearestLeftPos = nearestLeftPos - 1;
|
|
}
|
|
return (nearestLeftPos !== this._private__items.length && this._private__indexAt(nearestLeftPos) < index) ? nearestLeftPos : null;
|
|
}
|
|
_private__searchNearestRight(index) {
|
|
const nearestRightPos = this._private__upperbound(index);
|
|
return (nearestRightPos !== this._private__items.length && index < this._private__indexAt(nearestRightPos)) ? nearestRightPos : null;
|
|
}
|
|
_private__bsearch(index) {
|
|
const start = this._private__lowerbound(index);
|
|
if (start !== this._private__items.length && !(index < this._private__items[start]._internal_index)) {
|
|
return start;
|
|
}
|
|
return null;
|
|
}
|
|
_private__lowerbound(index) {
|
|
return lowerBound(this._private__items, index, (a, b) => a._internal_index < b);
|
|
}
|
|
_private__upperbound(index) {
|
|
return upperBound(this._private__items, index, (a, b) => a._internal_index > b);
|
|
}
|
|
_private__plotMinMax(startIndex, endIndexExclusive, plotIndex) {
|
|
let result = null;
|
|
for (let i = startIndex; i < endIndexExclusive; i++) {
|
|
const values = this._private__items[i]._internal_value;
|
|
const v = values[plotIndex];
|
|
if (Number.isNaN(v)) {
|
|
continue;
|
|
}
|
|
if (result === null) {
|
|
result = { _internal_min: v, _internal_max: v };
|
|
}
|
|
else {
|
|
if (v < result._internal_min) {
|
|
result._internal_min = v;
|
|
}
|
|
if (v > result._internal_max) {
|
|
result._internal_max = v;
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
_private__minMaxOnRangeCachedImpl(start, end, plotIndex) {
|
|
// this code works for single series only
|
|
// could fail after whitespaces implementation
|
|
if (this._internal_isEmpty()) {
|
|
return null;
|
|
}
|
|
let result = null;
|
|
// assume that bar indexes only increase
|
|
const firstIndex = ensureNotNull(this._internal_firstIndex());
|
|
const lastIndex = ensureNotNull(this._internal_lastIndex());
|
|
const s = Math.max(start, firstIndex);
|
|
const e = Math.min(end, lastIndex);
|
|
const cachedLow = Math.ceil(s / CHUNK_SIZE) * CHUNK_SIZE;
|
|
const cachedHigh = Math.max(cachedLow, Math.floor(e / CHUNK_SIZE) * CHUNK_SIZE);
|
|
{
|
|
const startIndex = this._private__lowerbound(s);
|
|
const endIndex = this._private__upperbound(Math.min(e, cachedLow, end)); // non-inclusive end
|
|
const plotMinMax = this._private__plotMinMax(startIndex, endIndex, plotIndex);
|
|
result = mergeMinMax(result, plotMinMax);
|
|
}
|
|
let minMaxCache = this._private__minMaxCache.get(plotIndex);
|
|
if (minMaxCache === undefined) {
|
|
minMaxCache = new Map();
|
|
this._private__minMaxCache.set(plotIndex, minMaxCache);
|
|
}
|
|
// now go cached
|
|
for (let c = Math.max(cachedLow + 1, s); c < cachedHigh; c += CHUNK_SIZE) {
|
|
const chunkIndex = Math.floor(c / CHUNK_SIZE);
|
|
let chunkMinMax = minMaxCache.get(chunkIndex);
|
|
if (chunkMinMax === undefined) {
|
|
const chunkStart = this._private__lowerbound(chunkIndex * CHUNK_SIZE);
|
|
const chunkEnd = this._private__upperbound((chunkIndex + 1) * CHUNK_SIZE - 1);
|
|
chunkMinMax = this._private__plotMinMax(chunkStart, chunkEnd, plotIndex);
|
|
minMaxCache.set(chunkIndex, chunkMinMax);
|
|
}
|
|
result = mergeMinMax(result, chunkMinMax);
|
|
}
|
|
// tail
|
|
{
|
|
const startIndex = this._private__lowerbound(cachedHigh);
|
|
const endIndex = this._private__upperbound(e); // non-inclusive end
|
|
const plotMinMax = this._private__plotMinMax(startIndex, endIndex, plotIndex);
|
|
result = mergeMinMax(result, plotMinMax);
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
function mergeMinMax(first, second) {
|
|
if (first === null) {
|
|
return second;
|
|
}
|
|
else {
|
|
if (second === null) {
|
|
return first;
|
|
}
|
|
else {
|
|
// merge MinMax values
|
|
const min = Math.min(first._internal_min, second._internal_min);
|
|
const max = Math.max(first._internal_max, second._internal_max);
|
|
return { _internal_min: min, _internal_max: max };
|
|
}
|
|
}
|
|
}
|
|
|
|
function createSeriesPlotList() {
|
|
return new PlotList();
|
|
}
|
|
|
|
const drawingUtils = {
|
|
setLineStyle: setLineStyle,
|
|
};
|
|
|
|
class PrimitiveRendererWrapper {
|
|
constructor(baseRenderer) {
|
|
this._private__baseRenderer = baseRenderer;
|
|
}
|
|
_internal_draw(target, isHovered, hitTestData) {
|
|
this._private__baseRenderer.draw(target, drawingUtils);
|
|
}
|
|
_internal_drawBackground(target, isHovered, hitTestData) {
|
|
this._private__baseRenderer.drawBackground?.(target, drawingUtils);
|
|
}
|
|
}
|
|
class PrimitivePaneViewWrapper {
|
|
constructor(paneView) {
|
|
this._private__cache = null;
|
|
this._private__paneView = paneView;
|
|
}
|
|
_internal_renderer() {
|
|
const baseRenderer = this._private__paneView.renderer();
|
|
if (baseRenderer === null) {
|
|
return null;
|
|
}
|
|
if (this._private__cache?._internal_base === baseRenderer) {
|
|
return this._private__cache._internal_wrapper;
|
|
}
|
|
const wrapper = new PrimitiveRendererWrapper(baseRenderer);
|
|
this._private__cache = {
|
|
_internal_base: baseRenderer,
|
|
_internal_wrapper: wrapper,
|
|
};
|
|
return wrapper;
|
|
}
|
|
_internal_zOrder() {
|
|
return this._private__paneView.zOrder?.() ?? 'normal';
|
|
}
|
|
}
|
|
class PrimitiveWrapper {
|
|
constructor(primitive) {
|
|
this._private__paneViewsCache = null;
|
|
this._internal__primitive = primitive;
|
|
}
|
|
_internal_primitive() {
|
|
return this._internal__primitive;
|
|
}
|
|
_internal_updateAllViews() {
|
|
this._internal__primitive.updateAllViews?.();
|
|
}
|
|
_internal_paneViews() {
|
|
const base = this._internal__primitive.paneViews?.() ?? [];
|
|
if (this._private__paneViewsCache?._internal_base === base) {
|
|
return this._private__paneViewsCache._internal_wrapper;
|
|
}
|
|
const wrapper = base.map((pw) => new PrimitivePaneViewWrapper(pw));
|
|
this._private__paneViewsCache = {
|
|
_internal_base: base,
|
|
_internal_wrapper: wrapper,
|
|
};
|
|
return wrapper;
|
|
}
|
|
_internal_hitTest(x, y) {
|
|
return this._internal__primitive.hitTest?.(x, y) ?? null;
|
|
}
|
|
}
|
|
let PanePrimitiveWrapper$1 = class PanePrimitiveWrapper extends PrimitiveWrapper {
|
|
_internal_labelPaneViews() {
|
|
return [];
|
|
}
|
|
};
|
|
|
|
class SeriesPrimitiveRendererWrapper {
|
|
constructor(baseRenderer) {
|
|
this._private__baseRenderer = baseRenderer;
|
|
}
|
|
_internal_draw(target, isHovered, hitTestData) {
|
|
this._private__baseRenderer.draw(target, drawingUtils);
|
|
}
|
|
_internal_drawBackground(target, isHovered, hitTestData) {
|
|
this._private__baseRenderer.drawBackground?.(target, drawingUtils);
|
|
}
|
|
}
|
|
class SeriesPrimitivePaneViewWrapper {
|
|
constructor(paneView) {
|
|
this._private__cache = null;
|
|
this._private__paneView = paneView;
|
|
}
|
|
_internal_renderer() {
|
|
const baseRenderer = this._private__paneView.renderer();
|
|
if (baseRenderer === null) {
|
|
return null;
|
|
}
|
|
if (this._private__cache?._internal_base === baseRenderer) {
|
|
return this._private__cache._internal_wrapper;
|
|
}
|
|
const wrapper = new SeriesPrimitiveRendererWrapper(baseRenderer);
|
|
this._private__cache = {
|
|
_internal_base: baseRenderer,
|
|
_internal_wrapper: wrapper,
|
|
};
|
|
return wrapper;
|
|
}
|
|
_internal_zOrder() {
|
|
return this._private__paneView.zOrder?.() ?? 'normal';
|
|
}
|
|
}
|
|
function getAxisViewData(baseView) {
|
|
return {
|
|
_internal_text: baseView.text(),
|
|
_internal_coordinate: baseView.coordinate(),
|
|
_internal_fixedCoordinate: baseView.fixedCoordinate?.(),
|
|
_internal_color: baseView.textColor(),
|
|
_internal_background: baseView.backColor(),
|
|
_internal_visible: baseView.visible?.() ?? true,
|
|
_internal_tickVisible: baseView.tickVisible?.() ?? true,
|
|
};
|
|
}
|
|
class SeriesPrimitiveTimeAxisViewWrapper {
|
|
constructor(baseView, timeScale) {
|
|
this._private__renderer = new TimeAxisViewRenderer();
|
|
this._private__baseView = baseView;
|
|
this._private__timeScale = timeScale;
|
|
}
|
|
_internal_renderer() {
|
|
this._private__renderer._internal_setData({
|
|
_internal_width: this._private__timeScale._internal_width(),
|
|
...getAxisViewData(this._private__baseView),
|
|
});
|
|
return this._private__renderer;
|
|
}
|
|
}
|
|
class SeriesPrimitivePriceAxisViewWrapper extends PriceAxisView {
|
|
constructor(baseView, priceScale) {
|
|
super();
|
|
this._private__baseView = baseView;
|
|
this._private__priceScale = priceScale;
|
|
}
|
|
_internal__updateRendererData(axisRendererData, paneRendererData, commonRendererData) {
|
|
const data = getAxisViewData(this._private__baseView);
|
|
commonRendererData._internal_background = data._internal_background;
|
|
axisRendererData._internal_color = data._internal_color;
|
|
const additionalPadding = 2 / 12 * this._private__priceScale._internal_fontSize();
|
|
commonRendererData._internal_additionalPaddingTop = additionalPadding;
|
|
commonRendererData._internal_additionalPaddingBottom = additionalPadding;
|
|
commonRendererData._internal_coordinate = data._internal_coordinate;
|
|
commonRendererData._internal_fixedCoordinate = data._internal_fixedCoordinate;
|
|
axisRendererData._internal_text = data._internal_text;
|
|
axisRendererData._internal_visible = data._internal_visible;
|
|
axisRendererData._internal_tickVisible = data._internal_tickVisible;
|
|
}
|
|
}
|
|
class SeriesPrimitiveWrapper extends PrimitiveWrapper {
|
|
constructor(primitive, series) {
|
|
super(primitive);
|
|
this._private__timeAxisViewsCache = null;
|
|
this._private__priceAxisViewsCache = null;
|
|
this._private__priceAxisPaneViewsCache = null;
|
|
this._private__timeAxisPaneViewsCache = null;
|
|
this._private__series = series;
|
|
}
|
|
_internal_timeAxisViews() {
|
|
const base = this._internal__primitive.timeAxisViews?.() ?? [];
|
|
if (this._private__timeAxisViewsCache?._internal_base === base) {
|
|
return this._private__timeAxisViewsCache._internal_wrapper;
|
|
}
|
|
const timeScale = this._private__series._internal_model()._internal_timeScale();
|
|
const wrapper = base.map((aw) => new SeriesPrimitiveTimeAxisViewWrapper(aw, timeScale));
|
|
this._private__timeAxisViewsCache = {
|
|
_internal_base: base,
|
|
_internal_wrapper: wrapper,
|
|
};
|
|
return wrapper;
|
|
}
|
|
_internal_priceAxisViews() {
|
|
const base = this._internal__primitive.priceAxisViews?.() ?? [];
|
|
if (this._private__priceAxisViewsCache?._internal_base === base) {
|
|
return this._private__priceAxisViewsCache._internal_wrapper;
|
|
}
|
|
const priceScale = this._private__series._internal_priceScale();
|
|
const wrapper = base.map((aw) => new SeriesPrimitivePriceAxisViewWrapper(aw, priceScale));
|
|
this._private__priceAxisViewsCache = {
|
|
_internal_base: base,
|
|
_internal_wrapper: wrapper,
|
|
};
|
|
return wrapper;
|
|
}
|
|
_internal_priceAxisPaneViews() {
|
|
const base = this._internal__primitive.priceAxisPaneViews?.() ?? [];
|
|
if (this._private__priceAxisPaneViewsCache?._internal_base === base) {
|
|
return this._private__priceAxisPaneViewsCache._internal_wrapper;
|
|
}
|
|
const wrapper = base.map((pw) => new SeriesPrimitivePaneViewWrapper(pw));
|
|
this._private__priceAxisPaneViewsCache = {
|
|
_internal_base: base,
|
|
_internal_wrapper: wrapper,
|
|
};
|
|
return wrapper;
|
|
}
|
|
_internal_timeAxisPaneViews() {
|
|
const base = this._internal__primitive.timeAxisPaneViews?.() ?? [];
|
|
if (this._private__timeAxisPaneViewsCache?._internal_base === base) {
|
|
return this._private__timeAxisPaneViewsCache._internal_wrapper;
|
|
}
|
|
const wrapper = base.map((pw) => new SeriesPrimitivePaneViewWrapper(pw));
|
|
this._private__timeAxisPaneViewsCache = {
|
|
_internal_base: base,
|
|
_internal_wrapper: wrapper,
|
|
};
|
|
return wrapper;
|
|
}
|
|
_internal_autoscaleInfo(startTimePoint, endTimePoint) {
|
|
return (this._internal__primitive.autoscaleInfo?.(startTimePoint, endTimePoint) ?? null);
|
|
}
|
|
}
|
|
|
|
function extractPrimitivePaneViews(primitives, extractor, zOrder, destination) {
|
|
primitives.forEach((wrapper) => {
|
|
extractor(wrapper).forEach((paneView) => {
|
|
if (paneView._internal_zOrder() !== zOrder) {
|
|
return;
|
|
}
|
|
destination.push(paneView);
|
|
});
|
|
});
|
|
}
|
|
function primitivePaneViewsExtractor(wrapper) {
|
|
return wrapper._internal_paneViews();
|
|
}
|
|
function primitivePricePaneViewsExtractor(wrapper) {
|
|
return wrapper._internal_priceAxisPaneViews();
|
|
}
|
|
function primitiveTimePaneViewsExtractor(wrapper) {
|
|
return wrapper._internal_timeAxisPaneViews();
|
|
}
|
|
const lineBasedSeries = ['Area', 'Line', 'Baseline'];
|
|
class Series extends PriceDataSource {
|
|
constructor(model, seriesType, options, createPaneView, customPaneView) {
|
|
super(model);
|
|
this._private__data = createSeriesPlotList();
|
|
this._private__priceLineView = new SeriesPriceLinePaneView(this);
|
|
this._private__customPriceLines = [];
|
|
this._private__baseHorizontalLineView = new SeriesHorizontalBaseLinePaneView(this);
|
|
this._private__lastPriceAnimationPaneView = null;
|
|
this._private__barColorerCache = null;
|
|
this._private__animationTimeoutId = null;
|
|
this._private__primitives = [];
|
|
this._private__dataConflater = new DataConflater();
|
|
this._private__conflationByFactorCache = new Map();
|
|
this._private__customConflationReducer = null;
|
|
this._private__options = options;
|
|
this._private__seriesType = seriesType;
|
|
const priceAxisView = new SeriesPriceAxisView(this);
|
|
this._private__priceAxisViews = [priceAxisView];
|
|
this._private__panePriceAxisView = new PanePriceAxisView(priceAxisView, this, model);
|
|
if (lineBasedSeries.includes(this._private__seriesType)) {
|
|
this._private__lastPriceAnimationPaneView = new SeriesLastPriceAnimationPaneView(this);
|
|
}
|
|
this._private__recreateFormatter();
|
|
this._private__paneView = createPaneView(this, this._internal_model(), customPaneView);
|
|
if (this._private__seriesType === 'Custom') {
|
|
const paneView = this._private__paneView;
|
|
if (paneView._internal_conflationReducer) {
|
|
this._internal_setCustomConflationReducer(paneView._internal_conflationReducer);
|
|
}
|
|
}
|
|
}
|
|
_internal_destroy() {
|
|
if (this._private__animationTimeoutId !== null) {
|
|
clearTimeout(this._private__animationTimeoutId);
|
|
}
|
|
}
|
|
_internal_priceLineColor(lastBarColor) {
|
|
return this._private__options.priceLineColor || lastBarColor;
|
|
}
|
|
_internal_lastValueData(globalLast) {
|
|
const noDataRes = { _internal_noData: true };
|
|
const priceScale = this._internal_priceScale();
|
|
if (this._internal_model()._internal_timeScale()._internal_isEmpty() || priceScale._internal_isEmpty() || this._private__data._internal_isEmpty()) {
|
|
return noDataRes;
|
|
}
|
|
const visibleBars = this._internal_model()._internal_timeScale()._internal_visibleStrictRange();
|
|
const firstValue = this._internal_firstValue();
|
|
if (visibleBars === null || firstValue === null) {
|
|
return noDataRes;
|
|
}
|
|
// find range of bars inside range
|
|
// TODO: make it more optimal
|
|
let bar;
|
|
let lastIndex;
|
|
if (globalLast) {
|
|
const lastBar = this._private__data._internal_last();
|
|
if (lastBar === null) {
|
|
return noDataRes;
|
|
}
|
|
bar = lastBar;
|
|
lastIndex = lastBar._internal_index;
|
|
}
|
|
else {
|
|
const endBar = this._private__data._internal_search(visibleBars._internal_right(), -1 /* MismatchDirection.NearestLeft */);
|
|
if (endBar === null) {
|
|
return noDataRes;
|
|
}
|
|
bar = this._private__data._internal_valueAt(endBar._internal_index);
|
|
if (bar === null) {
|
|
return noDataRes;
|
|
}
|
|
lastIndex = endBar._internal_index;
|
|
}
|
|
const price = bar._internal_value[3 /* PlotRowValueIndex.Close */];
|
|
const barColorer = this._internal_barColorer();
|
|
const style = barColorer._internal_barStyle(lastIndex, { _internal_value: bar });
|
|
const coordinate = priceScale._internal_priceToCoordinate(price, firstValue._internal_value);
|
|
return {
|
|
_internal_noData: false,
|
|
_internal_price: price,
|
|
_internal_text: priceScale._internal_formatPrice(price, firstValue._internal_value),
|
|
_internal_formattedPriceAbsolute: priceScale._internal_formatPriceAbsolute(price),
|
|
_internal_formattedPricePercentage: priceScale._internal_formatPricePercentage(price, firstValue._internal_value),
|
|
_internal_color: style._internal_barColor,
|
|
_internal_coordinate: coordinate,
|
|
_internal_index: lastIndex,
|
|
};
|
|
}
|
|
_internal_barColorer() {
|
|
if (this._private__barColorerCache !== null) {
|
|
return this._private__barColorerCache;
|
|
}
|
|
this._private__barColorerCache = new SeriesBarColorer(this);
|
|
return this._private__barColorerCache;
|
|
}
|
|
_internal_options() {
|
|
return this._private__options;
|
|
}
|
|
_internal_applyOptions(options) {
|
|
const model = this._internal_model();
|
|
const { priceScaleId, visible, priceFormat } = options;
|
|
if (priceScaleId !== undefined && priceScaleId !== this._private__options.priceScaleId) {
|
|
// series cannot do it itself, ask model
|
|
model._internal_moveSeriesToScale(this, priceScaleId);
|
|
}
|
|
if (visible !== undefined && visible !== this._private__options.visible) {
|
|
model._internal_invalidateVisibleSeries();
|
|
}
|
|
// Check if conflation-related options are changing
|
|
const conflationOptionsChanged = options.conflationThresholdFactor !== undefined;
|
|
merge(this._private__options, options);
|
|
if (conflationOptionsChanged) {
|
|
this._private__conflationByFactorCache.clear();
|
|
this._internal_model()._internal_lightUpdate();
|
|
}
|
|
if (priceFormat !== undefined) {
|
|
this._private__recreateFormatter();
|
|
// updated formatter might affect rendering and as a consequence of this the width of price axis might be changed
|
|
// thus we need to force the chart to do a full update to apply changes correctly
|
|
// full update is quite heavy operation in terms of performance
|
|
// but updating formatter looks like quite rare so forcing a full update here shouldn't affect the performance a lot
|
|
model._internal_fullUpdate();
|
|
}
|
|
model._internal_updateSource(this);
|
|
// a series might affect crosshair by some options (like crosshair markers)
|
|
// that's why we need to update crosshair as well
|
|
model._internal_updateCrosshair();
|
|
this._private__paneView._internal_update('options');
|
|
}
|
|
_internal_setData(data, updateInfo) {
|
|
this._private__data._internal_setData(data);
|
|
this._private__conflationByFactorCache.clear();
|
|
const ts = this._internal_model()._internal_timeScale();
|
|
const tsOptions = ts._internal_options();
|
|
if (tsOptions.enableConflation && tsOptions.precomputeConflationOnInit) {
|
|
this._private__precomputeConflationLevels(tsOptions.precomputeConflationPriority);
|
|
}
|
|
this._private__paneView._internal_update('data');
|
|
if (this._private__lastPriceAnimationPaneView !== null) {
|
|
if (updateInfo && updateInfo._internal_lastBarUpdatedOrNewBarsAddedToTheRight) {
|
|
this._private__lastPriceAnimationPaneView._internal_onNewRealtimeDataReceived();
|
|
}
|
|
else if (data.length === 0) {
|
|
this._private__lastPriceAnimationPaneView._internal_onDataCleared();
|
|
}
|
|
}
|
|
const sourcePane = this._internal_model()._internal_paneForSource(this);
|
|
this._internal_model()._internal_recalculatePane(sourcePane);
|
|
this._internal_model()._internal_updateSource(this);
|
|
this._internal_model()._internal_updateCrosshair();
|
|
this._internal_model()._internal_lightUpdate();
|
|
}
|
|
_internal_createPriceLine(options) {
|
|
const result = new CustomPriceLine(this, options);
|
|
this._private__customPriceLines.push(result);
|
|
this._internal_model()._internal_updateSource(this);
|
|
return result;
|
|
}
|
|
_internal_removePriceLine(line) {
|
|
const index = this._private__customPriceLines.indexOf(line);
|
|
if (index !== -1) {
|
|
this._private__customPriceLines.splice(index, 1);
|
|
}
|
|
this._internal_model()._internal_updateSource(this);
|
|
}
|
|
_internal_priceLines() {
|
|
return this._private__customPriceLines;
|
|
}
|
|
_internal_seriesType() {
|
|
return this._private__seriesType;
|
|
}
|
|
_internal_firstValue() {
|
|
const bar = this._internal_firstBar();
|
|
if (bar === null) {
|
|
return null;
|
|
}
|
|
return {
|
|
_internal_value: bar._internal_value[3 /* PlotRowValueIndex.Close */],
|
|
_internal_timePoint: bar._internal_time,
|
|
};
|
|
}
|
|
_internal_firstBar() {
|
|
const visibleBars = this._internal_model()._internal_timeScale()._internal_visibleStrictRange();
|
|
if (visibleBars === null) {
|
|
return null;
|
|
}
|
|
const startTimePoint = visibleBars._internal_left();
|
|
return this._private__data._internal_search(startTimePoint, 1 /* MismatchDirection.NearestRight */);
|
|
}
|
|
_internal_bars() {
|
|
return this._private__data;
|
|
}
|
|
_internal_setCustomConflationReducer(reducer) {
|
|
this._private__customConflationReducer = reducer;
|
|
// reset cache to respect new reducer
|
|
this._private__conflationByFactorCache.clear();
|
|
}
|
|
/**
|
|
* Check if conflation is currently enabled for this series.
|
|
*/
|
|
_internal_isConflationEnabled() {
|
|
const timeScale = this._internal_model()._internal_timeScale();
|
|
if (!timeScale._internal_options().enableConflation) {
|
|
return false;
|
|
}
|
|
return this._private__calculateConflationFactor() > 1;
|
|
}
|
|
/**
|
|
* Efficiently update conflation when only the last data point changes.
|
|
* This avoids rebuilding all conflated chunks.
|
|
*/
|
|
_internal_updateLastConflatedChunk(newLastRow) {
|
|
if (!this._internal_isConflationEnabled()) {
|
|
return;
|
|
}
|
|
const conflationFactor = this._private__calculateConflationFactor();
|
|
if (!this._private__conflationByFactorCache.has(conflationFactor)) {
|
|
return;
|
|
}
|
|
const isCustomSeries = this._private__seriesType === 'Custom';
|
|
const customReducer = isCustomSeries ? this._private__customConflationReducer || undefined : undefined;
|
|
const priceValueBuilder = isCustomSeries && this._private__paneView._internal_priceValueBuilder
|
|
? (item) => {
|
|
const customPaneView = this._private__paneView;
|
|
const plotRow = item;
|
|
const result = customPaneView._internal_priceValueBuilder(plotRow);
|
|
return Array.isArray(result) ? result : [typeof result === 'number' ? result : 0];
|
|
}
|
|
: undefined;
|
|
const updatedConflatedRows = this._private__dataConflater._internal_updateLastConflatedChunk(this._private__data._internal_rows(), newLastRow, conflationFactor, customReducer, isCustomSeries, priceValueBuilder);
|
|
const conflatedList = createSeriesPlotList();
|
|
conflatedList._internal_setData(updatedConflatedRows);
|
|
this._private__conflationByFactorCache.set(conflationFactor, conflatedList);
|
|
}
|
|
_internal_conflatedBars() {
|
|
const timeScale = this._internal_model()._internal_timeScale();
|
|
const conflationEnabled = timeScale._internal_options().enableConflation;
|
|
if (this._private__seriesType === 'Custom' && this._private__customConflationReducer === null) {
|
|
return this._private__data;
|
|
}
|
|
if (!conflationEnabled) {
|
|
return this._private__data;
|
|
}
|
|
const factor = this._private__calculateConflationFactor();
|
|
const cached = this._private__conflationByFactorCache.get(factor);
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
this._private__regenerateConflatedDataByFactor(factor);
|
|
const built = this._private__conflationByFactorCache.get(factor);
|
|
return built ?? this._private__data;
|
|
}
|
|
_internal_dataAt(time) {
|
|
const prices = this._private__data._internal_valueAt(time);
|
|
if (prices === null) {
|
|
return null;
|
|
}
|
|
if (this._private__seriesType === 'Bar' || this._private__seriesType === 'Candlestick' || this._private__seriesType === 'Custom') {
|
|
return {
|
|
_internal_open: prices._internal_value[0 /* PlotRowValueIndex.Open */],
|
|
_internal_high: prices._internal_value[1 /* PlotRowValueIndex.High */],
|
|
_internal_low: prices._internal_value[2 /* PlotRowValueIndex.Low */],
|
|
_internal_close: prices._internal_value[3 /* PlotRowValueIndex.Close */],
|
|
};
|
|
}
|
|
else {
|
|
return prices._internal_value[3 /* PlotRowValueIndex.Close */];
|
|
}
|
|
}
|
|
_internal_topPaneViews(pane) {
|
|
const res = [];
|
|
extractPrimitivePaneViews(this._private__primitives, primitivePaneViewsExtractor, 'top', res);
|
|
const animationPaneView = this._private__lastPriceAnimationPaneView;
|
|
if (animationPaneView === null || !animationPaneView._internal_visible()) {
|
|
return res;
|
|
}
|
|
if (this._private__animationTimeoutId === null && animationPaneView._internal_animationActive()) {
|
|
this._private__animationTimeoutId = setTimeout(() => {
|
|
this._private__animationTimeoutId = null;
|
|
this._internal_model()._internal_cursorUpdate();
|
|
}, 0);
|
|
}
|
|
animationPaneView._internal_invalidateStage();
|
|
res.unshift(animationPaneView);
|
|
return res;
|
|
}
|
|
_internal_paneViews() {
|
|
const res = [];
|
|
if (!this._private__isOverlay()) {
|
|
res.push(this._private__baseHorizontalLineView);
|
|
}
|
|
res.push(this._private__paneView, this._private__priceLineView);
|
|
const priceLineViews = this._private__customPriceLines.map((line) => line._internal_paneView());
|
|
res.push(...priceLineViews);
|
|
extractPrimitivePaneViews(this._private__primitives, primitivePaneViewsExtractor, 'normal', res);
|
|
return res;
|
|
}
|
|
_internal_bottomPaneViews() {
|
|
return this._private__extractPaneViews(primitivePaneViewsExtractor, 'bottom');
|
|
}
|
|
_internal_pricePaneViews(zOrder) {
|
|
return this._private__extractPaneViews(primitivePricePaneViewsExtractor, zOrder);
|
|
}
|
|
_internal_timePaneViews(zOrder) {
|
|
return this._private__extractPaneViews(primitiveTimePaneViewsExtractor, zOrder);
|
|
}
|
|
_internal_primitiveHitTest(x, y) {
|
|
return this._private__primitives
|
|
.map((primitive) => primitive._internal_hitTest(x, y))
|
|
.filter((result) => result !== null);
|
|
}
|
|
_internal_labelPaneViews() {
|
|
return [
|
|
this._private__panePriceAxisView,
|
|
...this._private__customPriceLines.map((line) => line._internal_labelPaneView()),
|
|
];
|
|
}
|
|
_internal_priceAxisViews(pane, priceScale) {
|
|
if (priceScale !== this._internal__priceScale && !this._private__isOverlay()) {
|
|
return [];
|
|
}
|
|
const result = [...this._private__priceAxisViews];
|
|
for (const customPriceLine of this._private__customPriceLines) {
|
|
result.push(customPriceLine._internal_priceAxisView());
|
|
}
|
|
this._private__primitives.forEach((wrapper) => {
|
|
result.push(...wrapper._internal_priceAxisViews());
|
|
});
|
|
return result;
|
|
}
|
|
_internal_timeAxisViews() {
|
|
const res = [];
|
|
this._private__primitives.forEach((wrapper) => {
|
|
res.push(...wrapper._internal_timeAxisViews());
|
|
});
|
|
return res;
|
|
}
|
|
_internal_autoscaleInfo(startTimePoint, endTimePoint) {
|
|
if (this._private__options.autoscaleInfoProvider !== undefined) {
|
|
const autoscaleInfo = this._private__options.autoscaleInfoProvider(() => {
|
|
const res = this._private__autoscaleInfoImpl(startTimePoint, endTimePoint);
|
|
return (res === null) ? null : res._internal_toRaw();
|
|
});
|
|
return AutoscaleInfoImpl._internal_fromRaw(autoscaleInfo);
|
|
}
|
|
return this._private__autoscaleInfoImpl(startTimePoint, endTimePoint);
|
|
}
|
|
_internal_base() {
|
|
const priceFormat = this._private__options.priceFormat;
|
|
return priceFormat.base ?? (1 / priceFormat.minMove);
|
|
}
|
|
_internal_formatter() {
|
|
return this._private__formatter;
|
|
}
|
|
_internal_updateAllViews() {
|
|
this._private__paneView._internal_update();
|
|
for (const priceAxisView of this._private__priceAxisViews) {
|
|
priceAxisView._internal_update();
|
|
}
|
|
for (const customPriceLine of this._private__customPriceLines) {
|
|
customPriceLine._internal_update();
|
|
}
|
|
this._private__priceLineView._internal_update();
|
|
this._private__baseHorizontalLineView._internal_update();
|
|
this._private__lastPriceAnimationPaneView?._internal_update();
|
|
this._private__primitives.forEach((wrapper) => wrapper._internal_updateAllViews());
|
|
}
|
|
_internal_priceScale() {
|
|
return ensureNotNull(super._internal_priceScale());
|
|
}
|
|
_internal_markerDataAtIndex(index) {
|
|
const getValue = (this._private__seriesType === 'Line' || this._private__seriesType === 'Area' || this._private__seriesType === 'Baseline') &&
|
|
this._private__options.crosshairMarkerVisible;
|
|
if (!getValue) {
|
|
return null;
|
|
}
|
|
const bar = this._private__data._internal_valueAt(index);
|
|
if (bar === null) {
|
|
return null;
|
|
}
|
|
const price = bar._internal_value[3 /* PlotRowValueIndex.Close */];
|
|
const radius = this._private__markerRadius();
|
|
const borderColor = this._private__markerBorderColor();
|
|
const borderWidth = this._private__markerBorderWidth();
|
|
const backgroundColor = this._private__markerBackgroundColor(index);
|
|
return { _internal_price: price, _internal_radius: radius, _internal_borderColor: borderColor, _internal_borderWidth: borderWidth, _internal_backgroundColor: backgroundColor };
|
|
}
|
|
_internal_title() {
|
|
return this._private__options.title;
|
|
}
|
|
_internal_visible() {
|
|
return this._private__options.visible;
|
|
}
|
|
_internal_attachPrimitive(primitive) {
|
|
this._private__primitives.push(new SeriesPrimitiveWrapper(primitive, this));
|
|
}
|
|
_internal_detachPrimitive(source) {
|
|
this._private__primitives = this._private__primitives.filter((wrapper) => wrapper._internal_primitive() !== source);
|
|
}
|
|
_internal_customSeriesPlotValuesBuilder() {
|
|
if (this._private__seriesType !== 'Custom') {
|
|
return undefined;
|
|
}
|
|
return (data) => {
|
|
return this._private__paneView._internal_priceValueBuilder(data);
|
|
};
|
|
}
|
|
_internal_customSeriesWhitespaceCheck() {
|
|
if (this._private__seriesType !== 'Custom') {
|
|
return undefined;
|
|
}
|
|
return (data) => {
|
|
return this._private__paneView._internal_isWhitespace(data);
|
|
};
|
|
}
|
|
_internal_fulfilledIndices() {
|
|
return this._private__data._internal_indices();
|
|
}
|
|
_private__isOverlay() {
|
|
const priceScale = this._internal_priceScale();
|
|
return !isDefaultPriceScale(priceScale._internal_id());
|
|
}
|
|
_private__autoscaleInfoImpl(startTimePoint, endTimePoint) {
|
|
if (!isInteger(startTimePoint) || !isInteger(endTimePoint) || this._private__data._internal_isEmpty()) {
|
|
return null;
|
|
}
|
|
// TODO: refactor this
|
|
// series data is strongly hardcoded to keep bars
|
|
const plots = this._private__seriesType === 'Line' || this._private__seriesType === 'Area' || this._private__seriesType === 'Baseline' || this._private__seriesType === 'Histogram'
|
|
? [3 /* PlotRowValueIndex.Close */]
|
|
: [2 /* PlotRowValueIndex.Low */, 1 /* PlotRowValueIndex.High */];
|
|
const barsMinMax = this._private__data._internal_minMaxOnRangeCached(startTimePoint, endTimePoint, plots);
|
|
let range = barsMinMax !== null ? new PriceRangeImpl(barsMinMax._internal_min, barsMinMax._internal_max) : null;
|
|
let margins = null;
|
|
if (this._internal_seriesType() === 'Histogram') {
|
|
const base = this._private__options.base;
|
|
const rangeWithBase = new PriceRangeImpl(base, base);
|
|
range = range !== null ? range._internal_merge(rangeWithBase) : rangeWithBase;
|
|
}
|
|
this._private__primitives.forEach((primitive) => {
|
|
const primitiveAutoscale = primitive._internal_autoscaleInfo(startTimePoint, endTimePoint);
|
|
if (primitiveAutoscale?.priceRange) {
|
|
const primitiveRange = new PriceRangeImpl(primitiveAutoscale.priceRange.minValue, primitiveAutoscale.priceRange.maxValue);
|
|
range = range !== null ? range._internal_merge(primitiveRange) : primitiveRange;
|
|
}
|
|
if (primitiveAutoscale?.margins) {
|
|
margins = primitiveAutoscale.margins;
|
|
}
|
|
});
|
|
return new AutoscaleInfoImpl(range, margins);
|
|
}
|
|
_private__markerRadius() {
|
|
switch (this._private__seriesType) {
|
|
case 'Line':
|
|
case 'Area':
|
|
case 'Baseline':
|
|
return this._private__options.crosshairMarkerRadius;
|
|
}
|
|
return 0;
|
|
}
|
|
_private__markerBorderColor() {
|
|
switch (this._private__seriesType) {
|
|
case 'Line':
|
|
case 'Area':
|
|
case 'Baseline': {
|
|
const crosshairMarkerBorderColor = this._private__options.crosshairMarkerBorderColor;
|
|
if (crosshairMarkerBorderColor.length !== 0) {
|
|
return crosshairMarkerBorderColor;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
_private__markerBorderWidth() {
|
|
switch (this._private__seriesType) {
|
|
case 'Line':
|
|
case 'Area':
|
|
case 'Baseline':
|
|
return this._private__options.crosshairMarkerBorderWidth;
|
|
}
|
|
return 0;
|
|
}
|
|
_private__markerBackgroundColor(index) {
|
|
switch (this._private__seriesType) {
|
|
case 'Line':
|
|
case 'Area':
|
|
case 'Baseline': {
|
|
const crosshairMarkerBackgroundColor = this._private__options.crosshairMarkerBackgroundColor;
|
|
if (crosshairMarkerBackgroundColor.length !== 0) {
|
|
return crosshairMarkerBackgroundColor;
|
|
}
|
|
}
|
|
}
|
|
return this._internal_barColorer()._internal_barStyle(index)._internal_barColor;
|
|
}
|
|
_private__recreateFormatter() {
|
|
switch (this._private__options.priceFormat.type) {
|
|
case 'custom': {
|
|
const formatter = this._private__options.priceFormat.formatter;
|
|
this._private__formatter = {
|
|
format: formatter,
|
|
formatTickmarks: this._private__options.priceFormat.tickmarksFormatter ?? ((prices) => prices.map(formatter)),
|
|
};
|
|
break;
|
|
}
|
|
case 'volume': {
|
|
this._private__formatter = new VolumeFormatter(this._private__options.priceFormat.precision);
|
|
break;
|
|
}
|
|
case 'percent': {
|
|
this._private__formatter = new PercentageFormatter(this._private__options.priceFormat.precision);
|
|
break;
|
|
}
|
|
default: {
|
|
const priceScale = Math.pow(10, this._private__options.priceFormat.precision);
|
|
this._private__formatter = new PriceFormatter(priceScale, this._private__options.priceFormat.minMove * priceScale);
|
|
}
|
|
}
|
|
if (this._internal__priceScale !== null) {
|
|
this._internal__priceScale._internal_updateFormatter();
|
|
}
|
|
}
|
|
_private__extractPaneViews(extractor, zOrder) {
|
|
const res = [];
|
|
extractPrimitivePaneViews(this._private__primitives, extractor, zOrder, res);
|
|
return res;
|
|
}
|
|
_private__calculateConflationFactor() {
|
|
const { _internal_barSpacing: barSpacing, _internal_devicePixelRatio: devicePixelRatio, _internal_effectiveSmoothing: effectiveSmoothing } = this._private__getConflationParams();
|
|
return this._private__dataConflater._internal_calculateConflationLevelWithSmoothing(barSpacing, devicePixelRatio, effectiveSmoothing);
|
|
}
|
|
_private__getConflationParams() {
|
|
const timeScale = this._internal_model()._internal_timeScale();
|
|
const barSpacing = timeScale._internal_barSpacing();
|
|
const devicePixelRatio = window.devicePixelRatio || 1;
|
|
const globalSmoothing = timeScale._internal_options().conflationThresholdFactor;
|
|
const seriesSmoothing = this._private__options.conflationThresholdFactor;
|
|
const effectiveSmoothing = seriesSmoothing ?? globalSmoothing ?? 1.0;
|
|
return { _internal_barSpacing: barSpacing, _internal_devicePixelRatio: devicePixelRatio, _internal_effectiveSmoothing: effectiveSmoothing };
|
|
}
|
|
_private__buildConflatedListByFactor(factor) {
|
|
const originalRows = this._private__data._internal_rows();
|
|
let conflatedRows;
|
|
if (this._private__seriesType === 'Custom' && this._private__customConflationReducer !== null) {
|
|
const priceValueBuilder = this._internal_customSeriesPlotValuesBuilder();
|
|
if (!priceValueBuilder) {
|
|
throw new Error(CONFLATION_ERROR_MESSAGES._internal_missingPriceValueBuilder);
|
|
}
|
|
conflatedRows = this._private__dataConflater._internal_conflateByFactor(originalRows, factor, this._private__customConflationReducer, true, (item) => priceValueBuilder(item));
|
|
}
|
|
else {
|
|
conflatedRows = this._private__dataConflater._internal_conflateByFactor(originalRows, factor);
|
|
}
|
|
const list = createSeriesPlotList();
|
|
list._internal_setData(conflatedRows);
|
|
return list;
|
|
}
|
|
_private__regenerateConflatedDataByFactor(factor) {
|
|
const list = this._private__buildConflatedListByFactor(factor);
|
|
this._private__conflationByFactorCache.set(factor, list);
|
|
}
|
|
_private__precomputeConflationLevels(priority) {
|
|
if (this._private__seriesType === 'Custom' &&
|
|
(this._private__customConflationReducer === null ||
|
|
!this._internal_customSeriesPlotValuesBuilder())) {
|
|
return;
|
|
}
|
|
// Clear precomputed cache when data changes
|
|
this._private__conflationByFactorCache.clear();
|
|
const conflateFactors = this._internal_model()._internal_timeScale()._internal_possibleConflationFactors();
|
|
for (const lvl of conflateFactors) {
|
|
const task = () => {
|
|
this._private__precomputeConflationLevel(lvl);
|
|
};
|
|
// Use Prioritized Task Scheduling API if available
|
|
const globalObj = ((typeof window === 'object' && window) || (typeof self === 'object' && self));
|
|
if (globalObj?._internal_scheduler?._internal_postTask) {
|
|
void globalObj._internal_scheduler._internal_postTask(() => { task(); }, { _internal_priority: priority });
|
|
}
|
|
else {
|
|
void Promise.resolve().then(() => task());
|
|
}
|
|
}
|
|
}
|
|
_private__precomputeConflationLevel(factor) {
|
|
// Check if already cached
|
|
if (this._private__conflationByFactorCache.has(factor)) {
|
|
return;
|
|
}
|
|
const originalRows = this._private__data._internal_rows();
|
|
if (originalRows.length === 0) {
|
|
return;
|
|
}
|
|
const list = this._private__buildConflatedListByFactor(factor);
|
|
this._private__conflationByFactorCache.set(factor, list);
|
|
}
|
|
}
|
|
|
|
const magnetPlotRowKeys = [
|
|
3 /* PlotRowValueIndex.Close */,
|
|
];
|
|
const magnetOHLCPlotRowKeys = [
|
|
0 /* PlotRowValueIndex.Open */,
|
|
1 /* PlotRowValueIndex.High */,
|
|
2 /* PlotRowValueIndex.Low */,
|
|
3 /* PlotRowValueIndex.Close */,
|
|
];
|
|
class Magnet {
|
|
constructor(options) {
|
|
this._private__options = options;
|
|
}
|
|
_internal_align(price, index, pane) {
|
|
let res = price;
|
|
if (this._private__options.mode === 0 /* CrosshairMode.Normal */) {
|
|
return res;
|
|
}
|
|
const defaultPriceScale = pane._internal_defaultPriceScale();
|
|
const firstValue = defaultPriceScale._internal_firstValue();
|
|
if (firstValue === null) {
|
|
return res;
|
|
}
|
|
const y = defaultPriceScale._internal_priceToCoordinate(price, firstValue);
|
|
// get all serieses from the pane
|
|
const serieses = pane._internal_dataSources().filter(((ds) => (ds instanceof Series)));
|
|
const candidates = serieses.reduce((acc, series) => {
|
|
if (pane._internal_isOverlay(series) || !series._internal_visible()) {
|
|
return acc;
|
|
}
|
|
const ps = series._internal_priceScale();
|
|
const bars = series._internal_bars();
|
|
if (ps._internal_isEmpty() || !bars._internal_contains(index)) {
|
|
return acc;
|
|
}
|
|
const bar = bars._internal_valueAt(index);
|
|
if (bar === null) {
|
|
return acc;
|
|
}
|
|
// convert bar to pixels
|
|
const firstPrice = ensure(series._internal_firstValue());
|
|
const plotRowKeys = this._private__options.mode === 3 /* CrosshairMode.MagnetOHLC */
|
|
? magnetOHLCPlotRowKeys
|
|
: magnetPlotRowKeys;
|
|
return acc.concat(plotRowKeys.map((key) => ps._internal_priceToCoordinate(bar._internal_value[key], firstPrice._internal_value)));
|
|
}, []);
|
|
if (candidates.length === 0) {
|
|
return res;
|
|
}
|
|
candidates.sort((y1, y2) => Math.abs(y1 - y) - Math.abs(y2 - y));
|
|
const nearest = candidates[0];
|
|
res = defaultPriceScale._internal_coordinateToPrice(nearest, firstValue);
|
|
return res;
|
|
}
|
|
}
|
|
|
|
function clamp(value, minVal, maxVal) {
|
|
return Math.min(Math.max(value, minVal), maxVal);
|
|
}
|
|
function isBaseDecimal(value) {
|
|
if (value < 0) {
|
|
return false;
|
|
}
|
|
// cannot calculate exactly due to rounding error
|
|
if (value > 1e18) {
|
|
return true;
|
|
}
|
|
for (let current = value; current > 1; current /= 10) {
|
|
if ((current % 10) !== 0) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
function greaterOrEqual(x1, x2, epsilon) {
|
|
return (x2 - x1) <= epsilon;
|
|
}
|
|
function equal(x1, x2, epsilon) {
|
|
return Math.abs(x1 - x2) < epsilon;
|
|
}
|
|
// We can't use Math.min(...arr) because that would only support arrays shorter than 65536 items.
|
|
function min(arr) {
|
|
if (arr.length < 1) {
|
|
throw Error('array is empty');
|
|
}
|
|
let minVal = arr[0];
|
|
for (let i = 1; i < arr.length; ++i) {
|
|
if (arr[i] < minVal) {
|
|
minVal = arr[i];
|
|
}
|
|
}
|
|
return minVal;
|
|
}
|
|
function ceiledEven(x) {
|
|
const ceiled = Math.ceil(x);
|
|
return (ceiled % 2 !== 0) ? ceiled - 1 : ceiled;
|
|
}
|
|
function ceiledOdd(x) {
|
|
const ceiled = Math.ceil(x);
|
|
return (ceiled % 2 === 0) ? ceiled - 1 : ceiled;
|
|
}
|
|
|
|
class GridRenderer extends BitmapCoordinatesPaneRenderer {
|
|
constructor() {
|
|
super(...arguments);
|
|
this._private__data = null;
|
|
}
|
|
_internal_setData(data) {
|
|
this._private__data = data;
|
|
}
|
|
_internal__drawImpl({ context: ctx, bitmapSize, horizontalPixelRatio, verticalPixelRatio }) {
|
|
if (this._private__data === null) {
|
|
return;
|
|
}
|
|
const lineWidth = Math.max(1, Math.floor(horizontalPixelRatio));
|
|
ctx.lineWidth = lineWidth;
|
|
strokeInPixel(ctx, () => {
|
|
const data = ensureNotNull(this._private__data);
|
|
if (data._internal_vertLinesVisible) {
|
|
ctx.strokeStyle = data._internal_vertLinesColor;
|
|
setLineStyle(ctx, data._internal_vertLineStyle);
|
|
ctx.beginPath();
|
|
for (const timeMark of data._internal_timeMarks) {
|
|
const x = Math.round(timeMark._internal_coord * horizontalPixelRatio);
|
|
ctx.moveTo(x, -lineWidth);
|
|
ctx.lineTo(x, bitmapSize.height + lineWidth);
|
|
}
|
|
ctx.stroke();
|
|
}
|
|
if (data._internal_horzLinesVisible) {
|
|
ctx.strokeStyle = data._internal_horzLinesColor;
|
|
setLineStyle(ctx, data._internal_horzLineStyle);
|
|
ctx.beginPath();
|
|
for (const priceMark of data._internal_priceMarks) {
|
|
const y = Math.round(priceMark._internal_coord * verticalPixelRatio);
|
|
ctx.moveTo(-lineWidth, y);
|
|
ctx.lineTo(bitmapSize.width + lineWidth, y);
|
|
}
|
|
ctx.stroke();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
class GridPaneView {
|
|
constructor(pane) {
|
|
this._private__renderer = new GridRenderer();
|
|
this._private__invalidated = true;
|
|
this._private__pane = pane;
|
|
}
|
|
_internal_update() {
|
|
this._private__invalidated = true;
|
|
}
|
|
_internal_renderer() {
|
|
if (this._private__invalidated) {
|
|
const gridOptions = this._private__pane._internal_model()._internal_options().grid;
|
|
const data = {
|
|
_internal_horzLinesVisible: gridOptions.horzLines.visible,
|
|
_internal_vertLinesVisible: gridOptions.vertLines.visible,
|
|
_internal_horzLinesColor: gridOptions.horzLines.color,
|
|
_internal_vertLinesColor: gridOptions.vertLines.color,
|
|
_internal_horzLineStyle: gridOptions.horzLines.style,
|
|
_internal_vertLineStyle: gridOptions.vertLines.style,
|
|
_internal_priceMarks: this._private__pane._internal_defaultPriceScale()._internal_marks(),
|
|
// need this conversiom because TimeMark is a part of external interface
|
|
// and fields inside TimeMark are not minified
|
|
_internal_timeMarks: (this._private__pane._internal_model()._internal_timeScale()._internal_marks() || []).map((tm) => {
|
|
return { _internal_coord: tm.coord };
|
|
}),
|
|
};
|
|
this._private__renderer._internal_setData(data);
|
|
this._private__invalidated = false;
|
|
}
|
|
return this._private__renderer;
|
|
}
|
|
}
|
|
|
|
class Grid {
|
|
constructor(pane) {
|
|
this._private__paneView = new GridPaneView(pane);
|
|
}
|
|
_internal_paneView() {
|
|
return this._private__paneView;
|
|
}
|
|
}
|
|
|
|
function hoveredSourceOnTopOrder(sources, hoveredSource, enabled) {
|
|
if (!enabled) {
|
|
return sources;
|
|
}
|
|
const hoveredIndex = sources.indexOf(hoveredSource);
|
|
if (hoveredIndex === -1 || hoveredIndex === sources.length - 1) {
|
|
return sources;
|
|
}
|
|
const reorderedSources = [];
|
|
for (let i = 0; i < sources.length; i++) {
|
|
if (i !== hoveredIndex) {
|
|
reorderedSources.push(sources[i]);
|
|
}
|
|
}
|
|
reorderedSources.push(sources[hoveredIndex]);
|
|
return reorderedSources;
|
|
}
|
|
|
|
const defLogFormula = {
|
|
_internal_logicalOffset: 4,
|
|
_internal_coordOffset: 0.0001,
|
|
};
|
|
function fromPercent(value, baseValue) {
|
|
if (baseValue < 0) {
|
|
value = -value;
|
|
}
|
|
return (value / 100) * baseValue + baseValue;
|
|
}
|
|
function toPercent(value, baseValue) {
|
|
const result = 100 * (value - baseValue) / baseValue;
|
|
return (baseValue < 0 ? -result : result);
|
|
}
|
|
function toPercentRange(priceRange, baseValue) {
|
|
const minPercent = toPercent(priceRange._internal_minValue(), baseValue);
|
|
const maxPercent = toPercent(priceRange._internal_maxValue(), baseValue);
|
|
return new PriceRangeImpl(minPercent, maxPercent);
|
|
}
|
|
function fromIndexedTo100(value, baseValue) {
|
|
value -= 100;
|
|
if (baseValue < 0) {
|
|
value = -value;
|
|
}
|
|
return (value / 100) * baseValue + baseValue;
|
|
}
|
|
function toIndexedTo100(value, baseValue) {
|
|
const result = 100 * (value - baseValue) / baseValue + 100;
|
|
return (baseValue < 0 ? -result : result);
|
|
}
|
|
function toIndexedTo100Range(priceRange, baseValue) {
|
|
const minPercent = toIndexedTo100(priceRange._internal_minValue(), baseValue);
|
|
const maxPercent = toIndexedTo100(priceRange._internal_maxValue(), baseValue);
|
|
return new PriceRangeImpl(minPercent, maxPercent);
|
|
}
|
|
function toLog(price, logFormula) {
|
|
const m = Math.abs(price);
|
|
if (m < 1e-15) {
|
|
return 0;
|
|
}
|
|
const res = Math.log10(m + logFormula._internal_coordOffset) + logFormula._internal_logicalOffset;
|
|
return ((price < 0) ? -res : res);
|
|
}
|
|
function fromLog(logical, logFormula) {
|
|
const m = Math.abs(logical);
|
|
if (m < 1e-15) {
|
|
return 0;
|
|
}
|
|
const res = Math.pow(10, m - logFormula._internal_logicalOffset) - logFormula._internal_coordOffset;
|
|
return (logical < 0) ? -res : res;
|
|
}
|
|
function convertPriceRangeToLog(priceRange, logFormula) {
|
|
if (priceRange === null) {
|
|
return null;
|
|
}
|
|
const min = toLog(priceRange._internal_minValue(), logFormula);
|
|
const max = toLog(priceRange._internal_maxValue(), logFormula);
|
|
return new PriceRangeImpl(min, max);
|
|
}
|
|
function canConvertPriceRangeFromLog(priceRange, logFormula) {
|
|
if (priceRange === null) {
|
|
return false;
|
|
}
|
|
const min = fromLog(priceRange._internal_minValue(), logFormula);
|
|
const max = fromLog(priceRange._internal_maxValue(), logFormula);
|
|
return isFinite(min) && isFinite(max);
|
|
}
|
|
function convertPriceRangeFromLog(priceRange, logFormula) {
|
|
if (priceRange === null) {
|
|
return null;
|
|
}
|
|
const min = fromLog(priceRange._internal_minValue(), logFormula);
|
|
const max = fromLog(priceRange._internal_maxValue(), logFormula);
|
|
return new PriceRangeImpl(min, max);
|
|
}
|
|
function logFormulaForPriceRange(range) {
|
|
if (range === null) {
|
|
return defLogFormula;
|
|
}
|
|
const diff = Math.abs(range._internal_maxValue() - range._internal_minValue());
|
|
if (diff >= 1 || diff < 1e-15) {
|
|
return defLogFormula;
|
|
}
|
|
const digits = Math.ceil(Math.abs(Math.log10(diff)));
|
|
const logicalOffset = defLogFormula._internal_logicalOffset + digits;
|
|
const coordOffset = 1 / Math.pow(10, logicalOffset);
|
|
return {
|
|
_internal_logicalOffset: logicalOffset,
|
|
_internal_coordOffset: coordOffset,
|
|
};
|
|
}
|
|
function logFormulasAreSame(f1, f2) {
|
|
return f1._internal_logicalOffset === f2._internal_logicalOffset && f1._internal_coordOffset === f2._internal_coordOffset;
|
|
}
|
|
|
|
class PriceTickSpanCalculator {
|
|
constructor(base, integralDividers) {
|
|
this._private__base = base;
|
|
this._private__integralDividers = integralDividers;
|
|
if (isBaseDecimal(this._private__base)) {
|
|
this._private__fractionalDividers = [2, 2.5, 2];
|
|
}
|
|
else {
|
|
this._private__fractionalDividers = [];
|
|
for (let baseRest = this._private__base; baseRest !== 1;) {
|
|
if ((baseRest % 2) === 0) {
|
|
this._private__fractionalDividers.push(2);
|
|
baseRest /= 2;
|
|
}
|
|
else if ((baseRest % 5) === 0) {
|
|
this._private__fractionalDividers.push(2, 2.5);
|
|
baseRest /= 5;
|
|
}
|
|
else {
|
|
throw new Error('unexpected base');
|
|
}
|
|
if (this._private__fractionalDividers.length > 100) {
|
|
throw new Error('something wrong with base');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
_internal_tickSpan(high, low, maxTickSpan) {
|
|
const minMovement = (this._private__base === 0) ? (0) : (1 / this._private__base);
|
|
let resultTickSpan = Math.pow(10, Math.max(0, Math.ceil(Math.log10(high - low))));
|
|
let index = 0;
|
|
let c = this._private__integralDividers[0];
|
|
// eslint-disable-next-line no-constant-condition
|
|
while (true) {
|
|
// the second part is actual for small with very small values like 1e-10
|
|
// greaterOrEqual fails for such values
|
|
const resultTickSpanLargerMinMovement = greaterOrEqual(resultTickSpan, minMovement, 1e-14 /* Constants.TickSpanEpsilon */) && resultTickSpan > (minMovement + 1e-14 /* Constants.TickSpanEpsilon */);
|
|
const resultTickSpanLargerMaxTickSpan = greaterOrEqual(resultTickSpan, maxTickSpan * c, 1e-14 /* Constants.TickSpanEpsilon */);
|
|
const resultTickSpanLarger1 = greaterOrEqual(resultTickSpan, 1, 1e-14 /* Constants.TickSpanEpsilon */);
|
|
const haveToContinue = resultTickSpanLargerMinMovement && resultTickSpanLargerMaxTickSpan && resultTickSpanLarger1;
|
|
if (!haveToContinue) {
|
|
break;
|
|
}
|
|
resultTickSpan /= c;
|
|
c = this._private__integralDividers[++index % this._private__integralDividers.length];
|
|
}
|
|
if (resultTickSpan <= (minMovement + 1e-14 /* Constants.TickSpanEpsilon */)) {
|
|
resultTickSpan = minMovement;
|
|
}
|
|
resultTickSpan = Math.max(1, resultTickSpan);
|
|
if ((this._private__fractionalDividers.length > 0) && equal(resultTickSpan, 1, 1e-14 /* Constants.TickSpanEpsilon */)) {
|
|
index = 0;
|
|
c = this._private__fractionalDividers[0];
|
|
while (greaterOrEqual(resultTickSpan, maxTickSpan * c, 1e-14 /* Constants.TickSpanEpsilon */) && resultTickSpan > (minMovement + 1e-14 /* Constants.TickSpanEpsilon */)) {
|
|
resultTickSpan /= c;
|
|
c = this._private__fractionalDividers[++index % this._private__fractionalDividers.length];
|
|
}
|
|
}
|
|
return resultTickSpan;
|
|
}
|
|
}
|
|
|
|
class PriceTickMarkBuilder {
|
|
constructor(priceScale, base, coordinateToLogicalFunc, logicalToCoordinateFunc) {
|
|
this._private__marks = [];
|
|
this._private__priceScale = priceScale;
|
|
this._private__base = base;
|
|
this._private__coordinateToLogicalFunc = coordinateToLogicalFunc;
|
|
this._private__logicalToCoordinateFunc = logicalToCoordinateFunc;
|
|
}
|
|
_internal_tickSpan(high, low) {
|
|
if (high < low) {
|
|
throw new Error('high < low');
|
|
}
|
|
const scaleHeight = this._private__priceScale._internal_height();
|
|
const markHeight = this._private__tickMarkHeight();
|
|
const maxTickSpan = (high - low) * markHeight / scaleHeight;
|
|
const spanCalculator1 = new PriceTickSpanCalculator(this._private__base, [2, 2.5, 2]);
|
|
const spanCalculator2 = new PriceTickSpanCalculator(this._private__base, [2, 2, 2.5]);
|
|
const spanCalculator3 = new PriceTickSpanCalculator(this._private__base, [2.5, 2, 2]);
|
|
const spans = [];
|
|
spans.push(spanCalculator1._internal_tickSpan(high, low, maxTickSpan), spanCalculator2._internal_tickSpan(high, low, maxTickSpan), spanCalculator3._internal_tickSpan(high, low, maxTickSpan));
|
|
return min(spans);
|
|
}
|
|
_internal_rebuildTickMarks() {
|
|
const priceScale = this._private__priceScale;
|
|
const firstValue = priceScale._internal_firstValue();
|
|
if (firstValue === null) {
|
|
this._private__marks = [];
|
|
return;
|
|
}
|
|
const scaleHeight = priceScale._internal_height();
|
|
const bottom = this._private__coordinateToLogicalFunc(scaleHeight - 1, firstValue);
|
|
const top = this._private__coordinateToLogicalFunc(0, firstValue);
|
|
const extraTopBottomMargin = this._private__priceScale._internal_options().entireTextOnly ? this._private__fontHeight() / 2 : 0;
|
|
const minCoord = extraTopBottomMargin;
|
|
const maxCoord = scaleHeight - 1 - extraTopBottomMargin;
|
|
const high = Math.max(bottom, top);
|
|
const low = Math.min(bottom, top);
|
|
if (high === low) {
|
|
this._private__marks = [];
|
|
return;
|
|
}
|
|
const span = this._internal_tickSpan(high, low);
|
|
this._private__updateMarks(firstValue, span, high, low, minCoord, maxCoord);
|
|
if (priceScale._internal_hasVisibleEdgeMarks() && this._private__shouldApplyEdgeMarks(span, low, high)) {
|
|
const padding = this._private__priceScale._internal_getEdgeMarksPadding();
|
|
this._private__applyEdgeMarks(firstValue, span, minCoord, maxCoord, padding, padding * 2);
|
|
}
|
|
const logicals = this._private__marks.map((mark) => mark._internal_logical);
|
|
const labels = this._private__priceScale._internal_formatLogicalTickmarks(logicals);
|
|
for (let i = 0; i < this._private__marks.length; i++) {
|
|
this._private__marks[i]._internal_label = labels[i];
|
|
}
|
|
}
|
|
_internal_marks() {
|
|
return this._private__marks;
|
|
}
|
|
_private__fontHeight() {
|
|
return this._private__priceScale._internal_fontSize();
|
|
}
|
|
_private__tickMarkHeight() {
|
|
return Math.ceil(this._private__fontHeight() * this._private__priceScale._internal_options().tickMarkDensity);
|
|
}
|
|
_private__updateMarks(firstValue, span, high, low, minCoord, maxCoord) {
|
|
const marks = this._private__marks;
|
|
const priceScale = this._private__priceScale;
|
|
let mod = high % span;
|
|
mod += mod < 0 ? span : 0;
|
|
const sign = (high >= low) ? 1 : -1;
|
|
let prevCoord = null;
|
|
let targetIndex = 0;
|
|
for (let logical = high - mod; logical > low; logical -= span) {
|
|
const coord = this._private__logicalToCoordinateFunc(logical, firstValue, true);
|
|
// check if there is place for it
|
|
// this is required for log scale
|
|
if (prevCoord !== null && Math.abs(coord - prevCoord) < this._private__tickMarkHeight()) {
|
|
continue;
|
|
}
|
|
// check if a tick mark is partially visible and skip it if entireTextOnly is true
|
|
if (coord < minCoord || coord > maxCoord) {
|
|
continue;
|
|
}
|
|
if (targetIndex < marks.length) {
|
|
marks[targetIndex]._internal_coord = coord;
|
|
marks[targetIndex]._internal_label = priceScale._internal_formatLogical(logical);
|
|
marks[targetIndex]._internal_logical = logical;
|
|
}
|
|
else {
|
|
marks.push({
|
|
_internal_coord: coord,
|
|
_internal_label: priceScale._internal_formatLogical(logical),
|
|
_internal_logical: logical,
|
|
});
|
|
}
|
|
targetIndex++;
|
|
prevCoord = coord;
|
|
if (priceScale._internal_isLog()) {
|
|
// recalc span
|
|
span = this._internal_tickSpan(logical * sign, low);
|
|
}
|
|
}
|
|
marks.length = targetIndex;
|
|
}
|
|
_private__applyEdgeMarks(firstValue, span, minCoord, maxCoord, minPadding, maxPadding) {
|
|
const marks = this._private__marks;
|
|
// top boundary
|
|
const topMark = this._private__computeBoundaryPriceMark(firstValue, minCoord, minPadding, maxPadding);
|
|
// bottom boundary
|
|
const bottomMark = this._private__computeBoundaryPriceMark(firstValue, maxCoord, -maxPadding, -minPadding);
|
|
const spanPx = this._private__logicalToCoordinateFunc(0, firstValue, true)
|
|
- this._private__logicalToCoordinateFunc(span, firstValue, true);
|
|
if (marks.length > 0 && marks[0]._internal_coord - topMark._internal_coord < spanPx / 2) {
|
|
marks.shift();
|
|
}
|
|
if (marks.length > 0 && bottomMark._internal_coord - marks[marks.length - 1]._internal_coord < spanPx / 2) {
|
|
marks.pop();
|
|
}
|
|
marks.unshift(topMark);
|
|
marks.push(bottomMark);
|
|
}
|
|
_private__computeBoundaryPriceMark(firstValue, coord, minPadding, maxPadding) {
|
|
const avgPadding = (minPadding + maxPadding) / 2;
|
|
const value1 = this._private__coordinateToLogicalFunc(coord + minPadding, firstValue);
|
|
const value2 = this._private__coordinateToLogicalFunc(coord + maxPadding, firstValue);
|
|
const minValue = Math.min(value1, value2);
|
|
const maxValue = Math.max(value1, value2);
|
|
const valueSpan = Math.max(0.1, this._internal_tickSpan(maxValue, minValue));
|
|
const value = this._private__coordinateToLogicalFunc(coord + avgPadding, firstValue);
|
|
const roundedValue = value - (value % valueSpan);
|
|
const roundedCoord = this._private__logicalToCoordinateFunc(roundedValue, firstValue, true);
|
|
return { _internal_label: this._private__priceScale._internal_formatLogical(roundedValue), _internal_coord: roundedCoord, _internal_logical: roundedValue };
|
|
}
|
|
_private__shouldApplyEdgeMarks(span, low, high) {
|
|
let range = ensure(this._private__priceScale._internal_priceRange());
|
|
if (this._private__priceScale._internal_isLog()) {
|
|
range = convertPriceRangeFromLog(range, this._private__priceScale._internal_getLogFormula());
|
|
}
|
|
return (range._internal_minValue() - low < span) && (high - range._internal_maxValue() < span);
|
|
}
|
|
}
|
|
|
|
function sortSources(sources) {
|
|
return sources.slice().sort((s1, s2) => {
|
|
return (ensureNotNull(s1._internal_zorder()) - ensureNotNull(s2._internal_zorder()));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Represents the price scale mode.
|
|
*/
|
|
var PriceScaleMode;
|
|
(function (PriceScaleMode) {
|
|
/**
|
|
* Price scale shows prices. Price range changes linearly.
|
|
*/
|
|
PriceScaleMode[PriceScaleMode["Normal"] = 0] = "Normal";
|
|
/**
|
|
* Price scale shows prices. Price range changes logarithmically.
|
|
*/
|
|
PriceScaleMode[PriceScaleMode["Logarithmic"] = 1] = "Logarithmic";
|
|
/**
|
|
* Price scale shows percentage values according the first visible value of the price scale.
|
|
* The first visible value is 0% in this mode.
|
|
*/
|
|
PriceScaleMode[PriceScaleMode["Percentage"] = 2] = "Percentage";
|
|
/**
|
|
* The same as percentage mode, but the first value is moved to 100.
|
|
*/
|
|
PriceScaleMode[PriceScaleMode["IndexedTo100"] = 3] = "IndexedTo100";
|
|
})(PriceScaleMode || (PriceScaleMode = {}));
|
|
const percentageFormatter = new PercentageFormatter();
|
|
const defaultPriceFormatter = new PriceFormatter(100, 1);
|
|
class PriceScale {
|
|
constructor(id, options, layoutOptions, localizationOptions, colorParser) {
|
|
this._private__height = 0;
|
|
this._private__internalHeightCache = null;
|
|
this._private__priceRange = null;
|
|
this._private__priceRangeSnapshot = null;
|
|
this._private__invalidatedForRange = { _internal_isValid: false, _internal_visibleBars: null };
|
|
this._private__isCustomPriceRange = false;
|
|
this._private__marginAbove = 0;
|
|
this._private__marginBelow = 0;
|
|
this._private__onMarksChanged = new Delegate();
|
|
this._private__modeChanged = new Delegate();
|
|
this._private__dataSources = [];
|
|
this._private__formatterSource = null;
|
|
this._private__cachedOrderedSources = null;
|
|
this._private__marksCache = null;
|
|
this._private__scaleStartPoint = null;
|
|
this._private__scrollStartPoint = null;
|
|
this._private__formatter = defaultPriceFormatter;
|
|
this._private__logFormula = logFormulaForPriceRange(null);
|
|
this._private__id = id;
|
|
this._private__options = options;
|
|
this._private__layoutOptions = layoutOptions;
|
|
this._private__localizationOptions = localizationOptions;
|
|
this._private__colorParser = colorParser;
|
|
this._private__markBuilder = new PriceTickMarkBuilder(this, 100, this._private__coordinateToLogical.bind(this), this._private__logicalToCoordinate.bind(this));
|
|
}
|
|
_internal_id() {
|
|
return this._private__id;
|
|
}
|
|
_internal_options() {
|
|
return this._private__options;
|
|
}
|
|
_internal_applyOptions(options) {
|
|
merge(this._private__options, options);
|
|
this._internal_updateFormatter();
|
|
if (options.mode !== undefined) {
|
|
this._internal_setMode({ _internal_mode: options.mode });
|
|
}
|
|
if (options.scaleMargins !== undefined) {
|
|
const top = ensureDefined(options.scaleMargins.top);
|
|
const bottom = ensureDefined(options.scaleMargins.bottom);
|
|
if (top < 0 || top > 1) {
|
|
throw new Error(`Invalid top margin - expect value between 0 and 1, given=${top}`);
|
|
}
|
|
if (bottom < 0 || bottom > 1) {
|
|
throw new Error(`Invalid bottom margin - expect value between 0 and 1, given=${bottom}`);
|
|
}
|
|
if (top + bottom > 1) {
|
|
throw new Error(`Invalid margins - sum of margins must be less than 1, given=${top + bottom}`);
|
|
}
|
|
this._private__invalidateInternalHeightCache();
|
|
this._private__marksCache = null;
|
|
}
|
|
}
|
|
_internal_isAutoScale() {
|
|
return this._private__options.autoScale;
|
|
}
|
|
_internal_isCustomPriceRange() {
|
|
return this._private__isCustomPriceRange;
|
|
}
|
|
_internal_isLog() {
|
|
return this._private__options.mode === 1 /* PriceScaleMode.Logarithmic */;
|
|
}
|
|
_internal_isPercentage() {
|
|
return this._private__options.mode === 2 /* PriceScaleMode.Percentage */;
|
|
}
|
|
_internal_isIndexedTo100() {
|
|
return this._private__options.mode === 3 /* PriceScaleMode.IndexedTo100 */;
|
|
}
|
|
_internal_getLogFormula() {
|
|
return this._private__logFormula;
|
|
}
|
|
_internal_mode() {
|
|
return {
|
|
_internal_autoScale: this._private__options.autoScale,
|
|
_internal_isInverted: this._private__options.invertScale,
|
|
_internal_mode: this._private__options.mode,
|
|
};
|
|
}
|
|
// eslint-disable-next-line complexity
|
|
_internal_setMode(newMode) {
|
|
const oldMode = this._internal_mode();
|
|
let priceRange = null;
|
|
if (newMode._internal_autoScale !== undefined) {
|
|
this._private__options.autoScale = newMode._internal_autoScale;
|
|
}
|
|
if (newMode._internal_mode !== undefined) {
|
|
this._private__options.mode = newMode._internal_mode;
|
|
if (newMode._internal_mode === 2 /* PriceScaleMode.Percentage */ || newMode._internal_mode === 3 /* PriceScaleMode.IndexedTo100 */) {
|
|
this._private__options.autoScale = true;
|
|
}
|
|
// TODO: Remove after making rebuildTickMarks lazy
|
|
this._private__invalidatedForRange._internal_isValid = false;
|
|
}
|
|
// define which scale converted from
|
|
if (oldMode._internal_mode === 1 /* PriceScaleMode.Logarithmic */ && newMode._internal_mode !== oldMode._internal_mode) {
|
|
if (canConvertPriceRangeFromLog(this._private__priceRange, this._private__logFormula)) {
|
|
priceRange = convertPriceRangeFromLog(this._private__priceRange, this._private__logFormula);
|
|
if (priceRange !== null) {
|
|
this._internal_setPriceRange(priceRange);
|
|
}
|
|
}
|
|
else {
|
|
this._private__options.autoScale = true;
|
|
}
|
|
}
|
|
// define which scale converted to
|
|
if (newMode._internal_mode === 1 /* PriceScaleMode.Logarithmic */ && newMode._internal_mode !== oldMode._internal_mode) {
|
|
priceRange = convertPriceRangeToLog(this._private__priceRange, this._private__logFormula);
|
|
if (priceRange !== null) {
|
|
this._internal_setPriceRange(priceRange);
|
|
}
|
|
}
|
|
const modeChanged = oldMode._internal_mode !== this._private__options.mode;
|
|
if (modeChanged && (oldMode._internal_mode === 2 /* PriceScaleMode.Percentage */ || this._internal_isPercentage())) {
|
|
this._internal_updateFormatter();
|
|
}
|
|
if (modeChanged && (oldMode._internal_mode === 3 /* PriceScaleMode.IndexedTo100 */ || this._internal_isIndexedTo100())) {
|
|
this._internal_updateFormatter();
|
|
}
|
|
if (newMode._internal_isInverted !== undefined && oldMode._internal_isInverted !== newMode._internal_isInverted) {
|
|
this._private__options.invertScale = newMode._internal_isInverted;
|
|
this._private__onIsInvertedChanged();
|
|
}
|
|
this._private__modeChanged._internal_fire(oldMode, this._internal_mode());
|
|
}
|
|
_internal_modeChanged() {
|
|
return this._private__modeChanged;
|
|
}
|
|
_internal_fontSize() {
|
|
return this._private__layoutOptions.fontSize;
|
|
}
|
|
_internal_height() {
|
|
return this._private__height;
|
|
}
|
|
_internal_setHeight(value) {
|
|
if (this._private__height === value) {
|
|
return;
|
|
}
|
|
this._private__height = value;
|
|
this._private__invalidateInternalHeightCache();
|
|
this._private__marksCache = null;
|
|
}
|
|
_internal_internalHeight() {
|
|
if (this._private__internalHeightCache) {
|
|
return this._private__internalHeightCache;
|
|
}
|
|
const res = this._internal_height() - this._private__topMarginPx() - this._private__bottomMarginPx();
|
|
this._private__internalHeightCache = res;
|
|
return res;
|
|
}
|
|
_internal_priceRange() {
|
|
this._private__makeSureItIsValid();
|
|
return this._private__priceRange;
|
|
}
|
|
_internal_setPriceRange(newPriceRange, isForceSetValue) {
|
|
const oldPriceRange = this._private__priceRange;
|
|
if (!isForceSetValue &&
|
|
!(oldPriceRange === null && newPriceRange !== null) &&
|
|
(oldPriceRange === null || oldPriceRange._internal_equals(newPriceRange))) {
|
|
return;
|
|
}
|
|
this._private__marksCache = null;
|
|
this._private__priceRange = newPriceRange;
|
|
}
|
|
_internal_setCustomPriceRange(newPriceRange) {
|
|
this._internal_setPriceRange(newPriceRange);
|
|
this._private__toggleCustomPriceRange(newPriceRange !== null);
|
|
}
|
|
_internal_isEmpty() {
|
|
this._private__makeSureItIsValid();
|
|
return this._private__height === 0 || !this._private__priceRange || this._private__priceRange._internal_isEmpty();
|
|
}
|
|
_internal_invertedCoordinate(coordinate) {
|
|
return this._internal_isInverted() ? coordinate : this._internal_height() - 1 - coordinate;
|
|
}
|
|
_internal_priceToCoordinate(price, baseValue) {
|
|
if (this._internal_isPercentage()) {
|
|
price = toPercent(price, baseValue);
|
|
}
|
|
else if (this._internal_isIndexedTo100()) {
|
|
price = toIndexedTo100(price, baseValue);
|
|
}
|
|
return this._private__logicalToCoordinate(price, baseValue);
|
|
}
|
|
_internal_pointsArrayToCoordinates(points, baseValue, visibleRange) {
|
|
this._private__makeSureItIsValid();
|
|
const bh = this._private__bottomMarginPx();
|
|
const range = ensureNotNull(this._internal_priceRange());
|
|
const min = range._internal_minValue();
|
|
const max = range._internal_maxValue();
|
|
const ih = (this._internal_internalHeight() - 1);
|
|
const isInverted = this._internal_isInverted();
|
|
const hmm = ih / (max - min);
|
|
const fromIndex = (visibleRange === undefined) ? 0 : visibleRange.from;
|
|
const toIndex = (visibleRange === undefined) ? points.length : visibleRange.to;
|
|
const transformFn = this._private__getCoordinateTransformer();
|
|
for (let i = fromIndex; i < toIndex; i++) {
|
|
const point = points[i];
|
|
const price = point._internal_price;
|
|
if (isNaN(price)) {
|
|
continue;
|
|
}
|
|
let logical = price;
|
|
if (transformFn !== null) {
|
|
logical = transformFn(point._internal_price, baseValue);
|
|
}
|
|
const invCoordinate = bh + hmm * (logical - min);
|
|
const coordinate = isInverted ? invCoordinate : this._private__height - 1 - invCoordinate;
|
|
point._internal_y = coordinate;
|
|
}
|
|
}
|
|
_internal_barPricesToCoordinates(pricesList, baseValue, visibleRange) {
|
|
this._private__makeSureItIsValid();
|
|
const bh = this._private__bottomMarginPx();
|
|
const range = ensureNotNull(this._internal_priceRange());
|
|
const min = range._internal_minValue();
|
|
const max = range._internal_maxValue();
|
|
const ih = (this._internal_internalHeight() - 1);
|
|
const isInverted = this._internal_isInverted();
|
|
const hmm = ih / (max - min);
|
|
const fromIndex = (visibleRange === undefined) ? 0 : visibleRange.from;
|
|
const toIndex = (visibleRange === undefined) ? pricesList.length : visibleRange.to;
|
|
const transformFn = this._private__getCoordinateTransformer();
|
|
for (let i = fromIndex; i < toIndex; i++) {
|
|
const bar = pricesList[i];
|
|
let openLogical = bar._internal_open;
|
|
let highLogical = bar._internal_high;
|
|
let lowLogical = bar._internal_low;
|
|
let closeLogical = bar._internal_close;
|
|
if (transformFn !== null) {
|
|
openLogical = transformFn(bar._internal_open, baseValue);
|
|
highLogical = transformFn(bar._internal_high, baseValue);
|
|
lowLogical = transformFn(bar._internal_low, baseValue);
|
|
closeLogical = transformFn(bar._internal_close, baseValue);
|
|
}
|
|
let invCoordinate = bh + hmm * (openLogical - min);
|
|
let coordinate = isInverted ? invCoordinate : this._private__height - 1 - invCoordinate;
|
|
bar._internal_openY = coordinate;
|
|
invCoordinate = bh + hmm * (highLogical - min);
|
|
coordinate = isInverted ? invCoordinate : this._private__height - 1 - invCoordinate;
|
|
bar._internal_highY = coordinate;
|
|
invCoordinate = bh + hmm * (lowLogical - min);
|
|
coordinate = isInverted ? invCoordinate : this._private__height - 1 - invCoordinate;
|
|
bar._internal_lowY = coordinate;
|
|
invCoordinate = bh + hmm * (closeLogical - min);
|
|
coordinate = isInverted ? invCoordinate : this._private__height - 1 - invCoordinate;
|
|
bar._internal_closeY = coordinate;
|
|
}
|
|
}
|
|
_internal_coordinateToPrice(coordinate, baseValue) {
|
|
const logical = this._private__coordinateToLogical(coordinate, baseValue);
|
|
return this._internal_logicalToPrice(logical, baseValue);
|
|
}
|
|
_internal_logicalToPrice(logical, baseValue) {
|
|
let value = logical;
|
|
if (this._internal_isPercentage()) {
|
|
value = fromPercent(value, baseValue);
|
|
}
|
|
else if (this._internal_isIndexedTo100()) {
|
|
value = fromIndexedTo100(value, baseValue);
|
|
}
|
|
return value;
|
|
}
|
|
_internal_dataSources() {
|
|
return this._private__dataSources;
|
|
}
|
|
_internal_orderedSources() {
|
|
if (!this._private__cachedOrderedSources) {
|
|
this._private__cachedOrderedSources = sortSources(this._private__dataSources);
|
|
}
|
|
return this._private__cachedOrderedSources;
|
|
}
|
|
_internal_addDataSource(source) {
|
|
if (this._private__dataSources.indexOf(source) !== -1) {
|
|
return;
|
|
}
|
|
this._private__dataSources.push(source);
|
|
this._internal_updateFormatter();
|
|
this._internal_invalidateSourcesCache();
|
|
}
|
|
_internal_removeDataSource(source) {
|
|
const index = this._private__dataSources.indexOf(source);
|
|
if (index === -1) {
|
|
throw new Error('source is not attached to scale');
|
|
}
|
|
this._private__dataSources.splice(index, 1);
|
|
if (this._private__dataSources.length === 0) {
|
|
this._internal_setMode({
|
|
_internal_autoScale: true,
|
|
});
|
|
// if no sources on price scale let's clear price range cache as well as enabling auto scale
|
|
this._internal_setPriceRange(null);
|
|
}
|
|
this._internal_updateFormatter();
|
|
this._internal_invalidateSourcesCache();
|
|
}
|
|
_internal_firstValue() {
|
|
// TODO: cache the result
|
|
let result = null;
|
|
for (const source of this._private__dataSources) {
|
|
const firstValue = source._internal_firstValue();
|
|
if (firstValue === null) {
|
|
continue;
|
|
}
|
|
if (result === null || firstValue._internal_timePoint < result._internal_timePoint) {
|
|
result = firstValue;
|
|
}
|
|
}
|
|
return result === null ? null : result._internal_value;
|
|
}
|
|
_internal_isInverted() {
|
|
return this._private__options.invertScale;
|
|
}
|
|
_internal_marks() {
|
|
const firstValueIsNull = this._internal_firstValue() === null;
|
|
// do not recalculate marks if firstValueIsNull is true because in this case we'll always get empty result
|
|
// this could happen in case when a series had some data and then you set empty data to it (in a simplified case)
|
|
// we could display an empty price scale, but this is not good from UX
|
|
// so in this case we need to keep an previous marks to display them on the scale
|
|
// as one of possible examples for this situation could be the following:
|
|
// let's say you have a study/indicator attached to a price scale and then you decide to stop it, i.e. remove its data because of its visibility
|
|
// a user will see the previous marks on the scale until you turn on your study back or remove it from the chart completely
|
|
if (this._private__marksCache !== null && (firstValueIsNull || this._private__marksCache._internal_firstValueIsNull === firstValueIsNull)) {
|
|
return this._private__marksCache._internal_marks;
|
|
}
|
|
this._private__markBuilder._internal_rebuildTickMarks();
|
|
const marks = this._private__markBuilder._internal_marks();
|
|
this._private__marksCache = { _internal_marks: marks, _internal_firstValueIsNull: firstValueIsNull };
|
|
this._private__onMarksChanged._internal_fire();
|
|
return marks;
|
|
}
|
|
_internal_onMarksChanged() {
|
|
return this._private__onMarksChanged;
|
|
}
|
|
_internal_startScale(x) {
|
|
if (this._internal_isPercentage() || this._internal_isIndexedTo100()) {
|
|
return;
|
|
}
|
|
if (this._private__scaleStartPoint !== null || this._private__priceRangeSnapshot !== null) {
|
|
return;
|
|
}
|
|
if (this._internal_isEmpty()) {
|
|
return;
|
|
}
|
|
// invert x
|
|
this._private__scaleStartPoint = this._private__height - x;
|
|
this._private__priceRangeSnapshot = ensureNotNull(this._internal_priceRange())._internal_clone();
|
|
}
|
|
_internal_scaleTo(x) {
|
|
if (this._internal_isPercentage() || this._internal_isIndexedTo100()) {
|
|
return;
|
|
}
|
|
if (this._private__scaleStartPoint === null) {
|
|
return;
|
|
}
|
|
this._internal_setMode({
|
|
_internal_autoScale: false,
|
|
});
|
|
// invert x
|
|
x = this._private__height - x;
|
|
if (x < 0) {
|
|
x = 0;
|
|
}
|
|
let scaleCoeff = (this._private__scaleStartPoint + (this._private__height - 1) * 0.2) / (x + (this._private__height - 1) * 0.2);
|
|
const newPriceRange = ensureNotNull(this._private__priceRangeSnapshot)._internal_clone();
|
|
scaleCoeff = Math.max(scaleCoeff, 0.1);
|
|
newPriceRange._internal_scaleAroundCenter(scaleCoeff);
|
|
this._internal_setPriceRange(newPriceRange);
|
|
}
|
|
_internal_endScale() {
|
|
if (this._internal_isPercentage() || this._internal_isIndexedTo100()) {
|
|
return;
|
|
}
|
|
this._private__scaleStartPoint = null;
|
|
this._private__priceRangeSnapshot = null;
|
|
}
|
|
_internal_startScroll(x) {
|
|
if (this._internal_isAutoScale()) {
|
|
return;
|
|
}
|
|
if (this._private__scrollStartPoint !== null || this._private__priceRangeSnapshot !== null) {
|
|
return;
|
|
}
|
|
if (this._internal_isEmpty()) {
|
|
return;
|
|
}
|
|
this._private__scrollStartPoint = x;
|
|
this._private__priceRangeSnapshot = ensureNotNull(this._internal_priceRange())._internal_clone();
|
|
}
|
|
_internal_scrollTo(x) {
|
|
if (this._internal_isAutoScale()) {
|
|
return;
|
|
}
|
|
if (this._private__scrollStartPoint === null) {
|
|
return;
|
|
}
|
|
const priceUnitsPerPixel = ensureNotNull(this._internal_priceRange())._internal_length() / (this._internal_internalHeight() - 1);
|
|
let pixelDelta = x - this._private__scrollStartPoint;
|
|
if (this._internal_isInverted()) {
|
|
pixelDelta *= -1;
|
|
}
|
|
const priceDelta = pixelDelta * priceUnitsPerPixel;
|
|
const newPriceRange = ensureNotNull(this._private__priceRangeSnapshot)._internal_clone();
|
|
newPriceRange._internal_shift(priceDelta);
|
|
this._internal_setPriceRange(newPriceRange, true);
|
|
this._private__marksCache = null;
|
|
}
|
|
_internal_endScroll() {
|
|
if (this._internal_isAutoScale()) {
|
|
return;
|
|
}
|
|
if (this._private__scrollStartPoint === null) {
|
|
return;
|
|
}
|
|
this._private__scrollStartPoint = null;
|
|
this._private__priceRangeSnapshot = null;
|
|
}
|
|
_internal_formatter() {
|
|
if (!this._private__formatter) {
|
|
this._internal_updateFormatter();
|
|
}
|
|
return this._private__formatter;
|
|
}
|
|
_internal_formatPrice(price, firstValue) {
|
|
switch (this._private__options.mode) {
|
|
case 2 /* PriceScaleMode.Percentage */:
|
|
return this._private__formatPercentage(toPercent(price, firstValue));
|
|
case 3 /* PriceScaleMode.IndexedTo100 */:
|
|
return this._internal_formatter().format(toIndexedTo100(price, firstValue));
|
|
default:
|
|
return this._private__formatPrice(price);
|
|
}
|
|
}
|
|
_internal_formatLogical(logical) {
|
|
switch (this._private__options.mode) {
|
|
case 2 /* PriceScaleMode.Percentage */:
|
|
return this._private__formatPercentage(logical);
|
|
case 3 /* PriceScaleMode.IndexedTo100 */:
|
|
return this._internal_formatter().format(logical);
|
|
default:
|
|
return this._private__formatPrice(logical);
|
|
}
|
|
}
|
|
_internal_formatLogicalTickmarks(logicals) {
|
|
switch (this._private__options.mode) {
|
|
case 2 /* PriceScaleMode.Percentage */:
|
|
return this._private__formatPercentageTickmarks(logicals);
|
|
case 3 /* PriceScaleMode.IndexedTo100 */:
|
|
return this._internal_formatter().formatTickmarks(logicals);
|
|
default:
|
|
return this._private__formatTickmarks(logicals);
|
|
}
|
|
}
|
|
_internal_formatPriceAbsolute(price) {
|
|
return this._private__formatPrice(price, ensureNotNull(this._private__formatterSource)._internal_formatter());
|
|
}
|
|
_internal_formatPricePercentage(price, baseValue) {
|
|
price = toPercent(price, baseValue);
|
|
return this._private__formatPercentage(price, percentageFormatter);
|
|
}
|
|
_internal_sourcesForAutoScale() {
|
|
return this._private__dataSources;
|
|
}
|
|
_internal_recalculatePriceRange(visibleBars) {
|
|
this._private__invalidatedForRange = {
|
|
_internal_visibleBars: visibleBars,
|
|
_internal_isValid: false,
|
|
};
|
|
}
|
|
_internal_updateAllViews() {
|
|
this._private__dataSources.forEach((s) => s._internal_updateAllViews());
|
|
}
|
|
_internal_hasVisibleEdgeMarks() {
|
|
return this._private__options.ensureEdgeTickMarksVisible && this._internal_isAutoScale();
|
|
}
|
|
_internal_getEdgeMarksPadding() {
|
|
return this._internal_fontSize() / 2;
|
|
}
|
|
_internal_updateFormatter() {
|
|
this._private__marksCache = null;
|
|
let zOrder = Infinity;
|
|
this._private__formatterSource = null;
|
|
// choose source with the lowest zorder
|
|
for (const source of this._private__dataSources) {
|
|
if (source._internal_zorder() < zOrder) {
|
|
zOrder = source._internal_zorder();
|
|
this._private__formatterSource = source;
|
|
}
|
|
}
|
|
let base = 100;
|
|
if (this._private__formatterSource !== null) {
|
|
base = Math.round(this._private__formatterSource._internal_base());
|
|
}
|
|
this._private__formatter = defaultPriceFormatter;
|
|
if (this._internal_isPercentage()) {
|
|
this._private__formatter = percentageFormatter;
|
|
base = 100;
|
|
}
|
|
else if (this._internal_isIndexedTo100()) {
|
|
this._private__formatter = new PriceFormatter(100, 1);
|
|
base = 100;
|
|
}
|
|
else {
|
|
if (this._private__formatterSource !== null) {
|
|
// user
|
|
this._private__formatter = this._private__formatterSource._internal_formatter();
|
|
}
|
|
}
|
|
this._private__markBuilder = new PriceTickMarkBuilder(this, base, this._private__coordinateToLogical.bind(this), this._private__logicalToCoordinate.bind(this));
|
|
this._private__markBuilder._internal_rebuildTickMarks();
|
|
}
|
|
_internal_invalidateSourcesCache() {
|
|
this._private__cachedOrderedSources = null;
|
|
}
|
|
_internal_minMove() {
|
|
return this._private__formatterSource === null || this._internal_isPercentage() || this._internal_isIndexedTo100() ? 1 : 1 / this._private__formatterSource._internal_base();
|
|
}
|
|
_internal_colorParser() {
|
|
return this._private__colorParser;
|
|
}
|
|
_private__toggleCustomPriceRange(v) {
|
|
this._private__isCustomPriceRange = v;
|
|
}
|
|
_private__topMarginPx() {
|
|
return this._internal_isInverted()
|
|
? this._private__options.scaleMargins.bottom * this._internal_height() + this._private__marginBelow
|
|
: this._private__options.scaleMargins.top * this._internal_height() + this._private__marginAbove;
|
|
}
|
|
_private__bottomMarginPx() {
|
|
return this._internal_isInverted()
|
|
? this._private__options.scaleMargins.top * this._internal_height() + this._private__marginAbove
|
|
: this._private__options.scaleMargins.bottom * this._internal_height() + this._private__marginBelow;
|
|
}
|
|
_private__makeSureItIsValid() {
|
|
if (!this._private__invalidatedForRange._internal_isValid) {
|
|
this._private__invalidatedForRange._internal_isValid = true;
|
|
this._private__recalculatePriceRangeImpl();
|
|
}
|
|
}
|
|
_private__invalidateInternalHeightCache() {
|
|
this._private__internalHeightCache = null;
|
|
}
|
|
_private__logicalToCoordinate(logical, baseValue) {
|
|
this._private__makeSureItIsValid();
|
|
if (this._internal_isEmpty()) {
|
|
return 0;
|
|
}
|
|
logical = this._internal_isLog() && logical ? toLog(logical, this._private__logFormula) : logical;
|
|
const range = ensureNotNull(this._internal_priceRange());
|
|
const invCoordinate = this._private__bottomMarginPx() +
|
|
(this._internal_internalHeight() - 1) * (logical - range._internal_minValue()) / range._internal_length();
|
|
const coordinate = this._internal_invertedCoordinate(invCoordinate);
|
|
return coordinate;
|
|
}
|
|
_private__coordinateToLogical(coordinate, baseValue) {
|
|
this._private__makeSureItIsValid();
|
|
if (this._internal_isEmpty()) {
|
|
return 0;
|
|
}
|
|
const invCoordinate = this._internal_invertedCoordinate(coordinate);
|
|
const range = ensureNotNull(this._internal_priceRange());
|
|
const logical = range._internal_minValue() + range._internal_length() *
|
|
((invCoordinate - this._private__bottomMarginPx()) / (this._internal_internalHeight() - 1));
|
|
return this._internal_isLog() ? fromLog(logical, this._private__logFormula) : logical;
|
|
}
|
|
_private__onIsInvertedChanged() {
|
|
this._private__marksCache = null;
|
|
this._private__markBuilder._internal_rebuildTickMarks();
|
|
}
|
|
// eslint-disable-next-line complexity
|
|
_private__recalculatePriceRangeImpl() {
|
|
if (this._internal_isCustomPriceRange() && !this._internal_isAutoScale()) {
|
|
return;
|
|
}
|
|
const visibleBars = this._private__invalidatedForRange._internal_visibleBars;
|
|
if (visibleBars === null) {
|
|
return;
|
|
}
|
|
let priceRange = null;
|
|
const sources = this._internal_sourcesForAutoScale();
|
|
let marginAbove = 0;
|
|
let marginBelow = 0;
|
|
for (const source of sources) {
|
|
if (!source._internal_visible()) {
|
|
continue;
|
|
}
|
|
const firstValue = source._internal_firstValue();
|
|
if (firstValue === null) {
|
|
continue;
|
|
}
|
|
const autoScaleInfo = source._internal_autoscaleInfo(visibleBars._internal_left(), visibleBars._internal_right());
|
|
let sourceRange = autoScaleInfo && autoScaleInfo._internal_priceRange();
|
|
if (sourceRange !== null) {
|
|
switch (this._private__options.mode) {
|
|
case 1 /* PriceScaleMode.Logarithmic */:
|
|
sourceRange = convertPriceRangeToLog(sourceRange, this._private__logFormula);
|
|
break;
|
|
case 2 /* PriceScaleMode.Percentage */:
|
|
sourceRange = toPercentRange(sourceRange, firstValue._internal_value);
|
|
break;
|
|
case 3 /* PriceScaleMode.IndexedTo100 */:
|
|
sourceRange = toIndexedTo100Range(sourceRange, firstValue._internal_value);
|
|
break;
|
|
}
|
|
if (priceRange === null) {
|
|
priceRange = sourceRange;
|
|
}
|
|
else {
|
|
priceRange = priceRange._internal_merge(ensureNotNull(sourceRange));
|
|
}
|
|
if (autoScaleInfo !== null) {
|
|
const margins = autoScaleInfo._internal_margins();
|
|
if (margins !== null) {
|
|
marginAbove = Math.max(marginAbove, margins.above);
|
|
marginBelow = Math.max(marginBelow, margins.below);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (this._internal_hasVisibleEdgeMarks()) {
|
|
marginAbove = Math.max(marginAbove, this._internal_getEdgeMarksPadding());
|
|
marginBelow = Math.max(marginBelow, this._internal_getEdgeMarksPadding());
|
|
}
|
|
if (marginAbove !== this._private__marginAbove || marginBelow !== this._private__marginBelow) {
|
|
this._private__marginAbove = marginAbove;
|
|
this._private__marginBelow = marginBelow;
|
|
this._private__marksCache = null;
|
|
this._private__invalidateInternalHeightCache();
|
|
}
|
|
if (priceRange !== null) {
|
|
// keep current range is new is empty
|
|
if (priceRange._internal_minValue() === priceRange._internal_maxValue()) {
|
|
// if price range is degenerated to 1 point let's extend it by 10 min move values
|
|
// to avoid incorrect range and empty (blank) scale (in case of min tick much greater than 1)
|
|
const extendValue = 5 * this._internal_minMove();
|
|
if (this._internal_isLog()) {
|
|
priceRange = convertPriceRangeFromLog(priceRange, this._private__logFormula);
|
|
}
|
|
priceRange = new PriceRangeImpl(priceRange._internal_minValue() - extendValue, priceRange._internal_maxValue() + extendValue);
|
|
if (this._internal_isLog()) {
|
|
priceRange = convertPriceRangeToLog(priceRange, this._private__logFormula);
|
|
}
|
|
}
|
|
if (this._internal_isLog()) {
|
|
const rawRange = convertPriceRangeFromLog(priceRange, this._private__logFormula);
|
|
const newLogFormula = logFormulaForPriceRange(rawRange);
|
|
if (!logFormulasAreSame(newLogFormula, this._private__logFormula)) {
|
|
const rawSnapshot = this._private__priceRangeSnapshot !== null ? convertPriceRangeFromLog(this._private__priceRangeSnapshot, this._private__logFormula) : null;
|
|
this._private__logFormula = newLogFormula;
|
|
priceRange = convertPriceRangeToLog(rawRange, newLogFormula);
|
|
if (rawSnapshot !== null) {
|
|
this._private__priceRangeSnapshot = convertPriceRangeToLog(rawSnapshot, newLogFormula);
|
|
}
|
|
}
|
|
}
|
|
this._internal_setPriceRange(priceRange);
|
|
}
|
|
else {
|
|
// reset empty to default
|
|
if (this._private__priceRange === null) {
|
|
this._internal_setPriceRange(new PriceRangeImpl(-0.5, 0.5));
|
|
this._private__logFormula = logFormulaForPriceRange(null);
|
|
}
|
|
}
|
|
}
|
|
_private__getCoordinateTransformer() {
|
|
if (this._internal_isPercentage()) {
|
|
return toPercent;
|
|
}
|
|
else if (this._internal_isIndexedTo100()) {
|
|
return toIndexedTo100;
|
|
}
|
|
else if (this._internal_isLog()) {
|
|
return (price) => toLog(price, this._private__logFormula);
|
|
}
|
|
return null;
|
|
}
|
|
_private__formatValue(value, formatter, fallbackFormatter) {
|
|
if (formatter === undefined) {
|
|
if (fallbackFormatter === undefined) {
|
|
fallbackFormatter = this._internal_formatter();
|
|
}
|
|
return fallbackFormatter.format(value);
|
|
}
|
|
return formatter(value);
|
|
}
|
|
_private__formatValues(values, formatter, fallbackFormatter) {
|
|
if (formatter === undefined) {
|
|
if (fallbackFormatter === undefined) {
|
|
fallbackFormatter = this._internal_formatter();
|
|
}
|
|
return fallbackFormatter.formatTickmarks(values);
|
|
}
|
|
return formatter(values);
|
|
}
|
|
_private__formatPrice(price, fallbackFormatter) {
|
|
return this._private__formatValue(price, this._private__localizationOptions.priceFormatter, fallbackFormatter);
|
|
}
|
|
_private__formatTickmarks(prices, fallbackFormatter) {
|
|
const priceFormatter = this._private__localizationOptions.priceFormatter;
|
|
return this._private__formatValues(prices, this._private__localizationOptions.tickmarksPriceFormatter ?? (priceFormatter ? (values) => values.map(priceFormatter) : undefined), fallbackFormatter);
|
|
}
|
|
_private__formatPercentage(percentage, fallbackFormatter) {
|
|
return this._private__formatValue(percentage, this._private__localizationOptions.percentageFormatter, fallbackFormatter);
|
|
}
|
|
_private__formatPercentageTickmarks(percentages, fallbackFormatter) {
|
|
const tickmarksPercentageFormatter = this._private__localizationOptions.percentageFormatter;
|
|
return this._private__formatValues(percentages, this._private__localizationOptions.tickmarksPercentageFormatter ?? (tickmarksPercentageFormatter ? (values) => values.map(tickmarksPercentageFormatter) : undefined), fallbackFormatter);
|
|
}
|
|
}
|
|
|
|
function isSeries(source) {
|
|
return source instanceof Series;
|
|
}
|
|
const DEFAULT_STRETCH_FACTOR = 1;
|
|
const MIN_PANE_HEIGHT = 30;
|
|
class Pane {
|
|
constructor(timeScale, model) {
|
|
this._private__dataSources = [];
|
|
this._private__overlaySourcesByScaleId = new Map();
|
|
this._private__height = 0;
|
|
this._private__width = 0;
|
|
this._private__stretchFactor = DEFAULT_STRETCH_FACTOR;
|
|
this._private__cachedOrderedSources = null;
|
|
this._private__cachedOrderedSourcesForRendering = null;
|
|
this._private__preserveEmptyPane = false;
|
|
this._private__destroyed = new Delegate();
|
|
this._private__primitives = [];
|
|
this._private__timeScale = timeScale;
|
|
this._private__model = model;
|
|
this._private__grid = new Grid(this);
|
|
const options = model._internal_options();
|
|
this._private__leftPriceScale = this._private__createPriceScale("left" /* DefaultPriceScaleId.Left */, options.leftPriceScale);
|
|
this._private__rightPriceScale = this._private__createPriceScale("right" /* DefaultPriceScaleId.Right */, options.rightPriceScale);
|
|
this._private__leftPriceScale._internal_modeChanged()._internal_subscribe(this._private__onPriceScaleModeChanged.bind(this, this._private__leftPriceScale), this);
|
|
this._private__rightPriceScale._internal_modeChanged()._internal_subscribe(this._private__onPriceScaleModeChanged.bind(this, this._private__rightPriceScale), this);
|
|
this._internal_applyScaleOptions(options);
|
|
}
|
|
_internal_applyScaleOptions(options) {
|
|
if (options.leftPriceScale) {
|
|
this._private__leftPriceScale._internal_applyOptions(options.leftPriceScale);
|
|
}
|
|
if (options.rightPriceScale) {
|
|
this._private__rightPriceScale._internal_applyOptions(options.rightPriceScale);
|
|
}
|
|
if (options.localization) {
|
|
this._private__leftPriceScale._internal_updateFormatter();
|
|
this._private__rightPriceScale._internal_updateFormatter();
|
|
}
|
|
if (options.overlayPriceScales) {
|
|
const sourceArrays = Array.from(this._private__overlaySourcesByScaleId.values());
|
|
for (const arr of sourceArrays) {
|
|
const priceScale = ensureNotNull(arr[0]._internal_priceScale());
|
|
priceScale._internal_applyOptions(options.overlayPriceScales);
|
|
if (options.localization) {
|
|
priceScale._internal_updateFormatter();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
_internal_priceScaleById(id) {
|
|
switch (id) {
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
case "left" /* DefaultPriceScaleId.Left */: {
|
|
return this._private__leftPriceScale;
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
case "right" /* DefaultPriceScaleId.Right */: {
|
|
return this._private__rightPriceScale;
|
|
}
|
|
}
|
|
if (this._private__overlaySourcesByScaleId.has(id)) {
|
|
return ensureDefined(this._private__overlaySourcesByScaleId.get(id))[0]._internal_priceScale();
|
|
}
|
|
return null;
|
|
}
|
|
_internal_destroy() {
|
|
this._internal_model()._internal_priceScalesOptionsChanged()._internal_unsubscribeAll(this);
|
|
this._private__leftPriceScale._internal_modeChanged()._internal_unsubscribeAll(this);
|
|
this._private__rightPriceScale._internal_modeChanged()._internal_unsubscribeAll(this);
|
|
this._private__dataSources.forEach((source) => {
|
|
if (source._internal_destroy) {
|
|
source._internal_destroy();
|
|
}
|
|
});
|
|
this._private__primitives = this._private__primitives.filter((primitive) => {
|
|
const p = primitive._internal_primitive();
|
|
if (p.detached) {
|
|
p.detached();
|
|
}
|
|
return false;
|
|
});
|
|
this._private__destroyed._internal_fire();
|
|
}
|
|
_internal_stretchFactor() {
|
|
return this._private__stretchFactor;
|
|
}
|
|
_internal_setStretchFactor(factor) {
|
|
this._private__stretchFactor = factor;
|
|
}
|
|
_internal_model() {
|
|
return this._private__model;
|
|
}
|
|
_internal_width() {
|
|
return this._private__width;
|
|
}
|
|
_internal_height() {
|
|
return this._private__height;
|
|
}
|
|
_internal_setWidth(width) {
|
|
this._private__width = width;
|
|
this._internal_updateAllSources();
|
|
}
|
|
_internal_setHeight(height) {
|
|
this._private__height = height;
|
|
this._private__leftPriceScale._internal_setHeight(height);
|
|
this._private__rightPriceScale._internal_setHeight(height);
|
|
// process overlays
|
|
this._private__dataSources.forEach((ds) => {
|
|
if (this._internal_isOverlay(ds)) {
|
|
const priceScale = ds._internal_priceScale();
|
|
if (priceScale !== null) {
|
|
priceScale._internal_setHeight(height);
|
|
}
|
|
}
|
|
});
|
|
this._internal_updateAllSources();
|
|
}
|
|
_internal_setPreserveEmptyPane(preserve) {
|
|
this._private__preserveEmptyPane = preserve;
|
|
}
|
|
_internal_preserveEmptyPane() {
|
|
return this._private__preserveEmptyPane;
|
|
}
|
|
_internal_series() {
|
|
return this._private__dataSources.filter(isSeries);
|
|
}
|
|
_internal_dataSources() {
|
|
return this._private__dataSources;
|
|
}
|
|
_internal_isOverlay(source) {
|
|
const priceScale = source._internal_priceScale();
|
|
if (priceScale === null) {
|
|
return true;
|
|
}
|
|
return this._private__leftPriceScale !== priceScale && this._private__rightPriceScale !== priceScale;
|
|
}
|
|
_internal_addDataSource(source, targetScaleId, keepSourcesOrder) {
|
|
this._private__insertDataSource(source, targetScaleId, keepSourcesOrder ? source._internal_zorder() : this._private__dataSources.length);
|
|
}
|
|
_internal_removeDataSource(source, keepSourceOrder) {
|
|
const index = this._private__dataSources.indexOf(source);
|
|
assert(index !== -1, 'removeDataSource: invalid data source');
|
|
this._private__dataSources.splice(index, 1);
|
|
if (!keepSourceOrder) {
|
|
this._private__dataSources.forEach((ds, i) => ds._internal_setZorder(i));
|
|
}
|
|
const priceScaleId = ensureNotNull(source._internal_priceScale())._internal_id();
|
|
if (this._private__overlaySourcesByScaleId.has(priceScaleId)) {
|
|
const overlaySources = ensureDefined(this._private__overlaySourcesByScaleId.get(priceScaleId));
|
|
const overlayIndex = overlaySources.indexOf(source);
|
|
if (overlayIndex !== -1) {
|
|
overlaySources.splice(overlayIndex, 1);
|
|
if (overlaySources.length === 0) {
|
|
this._private__overlaySourcesByScaleId.delete(priceScaleId);
|
|
}
|
|
}
|
|
}
|
|
const priceScale = source._internal_priceScale();
|
|
// if source has owner, it returns owner's price scale
|
|
// and it does not have source in their list
|
|
if (priceScale && priceScale._internal_dataSources().indexOf(source) >= 0) {
|
|
priceScale._internal_removeDataSource(source);
|
|
this._internal_recalculatePriceScale(priceScale);
|
|
}
|
|
this._private__invalidateSourceCaches();
|
|
}
|
|
_internal_priceScalePosition(priceScale) {
|
|
if (priceScale === this._private__leftPriceScale) {
|
|
return 'left';
|
|
}
|
|
if (priceScale === this._private__rightPriceScale) {
|
|
return 'right';
|
|
}
|
|
return 'overlay';
|
|
}
|
|
_internal_leftPriceScale() {
|
|
return this._private__leftPriceScale;
|
|
}
|
|
_internal_rightPriceScale() {
|
|
return this._private__rightPriceScale;
|
|
}
|
|
_internal_startScalePrice(priceScale, x) {
|
|
priceScale._internal_startScale(x);
|
|
}
|
|
_internal_scalePriceTo(priceScale, x) {
|
|
priceScale._internal_scaleTo(x);
|
|
// TODO: be more smart and update only affected views
|
|
this._internal_updateAllSources();
|
|
}
|
|
_internal_endScalePrice(priceScale) {
|
|
priceScale._internal_endScale();
|
|
}
|
|
_internal_startScrollPrice(priceScale, x) {
|
|
priceScale._internal_startScroll(x);
|
|
}
|
|
_internal_scrollPriceTo(priceScale, x) {
|
|
priceScale._internal_scrollTo(x);
|
|
this._internal_updateAllSources();
|
|
}
|
|
_internal_endScrollPrice(priceScale) {
|
|
priceScale._internal_endScroll();
|
|
}
|
|
_internal_updateAllSources() {
|
|
this._private__dataSources.forEach((source) => {
|
|
source._internal_updateAllViews();
|
|
});
|
|
}
|
|
_internal_defaultPriceScale() {
|
|
const [primaryPriceScale, secondaryPriceScale] = this._private__defaultPriceScalePair();
|
|
let priceScale = null;
|
|
if (primaryPriceScale._internal_options().visible && primaryPriceScale._internal_dataSources().length !== 0) {
|
|
priceScale = primaryPriceScale;
|
|
}
|
|
else if (secondaryPriceScale._internal_options().visible && secondaryPriceScale._internal_dataSources().length !== 0) {
|
|
priceScale = secondaryPriceScale;
|
|
}
|
|
else if (this._private__dataSources.length !== 0) {
|
|
priceScale = this._private__dataSources[0]._internal_priceScale();
|
|
}
|
|
if (priceScale === null) {
|
|
priceScale = this._internal_defaultVisiblePriceScale() ?? primaryPriceScale;
|
|
}
|
|
return priceScale;
|
|
}
|
|
_internal_defaultVisiblePriceScale() {
|
|
const [primaryPriceScale, secondaryPriceScale] = this._private__defaultPriceScalePair();
|
|
if (primaryPriceScale._internal_options().visible) {
|
|
return primaryPriceScale;
|
|
}
|
|
if (secondaryPriceScale._internal_options().visible) {
|
|
return secondaryPriceScale;
|
|
}
|
|
return null;
|
|
}
|
|
_internal_recalculatePriceScale(priceScale) {
|
|
if (priceScale === null || !priceScale._internal_isAutoScale()) {
|
|
return;
|
|
}
|
|
this._private__recalculatePriceScaleImpl(priceScale);
|
|
}
|
|
_internal_resetPriceScale(priceScale) {
|
|
const visibleBars = this._private__timeScale._internal_visibleStrictRange();
|
|
priceScale._internal_setMode({ _internal_autoScale: true });
|
|
if (visibleBars !== null) {
|
|
priceScale._internal_recalculatePriceRange(visibleBars);
|
|
}
|
|
this._internal_updateAllSources();
|
|
}
|
|
_internal_momentaryAutoScale() {
|
|
this._private__recalculatePriceScaleImpl(this._private__leftPriceScale);
|
|
this._private__recalculatePriceScaleImpl(this._private__rightPriceScale);
|
|
}
|
|
_internal_recalculate() {
|
|
this._internal_recalculatePriceScale(this._private__leftPriceScale);
|
|
this._internal_recalculatePriceScale(this._private__rightPriceScale);
|
|
this._private__dataSources.forEach((ds) => {
|
|
if (this._internal_isOverlay(ds)) {
|
|
this._internal_recalculatePriceScale(ds._internal_priceScale());
|
|
}
|
|
});
|
|
this._internal_updateAllSources();
|
|
this._private__model._internal_lightUpdate();
|
|
}
|
|
_internal_orderedSources() {
|
|
if (this._private__cachedOrderedSources === null) {
|
|
this._private__cachedOrderedSources = sortSources(this._private__dataSources);
|
|
}
|
|
return this._private__cachedOrderedSources;
|
|
}
|
|
_internal_orderedSourcesForRendering() {
|
|
const base = this._internal_orderedSources();
|
|
const hovered = this._private__model._internal_hoveredSource()?._internal_source;
|
|
const enabled = this._private__model._internal_options().hoveredSeriesOnTop;
|
|
const cached = this._private__cachedOrderedSourcesForRendering;
|
|
if (cached !== null && cached._internal_base === base && cached._internal_hovered === hovered && cached._internal_enabled === enabled) {
|
|
return cached._internal_result;
|
|
}
|
|
const result = hoveredSourceOnTopOrder(base, hovered, enabled);
|
|
this._private__cachedOrderedSourcesForRendering = {
|
|
_internal_base: base,
|
|
_internal_hovered: hovered,
|
|
_internal_enabled: enabled,
|
|
_internal_result: result,
|
|
};
|
|
return result;
|
|
}
|
|
_internal_setSeriesOrder(series, order) {
|
|
order = clamp(order, 0, this._private__dataSources.length - 1);
|
|
const index = this._private__dataSources.indexOf(series);
|
|
assert(index !== -1, 'setSeriesOrder: invalid data source');
|
|
this._private__dataSources.splice(index, 1);
|
|
this._private__dataSources.splice(order, 0, series);
|
|
this._private__dataSources.forEach((ps, i) => ps._internal_setZorder(i));
|
|
this._private__invalidateSourceCaches();
|
|
for (const ps of [this._private__leftPriceScale, this._private__rightPriceScale]) {
|
|
ps._internal_invalidateSourcesCache();
|
|
ps._internal_updateFormatter();
|
|
}
|
|
this._private__model._internal_lightUpdate();
|
|
}
|
|
_internal_orderedSeries() {
|
|
return this._internal_orderedSources().filter(isSeries);
|
|
}
|
|
_internal_onDestroyed() {
|
|
return this._private__destroyed;
|
|
}
|
|
_internal_grid() {
|
|
return this._private__grid;
|
|
}
|
|
_internal_attachPrimitive(primitive) {
|
|
this._private__primitives.push(new PanePrimitiveWrapper$1(primitive));
|
|
}
|
|
_internal_detachPrimitive(source) {
|
|
this._private__primitives = this._private__primitives.filter((wrapper) => wrapper._internal_primitive() !== source);
|
|
if (source.detached) {
|
|
source.detached();
|
|
}
|
|
this._private__model._internal_lightUpdate();
|
|
}
|
|
_internal_primitives() {
|
|
return this._private__primitives;
|
|
}
|
|
_internal_primitiveHitTest(x, y) {
|
|
return this._private__primitives
|
|
.map((primitive) => primitive._internal_hitTest(x, y))
|
|
.filter((result) => result !== null);
|
|
}
|
|
_private__recalculatePriceScaleImpl(priceScale) {
|
|
// TODO: can use this checks
|
|
const sourceForAutoScale = priceScale._internal_sourcesForAutoScale();
|
|
if (sourceForAutoScale && sourceForAutoScale.length > 0 && !this._private__timeScale._internal_isEmpty()) {
|
|
const visibleBars = this._private__timeScale._internal_visibleStrictRange();
|
|
if (visibleBars !== null) {
|
|
priceScale._internal_recalculatePriceRange(visibleBars);
|
|
}
|
|
}
|
|
priceScale._internal_updateAllViews();
|
|
}
|
|
_private__insertDataSource(source, priceScaleId, order) {
|
|
let priceScale = this._internal_priceScaleById(priceScaleId);
|
|
if (priceScale === null) {
|
|
priceScale = this._private__createPriceScale(priceScaleId, this._private__model._internal_options().overlayPriceScales);
|
|
}
|
|
this._private__dataSources.splice(order, 0, source);
|
|
if (!isDefaultPriceScale(priceScaleId)) {
|
|
const overlaySources = this._private__overlaySourcesByScaleId.get(priceScaleId) || [];
|
|
overlaySources.push(source);
|
|
this._private__overlaySourcesByScaleId.set(priceScaleId, overlaySources);
|
|
}
|
|
source._internal_setZorder(order);
|
|
priceScale._internal_addDataSource(source);
|
|
source._internal_setPriceScale(priceScale);
|
|
this._internal_recalculatePriceScale(priceScale);
|
|
this._private__invalidateSourceCaches();
|
|
}
|
|
_private__invalidateSourceCaches() {
|
|
this._private__cachedOrderedSources = null;
|
|
this._private__cachedOrderedSourcesForRendering = null;
|
|
}
|
|
_private__defaultPriceScalePair() {
|
|
return this._private__model._internal_options().defaultVisiblePriceScaleId === "left" /* DefaultPriceScaleId.Left */
|
|
? [this._private__leftPriceScale, this._private__rightPriceScale]
|
|
: [this._private__rightPriceScale, this._private__leftPriceScale];
|
|
}
|
|
_private__onPriceScaleModeChanged(priceScale, oldMode, newMode) {
|
|
if (oldMode._internal_mode === newMode._internal_mode) {
|
|
return;
|
|
}
|
|
// momentary auto scale if we toggle percentage/indexedTo100 mode
|
|
this._private__recalculatePriceScaleImpl(priceScale);
|
|
}
|
|
_private__createPriceScale(id, options) {
|
|
const actualOptions = { visible: true, autoScale: true, ...clone(options) };
|
|
const priceScale = new PriceScale(id, actualOptions, this._private__model._internal_options()['layout'], this._private__model._internal_options().localization, this._private__model._internal_colorParser());
|
|
priceScale._internal_setHeight(this._internal_height());
|
|
return priceScale;
|
|
}
|
|
}
|
|
|
|
function isBetterHit(candidate, currentBest) {
|
|
if (currentBest === null) {
|
|
return true;
|
|
}
|
|
if (candidate._internal_priority === 2 /* HitTestPriority.Point */ && currentBest._internal_priority !== 2 /* HitTestPriority.Point */) {
|
|
return true;
|
|
}
|
|
if (currentBest._internal_priority === 2 /* HitTestPriority.Point */ && candidate._internal_priority !== 2 /* HitTestPriority.Point */) {
|
|
return false;
|
|
}
|
|
if (candidate._internal_distance !== currentBest._internal_distance) {
|
|
return candidate._internal_distance < currentBest._internal_distance;
|
|
}
|
|
// Preserve the existing draw/source order for equal-distance non-point ties.
|
|
// This prevents hidden strokes from overtaking visually covering range hits.
|
|
return false;
|
|
}
|
|
|
|
function hoveredObjectFromCandidate(candidate) {
|
|
return {
|
|
_internal_externalId: candidate._internal_externalId,
|
|
_internal_hitTestData: candidate._internal_hitTestData,
|
|
};
|
|
}
|
|
// returns true if item is above reference
|
|
function comparePrimitiveZOrder(item, reference) {
|
|
return (!reference ||
|
|
(item === 'top' && reference !== 'top') ||
|
|
(item === 'normal' && reference === 'bottom'));
|
|
}
|
|
function primitiveHitCandidate(hitResult) {
|
|
return {
|
|
_internal_distance: hitResult.distance ?? 0,
|
|
_internal_priority: hitResult.hitTestPriority ?? (hitResult.itemType === 'marker' ? 2 /* HitTestPriority.Point */ : 0 /* HitTestPriority.Range */),
|
|
_internal_itemType: hitResult.itemType ?? 'primitive',
|
|
_internal_cursorStyle: hitResult.cursorStyle,
|
|
_internal_externalId: hitResult.externalId,
|
|
};
|
|
}
|
|
function findBestPrimitiveHitTest(sources, x, y) {
|
|
let bestPrimitiveHit;
|
|
let bestPrimitiveCandidate;
|
|
let bestHitSource;
|
|
for (const source of sources) {
|
|
const primitiveHitResults = source._internal_primitiveHitTest?.(x, y) ?? [];
|
|
for (const hitResult of primitiveHitResults) {
|
|
const candidate = primitiveHitCandidate(hitResult);
|
|
if (comparePrimitiveZOrder(hitResult.zOrder, bestPrimitiveHit?.zOrder) ||
|
|
(hitResult.zOrder === bestPrimitiveHit?.zOrder && bestPrimitiveCandidate !== undefined && isBetterHit(candidate, bestPrimitiveCandidate)) ||
|
|
(hitResult.zOrder === bestPrimitiveHit?.zOrder && bestPrimitiveCandidate === undefined)) {
|
|
bestPrimitiveHit = hitResult;
|
|
bestPrimitiveCandidate = candidate;
|
|
bestHitSource = source;
|
|
}
|
|
}
|
|
}
|
|
if (!bestPrimitiveHit || !bestHitSource || !bestPrimitiveCandidate) {
|
|
return null;
|
|
}
|
|
return {
|
|
_internal_candidate: bestPrimitiveCandidate,
|
|
_internal_hit: bestPrimitiveHit,
|
|
_internal_source: bestHitSource,
|
|
};
|
|
}
|
|
function convertPrimitiveHitResult(primitiveHit) {
|
|
return {
|
|
_internal_source: primitiveHit._internal_source,
|
|
_internal_object: hoveredObjectFromCandidate(primitiveHit._internal_candidate),
|
|
_internal_cursorStyle: primitiveHit._internal_candidate._internal_cursorStyle,
|
|
_internal_itemType: primitiveHit._internal_candidate._internal_itemType ?? 'primitive',
|
|
};
|
|
}
|
|
/**
|
|
* Performs a hit test on a collection of pane views to determine which view and object
|
|
* is located at a given coordinate (x, y) and returns the matching pane view and
|
|
* hit-tested result object, or null if no match is found.
|
|
*/
|
|
function hitTestPaneView(paneViews, x, y, pane) {
|
|
let bestResult = null;
|
|
for (const paneView of paneViews) {
|
|
// Pane-view hit tests are an internal contract, so we can trust the typed
|
|
// InternalHitTestCandidate directly instead of probing build-mangled fields.
|
|
let candidate = paneView._internal_hitTest?.(x, y, pane) ?? null;
|
|
if (candidate === null) {
|
|
const renderer = paneView._internal_renderer(pane);
|
|
candidate = renderer !== null && renderer._internal_hitTest ? renderer._internal_hitTest(x, y) : null;
|
|
}
|
|
if (candidate !== null) {
|
|
const candidateResult = {
|
|
_internal_view: paneView,
|
|
_internal_candidate: candidate,
|
|
};
|
|
if (bestResult === null || isBetterHit(candidateResult._internal_candidate, bestResult._internal_candidate)) {
|
|
bestResult = candidateResult;
|
|
}
|
|
}
|
|
}
|
|
return bestResult;
|
|
}
|
|
function isDataSource(source) {
|
|
return source._internal_paneViews !== undefined;
|
|
}
|
|
// eslint-disable-next-line complexity
|
|
function hitTestPane(pane, x, y) {
|
|
// Hover arbitration should use the pane's stable source order, not the temporary
|
|
// "hovered series on top" render order, otherwise the current hovered source can
|
|
// become sticky and keep winning equal-distance overlaps.
|
|
const sources = [pane, ...pane._internal_orderedSources()].reverse();
|
|
const bestPrimitiveHit = findBestPrimitiveHitTest(sources, x, y);
|
|
if (bestPrimitiveHit?._internal_hit.zOrder === 'top') {
|
|
// a primitive hit on the 'top' layer will always beat the built-in hit tests
|
|
// (on normal layer) so we can return early here.
|
|
return convertPrimitiveHitResult(bestPrimitiveHit);
|
|
}
|
|
let bestSourceHit = null;
|
|
let bestSourceCandidate = null;
|
|
for (const source of sources) {
|
|
if (bestPrimitiveHit && bestPrimitiveHit._internal_source === source && bestPrimitiveHit._internal_hit.zOrder !== 'bottom' && !bestPrimitiveHit._internal_hit.isBackground) {
|
|
// A foreground primitive sits above its source's built-in views and blocks all lower sources,
|
|
// but hits from higher sources should still keep precedence.
|
|
return bestSourceHit ?? convertPrimitiveHitResult(bestPrimitiveHit);
|
|
}
|
|
if (isDataSource(source)) {
|
|
const sourceResult = hitTestPaneView(source._internal_paneViews(pane), x, y, pane);
|
|
if (sourceResult !== null) {
|
|
const candidateHit = {
|
|
_internal_source: source,
|
|
_internal_view: sourceResult._internal_view,
|
|
_internal_object: hoveredObjectFromCandidate(sourceResult._internal_candidate),
|
|
_internal_cursorStyle: sourceResult._internal_candidate._internal_cursorStyle,
|
|
_internal_itemType: sourceResult._internal_candidate._internal_itemType ?? 'primitive',
|
|
};
|
|
if (bestSourceHit === null || isBetterHit(sourceResult._internal_candidate, bestSourceCandidate)) {
|
|
bestSourceHit = candidateHit;
|
|
bestSourceCandidate = sourceResult._internal_candidate;
|
|
}
|
|
}
|
|
}
|
|
if (bestPrimitiveHit && bestPrimitiveHit._internal_source === source && bestPrimitiveHit._internal_hit.zOrder !== 'bottom' && bestPrimitiveHit._internal_hit.isBackground) {
|
|
return bestSourceHit ?? convertPrimitiveHitResult(bestPrimitiveHit);
|
|
}
|
|
}
|
|
if (bestSourceHit !== null) {
|
|
return bestSourceHit;
|
|
}
|
|
if (bestPrimitiveHit?._internal_hit) {
|
|
// return primitive hits for the 'bottom' layer
|
|
return convertPrimitiveHitResult(bestPrimitiveHit);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
class FormattedLabelsCache {
|
|
constructor(format, horzScaleBehavior, size = 50) {
|
|
this._private__actualSize = 0;
|
|
this._private__usageTick = 1;
|
|
this._private__oldestTick = 1;
|
|
this._private__cache = new Map();
|
|
this._private__tick2Labels = new Map();
|
|
this._private__format = format;
|
|
this._private__horzScaleBehavior = horzScaleBehavior;
|
|
this._private__maxSize = size;
|
|
}
|
|
_internal_format(tickMark) {
|
|
const time = tickMark.time;
|
|
const cacheKey = this._private__horzScaleBehavior.cacheKey(time);
|
|
const tick = this._private__cache.get(cacheKey);
|
|
if (tick !== undefined) {
|
|
return tick._internal_string;
|
|
}
|
|
if (this._private__actualSize === this._private__maxSize) {
|
|
const oldestValue = this._private__tick2Labels.get(this._private__oldestTick);
|
|
this._private__tick2Labels.delete(this._private__oldestTick);
|
|
this._private__cache.delete(ensureDefined(oldestValue));
|
|
this._private__oldestTick++;
|
|
this._private__actualSize--;
|
|
}
|
|
const str = this._private__format(tickMark);
|
|
this._private__cache.set(cacheKey, { _internal_string: str, _internal_tick: this._private__usageTick });
|
|
this._private__tick2Labels.set(this._private__usageTick, cacheKey);
|
|
this._private__actualSize++;
|
|
this._private__usageTick++;
|
|
return str;
|
|
}
|
|
}
|
|
|
|
class RangeImpl {
|
|
constructor(left, right) {
|
|
assert(left <= right, 'right should be >= left');
|
|
this._private__left = left;
|
|
this._private__right = right;
|
|
}
|
|
_internal_left() {
|
|
return this._private__left;
|
|
}
|
|
_internal_right() {
|
|
return this._private__right;
|
|
}
|
|
_internal_count() {
|
|
return this._private__right - this._private__left + 1;
|
|
}
|
|
_internal_contains(index) {
|
|
return this._private__left <= index && index <= this._private__right;
|
|
}
|
|
_internal_equals(other) {
|
|
return this._private__left === other._internal_left() && this._private__right === other._internal_right();
|
|
}
|
|
}
|
|
function areRangesEqual(first, second) {
|
|
if (first === null || second === null) {
|
|
return first === second;
|
|
}
|
|
return first._internal_equals(second);
|
|
}
|
|
|
|
class TickMarks {
|
|
constructor() {
|
|
this._private__marksByWeight = new Map();
|
|
this._private__cache = null;
|
|
this._private__uniformDistribution = false;
|
|
}
|
|
_internal_setUniformDistribution(val) {
|
|
this._private__uniformDistribution = val;
|
|
this._private__cache = null;
|
|
}
|
|
_internal_setTimeScalePoints(newPoints, firstChangedPointIndex) {
|
|
this._private__removeMarksSinceIndex(firstChangedPointIndex);
|
|
this._private__cache = null;
|
|
for (let index = firstChangedPointIndex; index < newPoints.length; ++index) {
|
|
const point = newPoints[index];
|
|
let marksForWeight = this._private__marksByWeight.get(point.timeWeight);
|
|
if (marksForWeight === undefined) {
|
|
marksForWeight = [];
|
|
this._private__marksByWeight.set(point.timeWeight, marksForWeight);
|
|
}
|
|
marksForWeight.push({
|
|
index: index,
|
|
time: point.time,
|
|
weight: point.timeWeight,
|
|
originalTime: point.originalTime,
|
|
});
|
|
}
|
|
}
|
|
_internal_build(spacing, maxWidth, checkIndicesForData, indicesWithDataMap, indicesWithDataId) {
|
|
const maxIndexesPerMark = Math.ceil(maxWidth / spacing);
|
|
if (this._private__cache === null ||
|
|
this._private__cache._internal_maxIndexesPerMark !== maxIndexesPerMark ||
|
|
indicesWithDataId !== this._private__cache._internal_indicesWithDataId ||
|
|
checkIndicesForData !== this._private__cache._internal_checkIndicesForData) {
|
|
this._private__cache = {
|
|
_internal_indicesWithDataId: indicesWithDataId,
|
|
_internal_checkIndicesForData: checkIndicesForData,
|
|
_internal_marks: this._private__buildMarksImpl(maxIndexesPerMark, checkIndicesForData, indicesWithDataMap),
|
|
_internal_maxIndexesPerMark: maxIndexesPerMark,
|
|
};
|
|
}
|
|
return this._private__cache._internal_marks;
|
|
}
|
|
_private__removeMarksSinceIndex(sinceIndex) {
|
|
if (sinceIndex === 0) {
|
|
this._private__marksByWeight.clear();
|
|
return;
|
|
}
|
|
const weightsToClear = [];
|
|
this._private__marksByWeight.forEach((marks, timeWeight) => {
|
|
if (sinceIndex <= marks[0].index) {
|
|
weightsToClear.push(timeWeight);
|
|
}
|
|
else {
|
|
marks.splice(lowerBound(marks, sinceIndex, (tm) => tm.index < sinceIndex), Infinity);
|
|
}
|
|
});
|
|
for (const weight of weightsToClear) {
|
|
this._private__marksByWeight.delete(weight);
|
|
}
|
|
}
|
|
_private__buildMarksImpl(maxIndexesPerMark, checkIndicesForData, indicesWithDataMap) {
|
|
let marks = [];
|
|
const canBeIncluded = (mark) => !checkIndicesForData || indicesWithDataMap.has(mark.index);
|
|
for (const weight of Array.from(this._private__marksByWeight.keys()).sort((a, b) => b - a)) {
|
|
if (!this._private__marksByWeight.get(weight)) {
|
|
continue;
|
|
}
|
|
// Built tickMarks are now prevMarks, and marks it as new array
|
|
const prevMarks = marks;
|
|
marks = [];
|
|
const prevMarksLength = prevMarks.length;
|
|
let prevMarksPointer = 0;
|
|
const currentWeight = ensureDefined(this._private__marksByWeight.get(weight));
|
|
const currentWeightLength = currentWeight.length;
|
|
let rightIndex = Infinity;
|
|
let leftIndex = -Infinity;
|
|
for (let i = 0; i < currentWeightLength; i++) {
|
|
const mark = currentWeight[i];
|
|
const currentIndex = mark.index;
|
|
// Determine indexes with which current index will be compared
|
|
// All marks to the right is moved to new array
|
|
while (prevMarksPointer < prevMarksLength) {
|
|
const lastMark = prevMarks[prevMarksPointer];
|
|
const lastIndex = lastMark.index;
|
|
if (lastIndex < currentIndex && canBeIncluded(lastMark)) {
|
|
prevMarksPointer++;
|
|
marks.push(lastMark);
|
|
leftIndex = lastIndex;
|
|
rightIndex = Infinity;
|
|
}
|
|
else {
|
|
rightIndex = lastIndex;
|
|
break;
|
|
}
|
|
}
|
|
if (rightIndex - currentIndex >= maxIndexesPerMark &&
|
|
currentIndex - leftIndex >= maxIndexesPerMark &&
|
|
canBeIncluded(mark)) {
|
|
// TickMark fits. Place it into new array
|
|
marks.push(mark);
|
|
leftIndex = currentIndex;
|
|
}
|
|
else {
|
|
if (this._private__uniformDistribution) {
|
|
return prevMarks;
|
|
}
|
|
}
|
|
}
|
|
// Place all unused tickMarks into new array;
|
|
for (; prevMarksPointer < prevMarksLength; prevMarksPointer++) {
|
|
if (canBeIncluded(prevMarks[prevMarksPointer])) {
|
|
marks.push(prevMarks[prevMarksPointer]);
|
|
}
|
|
}
|
|
}
|
|
return marks;
|
|
}
|
|
}
|
|
|
|
class TimeScaleVisibleRange {
|
|
constructor(logicalRange) {
|
|
this._private__logicalRange = logicalRange;
|
|
}
|
|
_internal_strictRange() {
|
|
if (this._private__logicalRange === null) {
|
|
return null;
|
|
}
|
|
return new RangeImpl(Math.floor(this._private__logicalRange._internal_left()), Math.ceil(this._private__logicalRange._internal_right()));
|
|
}
|
|
_internal_logicalRange() {
|
|
return this._private__logicalRange;
|
|
}
|
|
static _internal_invalid() {
|
|
return new TimeScaleVisibleRange(null);
|
|
}
|
|
}
|
|
|
|
const defaultTickMarkMaxCharacterLength = 8;
|
|
function markWithGreaterWeight$2(a, b) {
|
|
return a.weight > b.weight ? a : b;
|
|
}
|
|
class TimeScale {
|
|
constructor(model, options, localizationOptions, horzScaleBehavior) {
|
|
this._private__width = 0;
|
|
this._private__baseIndexOrNull = null;
|
|
this._private__points = [];
|
|
this._private__scrollStartPoint = null;
|
|
this._private__scaleStartPoint = null;
|
|
this._private__tickMarks = new TickMarks();
|
|
this._private__formattedByWeight = new Map();
|
|
this._private__visibleRange = TimeScaleVisibleRange._internal_invalid();
|
|
this._private__visibleRangeInvalidated = true;
|
|
this._private__visibleBarsChanged = new Delegate();
|
|
this._private__logicalRangeChanged = new Delegate();
|
|
this._private__optionsApplied = new Delegate();
|
|
this._private__commonTransitionStartState = null;
|
|
this._private__timeMarksCache = null;
|
|
this._private__indicesWithData = new Map();
|
|
this._private__indicesWithDataUpdateId = -1;
|
|
this._private__labels = [];
|
|
this._private__conflationFactor = 1;
|
|
this._private__options = options;
|
|
this._private__localizationOptions = localizationOptions;
|
|
this._private__rightOffset = options.rightOffset;
|
|
this._private__barSpacing = options.barSpacing;
|
|
this._private__model = model;
|
|
this._private__checkRightOffsetPixels(options);
|
|
this._private__horzScaleBehavior = horzScaleBehavior;
|
|
this._private__updateDateTimeFormatter();
|
|
this._private__tickMarks._internal_setUniformDistribution(options.uniformDistribution);
|
|
this._private__updateConflationFactor();
|
|
this._internal_recalculateIndicesWithData();
|
|
}
|
|
_internal_options() {
|
|
return this._private__options;
|
|
}
|
|
_internal_applyLocalizationOptions(localizationOptions) {
|
|
merge(this._private__localizationOptions, localizationOptions);
|
|
this._private__invalidateTickMarks();
|
|
this._private__updateDateTimeFormatter();
|
|
}
|
|
_internal_applyOptions(options, localizationOptions) {
|
|
merge(this._private__options, options);
|
|
if (this._private__options.fixLeftEdge) {
|
|
this._private__doFixLeftEdge();
|
|
}
|
|
if (this._private__options.fixRightEdge) {
|
|
this._private__doFixRightEdge();
|
|
}
|
|
// note that bar spacing should be applied before right offset
|
|
// because right offset depends on bar spacing
|
|
if (options.barSpacing !== undefined) {
|
|
this._private__model._internal_setBarSpacing(options.barSpacing);
|
|
}
|
|
if (options.rightOffset !== undefined) {
|
|
this._private__model._internal_setRightOffset(options.rightOffset);
|
|
}
|
|
this._private__checkRightOffsetPixels(options);
|
|
if (options.minBarSpacing !== undefined || options.maxBarSpacing !== undefined) {
|
|
// yes, if we apply bar spacing constrains then we need to correct bar spacing
|
|
// the easiest way is to apply it once again
|
|
this._private__model._internal_setBarSpacing(options.barSpacing ?? this._private__barSpacing);
|
|
}
|
|
if (options.ignoreWhitespaceIndices !== undefined && options.ignoreWhitespaceIndices !== this._private__options.ignoreWhitespaceIndices) {
|
|
this._internal_recalculateIndicesWithData();
|
|
}
|
|
this._private__invalidateTickMarks();
|
|
this._private__updateDateTimeFormatter();
|
|
// Recompute conflation factor when options that may affect it change
|
|
if (options.enableConflation !== undefined || options.conflationThresholdFactor !== undefined) {
|
|
this._private__updateConflationFactor();
|
|
}
|
|
this._private__optionsApplied._internal_fire();
|
|
}
|
|
_internal_indexToTime(index) {
|
|
return this._private__points[index]?.time ?? null;
|
|
}
|
|
_internal_indexToTimeScalePoint(index) {
|
|
return this._private__points[index] ?? null;
|
|
}
|
|
_internal_timeToIndex(time, findNearest) {
|
|
if (this._private__points.length < 1) {
|
|
// no time points available
|
|
return null;
|
|
}
|
|
if (this._private__horzScaleBehavior.key(time) > this._private__horzScaleBehavior.key(this._private__points[this._private__points.length - 1].time)) {
|
|
// special case
|
|
return findNearest ? this._private__points.length - 1 : null;
|
|
}
|
|
const index = lowerBound(this._private__points, this._private__horzScaleBehavior.key(time), (a, b) => this._private__horzScaleBehavior.key(a.time) < b);
|
|
if (this._private__horzScaleBehavior.key(time) < this._private__horzScaleBehavior.key(this._private__points[index].time)) {
|
|
return findNearest ? index : null;
|
|
}
|
|
return index;
|
|
}
|
|
_internal_isEmpty() {
|
|
return this._private__width === 0 || this._private__points.length === 0 || this._private__baseIndexOrNull === null;
|
|
}
|
|
_internal_hasPoints() {
|
|
return this._private__points.length > 0;
|
|
}
|
|
// strict range: integer indices of the bars in the visible range rounded in more wide direction
|
|
_internal_visibleStrictRange() {
|
|
this._private__updateVisibleRange();
|
|
return this._private__visibleRange._internal_strictRange();
|
|
}
|
|
_internal_visibleLogicalRange() {
|
|
this._private__updateVisibleRange();
|
|
return this._private__visibleRange._internal_logicalRange();
|
|
}
|
|
_internal_visibleTimeRange() {
|
|
const visibleBars = this._internal_visibleStrictRange();
|
|
if (visibleBars === null) {
|
|
return null;
|
|
}
|
|
const range = {
|
|
from: visibleBars._internal_left(),
|
|
to: visibleBars._internal_right(),
|
|
};
|
|
return this._internal_timeRangeForLogicalRange(range);
|
|
}
|
|
_internal_timeRangeForLogicalRange(range) {
|
|
const from = Math.round(range.from);
|
|
const to = Math.round(range.to);
|
|
const firstIndex = ensureNotNull(this._private__firstIndex());
|
|
const lastIndex = ensureNotNull(this._private__lastIndex());
|
|
return {
|
|
from: ensureNotNull(this._internal_indexToTimeScalePoint(Math.max(firstIndex, from))),
|
|
to: ensureNotNull(this._internal_indexToTimeScalePoint(Math.min(lastIndex, to))),
|
|
};
|
|
}
|
|
_internal_logicalRangeForTimeRange(range) {
|
|
return {
|
|
from: ensureNotNull(this._internal_timeToIndex(range.from, true)),
|
|
to: ensureNotNull(this._internal_timeToIndex(range.to, true)),
|
|
};
|
|
}
|
|
_internal_width() {
|
|
return this._private__width;
|
|
}
|
|
_internal_setWidth(newWidth) {
|
|
if (!isFinite(newWidth) || newWidth <= 0) {
|
|
return;
|
|
}
|
|
if (this._private__width === newWidth) {
|
|
return;
|
|
}
|
|
// when we change the width and we need to correct visible range because of fixing left edge
|
|
// we need to check the previous visible range rather than the new one
|
|
// because it might be updated by changing width, bar spacing, etc
|
|
// but we need to try to keep the same range
|
|
const previousVisibleRange = this._internal_visibleLogicalRange();
|
|
const oldWidth = this._private__width;
|
|
this._private__width = newWidth;
|
|
this._private__visibleRangeInvalidated = true;
|
|
if (this._private__options.lockVisibleTimeRangeOnResize && oldWidth !== 0) {
|
|
// recalculate bar spacing
|
|
const newBarSpacing = this._private__barSpacing * newWidth / oldWidth;
|
|
this._private__barSpacing = newBarSpacing;
|
|
}
|
|
// if time scale is scrolled to the end of data and we have fixed right edge
|
|
// keep left edge instead of right
|
|
// we need it to avoid "shaking" if the last bar visibility affects time scale width
|
|
if (this._private__options.fixLeftEdge) {
|
|
// note that logical left range means not the middle of a bar (it's the left border)
|
|
if (previousVisibleRange !== null && previousVisibleRange._internal_left() <= 0) {
|
|
const delta = oldWidth - newWidth;
|
|
// reduce _rightOffset means move right
|
|
// we could move more than required - this will be fixed by _correctOffset()
|
|
this._private__rightOffset -= Math.round(delta / this._private__barSpacing) + 1;
|
|
this._private__visibleRangeInvalidated = true;
|
|
}
|
|
}
|
|
// updating bar spacing should be first because right offset depends on it
|
|
this._private__correctBarSpacing();
|
|
this._private__correctOffset();
|
|
}
|
|
_internal_indexToCoordinate(index) {
|
|
if (this._internal_isEmpty() || !isInteger(index)) {
|
|
return 0;
|
|
}
|
|
const baseIndex = this._internal_baseIndex();
|
|
const deltaFromRight = baseIndex + this._private__rightOffset - index;
|
|
const coordinate = this._private__width - (deltaFromRight + 0.5) * this._private__barSpacing - 1;
|
|
return coordinate;
|
|
}
|
|
_internal_indexesToCoordinates(points, visibleRange) {
|
|
const baseIndex = this._internal_baseIndex();
|
|
const indexFrom = (visibleRange === undefined) ? 0 : visibleRange.from;
|
|
const indexTo = (visibleRange === undefined) ? points.length : visibleRange.to;
|
|
for (let i = indexFrom; i < indexTo; i++) {
|
|
const index = points[i]._internal_time;
|
|
const deltaFromRight = baseIndex + this._private__rightOffset - index;
|
|
const coordinate = this._private__width - (deltaFromRight + 0.5) * this._private__barSpacing - 1;
|
|
points[i]._internal_x = coordinate;
|
|
}
|
|
}
|
|
_internal_coordinateToIndex(x, considerIgnoreWhitespace) {
|
|
const index = Math.ceil(this._private__coordinateToFloatIndex(x));
|
|
if (!considerIgnoreWhitespace ||
|
|
!this._private__options.ignoreWhitespaceIndices ||
|
|
this._private__shouldConsiderIndex(index)) {
|
|
return index;
|
|
}
|
|
return this._private__findNearestIndexWithData(index);
|
|
}
|
|
_internal_setRightOffset(offset) {
|
|
this._private__visibleRangeInvalidated = true;
|
|
this._private__rightOffset = offset;
|
|
this._private__correctOffset();
|
|
this._private__model._internal_recalculateAllPanes();
|
|
this._private__model._internal_lightUpdate();
|
|
}
|
|
_internal_barSpacing() {
|
|
return this._private__barSpacing;
|
|
}
|
|
_internal_setBarSpacing(newBarSpacing) {
|
|
const oldBarSpacing = this._private__barSpacing;
|
|
this._private__setBarSpacing(newBarSpacing);
|
|
if (this._private__options.rightOffsetPixels !== undefined && oldBarSpacing !== 0) {
|
|
// when in pixel mode, zooming should keep the pixel offset, so we need to
|
|
// recalculate the bar offset.
|
|
const newRightOffset = this._private__rightOffset * oldBarSpacing / this._private__barSpacing;
|
|
this._private__rightOffset = newRightOffset;
|
|
}
|
|
// do not allow scroll out of visible bars
|
|
this._private__correctOffset();
|
|
this._private__model._internal_recalculateAllPanes();
|
|
this._private__model._internal_lightUpdate();
|
|
}
|
|
_internal_rightOffset() {
|
|
return this._private__rightOffset;
|
|
}
|
|
// eslint-disable-next-line complexity
|
|
_internal_marks() {
|
|
if (this._internal_isEmpty()) {
|
|
return null;
|
|
}
|
|
if (this._private__timeMarksCache !== null) {
|
|
return this._private__timeMarksCache;
|
|
}
|
|
const spacing = this._private__barSpacing;
|
|
const fontSize = this._private__model._internal_options()['layout'].fontSize;
|
|
const pixelsPer8Characters = (fontSize + 4) * 5;
|
|
const pixelsPerCharacter = pixelsPer8Characters / defaultTickMarkMaxCharacterLength;
|
|
const maxLabelWidth = pixelsPerCharacter * (this._private__options.tickMarkMaxCharacterLength || defaultTickMarkMaxCharacterLength);
|
|
const indexPerLabel = Math.round(maxLabelWidth / spacing);
|
|
const visibleBars = ensureNotNull(this._internal_visibleStrictRange());
|
|
const firstBar = Math.max(visibleBars._internal_left(), visibleBars._internal_left() - indexPerLabel);
|
|
const lastBar = Math.max(visibleBars._internal_right(), visibleBars._internal_right() - indexPerLabel);
|
|
const items = this._private__tickMarks._internal_build(spacing, maxLabelWidth, this._private__options.ignoreWhitespaceIndices, this._private__indicesWithData, this._private__indicesWithDataUpdateId);
|
|
// according to indexPerLabel value this value means "earliest index which _might be_ used as the second label on time scale"
|
|
const earliestIndexOfSecondLabel = this._private__firstIndex() + indexPerLabel;
|
|
// according to indexPerLabel value this value means "earliest index which _might be_ used as the second last label on time scale"
|
|
const indexOfSecondLastLabel = this._private__lastIndex() - indexPerLabel;
|
|
const isAllScalingAndScrollingDisabled = this._private__isAllScalingAndScrollingDisabled();
|
|
const isLeftEdgeFixed = this._private__options.fixLeftEdge || isAllScalingAndScrollingDisabled;
|
|
const isRightEdgeFixed = this._private__options.fixRightEdge || isAllScalingAndScrollingDisabled;
|
|
let targetIndex = 0;
|
|
for (const tm of items) {
|
|
if (!(firstBar <= tm.index && tm.index <= lastBar)) {
|
|
continue;
|
|
}
|
|
let label;
|
|
if (targetIndex < this._private__labels.length) {
|
|
label = this._private__labels[targetIndex];
|
|
label.coord = this._internal_indexToCoordinate(tm.index);
|
|
label.label = this._private__formatLabel(tm);
|
|
label.weight = tm.weight;
|
|
}
|
|
else {
|
|
label = {
|
|
needAlignCoordinate: false,
|
|
coord: this._internal_indexToCoordinate(tm.index),
|
|
label: this._private__formatLabel(tm),
|
|
weight: tm.weight,
|
|
};
|
|
this._private__labels.push(label);
|
|
}
|
|
if (this._private__barSpacing > (maxLabelWidth / 2) && !isAllScalingAndScrollingDisabled) {
|
|
// if there is enough space then let's show all tick marks as usual
|
|
label.needAlignCoordinate = false;
|
|
}
|
|
else {
|
|
// if a user is able to scroll after a tick mark then show it as usual, otherwise the coordinate might be aligned
|
|
// if the index is for the second (last) label or later (earlier) then most likely this label might be displayed without correcting the coordinate
|
|
label.needAlignCoordinate = (isLeftEdgeFixed && tm.index <= earliestIndexOfSecondLabel) || (isRightEdgeFixed && tm.index >= indexOfSecondLastLabel);
|
|
}
|
|
targetIndex++;
|
|
}
|
|
this._private__labels.length = targetIndex;
|
|
this._private__timeMarksCache = this._private__labels;
|
|
return this._private__labels;
|
|
}
|
|
_internal_restoreDefault() {
|
|
this._private__visibleRangeInvalidated = true;
|
|
this._internal_setBarSpacing(this._private__options.barSpacing);
|
|
let newOffset;
|
|
if (this._private__options.rightOffsetPixels !== undefined) {
|
|
newOffset = this._private__options.rightOffsetPixels / this._internal_barSpacing();
|
|
}
|
|
else {
|
|
newOffset = this._private__options.rightOffset;
|
|
}
|
|
this._internal_setRightOffset(newOffset);
|
|
}
|
|
_internal_setBaseIndex(baseIndex) {
|
|
this._private__visibleRangeInvalidated = true;
|
|
this._private__baseIndexOrNull = baseIndex;
|
|
this._private__correctOffset();
|
|
this._private__doFixLeftEdge();
|
|
}
|
|
/**
|
|
* Zoom in/out the scale around a `zoomPoint` on `scale` value.
|
|
*
|
|
* @param zoomPoint - X coordinate of the point to apply the zoom.
|
|
* If `rightBarStaysOnScroll` option is disabled, then will be used to restore right offset.
|
|
* @param scale - Zoom value (in 1/10 parts of current bar spacing).
|
|
* Negative value means zoom out, positive - zoom in.
|
|
*/
|
|
_internal_zoom(zoomPoint, scale) {
|
|
const floatIndexAtZoomPoint = this._private__coordinateToFloatIndex(zoomPoint);
|
|
const barSpacing = this._internal_barSpacing();
|
|
const newBarSpacing = barSpacing + scale * (barSpacing / 10);
|
|
// zoom in/out bar spacing
|
|
this._internal_setBarSpacing(newBarSpacing);
|
|
if (!this._private__options.rightBarStaysOnScroll) {
|
|
// and then correct right offset to move index under zoomPoint back to its coordinate
|
|
this._internal_setRightOffset(this._internal_rightOffset() + (floatIndexAtZoomPoint - this._private__coordinateToFloatIndex(zoomPoint)));
|
|
}
|
|
}
|
|
_internal_startScale(x) {
|
|
if (this._private__scrollStartPoint) {
|
|
this._internal_endScroll();
|
|
}
|
|
if (this._private__scaleStartPoint !== null || this._private__commonTransitionStartState !== null) {
|
|
return;
|
|
}
|
|
if (this._internal_isEmpty()) {
|
|
return;
|
|
}
|
|
this._private__scaleStartPoint = x;
|
|
this._private__saveCommonTransitionsStartState();
|
|
}
|
|
_internal_scaleTo(x) {
|
|
if (this._private__commonTransitionStartState === null) {
|
|
return;
|
|
}
|
|
const startLengthFromRight = clamp(this._private__width - x, 0, this._private__width);
|
|
const currentLengthFromRight = clamp(this._private__width - ensureNotNull(this._private__scaleStartPoint), 0, this._private__width);
|
|
if (startLengthFromRight === 0 || currentLengthFromRight === 0) {
|
|
return;
|
|
}
|
|
this._internal_setBarSpacing(this._private__commonTransitionStartState._internal_barSpacing * startLengthFromRight / currentLengthFromRight);
|
|
}
|
|
_internal_endScale() {
|
|
if (this._private__scaleStartPoint === null) {
|
|
return;
|
|
}
|
|
this._private__scaleStartPoint = null;
|
|
this._private__clearCommonTransitionsStartState();
|
|
}
|
|
_internal_startScroll(x) {
|
|
if (this._private__scrollStartPoint !== null || this._private__commonTransitionStartState !== null) {
|
|
return;
|
|
}
|
|
if (this._internal_isEmpty()) {
|
|
return;
|
|
}
|
|
this._private__scrollStartPoint = x;
|
|
this._private__saveCommonTransitionsStartState();
|
|
}
|
|
_internal_scrollTo(x) {
|
|
if (this._private__scrollStartPoint === null) {
|
|
return;
|
|
}
|
|
const shiftInLogical = (this._private__scrollStartPoint - x) / this._internal_barSpacing();
|
|
this._private__rightOffset = ensureNotNull(this._private__commonTransitionStartState)._internal_rightOffset + shiftInLogical;
|
|
this._private__visibleRangeInvalidated = true;
|
|
// do not allow scroll out of visible bars
|
|
this._private__correctOffset();
|
|
}
|
|
_internal_endScroll() {
|
|
if (this._private__scrollStartPoint === null) {
|
|
return;
|
|
}
|
|
this._private__scrollStartPoint = null;
|
|
this._private__clearCommonTransitionsStartState();
|
|
}
|
|
_internal_scrollToRealTime() {
|
|
this._internal_scrollToOffsetAnimated(this._private__options.rightOffset);
|
|
}
|
|
_internal_scrollToOffsetAnimated(offset, animationDuration = 400 /* Constants.DefaultAnimationDuration */) {
|
|
if (!isFinite(offset)) {
|
|
throw new RangeError('offset is required and must be finite number');
|
|
}
|
|
if (!isFinite(animationDuration) || animationDuration <= 0) {
|
|
throw new RangeError('animationDuration (optional) must be finite positive number');
|
|
}
|
|
const source = this._private__rightOffset;
|
|
const animationStart = performance.now();
|
|
this._private__model._internal_setTimeScaleAnimation({
|
|
_internal_finished: (time) => (time - animationStart) / animationDuration >= 1,
|
|
_internal_getPosition: (time) => {
|
|
const animationProgress = (time - animationStart) / animationDuration;
|
|
const finishAnimation = animationProgress >= 1;
|
|
return finishAnimation ? offset : source + (offset - source) * animationProgress;
|
|
},
|
|
});
|
|
}
|
|
_internal_update(newPoints, firstChangedPointIndex) {
|
|
this._private__visibleRangeInvalidated = true;
|
|
this._private__points = newPoints;
|
|
this._private__tickMarks._internal_setTimeScalePoints(newPoints, firstChangedPointIndex);
|
|
this._private__correctOffset();
|
|
}
|
|
_internal_visibleBarsChanged() {
|
|
return this._private__visibleBarsChanged;
|
|
}
|
|
_internal_logicalRangeChanged() {
|
|
return this._private__logicalRangeChanged;
|
|
}
|
|
_internal_optionsApplied() {
|
|
return this._private__optionsApplied;
|
|
}
|
|
_internal_baseIndex() {
|
|
// null is used to known that baseIndex is not set yet
|
|
// so in methods which should known whether it is set or not
|
|
// we should check field `_baseIndexOrNull` instead of getter `baseIndex()`
|
|
// see minRightOffset for example
|
|
return this._private__baseIndexOrNull || 0;
|
|
}
|
|
_internal_setVisibleRange(range, applyDefaultOffset) {
|
|
const length = range._internal_count();
|
|
const pixelOffset = (applyDefaultOffset && this._private__options.rightOffsetPixels) || 0;
|
|
this._private__setBarSpacing((this._private__width - pixelOffset) / length);
|
|
this._private__rightOffset = range._internal_right() - this._internal_baseIndex();
|
|
if (applyDefaultOffset) {
|
|
this._private__rightOffset = pixelOffset
|
|
? pixelOffset / this._internal_barSpacing()
|
|
: this._private__options.rightOffset;
|
|
}
|
|
this._private__correctOffset();
|
|
this._private__visibleRangeInvalidated = true;
|
|
this._private__model._internal_recalculateAllPanes();
|
|
this._private__model._internal_lightUpdate();
|
|
}
|
|
_internal_fitContent() {
|
|
const first = this._private__firstIndex();
|
|
const last = this._private__lastIndex();
|
|
if (first === null || last === null) {
|
|
return;
|
|
}
|
|
// If we are not using rightOffsetPixels then we should include the user defined rightOffset
|
|
// in the Range so that the bar scaling considers the space required for the offset.
|
|
const rightOffsetBars = (!this._private__options.rightOffsetPixels && this._private__options.rightOffset) || 0;
|
|
this._internal_setVisibleRange(new RangeImpl(first, last + rightOffsetBars), true);
|
|
}
|
|
_internal_setLogicalRange(range) {
|
|
const barRange = new RangeImpl(range.from, range.to);
|
|
this._internal_setVisibleRange(barRange);
|
|
}
|
|
_internal_formatDateTime(timeScalePoint) {
|
|
if (this._private__localizationOptions.timeFormatter !== undefined) {
|
|
return this._private__localizationOptions.timeFormatter(timeScalePoint.originalTime);
|
|
}
|
|
return this._private__horzScaleBehavior.formatHorzItem(timeScalePoint.time);
|
|
}
|
|
_internal_recalculateIndicesWithData() {
|
|
if (!this._private__options.ignoreWhitespaceIndices) {
|
|
return;
|
|
}
|
|
this._private__indicesWithData.clear();
|
|
const series = this._private__model._internal_serieses();
|
|
for (const s of series) {
|
|
for (const index of s._internal_fulfilledIndices()) {
|
|
this._private__indicesWithData.set(index, true);
|
|
}
|
|
}
|
|
this._private__indicesWithDataUpdateId++;
|
|
}
|
|
/**
|
|
* Returns the current data conflation factor.
|
|
* Factor \> 1 means data points should be conflated for performance.
|
|
*/
|
|
_internal_conflationFactor() {
|
|
return this._private__conflationFactor;
|
|
}
|
|
/**
|
|
* Provides an array of possible conflations factors based on the current
|
|
* minBarSpacing setting for the chart.
|
|
* @returns Arrays of conflation factors (number of bars to merge)
|
|
*/
|
|
_internal_possibleConflationFactors() {
|
|
const devicePixelRatio = window.devicePixelRatio || 1;
|
|
const conflationThreshold = 1.0 / devicePixelRatio;
|
|
const minBarSpacing = this._private__options.minBarSpacing;
|
|
if (minBarSpacing >= conflationThreshold) {
|
|
return [1];
|
|
}
|
|
// Return all power-of-2 conflation levels that might be used
|
|
const factors = [1];
|
|
let currentLevel = 2;
|
|
const maxLevel = 512;
|
|
while (currentLevel <= maxLevel) {
|
|
const levelThreshold = conflationThreshold / currentLevel;
|
|
if (minBarSpacing < levelThreshold) {
|
|
factors.push(currentLevel);
|
|
}
|
|
currentLevel *= 2;
|
|
}
|
|
return factors;
|
|
}
|
|
_private__isAllScalingAndScrollingDisabled() {
|
|
const handleScroll = this._private__model._internal_options()['handleScroll'];
|
|
const handleScale = this._private__model._internal_options()['handleScale'];
|
|
return !handleScroll.horzTouchDrag
|
|
&& !handleScroll.mouseWheel
|
|
&& !handleScroll.pressedMouseMove
|
|
&& !handleScroll.vertTouchDrag
|
|
&& !handleScale.axisDoubleClickReset.time
|
|
&& !handleScale.axisPressedMouseMove.time
|
|
&& !handleScale.mouseWheel
|
|
&& !handleScale.pinch;
|
|
}
|
|
_private__firstIndex() {
|
|
return this._private__points.length === 0 ? null : 0;
|
|
}
|
|
_private__lastIndex() {
|
|
return this._private__points.length === 0 ? null : (this._private__points.length - 1);
|
|
}
|
|
_private__rightOffsetForCoordinate(x) {
|
|
return (this._private__width - 1 - x) / this._private__barSpacing;
|
|
}
|
|
_private__coordinateToFloatIndex(x) {
|
|
const deltaFromRight = this._private__rightOffsetForCoordinate(x);
|
|
const baseIndex = this._internal_baseIndex();
|
|
const index = baseIndex + this._private__rightOffset - deltaFromRight;
|
|
// JavaScript uses very strange rounding
|
|
// we need rounding to avoid problems with calculation errors
|
|
return Math.round(index * 1000000) / 1000000;
|
|
}
|
|
_private__setBarSpacing(newBarSpacing) {
|
|
const oldBarSpacing = this._private__barSpacing;
|
|
this._private__barSpacing = newBarSpacing;
|
|
this._private__correctBarSpacing();
|
|
// this._barSpacing might be changed in _correctBarSpacing
|
|
if (oldBarSpacing !== this._private__barSpacing) {
|
|
this._private__visibleRangeInvalidated = true;
|
|
this._private__resetTimeMarksCache();
|
|
this._private__updateConflationFactor();
|
|
}
|
|
}
|
|
_private__updateVisibleRange() {
|
|
if (!this._private__visibleRangeInvalidated) {
|
|
return;
|
|
}
|
|
this._private__visibleRangeInvalidated = false;
|
|
if (this._internal_isEmpty()) {
|
|
this._private__setVisibleRange(TimeScaleVisibleRange._internal_invalid());
|
|
return;
|
|
}
|
|
const baseIndex = this._internal_baseIndex();
|
|
const newBarsLength = this._private__width / this._private__barSpacing;
|
|
const rightBorder = this._private__rightOffset + baseIndex;
|
|
const leftBorder = rightBorder - newBarsLength + 1;
|
|
const logicalRange = new RangeImpl(leftBorder, rightBorder);
|
|
this._private__setVisibleRange(new TimeScaleVisibleRange(logicalRange));
|
|
}
|
|
_private__correctBarSpacing() {
|
|
const barSpacing = clamp(this._private__barSpacing, this._private__minBarSpacing(), this._private__maxBarSpacing());
|
|
if (this._private__barSpacing !== barSpacing) {
|
|
this._private__barSpacing = barSpacing;
|
|
this._private__visibleRangeInvalidated = true;
|
|
}
|
|
}
|
|
_private__maxBarSpacing() {
|
|
if (this._private__options.maxBarSpacing > 0) {
|
|
// option takes precedance
|
|
return this._private__options.maxBarSpacing;
|
|
}
|
|
else {
|
|
// half of the width is default value for maximum bar spacing
|
|
return this._private__width * 0.5;
|
|
}
|
|
}
|
|
_private__minBarSpacing() {
|
|
// if both options are enabled then limit bar spacing so that zooming-out is not possible
|
|
// if it would cause either the first or last points to move too far from an edge
|
|
if (this._private__options.fixLeftEdge && this._private__options.fixRightEdge && this._private__points.length !== 0) {
|
|
return this._private__width / this._private__points.length;
|
|
}
|
|
return this._private__options.minBarSpacing;
|
|
}
|
|
/**
|
|
* Updates the conflation factor based on current bar spacing using DPR-aware power-of-2 calculation with optional smoothing factor.
|
|
* The smoothing factor allows intentional over-conflation for smoother appearance in small charts and sparklines.
|
|
*/
|
|
_private__updateConflationFactor() {
|
|
if (!this._private__options.enableConflation) {
|
|
this._private__conflationFactor = 1;
|
|
return;
|
|
}
|
|
// Use DPR-aware threshold calculation with smoothing factor
|
|
const devicePixelRatio = window.devicePixelRatio || 1;
|
|
const smoothingFactor = this._private__options.conflationThresholdFactor ?? 1;
|
|
const adjustedThreshold = (1.0 / devicePixelRatio) * smoothingFactor;
|
|
if (this._private__barSpacing >= adjustedThreshold) {
|
|
this._private__conflationFactor = 1;
|
|
return;
|
|
}
|
|
// Calculate conflation level as power of 2
|
|
const ratio = adjustedThreshold / this._private__barSpacing;
|
|
const conflationLevel = Math.pow(2, Math.floor(Math.log2(ratio)));
|
|
// Ensure we don't exceed maximum conflation level (512)
|
|
this._private__conflationFactor = Math.min(conflationLevel, 512);
|
|
}
|
|
_private__correctOffset() {
|
|
// block scrolling of to past
|
|
const minRightOffset = this._private__minRightOffset();
|
|
if (minRightOffset !== null && this._private__rightOffset < minRightOffset) {
|
|
this._private__rightOffset = minRightOffset;
|
|
this._private__visibleRangeInvalidated = true;
|
|
}
|
|
// block scrolling of to future
|
|
const maxRightOffset = this._private__maxRightOffset();
|
|
if (this._private__rightOffset > maxRightOffset) {
|
|
this._private__rightOffset = maxRightOffset;
|
|
this._private__visibleRangeInvalidated = true;
|
|
}
|
|
}
|
|
_private__minRightOffset() {
|
|
const firstIndex = this._private__firstIndex();
|
|
const baseIndex = this._private__baseIndexOrNull;
|
|
if (firstIndex === null || baseIndex === null) {
|
|
return null;
|
|
}
|
|
const barsEstimation = this._private__options.fixLeftEdge
|
|
? this._private__width / this._private__barSpacing
|
|
: Math.min(2 /* Constants.MinVisibleBarsCount */, this._private__points.length);
|
|
return firstIndex - baseIndex - 1 + barsEstimation;
|
|
}
|
|
_private__maxRightOffset() {
|
|
return this._private__options.fixRightEdge
|
|
? 0
|
|
: (this._private__width / this._private__barSpacing) - Math.min(2 /* Constants.MinVisibleBarsCount */, this._private__points.length);
|
|
}
|
|
_private__saveCommonTransitionsStartState() {
|
|
this._private__commonTransitionStartState = {
|
|
_internal_barSpacing: this._internal_barSpacing(),
|
|
_internal_rightOffset: this._internal_rightOffset(),
|
|
};
|
|
}
|
|
_private__clearCommonTransitionsStartState() {
|
|
this._private__commonTransitionStartState = null;
|
|
}
|
|
_private__formatLabel(tickMark) {
|
|
let formatter = this._private__formattedByWeight.get(tickMark.weight);
|
|
if (formatter === undefined) {
|
|
formatter = new FormattedLabelsCache((mark) => {
|
|
return this._private__formatLabelImpl(mark);
|
|
}, this._private__horzScaleBehavior);
|
|
this._private__formattedByWeight.set(tickMark.weight, formatter);
|
|
}
|
|
return formatter._internal_format(tickMark);
|
|
}
|
|
_private__formatLabelImpl(tickMark) {
|
|
return this._private__horzScaleBehavior.formatTickmark(tickMark, this._private__localizationOptions);
|
|
}
|
|
_private__setVisibleRange(newVisibleRange) {
|
|
const oldVisibleRange = this._private__visibleRange;
|
|
this._private__visibleRange = newVisibleRange;
|
|
if (!areRangesEqual(oldVisibleRange._internal_strictRange(), this._private__visibleRange._internal_strictRange())) {
|
|
this._private__visibleBarsChanged._internal_fire();
|
|
}
|
|
if (!areRangesEqual(oldVisibleRange._internal_logicalRange(), this._private__visibleRange._internal_logicalRange())) {
|
|
this._private__logicalRangeChanged._internal_fire();
|
|
}
|
|
// TODO: reset only coords in case when this._visibleBars has not been changed
|
|
this._private__resetTimeMarksCache();
|
|
}
|
|
_private__resetTimeMarksCache() {
|
|
this._private__timeMarksCache = null;
|
|
}
|
|
_private__invalidateTickMarks() {
|
|
this._private__resetTimeMarksCache();
|
|
this._private__formattedByWeight.clear();
|
|
}
|
|
_private__updateDateTimeFormatter() {
|
|
this._private__horzScaleBehavior.updateFormatter(this._private__localizationOptions);
|
|
}
|
|
_private__doFixLeftEdge() {
|
|
if (!this._private__options.fixLeftEdge) {
|
|
return;
|
|
}
|
|
const firstIndex = this._private__firstIndex();
|
|
if (firstIndex === null) {
|
|
return;
|
|
}
|
|
const visibleRange = this._internal_visibleStrictRange();
|
|
if (visibleRange === null) {
|
|
return;
|
|
}
|
|
const delta = visibleRange._internal_left() - firstIndex;
|
|
if (delta < 0) {
|
|
const leftEdgeOffset = this._private__rightOffset - delta - 1;
|
|
this._internal_setRightOffset(leftEdgeOffset);
|
|
}
|
|
this._private__correctBarSpacing();
|
|
}
|
|
_private__doFixRightEdge() {
|
|
this._private__correctOffset();
|
|
this._private__correctBarSpacing();
|
|
}
|
|
_private__shouldConsiderIndex(index) {
|
|
if (!this._private__options.ignoreWhitespaceIndices) {
|
|
return true;
|
|
}
|
|
return this._private__indicesWithData.get(index) || false;
|
|
}
|
|
_private__findNearestIndexWithData(x) {
|
|
const gen = testNearestIntegers(x);
|
|
const maxIndex = this._private__lastIndex();
|
|
while (maxIndex) {
|
|
const index = gen.next().value;
|
|
if (this._private__indicesWithData.get(index)) {
|
|
return index;
|
|
}
|
|
if (index < 0 || index > maxIndex) {
|
|
break;
|
|
}
|
|
}
|
|
return x; // fallback to original index
|
|
}
|
|
_private__checkRightOffsetPixels(options) {
|
|
if (options.rightOffsetPixels !== undefined) {
|
|
const newOffset = options.rightOffsetPixels / (options.barSpacing || this._private__barSpacing);
|
|
this._private__model._internal_setRightOffset(newOffset);
|
|
}
|
|
}
|
|
}
|
|
function* testNearestIntegers(num) {
|
|
const rounded = Math.round(num);
|
|
const isRoundedDown = rounded < num;
|
|
let offset = 1;
|
|
while (true) {
|
|
if (isRoundedDown) {
|
|
yield rounded + offset;
|
|
yield rounded - offset;
|
|
}
|
|
else {
|
|
yield rounded - offset;
|
|
yield rounded + offset;
|
|
}
|
|
offset++;
|
|
}
|
|
}
|
|
|
|
/// <reference types="_build-time-constants" />
|
|
/**
|
|
* Determine how to exit the tracking mode.
|
|
*
|
|
* By default, mobile users will long press to deactivate the scroll and have the ability to check values and dates.
|
|
* Another press is required to activate the scroll, be able to move left/right, zoom, etc.
|
|
*/
|
|
var TrackingModeExitMode;
|
|
(function (TrackingModeExitMode) {
|
|
/**
|
|
* Tracking Mode will be deactivated on touch end event.
|
|
*/
|
|
TrackingModeExitMode[TrackingModeExitMode["OnTouchEnd"] = 0] = "OnTouchEnd";
|
|
/**
|
|
* Tracking Mode will be deactivated on the next tap event.
|
|
*/
|
|
TrackingModeExitMode[TrackingModeExitMode["OnNextTap"] = 1] = "OnNextTap";
|
|
})(TrackingModeExitMode || (TrackingModeExitMode = {}));
|
|
function isPanePrimitive(source) {
|
|
return source instanceof Pane;
|
|
}
|
|
class ChartModel {
|
|
constructor(invalidateHandler, options, horzScaleBehavior) {
|
|
this._private__panes = [];
|
|
this._private__serieses = [];
|
|
this._private__visibleSerieses = null;
|
|
this._private__width = 0;
|
|
this._private__hoveredSource = null;
|
|
this._private__priceScalesOptionsChanged = new Delegate();
|
|
this._private__crosshairMoved = new Delegate();
|
|
this._private__gradientColorsCache = null;
|
|
this._private__invalidateHandler = invalidateHandler;
|
|
this._private__options = options;
|
|
this._private__horzScaleBehavior = horzScaleBehavior;
|
|
this._private__colorParser = new ColorParser(this._private__options.layout.colorParsers);
|
|
this._private__rendererOptionsProvider = new PriceAxisRendererOptionsProvider(this);
|
|
this._private__timeScale = new TimeScale(this, options.timeScale, this._private__options.localization, horzScaleBehavior);
|
|
this._private__crosshair = new Crosshair(this, options.crosshair);
|
|
this._private__magnet = new Magnet(options.crosshair);
|
|
if (options.addDefaultPane) {
|
|
this._private__getOrCreatePane(0);
|
|
this._private__panes[0]._internal_setStretchFactor(DEFAULT_STRETCH_FACTOR * 2);
|
|
}
|
|
this._private__backgroundTopColor = this._private__getBackgroundColor(0 /* BackgroundColorSide.Top */);
|
|
this._private__backgroundBottomColor = this._private__getBackgroundColor(1 /* BackgroundColorSide.Bottom */);
|
|
}
|
|
_internal_fullUpdate() {
|
|
this._private__invalidate(InvalidateMask._internal_full());
|
|
}
|
|
_internal_lightUpdate() {
|
|
this._private__invalidate(InvalidateMask._internal_light());
|
|
}
|
|
_internal_cursorUpdate() {
|
|
this._private__invalidate(new InvalidateMask(1 /* InvalidationLevel.Cursor */));
|
|
}
|
|
_internal_updateSource(source) {
|
|
const inv = this._private__invalidationMaskForSource(source);
|
|
this._private__invalidate(inv);
|
|
}
|
|
_internal_hoveredSource() {
|
|
return this._private__hoveredSource;
|
|
}
|
|
_internal_setHoveredSource(source) {
|
|
if (this._private__hoveredSource?._internal_source === source?._internal_source &&
|
|
this._private__hoveredSource?._internal_object?._internal_externalId === source?._internal_object?._internal_externalId &&
|
|
this._private__hoveredSource?._internal_object?._internal_hitTestData === source?._internal_object?._internal_hitTestData &&
|
|
this._private__hoveredSource?._internal_cursorStyle === source?._internal_cursorStyle &&
|
|
this._private__hoveredSource?._internal_itemType === source?._internal_itemType) {
|
|
return;
|
|
}
|
|
const prevSource = this._private__hoveredSource;
|
|
this._private__hoveredSource = source;
|
|
if (prevSource !== null) {
|
|
this._internal_updateSource(prevSource._internal_source);
|
|
}
|
|
// additional check to prevent unnecessary updates of same source
|
|
if (source !== null && source._internal_source !== prevSource?._internal_source) {
|
|
this._internal_updateSource(source._internal_source);
|
|
}
|
|
}
|
|
_internal_options() {
|
|
return this._private__options;
|
|
}
|
|
_internal_applyOptions(options) {
|
|
merge(this._private__options, options);
|
|
this._private__panes.forEach((p) => p._internal_applyScaleOptions(options));
|
|
if (options.timeScale !== undefined) {
|
|
this._private__timeScale._internal_applyOptions(options.timeScale);
|
|
}
|
|
if (options.localization !== undefined) {
|
|
this._private__timeScale._internal_applyLocalizationOptions(options.localization);
|
|
}
|
|
if (options.leftPriceScale || options.rightPriceScale) {
|
|
this._private__priceScalesOptionsChanged._internal_fire();
|
|
}
|
|
this._private__backgroundTopColor = this._private__getBackgroundColor(0 /* BackgroundColorSide.Top */);
|
|
this._private__backgroundBottomColor = this._private__getBackgroundColor(1 /* BackgroundColorSide.Bottom */);
|
|
this._internal_fullUpdate();
|
|
}
|
|
_internal_applyPriceScaleOptions(priceScaleId, options, paneIndex = 0) {
|
|
const pane = this._private__panes[paneIndex];
|
|
if (pane === undefined) {
|
|
{
|
|
throw new Error(`Trying to apply price scale options with incorrect pane index: ${paneIndex}`);
|
|
}
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
if (priceScaleId === "left" /* DefaultPriceScaleId.Left */) {
|
|
merge(this._private__options, {
|
|
leftPriceScale: options,
|
|
});
|
|
pane._internal_applyScaleOptions({
|
|
leftPriceScale: options,
|
|
});
|
|
this._private__priceScalesOptionsChanged._internal_fire();
|
|
this._internal_fullUpdate();
|
|
return;
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
}
|
|
else if (priceScaleId === "right" /* DefaultPriceScaleId.Right */) {
|
|
merge(this._private__options, {
|
|
rightPriceScale: options,
|
|
});
|
|
pane._internal_applyScaleOptions({
|
|
rightPriceScale: options,
|
|
});
|
|
this._private__priceScalesOptionsChanged._internal_fire();
|
|
this._internal_fullUpdate();
|
|
return;
|
|
}
|
|
const res = this._internal_findPriceScale(priceScaleId, paneIndex);
|
|
if (res === null) {
|
|
{
|
|
throw new Error(`Trying to apply price scale options with incorrect ID: ${priceScaleId}`);
|
|
}
|
|
}
|
|
res._internal_priceScale._internal_applyOptions(options);
|
|
this._private__priceScalesOptionsChanged._internal_fire();
|
|
}
|
|
_internal_findPriceScale(priceScaleId, paneIndex) {
|
|
const pane = this._private__panes[paneIndex];
|
|
if (pane === undefined) {
|
|
return null;
|
|
}
|
|
const priceScale = pane._internal_priceScaleById(priceScaleId);
|
|
if (priceScale !== null) {
|
|
return {
|
|
_internal_pane: pane,
|
|
_internal_priceScale: priceScale,
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
_internal_timeScale() {
|
|
return this._private__timeScale;
|
|
}
|
|
_internal_panes() {
|
|
return this._private__panes;
|
|
}
|
|
_internal_crosshairSource() {
|
|
return this._private__crosshair;
|
|
}
|
|
_internal_crosshairMoved() {
|
|
return this._private__crosshairMoved;
|
|
}
|
|
_internal_setPaneHeight(pane, height) {
|
|
pane._internal_setHeight(height);
|
|
this._internal_recalculateAllPanes();
|
|
}
|
|
_internal_setWidth(width) {
|
|
this._private__width = width;
|
|
this._private__timeScale._internal_setWidth(this._private__width);
|
|
this._private__panes.forEach((pane) => pane._internal_setWidth(width));
|
|
this._internal_recalculateAllPanes();
|
|
}
|
|
_internal_removePane(index) {
|
|
if (this._private__panes.length === 1) {
|
|
return;
|
|
}
|
|
assert(index >= 0 && index < this._private__panes.length, 'Invalid pane index');
|
|
this._private__panes.splice(index, 1);
|
|
this._internal_fullUpdate();
|
|
}
|
|
_internal_changePanesHeight(paneIndex, height) {
|
|
if (this._private__panes.length < 2) {
|
|
return;
|
|
}
|
|
assert(paneIndex >= 0 && paneIndex < this._private__panes.length, 'Invalid pane index');
|
|
const targetPane = this._private__panes[paneIndex];
|
|
const totalStretch = this._private__panes.reduce((prevValue, pane) => prevValue + pane._internal_stretchFactor(), 0);
|
|
const totalHeight = this._private__panes.reduce((prevValue, pane) => prevValue + pane._internal_height(), 0);
|
|
const maxPaneHeight = totalHeight - MIN_PANE_HEIGHT * (this._private__panes.length - 1);
|
|
height = Math.min(maxPaneHeight, Math.max(MIN_PANE_HEIGHT, height));
|
|
const pixelStretchFactor = totalStretch / totalHeight;
|
|
const oldHeight = targetPane._internal_height();
|
|
targetPane._internal_setStretchFactor(height * pixelStretchFactor);
|
|
let otherPanesChange = height - oldHeight;
|
|
let panesCount = this._private__panes.length - 1;
|
|
for (const pane of this._private__panes) {
|
|
if (pane !== targetPane) {
|
|
const newPaneHeight = Math.min(maxPaneHeight, Math.max(30, pane._internal_height() - otherPanesChange / panesCount));
|
|
otherPanesChange -= (pane._internal_height() - newPaneHeight);
|
|
panesCount -= 1;
|
|
const newStretchFactor = newPaneHeight * pixelStretchFactor;
|
|
pane._internal_setStretchFactor(newStretchFactor);
|
|
}
|
|
}
|
|
this._internal_fullUpdate();
|
|
}
|
|
_internal_swapPanes(first, second) {
|
|
assert(first >= 0 && first < this._private__panes.length && second >= 0 && second < this._private__panes.length, 'Invalid pane index');
|
|
const firstPane = this._private__panes[first];
|
|
const secondPane = this._private__panes[second];
|
|
this._private__panes[first] = secondPane;
|
|
this._private__panes[second] = firstPane;
|
|
this._internal_fullUpdate();
|
|
}
|
|
_internal_movePane(from, to) {
|
|
assert(from >= 0 && from < this._private__panes.length && to >= 0 && to < this._private__panes.length, 'Invalid pane index');
|
|
if (from === to) {
|
|
return;
|
|
}
|
|
const [paneToMove] = this._private__panes.splice(from, 1);
|
|
this._private__panes.splice(to, 0, paneToMove);
|
|
this._internal_fullUpdate();
|
|
}
|
|
_internal_startScalePrice(pane, priceScale, x) {
|
|
pane._internal_startScalePrice(priceScale, x);
|
|
}
|
|
_internal_scalePriceTo(pane, priceScale, x) {
|
|
pane._internal_scalePriceTo(priceScale, x);
|
|
this._internal_updateCrosshair();
|
|
this._private__invalidate(this._private__paneInvalidationMask(pane, 2 /* InvalidationLevel.Light */));
|
|
}
|
|
_internal_endScalePrice(pane, priceScale) {
|
|
pane._internal_endScalePrice(priceScale);
|
|
this._private__invalidate(this._private__paneInvalidationMask(pane, 2 /* InvalidationLevel.Light */));
|
|
}
|
|
_internal_startScrollPrice(pane, priceScale, x) {
|
|
if (priceScale._internal_isAutoScale()) {
|
|
return;
|
|
}
|
|
pane._internal_startScrollPrice(priceScale, x);
|
|
}
|
|
_internal_scrollPriceTo(pane, priceScale, x) {
|
|
if (priceScale._internal_isAutoScale()) {
|
|
return;
|
|
}
|
|
pane._internal_scrollPriceTo(priceScale, x);
|
|
this._internal_updateCrosshair();
|
|
this._private__invalidate(this._private__paneInvalidationMask(pane, 2 /* InvalidationLevel.Light */));
|
|
}
|
|
_internal_endScrollPrice(pane, priceScale) {
|
|
if (priceScale._internal_isAutoScale()) {
|
|
return;
|
|
}
|
|
pane._internal_endScrollPrice(priceScale);
|
|
this._private__invalidate(this._private__paneInvalidationMask(pane, 2 /* InvalidationLevel.Light */));
|
|
}
|
|
_internal_resetPriceScale(pane, priceScale) {
|
|
pane._internal_resetPriceScale(priceScale);
|
|
this._private__invalidate(this._private__paneInvalidationMask(pane, 2 /* InvalidationLevel.Light */));
|
|
}
|
|
_internal_startScaleTime(position) {
|
|
this._private__timeScale._internal_startScale(position);
|
|
}
|
|
/**
|
|
* Zoom in/out the chart (depends on scale value).
|
|
*
|
|
* @param pointX - X coordinate of the point to apply the zoom (the point which should stay on its place)
|
|
* @param scale - Zoom value. Negative value means zoom out, positive - zoom in.
|
|
*/
|
|
_internal_zoomTime(pointX, scale) {
|
|
const timeScale = this._internal_timeScale();
|
|
if (timeScale._internal_isEmpty() || scale === 0) {
|
|
return;
|
|
}
|
|
const timeScaleWidth = timeScale._internal_width();
|
|
pointX = Math.max(1, Math.min(pointX, timeScaleWidth));
|
|
timeScale._internal_zoom(pointX, scale);
|
|
this._internal_recalculateAllPanes();
|
|
}
|
|
_internal_scrollChart(x) {
|
|
this._internal_startScrollTime(0);
|
|
this._internal_scrollTimeTo(x);
|
|
this._internal_endScrollTime();
|
|
}
|
|
_internal_scaleTimeTo(x) {
|
|
this._private__timeScale._internal_scaleTo(x);
|
|
this._internal_recalculateAllPanes();
|
|
}
|
|
_internal_endScaleTime() {
|
|
this._private__timeScale._internal_endScale();
|
|
this._internal_lightUpdate();
|
|
}
|
|
_internal_startScrollTime(x) {
|
|
this._private__timeScale._internal_startScroll(x);
|
|
}
|
|
_internal_scrollTimeTo(x) {
|
|
this._private__timeScale._internal_scrollTo(x);
|
|
this._internal_recalculateAllPanes();
|
|
}
|
|
_internal_endScrollTime() {
|
|
this._private__timeScale._internal_endScroll();
|
|
this._internal_lightUpdate();
|
|
}
|
|
_internal_serieses() {
|
|
return this._private__serieses;
|
|
}
|
|
_internal_visibleSerieses() {
|
|
if (this._private__visibleSerieses === null) {
|
|
this._private__visibleSerieses = this._private__serieses.filter((s) => s._internal_visible());
|
|
}
|
|
return this._private__visibleSerieses;
|
|
}
|
|
_internal_invalidateVisibleSeries() {
|
|
this._private__visibleSerieses = null;
|
|
}
|
|
_internal_setAndSaveCurrentPosition(x, y, event, pane, skipEvent) {
|
|
this._private__crosshair._internal_saveOriginCoord(x, y);
|
|
let price = NaN;
|
|
let index = this._private__timeScale._internal_coordinateToIndex(x, true);
|
|
const visibleBars = this._private__timeScale._internal_visibleStrictRange();
|
|
if (visibleBars !== null) {
|
|
index = Math.min(Math.max(visibleBars._internal_left(), index), visibleBars._internal_right());
|
|
}
|
|
index = this._private__crosshair._internal_snapToVisibleSeriesIfNeeded(index);
|
|
const priceScale = pane._internal_defaultPriceScale();
|
|
const firstValue = priceScale._internal_firstValue();
|
|
if (firstValue !== null) {
|
|
price = priceScale._internal_coordinateToPrice(y, firstValue);
|
|
}
|
|
price = this._private__magnet._internal_align(price, index, pane);
|
|
this._private__crosshair._internal_setPosition(index, price, pane);
|
|
this._internal_cursorUpdate();
|
|
if (!skipEvent) {
|
|
const hitTest = hitTestPane(pane, x, y);
|
|
this._internal_setHoveredSource(hitTest && {
|
|
_internal_source: hitTest._internal_source,
|
|
_internal_object: hitTest._internal_object,
|
|
_internal_cursorStyle: hitTest._internal_cursorStyle || null,
|
|
_internal_itemType: hitTest._internal_itemType,
|
|
});
|
|
this._private__crosshairMoved._internal_fire(this._private__crosshair._internal_appliedIndex(), { x, y }, event);
|
|
}
|
|
}
|
|
// A position provided external (not from an internal event listener)
|
|
_internal_setAndSaveSyntheticPosition(price, horizontalPosition, pane) {
|
|
const priceScale = pane._internal_defaultPriceScale();
|
|
const firstValue = priceScale._internal_firstValue();
|
|
const y = priceScale._internal_priceToCoordinate(price, ensureNotNull(firstValue));
|
|
const index = this._private__timeScale._internal_timeToIndex(horizontalPosition, true);
|
|
const x = this._private__timeScale._internal_indexToCoordinate(ensureNotNull(index));
|
|
this._internal_setAndSaveCurrentPosition(x, y, null, pane, true);
|
|
}
|
|
_internal_clearCurrentPosition(skipEvent) {
|
|
const crosshair = this._internal_crosshairSource();
|
|
crosshair._internal_clearPosition();
|
|
this._internal_cursorUpdate();
|
|
if (!skipEvent) {
|
|
this._private__crosshairMoved._internal_fire(null, null, null);
|
|
}
|
|
}
|
|
_internal_updateCrosshair() {
|
|
// apply magnet
|
|
const pane = this._private__crosshair._internal_pane();
|
|
if (pane !== null) {
|
|
const x = this._private__crosshair._internal_originCoordX();
|
|
const y = this._private__crosshair._internal_originCoordY();
|
|
this._internal_setAndSaveCurrentPosition(x, y, null, pane);
|
|
}
|
|
this._private__crosshair._internal_updateAllViews();
|
|
}
|
|
_internal_updateTimeScale(newBaseIndex, newPoints, firstChangedPointIndex) {
|
|
const oldFirstTime = this._private__timeScale._internal_indexToTime(0);
|
|
if (newPoints !== undefined && firstChangedPointIndex !== undefined) {
|
|
this._private__timeScale._internal_update(newPoints, firstChangedPointIndex);
|
|
}
|
|
const newFirstTime = this._private__timeScale._internal_indexToTime(0);
|
|
const currentBaseIndex = this._private__timeScale._internal_baseIndex();
|
|
const visibleBars = this._private__timeScale._internal_visibleStrictRange();
|
|
// if time scale cannot return current visible bars range (e.g. time scale has zero-width)
|
|
// then we do not need to update right offset to shift visible bars range to have the same right offset as we have before new bar
|
|
// (and actually we cannot)
|
|
if (visibleBars !== null && oldFirstTime !== null && newFirstTime !== null) {
|
|
const isLastSeriesBarVisible = visibleBars._internal_contains(currentBaseIndex);
|
|
const isLeftBarShiftToLeft = this._private__horzScaleBehavior.key(oldFirstTime) > this._private__horzScaleBehavior.key(newFirstTime);
|
|
const isSeriesPointsAdded = newBaseIndex !== null && newBaseIndex > currentBaseIndex;
|
|
const isSeriesPointsAddedToRight = isSeriesPointsAdded && !isLeftBarShiftToLeft;
|
|
const allowShiftWhenReplacingWhitespace = this._private__timeScale._internal_options().allowShiftVisibleRangeOnWhitespaceReplacement;
|
|
const replacedExistingWhitespace = firstChangedPointIndex === undefined;
|
|
const needShiftVisibleRangeOnNewBar = isLastSeriesBarVisible && (!replacedExistingWhitespace || allowShiftWhenReplacingWhitespace) && this._private__timeScale._internal_options().shiftVisibleRangeOnNewBar;
|
|
if (isSeriesPointsAddedToRight && !needShiftVisibleRangeOnNewBar) {
|
|
const compensationShift = newBaseIndex - currentBaseIndex;
|
|
this._private__timeScale._internal_setRightOffset(this._private__timeScale._internal_rightOffset() - compensationShift);
|
|
}
|
|
}
|
|
this._private__timeScale._internal_setBaseIndex(newBaseIndex);
|
|
}
|
|
_internal_recalculatePane(pane) {
|
|
if (pane !== null) {
|
|
pane._internal_recalculate();
|
|
}
|
|
}
|
|
_internal_paneForSource(source) {
|
|
if (isPanePrimitive(source)) {
|
|
return source;
|
|
}
|
|
const pane = this._private__panes.find((p) => p._internal_orderedSources().includes(source));
|
|
return pane === undefined ? null : pane;
|
|
}
|
|
_internal_recalculateAllPanes() {
|
|
this._private__panes.forEach((p) => p._internal_recalculate());
|
|
this._internal_updateCrosshair();
|
|
}
|
|
_internal_destroy() {
|
|
this._private__panes.forEach((p) => p._internal_destroy());
|
|
this._private__panes.length = 0;
|
|
// to avoid memleaks
|
|
this._private__options.localization.priceFormatter = undefined;
|
|
this._private__options.localization.percentageFormatter = undefined;
|
|
this._private__options.localization.timeFormatter = undefined;
|
|
}
|
|
_internal_rendererOptionsProvider() {
|
|
return this._private__rendererOptionsProvider;
|
|
}
|
|
_internal_priceAxisRendererOptions() {
|
|
return this._private__rendererOptionsProvider._internal_options();
|
|
}
|
|
_internal_priceScalesOptionsChanged() {
|
|
return this._private__priceScalesOptionsChanged;
|
|
}
|
|
_internal_addSeriesToPane(series, paneIndex) {
|
|
const pane = this._private__getOrCreatePane(paneIndex);
|
|
this._private__addSeriesToPane(series, pane);
|
|
this._private__serieses.push(series);
|
|
this._internal_invalidateVisibleSeries();
|
|
if (this._private__serieses.length === 1) {
|
|
// call fullUpdate to recalculate chart's parts geometry
|
|
this._internal_fullUpdate();
|
|
}
|
|
else {
|
|
this._internal_lightUpdate();
|
|
}
|
|
}
|
|
_internal_removeSeries(series) {
|
|
const pane = this._internal_paneForSource(series);
|
|
const seriesIndex = this._private__serieses.indexOf(series);
|
|
assert(seriesIndex !== -1, 'Series not found');
|
|
const paneImpl = ensureNotNull(pane);
|
|
this._private__serieses.splice(seriesIndex, 1);
|
|
paneImpl._internal_removeDataSource(series);
|
|
if (series._internal_destroy) {
|
|
series._internal_destroy();
|
|
}
|
|
this._internal_invalidateVisibleSeries();
|
|
this._private__timeScale._internal_recalculateIndicesWithData();
|
|
this._private__cleanupIfPaneIsEmpty(paneImpl);
|
|
}
|
|
_internal_moveSeriesToScale(series, targetScaleId) {
|
|
const pane = ensureNotNull(this._internal_paneForSource(series));
|
|
pane._internal_removeDataSource(series, true);
|
|
pane._internal_addDataSource(series, targetScaleId, true);
|
|
}
|
|
_internal_fitContent() {
|
|
const mask = InvalidateMask._internal_light();
|
|
mask._internal_setFitContent();
|
|
this._private__invalidate(mask);
|
|
}
|
|
_internal_setTargetLogicalRange(range) {
|
|
const mask = InvalidateMask._internal_light();
|
|
mask._internal_applyRange(range);
|
|
this._private__invalidate(mask);
|
|
}
|
|
_internal_resetTimeScale() {
|
|
const mask = InvalidateMask._internal_light();
|
|
mask._internal_resetTimeScale();
|
|
this._private__invalidate(mask);
|
|
}
|
|
_internal_setBarSpacing(spacing) {
|
|
const mask = InvalidateMask._internal_light();
|
|
mask._internal_setBarSpacing(spacing);
|
|
this._private__invalidate(mask);
|
|
}
|
|
_internal_setRightOffset(offset) {
|
|
const mask = InvalidateMask._internal_light();
|
|
mask._internal_setRightOffset(offset);
|
|
this._private__invalidate(mask);
|
|
}
|
|
_internal_setTimeScaleAnimation(animation) {
|
|
const mask = InvalidateMask._internal_light();
|
|
mask._internal_setTimeScaleAnimation(animation);
|
|
this._private__invalidate(mask);
|
|
}
|
|
_internal_stopTimeScaleAnimation() {
|
|
const mask = InvalidateMask._internal_light();
|
|
mask._internal_stopTimeScaleAnimation();
|
|
this._private__invalidate(mask);
|
|
}
|
|
_internal_defaultVisiblePriceScaleId() {
|
|
const preferredPriceScaleId = this._private__options.defaultVisiblePriceScaleId;
|
|
const leftVisible = this._private__options.leftPriceScale.visible;
|
|
const rightVisible = this._private__options.rightPriceScale.visible;
|
|
if (leftVisible !== rightVisible) {
|
|
return leftVisible ? "left" /* DefaultPriceScaleId.Left */ : "right" /* DefaultPriceScaleId.Right */;
|
|
}
|
|
return preferredPriceScaleId;
|
|
}
|
|
_internal_moveSeriesToPane(series, newPaneIndex) {
|
|
assert(newPaneIndex >= 0, 'Index should be greater or equal to 0');
|
|
const fromPaneIndex = this._private__seriesPaneIndex(series);
|
|
if (newPaneIndex === fromPaneIndex) {
|
|
return;
|
|
}
|
|
const previousPane = ensureNotNull(this._internal_paneForSource(series));
|
|
previousPane._internal_removeDataSource(series);
|
|
const newPane = this._private__getOrCreatePane(newPaneIndex);
|
|
this._private__addSeriesToPane(series, newPane);
|
|
let paneWasRemoved = false;
|
|
if (previousPane._internal_dataSources().length === 0) {
|
|
paneWasRemoved = this._private__cleanupIfPaneIsEmpty(previousPane);
|
|
}
|
|
if (!paneWasRemoved) {
|
|
this._internal_fullUpdate();
|
|
}
|
|
}
|
|
_internal_backgroundBottomColor() {
|
|
return this._private__backgroundBottomColor;
|
|
}
|
|
_internal_backgroundTopColor() {
|
|
return this._private__backgroundTopColor;
|
|
}
|
|
_internal_backgroundColorAtYPercentFromTop(percent) {
|
|
const bottomColor = this._private__backgroundBottomColor;
|
|
const topColor = this._private__backgroundTopColor;
|
|
if (bottomColor === topColor) {
|
|
// solid background
|
|
return bottomColor;
|
|
}
|
|
// gradient background
|
|
// percent should be from 0 to 100 (we're using only integer values to make cache more efficient)
|
|
percent = Math.max(0, Math.min(100, Math.round(percent * 100)));
|
|
if (this._private__gradientColorsCache === null ||
|
|
this._private__gradientColorsCache._internal_topColor !== topColor || this._private__gradientColorsCache._internal_bottomColor !== bottomColor) {
|
|
this._private__gradientColorsCache = {
|
|
_internal_topColor: topColor,
|
|
_internal_bottomColor: bottomColor,
|
|
_internal_colors: new Map(),
|
|
};
|
|
}
|
|
else {
|
|
const cachedValue = this._private__gradientColorsCache._internal_colors.get(percent);
|
|
if (cachedValue !== undefined) {
|
|
return cachedValue;
|
|
}
|
|
}
|
|
const result = this._private__colorParser._internal_gradientColorAtPercent(topColor, bottomColor, percent / 100);
|
|
this._private__gradientColorsCache._internal_colors.set(percent, result);
|
|
return result;
|
|
}
|
|
_internal_getPaneIndex(pane) {
|
|
return this._private__panes.indexOf(pane);
|
|
}
|
|
_internal_colorParser() {
|
|
return this._private__colorParser;
|
|
}
|
|
_internal_addPane() {
|
|
return this._private__addPane();
|
|
}
|
|
_private__addPane(index) {
|
|
const pane = new Pane(this._private__timeScale, this);
|
|
this._private__panes.push(pane);
|
|
const idx = index ?? this._private__panes.length - 1;
|
|
// we always do autoscaling on the creation
|
|
// if autoscale option is true, it is ok, just recalculate by invalidation mask
|
|
// if autoscale option is false, autoscale anyway on the first draw
|
|
// also there is a scenario when autoscale is true in constructor and false later on applyOptions
|
|
const mask = InvalidateMask._internal_full();
|
|
mask._internal_invalidatePane(idx, {
|
|
_internal_level: 0 /* InvalidationLevel.None */,
|
|
_internal_autoScale: true,
|
|
});
|
|
this._private__invalidate(mask);
|
|
return pane;
|
|
}
|
|
_private__getOrCreatePane(index) {
|
|
assert(index >= 0, 'Index should be greater or equal to 0');
|
|
index = Math.min(this._private__panes.length, index);
|
|
if (index < this._private__panes.length) {
|
|
return this._private__panes[index];
|
|
}
|
|
return this._private__addPane(index);
|
|
}
|
|
_private__seriesPaneIndex(series) {
|
|
return this._private__panes.findIndex((pane) => pane._internal_series().includes(series));
|
|
}
|
|
_private__paneInvalidationMask(pane, level) {
|
|
const inv = new InvalidateMask(level);
|
|
if (pane !== null) {
|
|
const index = this._private__panes.indexOf(pane);
|
|
inv._internal_invalidatePane(index, {
|
|
_internal_level: level,
|
|
});
|
|
}
|
|
return inv;
|
|
}
|
|
_private__invalidationMaskForSource(source, invalidateType) {
|
|
if (invalidateType === undefined) {
|
|
invalidateType = 2 /* InvalidationLevel.Light */;
|
|
}
|
|
return this._private__paneInvalidationMask(this._internal_paneForSource(source), invalidateType);
|
|
}
|
|
_private__invalidate(mask) {
|
|
if (this._private__invalidateHandler) {
|
|
this._private__invalidateHandler(mask);
|
|
}
|
|
this._private__panes.forEach((pane) => pane._internal_grid()._internal_paneView()._internal_update());
|
|
}
|
|
_private__addSeriesToPane(series, pane) {
|
|
const priceScaleId = series._internal_options().priceScaleId;
|
|
const targetScaleId = priceScaleId !== undefined ? priceScaleId : this._internal_defaultVisiblePriceScaleId();
|
|
pane._internal_addDataSource(series, targetScaleId);
|
|
if (!isDefaultPriceScale(targetScaleId)) {
|
|
// let's apply that options again to apply margins
|
|
series._internal_applyOptions(series._internal_options());
|
|
}
|
|
}
|
|
_private__getBackgroundColor(side) {
|
|
const layoutOptions = this._private__options['layout'];
|
|
if (layoutOptions.background.type === "gradient" /* ColorType.VerticalGradient */) {
|
|
return side === 0 /* BackgroundColorSide.Top */ ?
|
|
layoutOptions.background.topColor :
|
|
layoutOptions.background.bottomColor;
|
|
}
|
|
return layoutOptions.background.color;
|
|
}
|
|
_private__cleanupIfPaneIsEmpty(pane) {
|
|
if (!pane._internal_preserveEmptyPane() && (pane._internal_dataSources().length === 0 && this._private__panes.length > 1)) {
|
|
this._private__panes.splice(this._internal_getPaneIndex(pane), 1);
|
|
this._internal_fullUpdate();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function fillUpDownCandlesticksColors(options) {
|
|
if (options.borderColor !== undefined) {
|
|
options.borderUpColor = options.borderColor;
|
|
options.borderDownColor = options.borderColor;
|
|
}
|
|
if (options.wickColor !== undefined) {
|
|
options.wickUpColor = options.wickColor;
|
|
options.wickDownColor = options.wickColor;
|
|
}
|
|
}
|
|
/**
|
|
* Represents the type of the last price animation for series such as area or line.
|
|
*/
|
|
var LastPriceAnimationMode;
|
|
(function (LastPriceAnimationMode) {
|
|
/**
|
|
* Animation is always disabled
|
|
*/
|
|
LastPriceAnimationMode[LastPriceAnimationMode["Disabled"] = 0] = "Disabled";
|
|
/**
|
|
* Animation is always enabled.
|
|
*/
|
|
LastPriceAnimationMode[LastPriceAnimationMode["Continuous"] = 1] = "Continuous";
|
|
/**
|
|
* Animation is active after new data.
|
|
*/
|
|
LastPriceAnimationMode[LastPriceAnimationMode["OnDataUpdate"] = 2] = "OnDataUpdate";
|
|
})(LastPriceAnimationMode || (LastPriceAnimationMode = {}));
|
|
function precisionByMinMove(minMove) {
|
|
if (minMove >= 1) {
|
|
return 0;
|
|
}
|
|
let i = 0;
|
|
for (; i < 8; i++) {
|
|
const intPart = Math.round(minMove);
|
|
const fractPart = Math.abs(intPart - minMove);
|
|
if (fractPart < 1e-8) {
|
|
return i;
|
|
}
|
|
minMove = minMove * 10;
|
|
}
|
|
return i;
|
|
}
|
|
/**
|
|
* Represents the source of data to be used for the horizontal price line.
|
|
*/
|
|
var PriceLineSource;
|
|
(function (PriceLineSource) {
|
|
/**
|
|
* Use the last bar data.
|
|
*/
|
|
PriceLineSource[PriceLineSource["LastBar"] = 0] = "LastBar";
|
|
/**
|
|
* Use the last visible data of the chart viewport.
|
|
*/
|
|
PriceLineSource[PriceLineSource["LastVisible"] = 1] = "LastVisible";
|
|
})(PriceLineSource || (PriceLineSource = {}));
|
|
|
|
/**
|
|
* Represents a type of color.
|
|
*/
|
|
var ColorType;
|
|
(function (ColorType) {
|
|
/** Solid color */
|
|
ColorType["Solid"] = "solid";
|
|
/** Vertical gradient color */
|
|
ColorType["VerticalGradient"] = "gradient";
|
|
})(ColorType || (ColorType = {}));
|
|
|
|
/**
|
|
* Check if a time value is a business day object.
|
|
*
|
|
* @param time - The time to check.
|
|
* @returns `true` if `time` is a {@link BusinessDay} object, false otherwise.
|
|
*/
|
|
function isBusinessDay(time) {
|
|
return !isNumber(time) && !isString(time);
|
|
}
|
|
/**
|
|
* Check if a time value is a UTC timestamp number.
|
|
*
|
|
* @param time - The time to check.
|
|
* @returns `true` if `time` is a {@link UTCTimestamp} number, false otherwise.
|
|
*/
|
|
function isUTCTimestamp(time) {
|
|
return isNumber(time);
|
|
}
|
|
/**
|
|
* Represents the type of a tick mark on the time axis.
|
|
*/
|
|
var TickMarkType;
|
|
(function (TickMarkType) {
|
|
/**
|
|
* The start of the year (e.g. it's the first tick mark in a year).
|
|
*/
|
|
TickMarkType[TickMarkType["Year"] = 0] = "Year";
|
|
/**
|
|
* The start of the month (e.g. it's the first tick mark in a month).
|
|
*/
|
|
TickMarkType[TickMarkType["Month"] = 1] = "Month";
|
|
/**
|
|
* A day of the month.
|
|
*/
|
|
TickMarkType[TickMarkType["DayOfMonth"] = 2] = "DayOfMonth";
|
|
/**
|
|
* A time without seconds.
|
|
*/
|
|
TickMarkType[TickMarkType["Time"] = 3] = "Time";
|
|
/**
|
|
* A time with seconds.
|
|
*/
|
|
TickMarkType[TickMarkType["TimeWithSeconds"] = 4] = "TimeWithSeconds";
|
|
})(TickMarkType || (TickMarkType = {}));
|
|
|
|
const getMonth = (date) => date.getUTCMonth() + 1;
|
|
const getDay = (date) => date.getUTCDate();
|
|
const getYear = (date) => date.getUTCFullYear();
|
|
const dd = (date) => numberToStringWithLeadingZero(getDay(date), 2);
|
|
const MMMM = (date, locale) => new Date(date.getUTCFullYear(), date.getUTCMonth(), 1)
|
|
.toLocaleString(locale, { month: 'long' });
|
|
const MMM = (date, locale) => new Date(date.getUTCFullYear(), date.getUTCMonth(), 1)
|
|
.toLocaleString(locale, { month: 'short' });
|
|
const MM = (date) => numberToStringWithLeadingZero(getMonth(date), 2);
|
|
const yy = (date) => numberToStringWithLeadingZero(getYear(date) % 100, 2);
|
|
const yyyy = (date) => numberToStringWithLeadingZero(getYear(date), 4);
|
|
function formatDate(date, format, locale) {
|
|
return format
|
|
.replace(/yyyy/g, yyyy(date))
|
|
.replace(/yy/g, yy(date))
|
|
.replace(/MMMM/g, MMMM(date, locale))
|
|
.replace(/MMM/g, MMM(date, locale))
|
|
.replace(/MM/g, MM(date))
|
|
.replace(/dd/g, dd(date));
|
|
}
|
|
|
|
class DateFormatter {
|
|
constructor(dateFormat = 'yyyy-MM-dd', locale = 'default') {
|
|
this._private__dateFormat = dateFormat;
|
|
this._private__locale = locale;
|
|
}
|
|
_internal_format(date) {
|
|
return formatDate(date, this._private__dateFormat, this._private__locale);
|
|
}
|
|
}
|
|
|
|
class TimeFormatter {
|
|
constructor(format) {
|
|
this._private__formatStr = format || '%h:%m:%s';
|
|
}
|
|
_internal_format(date) {
|
|
return this._private__formatStr.replace('%h', numberToStringWithLeadingZero(date.getUTCHours(), 2)).
|
|
replace('%m', numberToStringWithLeadingZero(date.getUTCMinutes(), 2)).
|
|
replace('%s', numberToStringWithLeadingZero(date.getUTCSeconds(), 2));
|
|
}
|
|
}
|
|
|
|
const defaultParams = {
|
|
_internal_dateFormat: 'yyyy-MM-dd',
|
|
_internal_timeFormat: '%h:%m:%s',
|
|
_internal_dateTimeSeparator: ' ',
|
|
_internal_locale: 'default',
|
|
};
|
|
class DateTimeFormatter {
|
|
constructor(params = {}) {
|
|
const formatterParams = { ...defaultParams, ...params };
|
|
this._private__dateFormatter = new DateFormatter(formatterParams._internal_dateFormat, formatterParams._internal_locale);
|
|
this._private__timeFormatter = new TimeFormatter(formatterParams._internal_timeFormat);
|
|
this._private__separator = formatterParams._internal_dateTimeSeparator;
|
|
}
|
|
_internal_format(dateTime) {
|
|
return `${this._private__dateFormatter._internal_format(dateTime)}${this._private__separator}${this._private__timeFormatter._internal_format(dateTime)}`;
|
|
}
|
|
}
|
|
|
|
function defaultTickMarkFormatter(timePoint, tickMarkType, locale) {
|
|
const formatOptions = {};
|
|
switch (tickMarkType) {
|
|
case 0 /* TickMarkType.Year */:
|
|
formatOptions.year = 'numeric';
|
|
break;
|
|
case 1 /* TickMarkType.Month */:
|
|
formatOptions.month = 'short';
|
|
break;
|
|
case 2 /* TickMarkType.DayOfMonth */:
|
|
formatOptions.day = 'numeric';
|
|
break;
|
|
case 3 /* TickMarkType.Time */:
|
|
formatOptions.hour12 = false;
|
|
formatOptions.hour = '2-digit';
|
|
formatOptions.minute = '2-digit';
|
|
break;
|
|
case 4 /* TickMarkType.TimeWithSeconds */:
|
|
formatOptions.hour12 = false;
|
|
formatOptions.hour = '2-digit';
|
|
formatOptions.minute = '2-digit';
|
|
formatOptions.second = '2-digit';
|
|
break;
|
|
}
|
|
const date = timePoint._internal_businessDay === undefined
|
|
? new Date(timePoint._internal_timestamp * 1000)
|
|
: new Date(Date.UTC(timePoint._internal_businessDay.year, timePoint._internal_businessDay.month - 1, timePoint._internal_businessDay.day));
|
|
// from given date we should use only as UTC date or timestamp
|
|
// but to format as locale date we can convert UTC date to local date
|
|
const localDateFromUtc = new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds(), date.getUTCMilliseconds());
|
|
return localDateFromUtc.toLocaleString(locale, formatOptions);
|
|
}
|
|
|
|
function hours(count) {
|
|
return count * 60 * 60 * 1000;
|
|
}
|
|
function minutes(count) {
|
|
return count * 60 * 1000;
|
|
}
|
|
function seconds(count) {
|
|
return count * 1000;
|
|
}
|
|
const intradayWeightDivisors = [
|
|
{ _internal_divisor: seconds(1), _internal_weight: 10 /* TickMarkWeight.Second */ },
|
|
{ _internal_divisor: minutes(1), _internal_weight: 20 /* TickMarkWeight.Minute1 */ },
|
|
{ _internal_divisor: minutes(5), _internal_weight: 21 /* TickMarkWeight.Minute5 */ },
|
|
{ _internal_divisor: minutes(30), _internal_weight: 22 /* TickMarkWeight.Minute30 */ },
|
|
{ _internal_divisor: hours(1), _internal_weight: 30 /* TickMarkWeight.Hour1 */ },
|
|
{ _internal_divisor: hours(3), _internal_weight: 31 /* TickMarkWeight.Hour3 */ },
|
|
{ _internal_divisor: hours(6), _internal_weight: 32 /* TickMarkWeight.Hour6 */ },
|
|
{ _internal_divisor: hours(12), _internal_weight: 33 /* TickMarkWeight.Hour12 */ },
|
|
];
|
|
function weightByTime(currentDate, prevDate) {
|
|
if (currentDate.getUTCFullYear() !== prevDate.getUTCFullYear()) {
|
|
return 70 /* TickMarkWeight.Year */;
|
|
}
|
|
else if (currentDate.getUTCMonth() !== prevDate.getUTCMonth()) {
|
|
return 60 /* TickMarkWeight.Month */;
|
|
}
|
|
else if (currentDate.getUTCDate() !== prevDate.getUTCDate()) {
|
|
return 50 /* TickMarkWeight.Day */;
|
|
}
|
|
for (let i = intradayWeightDivisors.length - 1; i >= 0; --i) {
|
|
if (Math.floor(prevDate.getTime() / intradayWeightDivisors[i]._internal_divisor) !== Math.floor(currentDate.getTime() / intradayWeightDivisors[i]._internal_divisor)) {
|
|
return intradayWeightDivisors[i]._internal_weight;
|
|
}
|
|
}
|
|
return 0 /* TickMarkWeight.LessThanSecond */;
|
|
}
|
|
function cast(t) {
|
|
return t;
|
|
}
|
|
function fillWeightsForPoints(sortedTimePoints, startIndex = 0) {
|
|
if (sortedTimePoints.length === 0) {
|
|
return;
|
|
}
|
|
let prevTime = startIndex === 0 ? null : cast(sortedTimePoints[startIndex - 1].time)._internal_timestamp;
|
|
let prevDate = prevTime !== null ? new Date(prevTime * 1000) : null;
|
|
let totalTimeDiff = 0;
|
|
for (let index = startIndex; index < sortedTimePoints.length; ++index) {
|
|
const currentPoint = sortedTimePoints[index];
|
|
const currentDate = new Date(cast(currentPoint.time)._internal_timestamp * 1000);
|
|
if (prevDate !== null) {
|
|
currentPoint.timeWeight = weightByTime(currentDate, prevDate);
|
|
}
|
|
totalTimeDiff += cast(currentPoint.time)._internal_timestamp - (prevTime || cast(currentPoint.time)._internal_timestamp);
|
|
prevTime = cast(currentPoint.time)._internal_timestamp;
|
|
prevDate = currentDate;
|
|
}
|
|
if (startIndex === 0 && sortedTimePoints.length > 1) {
|
|
// let's guess a weight for the first point
|
|
// let's say the previous point was average time back in the history
|
|
const averageTimeDiff = Math.ceil(totalTimeDiff / (sortedTimePoints.length - 1));
|
|
const approxPrevDate = new Date((cast(sortedTimePoints[0].time)._internal_timestamp - averageTimeDiff) * 1000);
|
|
sortedTimePoints[0].timeWeight = weightByTime(new Date(cast(sortedTimePoints[0].time)._internal_timestamp * 1000), approxPrevDate);
|
|
}
|
|
}
|
|
|
|
function businessDayConverter(time) {
|
|
let businessDay = time;
|
|
if (isString(time)) {
|
|
businessDay = stringToBusinessDay(time);
|
|
}
|
|
if (!isBusinessDay(businessDay)) {
|
|
throw new Error('time must be of type BusinessDay');
|
|
}
|
|
const date = new Date(Date.UTC(businessDay.year, businessDay.month - 1, businessDay.day, 0, 0, 0, 0));
|
|
return {
|
|
_internal_timestamp: Math.round(date.getTime() / 1000),
|
|
_internal_businessDay: businessDay,
|
|
};
|
|
}
|
|
function timestampConverter(time) {
|
|
if (!isUTCTimestamp(time)) {
|
|
throw new Error('time must be of type isUTCTimestamp');
|
|
}
|
|
return {
|
|
_internal_timestamp: time,
|
|
};
|
|
}
|
|
function selectTimeConverter(data) {
|
|
if (data.length === 0) {
|
|
return null;
|
|
}
|
|
if (isBusinessDay(data[0].time) || isString(data[0].time)) {
|
|
return businessDayConverter;
|
|
}
|
|
return timestampConverter;
|
|
}
|
|
const validDateRegex = /^\d\d\d\d-\d\d-\d\d$/;
|
|
function convertTime(time) {
|
|
if (isUTCTimestamp(time)) {
|
|
return timestampConverter(time);
|
|
}
|
|
if (!isBusinessDay(time)) {
|
|
return businessDayConverter(stringToBusinessDay(time));
|
|
}
|
|
return businessDayConverter(time);
|
|
}
|
|
function stringToBusinessDay(value) {
|
|
{
|
|
// in some browsers (I look at your Chrome) the Date constructor may accept invalid date string
|
|
// but parses them in 'implementation specific' way
|
|
// for example 2019-1-1 isn't the same as 2019-01-01 (for Chrome both are 'valid' date strings)
|
|
// see https://bugs.chromium.org/p/chromium/issues/detail?id=968939
|
|
// so, we need to be sure that date has valid format to avoid strange behavior and hours of debugging
|
|
// but let's do this in development build only because of perf
|
|
if (!validDateRegex.test(value)) {
|
|
throw new Error(`Invalid date string=${value}, expected format=yyyy-mm-dd`);
|
|
}
|
|
}
|
|
const d = new Date(value);
|
|
if (isNaN(d.getTime())) {
|
|
throw new Error(`Invalid date string=${value}, expected format=yyyy-mm-dd`);
|
|
}
|
|
return {
|
|
day: d.getUTCDate(),
|
|
month: d.getUTCMonth() + 1,
|
|
year: d.getUTCFullYear(),
|
|
};
|
|
}
|
|
function convertStringToBusinessDay(value) {
|
|
if (isString(value.time)) {
|
|
value.time = stringToBusinessDay(value.time);
|
|
}
|
|
}
|
|
function convertStringsToBusinessDays(data) {
|
|
return data.forEach(convertStringToBusinessDay);
|
|
}
|
|
|
|
// eslint-disable-next-line complexity
|
|
function weightToTickMarkType(weight, timeVisible, secondsVisible) {
|
|
switch (weight) {
|
|
case 0 /* TickMarkWeight.LessThanSecond */:
|
|
case 10 /* TickMarkWeight.Second */:
|
|
return timeVisible
|
|
? (secondsVisible ? 4 /* TickMarkType.TimeWithSeconds */ : 3 /* TickMarkType.Time */)
|
|
: 2 /* TickMarkType.DayOfMonth */;
|
|
case 20 /* TickMarkWeight.Minute1 */:
|
|
case 21 /* TickMarkWeight.Minute5 */:
|
|
case 22 /* TickMarkWeight.Minute30 */:
|
|
case 30 /* TickMarkWeight.Hour1 */:
|
|
case 31 /* TickMarkWeight.Hour3 */:
|
|
case 32 /* TickMarkWeight.Hour6 */:
|
|
case 33 /* TickMarkWeight.Hour12 */:
|
|
return timeVisible ? 3 /* TickMarkType.Time */ : 2 /* TickMarkType.DayOfMonth */;
|
|
case 50 /* TickMarkWeight.Day */:
|
|
return 2 /* TickMarkType.DayOfMonth */;
|
|
case 60 /* TickMarkWeight.Month */:
|
|
return 1 /* TickMarkType.Month */;
|
|
case 70 /* TickMarkWeight.Year */:
|
|
return 0 /* TickMarkType.Year */;
|
|
}
|
|
}
|
|
class HorzScaleBehaviorTime {
|
|
options() {
|
|
return this._private__options;
|
|
}
|
|
setOptions(options) {
|
|
this._private__options = options;
|
|
this.updateFormatter(options.localization);
|
|
}
|
|
preprocessData(data) {
|
|
if (Array.isArray(data)) {
|
|
convertStringsToBusinessDays(data);
|
|
}
|
|
else {
|
|
convertStringToBusinessDay(data);
|
|
}
|
|
}
|
|
createConverterToInternalObj(data) {
|
|
return ensureNotNull(selectTimeConverter(data));
|
|
}
|
|
key(item) {
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
if (typeof item === 'object' && "_internal_timestamp" in item) {
|
|
return item._internal_timestamp;
|
|
}
|
|
else {
|
|
return this.key(this.convertHorzItemToInternal(item));
|
|
}
|
|
}
|
|
cacheKey(item) {
|
|
const time = item;
|
|
return time._internal_businessDay === undefined
|
|
? new Date(time._internal_timestamp * 1000).getTime()
|
|
: new Date(Date.UTC(time._internal_businessDay.year, time._internal_businessDay.month - 1, time._internal_businessDay.day)).getTime();
|
|
}
|
|
convertHorzItemToInternal(item) {
|
|
return convertTime(item);
|
|
}
|
|
updateFormatter(options) {
|
|
if (!this._private__options) {
|
|
return;
|
|
}
|
|
const dateFormat = options.dateFormat;
|
|
if (this._private__options.timeScale.timeVisible) {
|
|
this._private__dateTimeFormatter = new DateTimeFormatter({
|
|
_internal_dateFormat: dateFormat,
|
|
_internal_timeFormat: this._private__options.timeScale.secondsVisible ? '%h:%m:%s' : '%h:%m',
|
|
_internal_dateTimeSeparator: ' ',
|
|
_internal_locale: options.locale,
|
|
});
|
|
}
|
|
else {
|
|
this._private__dateTimeFormatter = new DateFormatter(dateFormat, options.locale);
|
|
}
|
|
}
|
|
formatHorzItem(item) {
|
|
const tp = item;
|
|
return this._private__dateTimeFormatter._internal_format(new Date(tp._internal_timestamp * 1000));
|
|
}
|
|
formatTickmark(tickMark, localizationOptions) {
|
|
const tickMarkType = weightToTickMarkType(tickMark.weight, this._private__options.timeScale.timeVisible, this._private__options.timeScale.secondsVisible);
|
|
const options = this._private__options.timeScale;
|
|
if (options.tickMarkFormatter !== undefined) {
|
|
const tickMarkString = options.tickMarkFormatter(tickMark.originalTime, tickMarkType, localizationOptions.locale);
|
|
if (tickMarkString !== null) {
|
|
return tickMarkString;
|
|
}
|
|
}
|
|
return defaultTickMarkFormatter(tickMark.time, tickMarkType, localizationOptions.locale);
|
|
}
|
|
maxTickMarkWeight(tickMarks) {
|
|
let maxWeight = tickMarks.reduce(markWithGreaterWeight$2, tickMarks[0]).weight;
|
|
// special case: it looks strange if 15:00 is bold but 14:00 is not
|
|
// so if maxWeight > TickMarkWeight.Hour1 and < TickMarkWeight.Day reduce it to TickMarkWeight.Hour1
|
|
if (maxWeight > 30 /* TickMarkWeight.Hour1 */ && maxWeight < 50 /* TickMarkWeight.Day */) {
|
|
maxWeight = 30 /* TickMarkWeight.Hour1 */;
|
|
}
|
|
return maxWeight;
|
|
}
|
|
fillWeightsForPoints(sortedTimePoints, startIndex) {
|
|
fillWeightsForPoints(sortedTimePoints, startIndex);
|
|
}
|
|
static _internal_applyDefaults(options) {
|
|
return merge({ localization: { dateFormat: 'dd MMM \'yy' } }, options ?? {});
|
|
}
|
|
}
|
|
|
|
function size$1(_a) {
|
|
var width = _a.width, height = _a.height;
|
|
if (width < 0) {
|
|
throw new Error('Negative width is not allowed for Size');
|
|
}
|
|
if (height < 0) {
|
|
throw new Error('Negative height is not allowed for Size');
|
|
}
|
|
return {
|
|
width: width,
|
|
height: height,
|
|
};
|
|
}
|
|
function equalSizes(first, second) {
|
|
return (first.width === second.width) &&
|
|
(first.height === second.height);
|
|
}
|
|
|
|
var Observable = /** @class */ (function () {
|
|
function Observable(win) {
|
|
var _this = this;
|
|
this._resolutionListener = function () { return _this._onResolutionChanged(); };
|
|
this._resolutionMediaQueryList = null;
|
|
this._observers = [];
|
|
this._window = win;
|
|
this._installResolutionListener();
|
|
}
|
|
Observable.prototype.dispose = function () {
|
|
this._uninstallResolutionListener();
|
|
this._window = null;
|
|
};
|
|
Object.defineProperty(Observable.prototype, "value", {
|
|
get: function () {
|
|
return this._window.devicePixelRatio;
|
|
},
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
Observable.prototype.subscribe = function (next) {
|
|
var _this = this;
|
|
var observer = { next: next };
|
|
this._observers.push(observer);
|
|
return {
|
|
unsubscribe: function () {
|
|
_this._observers = _this._observers.filter(function (o) { return o !== observer; });
|
|
},
|
|
};
|
|
};
|
|
Observable.prototype._installResolutionListener = function () {
|
|
if (this._resolutionMediaQueryList !== null) {
|
|
throw new Error('Resolution listener is already installed');
|
|
}
|
|
var dppx = this._window.devicePixelRatio;
|
|
this._resolutionMediaQueryList = this._window.matchMedia("all and (resolution: ".concat(dppx, "dppx)"));
|
|
// IE and some versions of Edge do not support addEventListener/removeEventListener, and we are going to use the deprecated addListener/removeListener
|
|
this._resolutionMediaQueryList.addListener(this._resolutionListener);
|
|
};
|
|
Observable.prototype._uninstallResolutionListener = function () {
|
|
if (this._resolutionMediaQueryList !== null) {
|
|
// IE and some versions of Edge do not support addEventListener/removeEventListener, and we are going to use the deprecated addListener/removeListener
|
|
this._resolutionMediaQueryList.removeListener(this._resolutionListener);
|
|
this._resolutionMediaQueryList = null;
|
|
}
|
|
};
|
|
Observable.prototype._reinstallResolutionListener = function () {
|
|
this._uninstallResolutionListener();
|
|
this._installResolutionListener();
|
|
};
|
|
Observable.prototype._onResolutionChanged = function () {
|
|
var _this = this;
|
|
this._observers.forEach(function (observer) { return observer.next(_this._window.devicePixelRatio); });
|
|
this._reinstallResolutionListener();
|
|
};
|
|
return Observable;
|
|
}());
|
|
function createObservable(win) {
|
|
return new Observable(win);
|
|
}
|
|
|
|
var DevicePixelContentBoxBinding = /** @class */ (function () {
|
|
function DevicePixelContentBoxBinding(canvasElement, transformBitmapSize, options) {
|
|
var _a;
|
|
this._canvasElement = null;
|
|
this._bitmapSizeChangedListeners = [];
|
|
this._suggestedBitmapSize = null;
|
|
this._suggestedBitmapSizeChangedListeners = [];
|
|
// devicePixelRatio approach
|
|
this._devicePixelRatioObservable = null;
|
|
// ResizeObserver approach
|
|
this._canvasElementResizeObserver = null;
|
|
this._canvasElement = canvasElement;
|
|
this._canvasElementClientSize = size$1({
|
|
width: this._canvasElement.clientWidth,
|
|
height: this._canvasElement.clientHeight,
|
|
});
|
|
this._transformBitmapSize = transformBitmapSize !== null && transformBitmapSize !== void 0 ? transformBitmapSize : (function (size) { return size; });
|
|
this._allowResizeObserver = (_a = options === null || options === void 0 ? void 0 : options.allowResizeObserver) !== null && _a !== void 0 ? _a : true;
|
|
this._chooseAndInitObserver();
|
|
// we MAY leave the constuctor without any bitmap size observation mechanics initialized
|
|
}
|
|
DevicePixelContentBoxBinding.prototype.dispose = function () {
|
|
var _a, _b;
|
|
if (this._canvasElement === null) {
|
|
throw new Error('Object is disposed');
|
|
}
|
|
(_a = this._canvasElementResizeObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
this._canvasElementResizeObserver = null;
|
|
(_b = this._devicePixelRatioObservable) === null || _b === void 0 ? void 0 : _b.dispose();
|
|
this._devicePixelRatioObservable = null;
|
|
this._suggestedBitmapSizeChangedListeners.length = 0;
|
|
this._bitmapSizeChangedListeners.length = 0;
|
|
this._canvasElement = null;
|
|
};
|
|
Object.defineProperty(DevicePixelContentBoxBinding.prototype, "canvasElement", {
|
|
get: function () {
|
|
if (this._canvasElement === null) {
|
|
throw new Error('Object is disposed');
|
|
}
|
|
return this._canvasElement;
|
|
},
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
Object.defineProperty(DevicePixelContentBoxBinding.prototype, "canvasElementClientSize", {
|
|
get: function () {
|
|
return this._canvasElementClientSize;
|
|
},
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
Object.defineProperty(DevicePixelContentBoxBinding.prototype, "bitmapSize", {
|
|
get: function () {
|
|
return size$1({
|
|
width: this.canvasElement.width,
|
|
height: this.canvasElement.height,
|
|
});
|
|
},
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
/**
|
|
* Use this function to change canvas element client size until binding is disposed
|
|
* @param clientSize New client size for bound HTMLCanvasElement
|
|
*/
|
|
DevicePixelContentBoxBinding.prototype.resizeCanvasElement = function (clientSize) {
|
|
this._canvasElementClientSize = size$1(clientSize);
|
|
this.canvasElement.style.width = "".concat(this._canvasElementClientSize.width, "px");
|
|
this.canvasElement.style.height = "".concat(this._canvasElementClientSize.height, "px");
|
|
this._invalidateBitmapSize();
|
|
};
|
|
DevicePixelContentBoxBinding.prototype.subscribeBitmapSizeChanged = function (listener) {
|
|
this._bitmapSizeChangedListeners.push(listener);
|
|
};
|
|
DevicePixelContentBoxBinding.prototype.unsubscribeBitmapSizeChanged = function (listener) {
|
|
this._bitmapSizeChangedListeners = this._bitmapSizeChangedListeners.filter(function (l) { return l !== listener; });
|
|
};
|
|
Object.defineProperty(DevicePixelContentBoxBinding.prototype, "suggestedBitmapSize", {
|
|
get: function () {
|
|
return this._suggestedBitmapSize;
|
|
},
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
DevicePixelContentBoxBinding.prototype.subscribeSuggestedBitmapSizeChanged = function (listener) {
|
|
this._suggestedBitmapSizeChangedListeners.push(listener);
|
|
};
|
|
DevicePixelContentBoxBinding.prototype.unsubscribeSuggestedBitmapSizeChanged = function (listener) {
|
|
this._suggestedBitmapSizeChangedListeners = this._suggestedBitmapSizeChangedListeners.filter(function (l) { return l !== listener; });
|
|
};
|
|
DevicePixelContentBoxBinding.prototype.applySuggestedBitmapSize = function () {
|
|
if (this._suggestedBitmapSize === null) {
|
|
// nothing to apply
|
|
return;
|
|
}
|
|
var oldSuggestedSize = this._suggestedBitmapSize;
|
|
this._suggestedBitmapSize = null;
|
|
this._resizeBitmap(oldSuggestedSize);
|
|
this._emitSuggestedBitmapSizeChanged(oldSuggestedSize, this._suggestedBitmapSize);
|
|
};
|
|
DevicePixelContentBoxBinding.prototype._resizeBitmap = function (newSize) {
|
|
var oldSize = this.bitmapSize;
|
|
if (equalSizes(oldSize, newSize)) {
|
|
return;
|
|
}
|
|
this.canvasElement.width = newSize.width;
|
|
this.canvasElement.height = newSize.height;
|
|
this._emitBitmapSizeChanged(oldSize, newSize);
|
|
};
|
|
DevicePixelContentBoxBinding.prototype._emitBitmapSizeChanged = function (oldSize, newSize) {
|
|
var _this = this;
|
|
this._bitmapSizeChangedListeners.forEach(function (listener) { return listener.call(_this, oldSize, newSize); });
|
|
};
|
|
DevicePixelContentBoxBinding.prototype._suggestNewBitmapSize = function (newSize) {
|
|
var oldSuggestedSize = this._suggestedBitmapSize;
|
|
var finalNewSize = size$1(this._transformBitmapSize(newSize, this._canvasElementClientSize));
|
|
var newSuggestedSize = equalSizes(this.bitmapSize, finalNewSize) ? null : finalNewSize;
|
|
if (oldSuggestedSize === null && newSuggestedSize === null) {
|
|
return;
|
|
}
|
|
if (oldSuggestedSize !== null && newSuggestedSize !== null
|
|
&& equalSizes(oldSuggestedSize, newSuggestedSize)) {
|
|
return;
|
|
}
|
|
this._suggestedBitmapSize = newSuggestedSize;
|
|
this._emitSuggestedBitmapSizeChanged(oldSuggestedSize, newSuggestedSize);
|
|
};
|
|
DevicePixelContentBoxBinding.prototype._emitSuggestedBitmapSizeChanged = function (oldSize, newSize) {
|
|
var _this = this;
|
|
this._suggestedBitmapSizeChangedListeners.forEach(function (listener) { return listener.call(_this, oldSize, newSize); });
|
|
};
|
|
DevicePixelContentBoxBinding.prototype._chooseAndInitObserver = function () {
|
|
var _this = this;
|
|
if (!this._allowResizeObserver) {
|
|
this._initDevicePixelRatioObservable();
|
|
return;
|
|
}
|
|
isDevicePixelContentBoxSupported()
|
|
.then(function (isSupported) {
|
|
return isSupported ?
|
|
_this._initResizeObserver() :
|
|
_this._initDevicePixelRatioObservable();
|
|
});
|
|
};
|
|
// devicePixelRatio approach
|
|
DevicePixelContentBoxBinding.prototype._initDevicePixelRatioObservable = function () {
|
|
var _this = this;
|
|
if (this._canvasElement === null) {
|
|
// it looks like we are already dead
|
|
return;
|
|
}
|
|
var win = canvasElementWindow(this._canvasElement);
|
|
if (win === null) {
|
|
throw new Error('No window is associated with the canvas');
|
|
}
|
|
this._devicePixelRatioObservable = createObservable(win);
|
|
this._devicePixelRatioObservable.subscribe(function () { return _this._invalidateBitmapSize(); });
|
|
this._invalidateBitmapSize();
|
|
};
|
|
DevicePixelContentBoxBinding.prototype._invalidateBitmapSize = function () {
|
|
var _a, _b;
|
|
if (this._canvasElement === null) {
|
|
// it looks like we are already dead
|
|
return;
|
|
}
|
|
var win = canvasElementWindow(this._canvasElement);
|
|
if (win === null) {
|
|
return;
|
|
}
|
|
var ratio = (_b = (_a = this._devicePixelRatioObservable) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : win.devicePixelRatio;
|
|
var canvasRects = this._canvasElement.getClientRects();
|
|
var newSize =
|
|
// eslint-disable-next-line no-negated-condition
|
|
canvasRects[0] !== undefined ?
|
|
predictedBitmapSize(canvasRects[0], ratio) :
|
|
size$1({
|
|
width: this._canvasElementClientSize.width * ratio,
|
|
height: this._canvasElementClientSize.height * ratio,
|
|
});
|
|
this._suggestNewBitmapSize(newSize);
|
|
};
|
|
// ResizeObserver approach
|
|
DevicePixelContentBoxBinding.prototype._initResizeObserver = function () {
|
|
var _this = this;
|
|
if (this._canvasElement === null) {
|
|
// it looks like we are already dead
|
|
return;
|
|
}
|
|
this._canvasElementResizeObserver = new ResizeObserver(function (entries) {
|
|
var entry = entries.find(function (entry) { return entry.target === _this._canvasElement; });
|
|
if (!entry || !entry.devicePixelContentBoxSize || !entry.devicePixelContentBoxSize[0]) {
|
|
return;
|
|
}
|
|
var entrySize = entry.devicePixelContentBoxSize[0];
|
|
var newSize = size$1({
|
|
width: entrySize.inlineSize,
|
|
height: entrySize.blockSize,
|
|
});
|
|
_this._suggestNewBitmapSize(newSize);
|
|
});
|
|
this._canvasElementResizeObserver.observe(this._canvasElement, { box: 'device-pixel-content-box' });
|
|
};
|
|
return DevicePixelContentBoxBinding;
|
|
}());
|
|
function bindTo(canvasElement, target) {
|
|
{
|
|
return new DevicePixelContentBoxBinding(canvasElement, target.transform, target.options);
|
|
}
|
|
}
|
|
function canvasElementWindow(canvasElement) {
|
|
// According to DOM Level 2 Core specification, ownerDocument should never be null for HTMLCanvasElement
|
|
// see https://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html#node-ownerDoc
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
return canvasElement.ownerDocument.defaultView;
|
|
}
|
|
function isDevicePixelContentBoxSupported() {
|
|
return new Promise(function (resolve) {
|
|
var ro = new ResizeObserver(function (entries) {
|
|
resolve(entries.every(function (entry) { return 'devicePixelContentBoxSize' in entry; }));
|
|
ro.disconnect();
|
|
});
|
|
ro.observe(document.body, { box: 'device-pixel-content-box' });
|
|
})
|
|
.catch(function () { return false; });
|
|
}
|
|
function predictedBitmapSize(canvasRect, ratio) {
|
|
return size$1({
|
|
width: Math.round(canvasRect.left * ratio + canvasRect.width * ratio) -
|
|
Math.round(canvasRect.left * ratio),
|
|
height: Math.round(canvasRect.top * ratio + canvasRect.height * ratio) -
|
|
Math.round(canvasRect.top * ratio),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @experimental
|
|
*/
|
|
var CanvasRenderingTarget2D = /** @class */ (function () {
|
|
function CanvasRenderingTarget2D(context, mediaSize, bitmapSize) {
|
|
if (mediaSize.width === 0 || mediaSize.height === 0) {
|
|
throw new TypeError('Rendering target could only be created on a media with positive width and height');
|
|
}
|
|
this._mediaSize = mediaSize;
|
|
// !Number.isInteger(bitmapSize.width) || !Number.isInteger(bitmapSize.height)
|
|
if (bitmapSize.width === 0 || bitmapSize.height === 0) {
|
|
throw new TypeError('Rendering target could only be created using a bitmap with positive integer width and height');
|
|
}
|
|
this._bitmapSize = bitmapSize;
|
|
this._context = context;
|
|
}
|
|
CanvasRenderingTarget2D.prototype.useMediaCoordinateSpace = function (f) {
|
|
try {
|
|
this._context.save();
|
|
// do not use resetTransform to support old versions of Edge
|
|
this._context.setTransform(1, 0, 0, 1, 0, 0);
|
|
this._context.scale(this._horizontalPixelRatio, this._verticalPixelRatio);
|
|
return f({
|
|
context: this._context,
|
|
mediaSize: this._mediaSize,
|
|
});
|
|
}
|
|
finally {
|
|
this._context.restore();
|
|
}
|
|
};
|
|
CanvasRenderingTarget2D.prototype.useBitmapCoordinateSpace = function (f) {
|
|
try {
|
|
this._context.save();
|
|
// do not use resetTransform to support old versions of Edge
|
|
this._context.setTransform(1, 0, 0, 1, 0, 0);
|
|
return f({
|
|
context: this._context,
|
|
mediaSize: this._mediaSize,
|
|
bitmapSize: this._bitmapSize,
|
|
horizontalPixelRatio: this._horizontalPixelRatio,
|
|
verticalPixelRatio: this._verticalPixelRatio,
|
|
});
|
|
}
|
|
finally {
|
|
this._context.restore();
|
|
}
|
|
};
|
|
Object.defineProperty(CanvasRenderingTarget2D.prototype, "_horizontalPixelRatio", {
|
|
get: function () {
|
|
return this._bitmapSize.width / this._mediaSize.width;
|
|
},
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
Object.defineProperty(CanvasRenderingTarget2D.prototype, "_verticalPixelRatio", {
|
|
get: function () {
|
|
return this._bitmapSize.height / this._mediaSize.height;
|
|
},
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
return CanvasRenderingTarget2D;
|
|
}());
|
|
/**
|
|
* @experimental
|
|
*/
|
|
function tryCreateCanvasRenderingTarget2D(binding, contextOptions) {
|
|
var mediaSize = binding.canvasElementClientSize;
|
|
if (mediaSize.width === 0 || mediaSize.height === 0) {
|
|
return null;
|
|
}
|
|
var bitmapSize = binding.bitmapSize;
|
|
if (bitmapSize.width === 0 || bitmapSize.height === 0) {
|
|
return null;
|
|
}
|
|
var context = binding.canvasElement.getContext('2d', contextOptions);
|
|
if (context === null) {
|
|
return null;
|
|
}
|
|
return new CanvasRenderingTarget2D(context, mediaSize, bitmapSize);
|
|
}
|
|
|
|
/**
|
|
* When you're trying to use the library in server-side context (for instance in SSR)
|
|
* you don't have some browser-specific variables like navigator or window
|
|
* and if the library will use them on the top level of the library
|
|
* the import will fail due ReferenceError
|
|
* thus, this allows use the navigator on the top level and being imported in server-side context as well
|
|
* See issue #446
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/tslint/config
|
|
const isRunningOnClientSide = typeof window !== 'undefined';
|
|
|
|
function isFF() {
|
|
if (!isRunningOnClientSide) {
|
|
return false;
|
|
}
|
|
return window.navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
|
|
}
|
|
function isIOS() {
|
|
if (!isRunningOnClientSide) {
|
|
return false;
|
|
}
|
|
// eslint-disable-next-line deprecation/deprecation
|
|
return /iPhone|iPad|iPod/.test(window.navigator.platform);
|
|
}
|
|
function isChrome() {
|
|
if (!isRunningOnClientSide) {
|
|
return false;
|
|
}
|
|
return window.chrome !== undefined;
|
|
}
|
|
// Determine whether the browser is running on windows.
|
|
function isWindows() {
|
|
if (!isRunningOnClientSide) {
|
|
return false;
|
|
}
|
|
// more accurate if available
|
|
if (navigator?.userAgentData?.platform) {
|
|
return navigator.userAgentData.platform === 'Windows';
|
|
}
|
|
return navigator.userAgent.toLowerCase().indexOf('win') >= 0;
|
|
}
|
|
// Determine whether the browser is Chromium based.
|
|
function isChromiumBased() {
|
|
if (!isRunningOnClientSide) {
|
|
return false;
|
|
}
|
|
if (!navigator.userAgentData) {
|
|
return false;
|
|
}
|
|
return navigator.userAgentData.brands.some((brand) => {
|
|
return brand.brand.includes('Chromium');
|
|
});
|
|
}
|
|
|
|
/// <reference types="_build-time-constants" />
|
|
function warn(msg) {
|
|
{
|
|
// eslint-disable-next-line no-console
|
|
console.warn(msg);
|
|
}
|
|
}
|
|
|
|
function hoveredTargetSourceKind(source, itemType) {
|
|
if (source instanceof Pane) {
|
|
return 'pane-primitive';
|
|
}
|
|
if (itemType === 'marker' || itemType === 'primitive') {
|
|
return 'series-primitive';
|
|
}
|
|
return 'series';
|
|
}
|
|
function hoveredTargetObjectKind(itemType, objectId) {
|
|
switch (itemType) {
|
|
case 'custom':
|
|
return objectId !== undefined ? 'custom-object' : 'series';
|
|
case 'price-line':
|
|
return 'custom-price-line';
|
|
case 'marker':
|
|
return 'series-marker';
|
|
case 'primitive':
|
|
return 'primitive';
|
|
case 'series-point':
|
|
case 'series-line':
|
|
case 'series-range':
|
|
default:
|
|
return 'series';
|
|
}
|
|
}
|
|
function buildHoveredEventInfo(hoveredSource, paneIndex) {
|
|
const hoveredSeries = hoveredSource !== null && hoveredSource._internal_source instanceof Series
|
|
? hoveredSource._internal_source
|
|
: undefined;
|
|
const hoveredObject = hoveredSource?._internal_object?._internal_externalId;
|
|
const normalizedPaneIndex = paneIndex !== undefined && paneIndex !== -1 ? paneIndex : undefined;
|
|
if (hoveredSource === null || hoveredSource._internal_itemType === undefined) {
|
|
return {
|
|
_internal_hoveredSeries: hoveredSeries,
|
|
_internal_hoveredObject: hoveredObject,
|
|
};
|
|
}
|
|
const hoveredInfo = {
|
|
_internal_type: hoveredSource._internal_itemType,
|
|
_internal_sourceKind: hoveredTargetSourceKind(hoveredSource._internal_source, hoveredSource._internal_itemType),
|
|
_internal_objectKind: hoveredTargetObjectKind(hoveredSource._internal_itemType, hoveredObject),
|
|
_internal_series: hoveredSeries,
|
|
_internal_objectId: hoveredObject,
|
|
_internal_paneIndex: normalizedPaneIndex,
|
|
};
|
|
return {
|
|
_internal_hoveredSeries: hoveredSeries,
|
|
_internal_hoveredObject: hoveredObject,
|
|
_internal_hoveredInfo: hoveredInfo,
|
|
};
|
|
}
|
|
|
|
// on Hi-DPI CSS size * Device Pixel Ratio should be integer to avoid smoothing
|
|
// For chart widget we decrease the size because we must be inside container.
|
|
// For time axis this is not important, since it just affects space for pane widgets
|
|
function suggestChartSize(originalSize) {
|
|
const integerWidth = Math.floor(originalSize.width);
|
|
const integerHeight = Math.floor(originalSize.height);
|
|
const width = integerWidth - (integerWidth % 2);
|
|
const height = integerHeight - (integerHeight % 2);
|
|
return size$1({ width, height });
|
|
}
|
|
function suggestTimeScaleHeight(originalHeight) {
|
|
return originalHeight + (originalHeight % 2);
|
|
}
|
|
function suggestPriceScaleWidth(originalWidth) {
|
|
return originalWidth + (originalWidth % 2);
|
|
}
|
|
|
|
function preventScrollByWheelClick(el) {
|
|
if (!isChrome()) {
|
|
return;
|
|
}
|
|
el.addEventListener('mousedown', (e) => {
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
if (e.button === 1 /* MouseEventButton.Middle */) {
|
|
// prevent incorrect scrolling event
|
|
e.preventDefault();
|
|
return false;
|
|
}
|
|
return undefined;
|
|
});
|
|
}
|
|
|
|
// TODO: get rid of a lot of boolean flags, probably we should replace it with some enum
|
|
class MouseEventHandler {
|
|
constructor(target, handler, options) {
|
|
this._private__clickCount = 0;
|
|
this._private__clickTimeoutId = null;
|
|
this._private__clickPosition = { _internal_x: Number.NEGATIVE_INFINITY, _internal_y: Number.POSITIVE_INFINITY };
|
|
this._private__tapCount = 0;
|
|
this._private__tapTimeoutId = null;
|
|
this._private__tapPosition = { _internal_x: Number.NEGATIVE_INFINITY, _internal_y: Number.POSITIVE_INFINITY };
|
|
this._private__longTapTimeoutId = null;
|
|
this._private__longTapActive = false;
|
|
this._private__mouseMoveStartPosition = null;
|
|
this._private__touchMoveStartPosition = null;
|
|
this._private__touchMoveExceededManhattanDistance = false;
|
|
this._private__cancelClick = false;
|
|
this._private__cancelTap = false;
|
|
this._private__unsubscribeOutsideMouseEvents = null;
|
|
this._private__unsubscribeOutsideTouchEvents = null;
|
|
this._private__unsubscribeMobileSafariEvents = null;
|
|
this._private__unsubscribeMousemove = null;
|
|
this._private__unsubscribeRootMouseEvents = null;
|
|
this._private__unsubscribeRootTouchEvents = null;
|
|
this._private__startPinchMiddlePoint = null;
|
|
this._private__startPinchDistance = 0;
|
|
this._private__pinchPrevented = false;
|
|
this._private__preventTouchDragProcess = false;
|
|
this._private__mousePressed = false;
|
|
this._private__lastTouchEventTimeStamp = 0;
|
|
// for touchstart/touchmove/touchend events we handle only first touch
|
|
// i.e. we don't support several active touches at the same time (except pinch event)
|
|
this._private__activeTouchId = null;
|
|
// accept all mouse leave events if it's not an iOS device
|
|
// see _mouseEnterHandler, _mouseMoveHandler, _mouseLeaveHandler
|
|
this._private__acceptMouseLeave = !isIOS();
|
|
/**
|
|
* In Firefox mouse events dont't fire if the mouse position is outside of the browser's border.
|
|
* To prevent the mouse from hanging while pressed we're subscribing on the mouseleave event of the document element.
|
|
* We're subscribing on mouseleave, but this event is actually fired on mouseup outside of the browser's border.
|
|
*/
|
|
this._private__onFirefoxOutsideMouseUp = (mouseUpEvent) => {
|
|
this._private__mouseUpHandler(mouseUpEvent);
|
|
};
|
|
/**
|
|
* Safari doesn't fire touchstart/mousedown events on double tap since iOS 13.
|
|
* There are two possible solutions:
|
|
* 1) Call preventDefault in touchEnd handler. But it also prevents click event from firing.
|
|
* 2) Add listener on dblclick event that fires with the preceding mousedown/mouseup.
|
|
* https://developer.apple.com/forums/thread/125073
|
|
*/
|
|
this._private__onMobileSafariDoubleClick = (dblClickEvent) => {
|
|
if (this._private__firesTouchEvents(dblClickEvent)) {
|
|
const compatEvent = this._private__makeCompatEvent(dblClickEvent);
|
|
++this._private__tapCount;
|
|
if (this._private__tapTimeoutId && this._private__tapCount > 1) {
|
|
const { _internal_manhattanDistance: manhattanDistance } = this._private__touchMouseMoveWithDownInfo(getPosition(dblClickEvent), this._private__tapPosition);
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
if (manhattanDistance < 30 /* Constants.DoubleTapManhattanDistance */ && !this._private__cancelTap) {
|
|
this._private__processTouchEvent(compatEvent, this._private__handler._internal_doubleTapEvent);
|
|
}
|
|
this._private__resetTapTimeout();
|
|
}
|
|
}
|
|
else {
|
|
const compatEvent = this._private__makeCompatEvent(dblClickEvent);
|
|
++this._private__clickCount;
|
|
if (this._private__clickTimeoutId && this._private__clickCount > 1) {
|
|
const { _internal_manhattanDistance: manhattanDistance } = this._private__touchMouseMoveWithDownInfo(getPosition(dblClickEvent), this._private__clickPosition);
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
if (manhattanDistance < 5 /* Constants.DoubleClickManhattanDistance */ && !this._private__cancelClick) {
|
|
this._private__processMouseEvent(compatEvent, this._private__handler._internal_mouseDoubleClickEvent);
|
|
}
|
|
this._private__resetClickTimeout();
|
|
}
|
|
}
|
|
};
|
|
this._private__target = target;
|
|
this._private__handler = handler;
|
|
this._private__options = options;
|
|
this._private__init();
|
|
}
|
|
_internal_destroy() {
|
|
if (this._private__unsubscribeOutsideMouseEvents !== null) {
|
|
this._private__unsubscribeOutsideMouseEvents();
|
|
this._private__unsubscribeOutsideMouseEvents = null;
|
|
}
|
|
if (this._private__unsubscribeOutsideTouchEvents !== null) {
|
|
this._private__unsubscribeOutsideTouchEvents();
|
|
this._private__unsubscribeOutsideTouchEvents = null;
|
|
}
|
|
if (this._private__unsubscribeMousemove !== null) {
|
|
this._private__unsubscribeMousemove();
|
|
this._private__unsubscribeMousemove = null;
|
|
}
|
|
if (this._private__unsubscribeRootMouseEvents !== null) {
|
|
this._private__unsubscribeRootMouseEvents();
|
|
this._private__unsubscribeRootMouseEvents = null;
|
|
}
|
|
if (this._private__unsubscribeRootTouchEvents !== null) {
|
|
this._private__unsubscribeRootTouchEvents();
|
|
this._private__unsubscribeRootTouchEvents = null;
|
|
}
|
|
if (this._private__unsubscribeMobileSafariEvents !== null) {
|
|
this._private__unsubscribeMobileSafariEvents();
|
|
this._private__unsubscribeMobileSafariEvents = null;
|
|
}
|
|
this._private__clearLongTapTimeout();
|
|
this._private__resetClickTimeout();
|
|
}
|
|
_private__mouseEnterHandler(enterEvent) {
|
|
if (this._private__unsubscribeMousemove) {
|
|
this._private__unsubscribeMousemove();
|
|
}
|
|
const boundMouseMoveHandler = this._private__mouseMoveHandler.bind(this);
|
|
this._private__unsubscribeMousemove = () => {
|
|
this._private__target.removeEventListener('mousemove', boundMouseMoveHandler);
|
|
};
|
|
this._private__target.addEventListener('mousemove', boundMouseMoveHandler);
|
|
if (this._private__firesTouchEvents(enterEvent)) {
|
|
return;
|
|
}
|
|
const compatEvent = this._private__makeCompatEvent(enterEvent);
|
|
this._private__processMouseEvent(compatEvent, this._private__handler._internal_mouseEnterEvent);
|
|
this._private__acceptMouseLeave = true;
|
|
}
|
|
_private__resetClickTimeout() {
|
|
if (this._private__clickTimeoutId !== null) {
|
|
clearTimeout(this._private__clickTimeoutId);
|
|
}
|
|
this._private__clickCount = 0;
|
|
this._private__clickTimeoutId = null;
|
|
this._private__clickPosition = { _internal_x: Number.NEGATIVE_INFINITY, _internal_y: Number.POSITIVE_INFINITY };
|
|
}
|
|
_private__resetTapTimeout() {
|
|
if (this._private__tapTimeoutId !== null) {
|
|
clearTimeout(this._private__tapTimeoutId);
|
|
}
|
|
this._private__tapCount = 0;
|
|
this._private__tapTimeoutId = null;
|
|
this._private__tapPosition = { _internal_x: Number.NEGATIVE_INFINITY, _internal_y: Number.POSITIVE_INFINITY };
|
|
}
|
|
_private__mouseMoveHandler(moveEvent) {
|
|
if (this._private__mousePressed || this._private__touchMoveStartPosition !== null) {
|
|
return;
|
|
}
|
|
if (this._private__firesTouchEvents(moveEvent)) {
|
|
return;
|
|
}
|
|
const compatEvent = this._private__makeCompatEvent(moveEvent);
|
|
this._private__processMouseEvent(compatEvent, this._private__handler._internal_mouseMoveEvent);
|
|
this._private__acceptMouseLeave = true;
|
|
}
|
|
_private__touchMoveHandler(moveEvent) {
|
|
const touch = touchWithId(moveEvent.changedTouches, ensureNotNull(this._private__activeTouchId));
|
|
if (touch === null) {
|
|
return;
|
|
}
|
|
this._private__lastTouchEventTimeStamp = eventTimeStamp(moveEvent);
|
|
if (this._private__startPinchMiddlePoint !== null) {
|
|
return;
|
|
}
|
|
if (this._private__preventTouchDragProcess) {
|
|
return;
|
|
}
|
|
// prevent pinch if move event comes faster than the second touch
|
|
this._private__pinchPrevented = true;
|
|
const moveInfo = this._private__touchMouseMoveWithDownInfo(getPosition(touch), ensureNotNull(this._private__touchMoveStartPosition));
|
|
const { _internal_xOffset: xOffset, _internal_yOffset: yOffset, _internal_manhattanDistance: manhattanDistance } = moveInfo;
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
if (!this._private__touchMoveExceededManhattanDistance && manhattanDistance < 5 /* Constants.CancelTapManhattanDistance */) {
|
|
return;
|
|
}
|
|
if (!this._private__touchMoveExceededManhattanDistance) {
|
|
// first time when current position exceeded manhattan distance
|
|
// vertical drag is more important than horizontal drag
|
|
// because we scroll the page vertically often than horizontally
|
|
const correctedXOffset = xOffset * 0.5;
|
|
// a drag can be only if touch page scroll isn't allowed
|
|
const isVertDrag = yOffset >= correctedXOffset && !this._private__options._internal_treatVertTouchDragAsPageScroll();
|
|
const isHorzDrag = correctedXOffset > yOffset && !this._private__options._internal_treatHorzTouchDragAsPageScroll();
|
|
// if drag event happened then we should revert preventDefault state to original one
|
|
// and try to process the drag event
|
|
// else we shouldn't prevent default of the event and ignore processing the drag event
|
|
if (!isVertDrag && !isHorzDrag) {
|
|
this._private__preventTouchDragProcess = true;
|
|
}
|
|
this._private__touchMoveExceededManhattanDistance = true;
|
|
// if manhattan distance is more that 5 - we should cancel tap event
|
|
this._private__cancelTap = true;
|
|
this._private__clearLongTapTimeout();
|
|
this._private__resetTapTimeout();
|
|
}
|
|
if (!this._private__preventTouchDragProcess) {
|
|
const compatEvent = this._private__makeCompatEvent(moveEvent, touch);
|
|
this._private__processTouchEvent(compatEvent, this._private__handler._internal_touchMoveEvent);
|
|
// we should prevent default in case of touch only
|
|
// to prevent scroll of the page
|
|
preventDefault(moveEvent);
|
|
}
|
|
}
|
|
_private__mouseMoveWithDownHandler(moveEvent) {
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
if (moveEvent.button !== 0 /* MouseEventButton.Left */) {
|
|
return;
|
|
}
|
|
const moveInfo = this._private__touchMouseMoveWithDownInfo(getPosition(moveEvent), ensureNotNull(this._private__mouseMoveStartPosition));
|
|
const { _internal_manhattanDistance: manhattanDistance } = moveInfo;
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
if (manhattanDistance >= 5 /* Constants.CancelClickManhattanDistance */) {
|
|
// if manhattan distance is more that 5 - we should cancel click event
|
|
this._private__cancelClick = true;
|
|
this._private__resetClickTimeout();
|
|
}
|
|
if (this._private__cancelClick) {
|
|
// if this._cancelClick is true, that means that minimum manhattan distance is already exceeded
|
|
const compatEvent = this._private__makeCompatEvent(moveEvent);
|
|
this._private__processMouseEvent(compatEvent, this._private__handler._internal_pressedMouseMoveEvent);
|
|
}
|
|
}
|
|
_private__touchMouseMoveWithDownInfo(currentPosition, startPosition) {
|
|
const xOffset = Math.abs(startPosition._internal_x - currentPosition._internal_x);
|
|
const yOffset = Math.abs(startPosition._internal_y - currentPosition._internal_y);
|
|
const manhattanDistance = xOffset + yOffset;
|
|
return {
|
|
_internal_xOffset: xOffset,
|
|
_internal_yOffset: yOffset,
|
|
_internal_manhattanDistance: manhattanDistance,
|
|
};
|
|
}
|
|
// eslint-disable-next-line complexity
|
|
_private__touchEndHandler(touchEndEvent) {
|
|
let touch = touchWithId(touchEndEvent.changedTouches, ensureNotNull(this._private__activeTouchId));
|
|
if (touch === null && touchEndEvent.touches.length === 0) {
|
|
// something went wrong, somehow we missed the required touchend event
|
|
// probably the browser has not sent this event
|
|
touch = touchEndEvent.changedTouches[0];
|
|
}
|
|
if (touch === null) {
|
|
return;
|
|
}
|
|
this._private__activeTouchId = null;
|
|
this._private__lastTouchEventTimeStamp = eventTimeStamp(touchEndEvent);
|
|
this._private__clearLongTapTimeout();
|
|
this._private__touchMoveStartPosition = null;
|
|
if (this._private__unsubscribeRootTouchEvents) {
|
|
this._private__unsubscribeRootTouchEvents();
|
|
this._private__unsubscribeRootTouchEvents = null;
|
|
}
|
|
const compatEvent = this._private__makeCompatEvent(touchEndEvent, touch);
|
|
this._private__processTouchEvent(compatEvent, this._private__handler._internal_touchEndEvent);
|
|
++this._private__tapCount;
|
|
if (this._private__tapTimeoutId && this._private__tapCount > 1) {
|
|
// check that both clicks are near enough
|
|
const { _internal_manhattanDistance: manhattanDistance } = this._private__touchMouseMoveWithDownInfo(getPosition(touch), this._private__tapPosition);
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
if (manhattanDistance < 30 /* Constants.DoubleTapManhattanDistance */ && !this._private__cancelTap) {
|
|
this._private__processTouchEvent(compatEvent, this._private__handler._internal_doubleTapEvent);
|
|
}
|
|
this._private__resetTapTimeout();
|
|
}
|
|
else {
|
|
if (!this._private__cancelTap) {
|
|
this._private__processTouchEvent(compatEvent, this._private__handler._internal_tapEvent);
|
|
// do not fire mouse events if tap handler was executed
|
|
// prevent click event on new dom element (who appeared after tap)
|
|
if (this._private__handler._internal_tapEvent) {
|
|
preventDefault(touchEndEvent);
|
|
}
|
|
}
|
|
}
|
|
// prevent, for example, safari's dblclick-to-zoom or fast-click after long-tap
|
|
// we handle mouseDoubleClickEvent here ourselves
|
|
if (this._private__tapCount === 0) {
|
|
preventDefault(touchEndEvent);
|
|
}
|
|
if (touchEndEvent.touches.length === 0) {
|
|
if (this._private__longTapActive) {
|
|
this._private__longTapActive = false;
|
|
// prevent native click event
|
|
preventDefault(touchEndEvent);
|
|
}
|
|
}
|
|
}
|
|
_private__mouseUpHandler(mouseUpEvent) {
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
if (mouseUpEvent.button !== 0 /* MouseEventButton.Left */) {
|
|
return;
|
|
}
|
|
const compatEvent = this._private__makeCompatEvent(mouseUpEvent);
|
|
this._private__mouseMoveStartPosition = null;
|
|
this._private__mousePressed = false;
|
|
if (this._private__unsubscribeRootMouseEvents) {
|
|
this._private__unsubscribeRootMouseEvents();
|
|
this._private__unsubscribeRootMouseEvents = null;
|
|
}
|
|
if (isFF()) {
|
|
const rootElement = this._private__target.ownerDocument.documentElement;
|
|
rootElement.removeEventListener('mouseleave', this._private__onFirefoxOutsideMouseUp);
|
|
}
|
|
if (this._private__firesTouchEvents(mouseUpEvent)) {
|
|
return;
|
|
}
|
|
this._private__processMouseEvent(compatEvent, this._private__handler._internal_mouseUpEvent);
|
|
++this._private__clickCount;
|
|
if (this._private__clickTimeoutId && this._private__clickCount > 1) {
|
|
// check that both clicks are near enough
|
|
const { _internal_manhattanDistance: manhattanDistance } = this._private__touchMouseMoveWithDownInfo(getPosition(mouseUpEvent), this._private__clickPosition);
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
if (manhattanDistance < 5 /* Constants.DoubleClickManhattanDistance */ && !this._private__cancelClick) {
|
|
this._private__processMouseEvent(compatEvent, this._private__handler._internal_mouseDoubleClickEvent);
|
|
}
|
|
this._private__resetClickTimeout();
|
|
}
|
|
else {
|
|
if (!this._private__cancelClick) {
|
|
this._private__processMouseEvent(compatEvent, this._private__handler._internal_mouseClickEvent);
|
|
}
|
|
}
|
|
}
|
|
_private__clearLongTapTimeout() {
|
|
if (this._private__longTapTimeoutId === null) {
|
|
return;
|
|
}
|
|
clearTimeout(this._private__longTapTimeoutId);
|
|
this._private__longTapTimeoutId = null;
|
|
}
|
|
_private__touchStartHandler(downEvent) {
|
|
if (this._private__activeTouchId !== null) {
|
|
return;
|
|
}
|
|
const touch = downEvent.changedTouches[0];
|
|
this._private__activeTouchId = touch.identifier;
|
|
this._private__lastTouchEventTimeStamp = eventTimeStamp(downEvent);
|
|
const rootElement = this._private__target.ownerDocument.documentElement;
|
|
this._private__cancelTap = false;
|
|
this._private__touchMoveExceededManhattanDistance = false;
|
|
this._private__preventTouchDragProcess = false;
|
|
this._private__touchMoveStartPosition = getPosition(touch);
|
|
if (this._private__unsubscribeRootTouchEvents) {
|
|
this._private__unsubscribeRootTouchEvents();
|
|
this._private__unsubscribeRootTouchEvents = null;
|
|
}
|
|
{
|
|
const boundTouchMoveWithDownHandler = this._private__touchMoveHandler.bind(this);
|
|
const boundTouchEndHandler = this._private__touchEndHandler.bind(this);
|
|
this._private__unsubscribeRootTouchEvents = () => {
|
|
rootElement.removeEventListener('touchmove', boundTouchMoveWithDownHandler);
|
|
rootElement.removeEventListener('touchend', boundTouchEndHandler);
|
|
};
|
|
rootElement.addEventListener('touchmove', boundTouchMoveWithDownHandler, { passive: false });
|
|
rootElement.addEventListener('touchend', boundTouchEndHandler, { passive: false });
|
|
this._private__clearLongTapTimeout();
|
|
this._private__longTapTimeoutId = setTimeout(this._private__longTapHandler.bind(this, downEvent), 240 /* Delay.LongTap */);
|
|
}
|
|
const compatEvent = this._private__makeCompatEvent(downEvent, touch);
|
|
this._private__processTouchEvent(compatEvent, this._private__handler._internal_touchStartEvent);
|
|
if (!this._private__tapTimeoutId) {
|
|
this._private__tapCount = 0;
|
|
this._private__tapTimeoutId = setTimeout(this._private__resetTapTimeout.bind(this), 500 /* Delay.ResetClick */);
|
|
this._private__tapPosition = getPosition(touch);
|
|
}
|
|
}
|
|
_private__mouseDownHandler(downEvent) {
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
if (downEvent.button !== 0 /* MouseEventButton.Left */) {
|
|
return;
|
|
}
|
|
const rootElement = this._private__target.ownerDocument.documentElement;
|
|
if (isFF()) {
|
|
rootElement.addEventListener('mouseleave', this._private__onFirefoxOutsideMouseUp);
|
|
}
|
|
this._private__cancelClick = false;
|
|
this._private__mouseMoveStartPosition = getPosition(downEvent);
|
|
if (this._private__unsubscribeRootMouseEvents) {
|
|
this._private__unsubscribeRootMouseEvents();
|
|
this._private__unsubscribeRootMouseEvents = null;
|
|
}
|
|
{
|
|
const boundMouseMoveWithDownHandler = this._private__mouseMoveWithDownHandler.bind(this);
|
|
const boundMouseUpHandler = this._private__mouseUpHandler.bind(this);
|
|
this._private__unsubscribeRootMouseEvents = () => {
|
|
rootElement.removeEventListener('mousemove', boundMouseMoveWithDownHandler);
|
|
rootElement.removeEventListener('mouseup', boundMouseUpHandler);
|
|
};
|
|
rootElement.addEventListener('mousemove', boundMouseMoveWithDownHandler);
|
|
rootElement.addEventListener('mouseup', boundMouseUpHandler);
|
|
}
|
|
this._private__mousePressed = true;
|
|
if (this._private__firesTouchEvents(downEvent)) {
|
|
return;
|
|
}
|
|
const compatEvent = this._private__makeCompatEvent(downEvent);
|
|
this._private__processMouseEvent(compatEvent, this._private__handler._internal_mouseDownEvent);
|
|
if (!this._private__clickTimeoutId) {
|
|
this._private__clickCount = 0;
|
|
this._private__clickTimeoutId = setTimeout(this._private__resetClickTimeout.bind(this), 500 /* Delay.ResetClick */);
|
|
this._private__clickPosition = getPosition(downEvent);
|
|
}
|
|
}
|
|
_private__init() {
|
|
this._private__target.addEventListener('mouseenter', this._private__mouseEnterHandler.bind(this));
|
|
// Do not show context menu when something went wrong
|
|
this._private__target.addEventListener('touchcancel', this._private__clearLongTapTimeout.bind(this));
|
|
{
|
|
const doc = this._private__target.ownerDocument;
|
|
const outsideHandler = (event) => {
|
|
if (!this._private__handler._internal_mouseDownOutsideEvent) {
|
|
return;
|
|
}
|
|
if (event.composed && this._private__target.contains(event.composedPath()[0])) {
|
|
return;
|
|
}
|
|
if (event.target && this._private__target.contains(event.target)) {
|
|
return;
|
|
}
|
|
this._private__handler._internal_mouseDownOutsideEvent();
|
|
};
|
|
this._private__unsubscribeOutsideTouchEvents = () => {
|
|
doc.removeEventListener('touchstart', outsideHandler);
|
|
};
|
|
this._private__unsubscribeOutsideMouseEvents = () => {
|
|
doc.removeEventListener('mousedown', outsideHandler);
|
|
};
|
|
doc.addEventListener('mousedown', outsideHandler);
|
|
doc.addEventListener('touchstart', outsideHandler, { passive: true });
|
|
}
|
|
if (isIOS()) {
|
|
this._private__unsubscribeMobileSafariEvents = () => {
|
|
this._private__target.removeEventListener('dblclick', this._private__onMobileSafariDoubleClick);
|
|
};
|
|
this._private__target.addEventListener('dblclick', this._private__onMobileSafariDoubleClick);
|
|
}
|
|
this._private__target.addEventListener('mouseleave', this._private__mouseLeaveHandler.bind(this));
|
|
this._private__target.addEventListener('touchstart', this._private__touchStartHandler.bind(this), { passive: true });
|
|
preventScrollByWheelClick(this._private__target);
|
|
this._private__target.addEventListener('mousedown', this._private__mouseDownHandler.bind(this));
|
|
this._private__initPinch();
|
|
// Hey mobile Safari, what's up?
|
|
// If mobile Safari doesn't have any touchmove handler with passive=false
|
|
// it treats a touchstart and the following touchmove events as cancelable=false,
|
|
// so we can't prevent them (as soon we subscribe on touchmove inside touchstart's handler).
|
|
// And we'll get scroll of the page along with chart's one instead of only chart's scroll.
|
|
this._private__target.addEventListener('touchmove', () => { }, { passive: false });
|
|
}
|
|
_private__initPinch() {
|
|
if (this._private__handler._internal_pinchStartEvent === undefined &&
|
|
this._private__handler._internal_pinchEvent === undefined &&
|
|
this._private__handler._internal_pinchEndEvent === undefined) {
|
|
return;
|
|
}
|
|
this._private__target.addEventListener('touchstart', (event) => this._private__checkPinchState(event.touches), { passive: true });
|
|
this._private__target.addEventListener('touchmove', (event) => {
|
|
if (event.touches.length !== 2 || this._private__startPinchMiddlePoint === null) {
|
|
return;
|
|
}
|
|
if (this._private__handler._internal_pinchEvent !== undefined) {
|
|
const currentDistance = getDistance(event.touches[0], event.touches[1]);
|
|
const scale = currentDistance / this._private__startPinchDistance;
|
|
this._private__handler._internal_pinchEvent(this._private__startPinchMiddlePoint, scale);
|
|
preventDefault(event);
|
|
}
|
|
}, { passive: false });
|
|
this._private__target.addEventListener('touchend', (event) => {
|
|
this._private__checkPinchState(event.touches);
|
|
});
|
|
}
|
|
_private__checkPinchState(touches) {
|
|
if (touches.length === 1) {
|
|
this._private__pinchPrevented = false;
|
|
}
|
|
if (touches.length !== 2 || this._private__pinchPrevented || this._private__longTapActive) {
|
|
this._private__stopPinch();
|
|
}
|
|
else {
|
|
this._private__startPinch(touches);
|
|
}
|
|
}
|
|
_private__startPinch(touches) {
|
|
const box = getBoundingClientRect(this._private__target);
|
|
this._private__startPinchMiddlePoint = {
|
|
_internal_x: ((touches[0].clientX - box.left) + (touches[1].clientX - box.left)) / 2,
|
|
_internal_y: ((touches[0].clientY - box.top) + (touches[1].clientY - box.top)) / 2,
|
|
};
|
|
this._private__startPinchDistance = getDistance(touches[0], touches[1]);
|
|
if (this._private__handler._internal_pinchStartEvent !== undefined) {
|
|
this._private__handler._internal_pinchStartEvent();
|
|
}
|
|
this._private__clearLongTapTimeout();
|
|
}
|
|
_private__stopPinch() {
|
|
if (this._private__startPinchMiddlePoint === null) {
|
|
return;
|
|
}
|
|
this._private__startPinchMiddlePoint = null;
|
|
if (this._private__handler._internal_pinchEndEvent !== undefined) {
|
|
this._private__handler._internal_pinchEndEvent();
|
|
}
|
|
}
|
|
_private__mouseLeaveHandler(event) {
|
|
if (this._private__unsubscribeMousemove) {
|
|
this._private__unsubscribeMousemove();
|
|
}
|
|
if (this._private__firesTouchEvents(event)) {
|
|
return;
|
|
}
|
|
if (!this._private__acceptMouseLeave) {
|
|
// mobile Safari sometimes emits mouse leave event for no reason, there is no way to handle it in other way
|
|
// just ignore this event if there was no mouse move or mouse enter events
|
|
return;
|
|
}
|
|
const compatEvent = this._private__makeCompatEvent(event);
|
|
this._private__processMouseEvent(compatEvent, this._private__handler._internal_mouseLeaveEvent);
|
|
// accept all mouse leave events if it's not an iOS device
|
|
this._private__acceptMouseLeave = !isIOS();
|
|
}
|
|
_private__longTapHandler(event) {
|
|
const touch = touchWithId(event.touches, ensureNotNull(this._private__activeTouchId));
|
|
if (touch === null) {
|
|
return;
|
|
}
|
|
const compatEvent = this._private__makeCompatEvent(event, touch);
|
|
this._private__processTouchEvent(compatEvent, this._private__handler._internal_longTapEvent);
|
|
this._private__cancelTap = true;
|
|
// long tap is active until touchend event with 0 touches occurred
|
|
this._private__longTapActive = true;
|
|
}
|
|
_private__firesTouchEvents(e) {
|
|
if (e.sourceCapabilities && e.sourceCapabilities.firesTouchEvents !== undefined) {
|
|
return e.sourceCapabilities.firesTouchEvents;
|
|
}
|
|
return eventTimeStamp(e) < this._private__lastTouchEventTimeStamp + 500 /* Delay.PreventFiresTouchEvents */;
|
|
}
|
|
_private__processTouchEvent(event, callback) {
|
|
if (callback) {
|
|
callback.call(this._private__handler, event);
|
|
}
|
|
}
|
|
_private__processMouseEvent(event, callback) {
|
|
if (!callback) {
|
|
return;
|
|
}
|
|
callback.call(this._private__handler, event);
|
|
}
|
|
_private__makeCompatEvent(event, touch) {
|
|
// TouchEvent has no clientX/Y coordinates:
|
|
// We have to use the last Touch instead
|
|
const eventLike = touch || event;
|
|
const box = this._private__target.getBoundingClientRect() || { left: 0, top: 0 };
|
|
return {
|
|
clientX: eventLike.clientX,
|
|
clientY: eventLike.clientY,
|
|
pageX: eventLike.pageX,
|
|
pageY: eventLike.pageY,
|
|
screenX: eventLike.screenX,
|
|
screenY: eventLike.screenY,
|
|
localX: (eventLike.clientX - box.left),
|
|
localY: (eventLike.clientY - box.top),
|
|
ctrlKey: event.ctrlKey,
|
|
altKey: event.altKey,
|
|
shiftKey: event.shiftKey,
|
|
metaKey: event.metaKey,
|
|
_internal_isTouch: !event.type.startsWith('mouse') && event.type !== 'contextmenu' && event.type !== 'click',
|
|
_internal_srcType: event.type,
|
|
_internal_target: eventLike.target,
|
|
_internal_view: event.view,
|
|
_internal_preventDefault: () => {
|
|
if (event.type !== 'touchstart') {
|
|
// touchstart is passive and cannot be prevented
|
|
preventDefault(event);
|
|
}
|
|
},
|
|
};
|
|
}
|
|
}
|
|
function getBoundingClientRect(element) {
|
|
return element.getBoundingClientRect() || { left: 0, top: 0 };
|
|
}
|
|
function getDistance(p1, p2) {
|
|
const xDiff = p1.clientX - p2.clientX;
|
|
const yDiff = p1.clientY - p2.clientY;
|
|
return Math.sqrt(xDiff * xDiff + yDiff * yDiff);
|
|
}
|
|
function preventDefault(event) {
|
|
if (event.cancelable) {
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
function getPosition(eventLike) {
|
|
return {
|
|
_internal_x: eventLike.pageX,
|
|
_internal_y: eventLike.pageY,
|
|
};
|
|
}
|
|
function eventTimeStamp(e) {
|
|
// for some reason e.timestamp is always 0 on iPad with magic mouse, so we use performance.now() as a fallback
|
|
return e.timeStamp || performance.now();
|
|
}
|
|
function touchWithId(touches, id) {
|
|
for (let i = 0; i < touches.length; ++i) {
|
|
if (touches[i].identifier === id) {
|
|
return touches[i];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
class PaneSeparator {
|
|
constructor(chartWidget, topPaneIndex, bottomPaneIndex) {
|
|
this._private__handle = null;
|
|
this._private__mouseEventHandler = null;
|
|
this._private__resizeEnabled = true;
|
|
this._private__resizeInfo = null;
|
|
this._private__chartWidget = chartWidget;
|
|
this._private__topPane = chartWidget._internal_paneWidgets()[topPaneIndex];
|
|
this._private__bottomPane = chartWidget._internal_paneWidgets()[bottomPaneIndex];
|
|
this._private__rowElement = document.createElement('tr');
|
|
this._private__rowElement.style.height = 1 /* SeparatorConstants.SeparatorHeight */ + 'px';
|
|
this._private__cell = document.createElement('td');
|
|
this._private__cell.style.position = 'relative';
|
|
this._private__cell.style.padding = '0';
|
|
this._private__cell.style.margin = '0';
|
|
this._private__cell.setAttribute('colspan', '3');
|
|
this._private__updateBorderColor();
|
|
this._private__rowElement.appendChild(this._private__cell);
|
|
this._private__resizeEnabled = this._private__chartWidget._internal_options()['layout'].panes.enableResize;
|
|
if (!this._private__resizeEnabled) {
|
|
this._private__handle = null;
|
|
this._private__mouseEventHandler = null;
|
|
}
|
|
else {
|
|
this._private__addResizableHandle();
|
|
}
|
|
}
|
|
_internal_destroy() {
|
|
if (this._private__mouseEventHandler !== null) {
|
|
this._private__mouseEventHandler._internal_destroy();
|
|
}
|
|
}
|
|
_internal_getElement() {
|
|
return this._private__rowElement;
|
|
}
|
|
_internal_getSize() {
|
|
return size$1({
|
|
width: this._private__topPane._internal_getSize().width,
|
|
height: 1 /* SeparatorConstants.SeparatorHeight */,
|
|
});
|
|
}
|
|
_internal_getBitmapSize() {
|
|
return size$1({
|
|
width: this._private__topPane._internal_getBitmapSize().width,
|
|
height: 1 /* SeparatorConstants.SeparatorHeight */ * window.devicePixelRatio,
|
|
});
|
|
}
|
|
_internal_drawBitmap(ctx, x, y) {
|
|
const bitmapSize = this._internal_getBitmapSize();
|
|
ctx.fillStyle = this._private__chartWidget._internal_options()['layout'].panes.separatorColor;
|
|
ctx.fillRect(x, y, bitmapSize.width, bitmapSize.height);
|
|
}
|
|
_internal_update() {
|
|
this._private__updateBorderColor();
|
|
if (this._private__chartWidget._internal_options()['layout'].panes.enableResize !== this._private__resizeEnabled) {
|
|
this._private__resizeEnabled = this._private__chartWidget._internal_options()['layout'].panes.enableResize;
|
|
if (this._private__resizeEnabled) {
|
|
this._private__addResizableHandle();
|
|
}
|
|
else {
|
|
if (this._private__handle !== null) {
|
|
this._private__cell.removeChild(this._private__handle._internal_backgroundElement);
|
|
this._private__cell.removeChild(this._private__handle._internal_element);
|
|
this._private__handle = null;
|
|
}
|
|
if (this._private__mouseEventHandler !== null) {
|
|
this._private__mouseEventHandler._internal_destroy();
|
|
this._private__mouseEventHandler = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
_private__addResizableHandle() {
|
|
const backgroundElement = document.createElement('div');
|
|
const bgStyle = backgroundElement.style;
|
|
bgStyle.position = 'fixed';
|
|
bgStyle.display = 'none';
|
|
bgStyle.zIndex = '49';
|
|
bgStyle.top = '0';
|
|
bgStyle.left = '0';
|
|
bgStyle.width = '100%';
|
|
bgStyle.height = '100%';
|
|
bgStyle.cursor = 'row-resize';
|
|
this._private__cell.appendChild(backgroundElement);
|
|
const element = document.createElement('div');
|
|
const style = element.style;
|
|
style.position = 'absolute';
|
|
style.zIndex = '50';
|
|
style.top = '-4px';
|
|
style.height = '9px';
|
|
style.width = '100%';
|
|
style.backgroundColor = '';
|
|
style.cursor = 'row-resize';
|
|
this._private__cell.appendChild(element);
|
|
const handlers = {
|
|
_internal_mouseEnterEvent: this._private__mouseOverEvent.bind(this),
|
|
_internal_mouseLeaveEvent: this._private__mouseLeaveEvent.bind(this),
|
|
_internal_mouseDownEvent: this._private__mouseDownEvent.bind(this),
|
|
_internal_touchStartEvent: this._private__mouseDownEvent.bind(this),
|
|
_internal_pressedMouseMoveEvent: this._private__pressedMouseMoveEvent.bind(this),
|
|
_internal_touchMoveEvent: this._private__pressedMouseMoveEvent.bind(this),
|
|
_internal_mouseUpEvent: this._private__mouseUpEvent.bind(this),
|
|
_internal_touchEndEvent: this._private__mouseUpEvent.bind(this),
|
|
};
|
|
this._private__mouseEventHandler = new MouseEventHandler(element, handlers, {
|
|
_internal_treatVertTouchDragAsPageScroll: () => false,
|
|
_internal_treatHorzTouchDragAsPageScroll: () => true,
|
|
});
|
|
this._private__handle = { _internal_element: element, _internal_backgroundElement: backgroundElement };
|
|
}
|
|
_private__updateBorderColor() {
|
|
this._private__cell.style.background = this._private__chartWidget._internal_options()['layout'].panes.separatorColor;
|
|
}
|
|
_private__mouseOverEvent(event) {
|
|
if (this._private__handle !== null) {
|
|
this._private__handle._internal_element.style.backgroundColor = this._private__chartWidget._internal_options()['layout'].panes.separatorHoverColor;
|
|
}
|
|
}
|
|
_private__mouseLeaveEvent(event) {
|
|
if (this._private__handle !== null && this._private__resizeInfo === null) {
|
|
this._private__handle._internal_element.style.backgroundColor = '';
|
|
}
|
|
}
|
|
_private__mouseDownEvent(event) {
|
|
if (this._private__handle === null) {
|
|
return;
|
|
}
|
|
const totalStretch = this._private__topPane._internal_state()._internal_stretchFactor() + this._private__bottomPane._internal_state()._internal_stretchFactor();
|
|
const totalHeight = this._private__topPane._internal_getSize().height + this._private__bottomPane._internal_getSize().height;
|
|
const pixelStretchFactor = totalStretch / totalHeight;
|
|
const minPaneStretch = 30 /* SeparatorConstants.MinPaneHeight */ * pixelStretchFactor;
|
|
if (totalStretch <= minPaneStretch * 2) {
|
|
// cannot resize panes that already have less than minimal height
|
|
// that's possible if there are many panes on the chart
|
|
return;
|
|
}
|
|
this._private__resizeInfo = {
|
|
_internal_startY: event.pageY,
|
|
_internal_prevStretchTopPane: this._private__topPane._internal_state()._internal_stretchFactor(),
|
|
_internal_maxPaneStretch: totalStretch - minPaneStretch,
|
|
_internal_totalStretch: totalStretch,
|
|
_internal_pixelStretchFactor: pixelStretchFactor,
|
|
_internal_minPaneStretch: minPaneStretch,
|
|
};
|
|
this._private__handle._internal_backgroundElement.style.display = 'block';
|
|
}
|
|
_private__pressedMouseMoveEvent(event) {
|
|
const resizeInfo = this._private__resizeInfo;
|
|
if (resizeInfo === null) {
|
|
return;
|
|
}
|
|
const deltaY = event.pageY - resizeInfo._internal_startY;
|
|
const deltaStretchFactor = deltaY * resizeInfo._internal_pixelStretchFactor;
|
|
const upperPaneNewStretch = clamp(resizeInfo._internal_prevStretchTopPane + deltaStretchFactor, resizeInfo._internal_minPaneStretch, resizeInfo._internal_maxPaneStretch);
|
|
this._private__topPane._internal_state()._internal_setStretchFactor(upperPaneNewStretch);
|
|
this._private__bottomPane._internal_state()._internal_setStretchFactor(resizeInfo._internal_totalStretch - upperPaneNewStretch);
|
|
this._private__chartWidget._internal_model()._internal_fullUpdate();
|
|
}
|
|
_private__mouseUpEvent(event) {
|
|
if (this._private__resizeInfo === null || this._private__handle === null) {
|
|
return;
|
|
}
|
|
this._private__resizeInfo = null;
|
|
this._private__handle._internal_backgroundElement.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
function distanceBetweenPoints(pos1, pos2) {
|
|
return pos1._internal_position - pos2._internal_position;
|
|
}
|
|
function speedPxPerMSec(pos1, pos2, maxSpeed) {
|
|
const speed = (pos1._internal_position - pos2._internal_position) / (pos1._internal_time - pos2._internal_time);
|
|
return Math.sign(speed) * Math.min(Math.abs(speed), maxSpeed);
|
|
}
|
|
function durationMSec(speed, dumpingCoeff) {
|
|
const lnDumpingCoeff = Math.log(dumpingCoeff);
|
|
return Math.log((1 /* Constants.EpsilonDistance */ * lnDumpingCoeff) / -speed) / (lnDumpingCoeff);
|
|
}
|
|
class KineticAnimation {
|
|
constructor(minSpeed, maxSpeed, dumpingCoeff, minMove) {
|
|
this._private__position1 = null;
|
|
this._private__position2 = null;
|
|
this._private__position3 = null;
|
|
this._private__position4 = null;
|
|
this._private__animationStartPosition = null;
|
|
this._private__durationMsecs = 0;
|
|
this._private__speedPxPerMsec = 0;
|
|
this._private__minSpeed = minSpeed;
|
|
this._private__maxSpeed = maxSpeed;
|
|
this._private__dumpingCoeff = dumpingCoeff;
|
|
this._private__minMove = minMove;
|
|
}
|
|
_internal_addPosition(position, time) {
|
|
if (this._private__position1 !== null) {
|
|
if (this._private__position1._internal_time === time) {
|
|
this._private__position1._internal_position = position;
|
|
return;
|
|
}
|
|
if (Math.abs(this._private__position1._internal_position - position) < this._private__minMove) {
|
|
return;
|
|
}
|
|
}
|
|
this._private__position4 = this._private__position3;
|
|
this._private__position3 = this._private__position2;
|
|
this._private__position2 = this._private__position1;
|
|
this._private__position1 = { _internal_time: time, _internal_position: position };
|
|
}
|
|
_internal_start(position, time) {
|
|
if (this._private__position1 === null || this._private__position2 === null) {
|
|
return;
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
if (time - this._private__position1._internal_time > 50 /* Constants.MaxStartDelay */) {
|
|
return;
|
|
}
|
|
// To calculate all the rest parameters we should calculate the speed af first
|
|
let totalDistance = 0;
|
|
const speed1 = speedPxPerMSec(this._private__position1, this._private__position2, this._private__maxSpeed);
|
|
const distance1 = distanceBetweenPoints(this._private__position1, this._private__position2);
|
|
// We're calculating weighted average speed
|
|
// Than more distance for a segment, than more its weight
|
|
const speedItems = [speed1];
|
|
const distanceItems = [distance1];
|
|
totalDistance += distance1;
|
|
if (this._private__position3 !== null) {
|
|
const speed2 = speedPxPerMSec(this._private__position2, this._private__position3, this._private__maxSpeed);
|
|
// stop at this moment if direction of the segment is opposite
|
|
if (Math.sign(speed2) === Math.sign(speed1)) {
|
|
const distance2 = distanceBetweenPoints(this._private__position2, this._private__position3);
|
|
speedItems.push(speed2);
|
|
distanceItems.push(distance2);
|
|
totalDistance += distance2;
|
|
if (this._private__position4 !== null) {
|
|
const speed3 = speedPxPerMSec(this._private__position3, this._private__position4, this._private__maxSpeed);
|
|
if (Math.sign(speed3) === Math.sign(speed1)) {
|
|
const distance3 = distanceBetweenPoints(this._private__position3, this._private__position4);
|
|
speedItems.push(speed3);
|
|
distanceItems.push(distance3);
|
|
totalDistance += distance3;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let resultSpeed = 0;
|
|
for (let i = 0; i < speedItems.length; ++i) {
|
|
resultSpeed += distanceItems[i] / totalDistance * speedItems[i];
|
|
}
|
|
if (Math.abs(resultSpeed) < this._private__minSpeed) {
|
|
return;
|
|
}
|
|
this._private__animationStartPosition = { _internal_position: position, _internal_time: time };
|
|
this._private__speedPxPerMsec = resultSpeed;
|
|
this._private__durationMsecs = durationMSec(Math.abs(resultSpeed), this._private__dumpingCoeff);
|
|
}
|
|
_internal_getPosition(time) {
|
|
const startPosition = ensureNotNull(this._private__animationStartPosition);
|
|
const durationMsecs = time - startPosition._internal_time;
|
|
return startPosition._internal_position + this._private__speedPxPerMsec * (Math.pow(this._private__dumpingCoeff, durationMsecs) - 1) / (Math.log(this._private__dumpingCoeff));
|
|
}
|
|
_internal_finished(time) {
|
|
return this._private__animationStartPosition === null || this._private__progressDuration(time) === this._private__durationMsecs;
|
|
}
|
|
_private__progressDuration(time) {
|
|
const startPosition = ensureNotNull(this._private__animationStartPosition);
|
|
const progress = time - startPosition._internal_time;
|
|
return Math.min(progress, this._private__durationMsecs);
|
|
}
|
|
}
|
|
|
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="35" height="19" fill="none"><g fill-rule="evenodd" clip-path="url(#a)" clip-rule="evenodd"><path fill="var(--stroke)" d="M2 0H0v10h6v9h21.4l.5-1.3 6-15 1-2.7H23.7l-.5 1.3-.2.6a5 5 0 0 0-7-.9V0H2Zm20 17h4l5.2-13 .8-2h-7l-1 2.5-.2.5-1.5 3.8-.3.7V17Zm-.8-10a3 3 0 0 0 .7-2.7A3 3 0 1 0 16.8 7h4.4ZM14 7V2H2v6h6v9h4V7h2Z"/><path fill="var(--fill)" d="M14 2H2v6h6v9h6V2Zm12 15h-7l6-15h7l-6 15Zm-7-9a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"/></g><defs><clipPath id="a"><path fill="var(--stroke)" d="M0 0h35v19H0z"/></clipPath></defs></svg>`;
|
|
const css = `a#tv-attr-logo{--fill:#131722;--stroke:#fff;position:absolute;left:10px;bottom:10px;height:19px;width:35px;margin:0;padding:0;border:0;z-index:3;}a#tv-attr-logo[data-dark]{--fill:#D1D4DC;--stroke:#131722;}`;
|
|
// This widget doesn't support dynamically responding to options changes
|
|
// because it is expected that the `attributionLogo` option won't be changed
|
|
// and this saves some bundle size.
|
|
class AttributionLogoWidget {
|
|
constructor(container, chart) {
|
|
this._private__element = undefined;
|
|
this._private__cssElement = undefined;
|
|
this._private__theme = undefined;
|
|
this._private__visible = false;
|
|
this._private__container = container;
|
|
this._private__chart = chart;
|
|
this._private__render();
|
|
}
|
|
_internal_update() {
|
|
this._private__render();
|
|
}
|
|
_internal_removeElement() {
|
|
if (this._private__element) {
|
|
this._private__container.removeChild(this._private__element);
|
|
}
|
|
if (this._private__cssElement) {
|
|
this._private__container.removeChild(this._private__cssElement);
|
|
}
|
|
this._private__element = undefined;
|
|
this._private__cssElement = undefined;
|
|
}
|
|
_private__shouldUpdate() {
|
|
return this._private__visible !== this._private__shouldBeVisible() || this._private__theme !== this._private__themeToUse();
|
|
}
|
|
_private__themeToUse() {
|
|
return this._private__chart._internal_model()._internal_colorParser()._internal_colorStringToGrayscale(this._private__chart._internal_options()['layout'].textColor) > 160
|
|
? 'dark'
|
|
: 'light';
|
|
}
|
|
_private__shouldBeVisible() {
|
|
return this._private__chart._internal_options()['layout'].attributionLogo;
|
|
}
|
|
_private__getUTMSource() {
|
|
const url = new URL(location.href);
|
|
if (!url.hostname) {
|
|
// ignore local testing
|
|
return '';
|
|
}
|
|
return '&utm_source=' + url.hostname + url.pathname;
|
|
}
|
|
_private__render() {
|
|
if (!this._private__shouldUpdate()) {
|
|
return;
|
|
}
|
|
this._internal_removeElement();
|
|
this._private__visible = this._private__shouldBeVisible();
|
|
if (this._private__visible) {
|
|
this._private__theme = this._private__themeToUse();
|
|
this._private__cssElement = document.createElement('style');
|
|
this._private__cssElement.innerText = css;
|
|
this._private__element = document.createElement('a');
|
|
this._private__element.href = `https://www.tradingview.com/?utm_medium=lwc-link&utm_campaign=lwc-chart${this._private__getUTMSource()}`;
|
|
this._private__element.title = 'Charting by TradingView';
|
|
this._private__element.id = 'tv-attr-logo';
|
|
this._private__element.target = '_blank';
|
|
this._private__element.innerHTML = svg;
|
|
this._private__element.toggleAttribute('data-dark', this._private__theme === 'dark');
|
|
this._private__container.appendChild(this._private__cssElement);
|
|
this._private__container.appendChild(this._private__element);
|
|
}
|
|
}
|
|
}
|
|
|
|
function createBoundCanvas(parentElement, size) {
|
|
const doc = ensureNotNull(parentElement.ownerDocument);
|
|
const canvas = doc.createElement('canvas');
|
|
parentElement.appendChild(canvas);
|
|
const binding = bindTo(canvas, {
|
|
options: {
|
|
allowResizeObserver: true,
|
|
},
|
|
transform: (bitmapSize, canvasElementClientSize) => ({
|
|
width: Math.max(bitmapSize.width, canvasElementClientSize.width),
|
|
height: Math.max(bitmapSize.height, canvasElementClientSize.height),
|
|
}),
|
|
});
|
|
binding.resizeCanvasElement(size);
|
|
return binding;
|
|
}
|
|
function releaseCanvas(canvas) {
|
|
// This function fixes the iOS Safari error "Total canvas memory use exceeds the maximum limit".
|
|
// Seems that iOS Safari stores canvas elements for some additional time internally.
|
|
// So if we create/destroy a lot of canvas elements in a short period of time we can get this error.
|
|
// We resize the canvas to 1x1 pixels to force it to release memmory resources.
|
|
canvas.width = 1;
|
|
canvas.height = 1;
|
|
canvas.getContext('2d')?.clearRect(0, 0, 1, 1);
|
|
}
|
|
|
|
function drawBackground(renderer, target, isHovered, hitTestData) {
|
|
if (renderer._internal_drawBackground) {
|
|
renderer._internal_drawBackground(target, isHovered, hitTestData);
|
|
}
|
|
}
|
|
function drawForeground(renderer, target, isHovered, hitTestData) {
|
|
renderer._internal_draw(target, isHovered, hitTestData);
|
|
}
|
|
function drawSourceViews(paneViewsGetter, drawRendererFn, source, pane) {
|
|
const views = paneViewsGetter(source, pane);
|
|
for (const view of views) {
|
|
const renderer = view._internal_renderer(pane);
|
|
if (renderer !== null) {
|
|
drawRendererFn(renderer);
|
|
}
|
|
}
|
|
}
|
|
|
|
function hasPriceScale(source) {
|
|
return source._internal_priceScale !== undefined;
|
|
}
|
|
function buildPriceAxisViewsGetter(zOrder, priceScaleId) {
|
|
return (source) => {
|
|
if (!hasPriceScale(source)) {
|
|
return [];
|
|
}
|
|
const psId = source._internal_priceScale()?._internal_id() ?? '';
|
|
if (psId !== priceScaleId) {
|
|
// exclude if source is using a different price scale.
|
|
return [];
|
|
}
|
|
return source._internal_pricePaneViews?.(zOrder) ?? [];
|
|
};
|
|
}
|
|
function recalculateOverlapping(views, direction, scaleHeight, rendererOptions) {
|
|
if (!views.length) {
|
|
return;
|
|
}
|
|
let currentGroupStart = 0;
|
|
const initLabelHeight = views[0]._internal_height(rendererOptions, true);
|
|
let spaceBeforeCurrentGroup = direction === 1
|
|
? scaleHeight / 2 - (views[0]._internal_getRenderCoordinate() - initLabelHeight / 2)
|
|
: views[0]._internal_getRenderCoordinate() - initLabelHeight / 2 - scaleHeight / 2;
|
|
spaceBeforeCurrentGroup = Math.max(0, spaceBeforeCurrentGroup);
|
|
for (let i = 1; i < views.length; i++) {
|
|
const view = views[i];
|
|
const prev = views[i - 1];
|
|
const height = prev._internal_height(rendererOptions, false);
|
|
const coordinate = view._internal_getRenderCoordinate();
|
|
const prevCoordinate = prev._internal_getRenderCoordinate();
|
|
const overlap = direction === 1
|
|
? coordinate > prevCoordinate - height
|
|
: coordinate < prevCoordinate + height;
|
|
if (overlap) {
|
|
const renderCoordinate = prevCoordinate - height * direction;
|
|
view._internal_setRenderCoordinate(renderCoordinate);
|
|
const edgePoint = renderCoordinate - direction * height / 2;
|
|
const outOfViewport = direction === 1 ? edgePoint < 0 : edgePoint > scaleHeight;
|
|
if (outOfViewport && spaceBeforeCurrentGroup > 0) {
|
|
// shift the whole group up or down
|
|
const desiredGroupShift = direction === 1 ? -1 - edgePoint : edgePoint - scaleHeight;
|
|
const possibleShift = Math.min(desiredGroupShift, spaceBeforeCurrentGroup);
|
|
for (let k = currentGroupStart; k < views.length; k++) {
|
|
views[k]._internal_setRenderCoordinate(views[k]._internal_getRenderCoordinate() + direction * possibleShift);
|
|
}
|
|
spaceBeforeCurrentGroup -= possibleShift;
|
|
}
|
|
}
|
|
else {
|
|
currentGroupStart = i;
|
|
spaceBeforeCurrentGroup = direction === 1
|
|
? prevCoordinate - height - coordinate
|
|
: coordinate - (prevCoordinate + height);
|
|
}
|
|
}
|
|
}
|
|
function priceScaleCrosshairLabelVisible(crosshair) {
|
|
return crosshair.mode !== 2 /* CrosshairMode.Hidden */ && crosshair.horzLine.visible && crosshair.horzLine.labelVisible;
|
|
}
|
|
class PriceAxisWidget {
|
|
constructor(pane, options, rendererOptionsProvider, side) {
|
|
this._private__priceScale = null;
|
|
this._private__size = null;
|
|
this._private__mousedown = false;
|
|
this._private__widthCache = new TextWidthCache(200);
|
|
this._private__font = null;
|
|
this._private__prevOptimalWidth = 0;
|
|
this._private__isSettingSize = false;
|
|
this._private__canvasSuggestedBitmapSizeChangedHandler = () => {
|
|
if (this._private__isSettingSize) {
|
|
return;
|
|
}
|
|
this._private__pane._internal_chart()._internal_model()._internal_lightUpdate();
|
|
};
|
|
this._private__topCanvasSuggestedBitmapSizeChangedHandler = () => {
|
|
if (this._private__isSettingSize) {
|
|
return;
|
|
}
|
|
this._private__pane._internal_chart()._internal_model()._internal_lightUpdate();
|
|
};
|
|
this._private__pane = pane;
|
|
this._private__options = options;
|
|
this._private__layoutOptions = options['layout'];
|
|
this._private__rendererOptionsProvider = rendererOptionsProvider;
|
|
this._private__isLeft = side === 'left';
|
|
this._private__sourcePaneViews = buildPriceAxisViewsGetter('normal', side);
|
|
this._private__sourceTopPaneViews = buildPriceAxisViewsGetter('top', side);
|
|
this._private__sourceBottomPaneViews = buildPriceAxisViewsGetter('bottom', side);
|
|
this._private__cell = document.createElement('div');
|
|
this._private__cell.style.height = '100%';
|
|
this._private__cell.style.overflow = 'hidden';
|
|
this._private__cell.style.width = '25px';
|
|
this._private__cell.style.left = '0';
|
|
this._private__cell.style.position = 'relative';
|
|
this._private__canvasBinding = createBoundCanvas(this._private__cell, size$1({ width: 16, height: 16 }));
|
|
this._private__canvasBinding.subscribeSuggestedBitmapSizeChanged(this._private__canvasSuggestedBitmapSizeChangedHandler);
|
|
const canvas = this._private__canvasBinding.canvasElement;
|
|
canvas.style.position = 'absolute';
|
|
canvas.style.zIndex = '1';
|
|
canvas.style.left = '0';
|
|
canvas.style.top = '0';
|
|
this._private__topCanvasBinding = createBoundCanvas(this._private__cell, size$1({ width: 16, height: 16 }));
|
|
this._private__topCanvasBinding.subscribeSuggestedBitmapSizeChanged(this._private__topCanvasSuggestedBitmapSizeChangedHandler);
|
|
const topCanvas = this._private__topCanvasBinding.canvasElement;
|
|
topCanvas.style.position = 'absolute';
|
|
topCanvas.style.zIndex = '2';
|
|
topCanvas.style.left = '0';
|
|
topCanvas.style.top = '0';
|
|
const handler = {
|
|
_internal_mouseDownEvent: this._private__mouseDownEvent.bind(this),
|
|
_internal_touchStartEvent: this._private__mouseDownEvent.bind(this),
|
|
_internal_pressedMouseMoveEvent: this._private__pressedMouseMoveEvent.bind(this),
|
|
_internal_touchMoveEvent: this._private__pressedMouseMoveEvent.bind(this),
|
|
_internal_mouseDownOutsideEvent: this._private__mouseDownOutsideEvent.bind(this),
|
|
_internal_mouseUpEvent: this._private__mouseUpEvent.bind(this),
|
|
_internal_touchEndEvent: this._private__mouseUpEvent.bind(this),
|
|
_internal_mouseDoubleClickEvent: this._private__mouseDoubleClickEvent.bind(this),
|
|
_internal_doubleTapEvent: this._private__mouseDoubleClickEvent.bind(this),
|
|
_internal_mouseEnterEvent: this._private__mouseEnterEvent.bind(this),
|
|
_internal_mouseLeaveEvent: this._private__mouseLeaveEvent.bind(this),
|
|
};
|
|
this._private__mouseEventHandler = new MouseEventHandler(this._private__topCanvasBinding.canvasElement, handler, {
|
|
_internal_treatVertTouchDragAsPageScroll: () => !this._private__options['handleScroll'].vertTouchDrag,
|
|
_internal_treatHorzTouchDragAsPageScroll: () => true,
|
|
});
|
|
}
|
|
_internal_destroy() {
|
|
this._private__mouseEventHandler._internal_destroy();
|
|
this._private__topCanvasBinding.unsubscribeSuggestedBitmapSizeChanged(this._private__topCanvasSuggestedBitmapSizeChangedHandler);
|
|
releaseCanvas(this._private__topCanvasBinding.canvasElement);
|
|
this._private__topCanvasBinding.dispose();
|
|
this._private__canvasBinding.unsubscribeSuggestedBitmapSizeChanged(this._private__canvasSuggestedBitmapSizeChangedHandler);
|
|
releaseCanvas(this._private__canvasBinding.canvasElement);
|
|
this._private__canvasBinding.dispose();
|
|
if (this._private__priceScale !== null) {
|
|
this._private__priceScale._internal_onMarksChanged()._internal_unsubscribeAll(this);
|
|
}
|
|
this._private__priceScale = null;
|
|
}
|
|
_internal_getElement() {
|
|
return this._private__cell;
|
|
}
|
|
_internal_fontSize() {
|
|
return this._private__layoutOptions.fontSize;
|
|
}
|
|
_internal_rendererOptions() {
|
|
const options = this._private__rendererOptionsProvider._internal_options();
|
|
const isFontChanged = this._private__font !== options._internal_font;
|
|
if (isFontChanged) {
|
|
this._private__widthCache._internal_reset();
|
|
this._private__font = options._internal_font;
|
|
}
|
|
return options;
|
|
}
|
|
_internal_optimalWidth() {
|
|
if (this._private__priceScale === null) {
|
|
return 0;
|
|
}
|
|
let tickMarkMaxWidth = 0;
|
|
const rendererOptions = this._internal_rendererOptions();
|
|
const ctx = ensureNotNull(this._private__canvasBinding.canvasElement.getContext('2d', {
|
|
colorSpace: this._private__pane._internal_chart()._internal_options().layout.colorSpace,
|
|
}));
|
|
ctx.save();
|
|
const tickMarks = this._private__priceScale._internal_marks();
|
|
ctx.font = this._private__baseFont();
|
|
if (tickMarks.length > 0) {
|
|
tickMarkMaxWidth = Math.max(this._private__widthCache._internal_measureText(ctx, tickMarks[0]._internal_label), this._private__widthCache._internal_measureText(ctx, tickMarks[tickMarks.length - 1]._internal_label));
|
|
}
|
|
const views = this._private__backLabels();
|
|
for (let j = views.length; j--;) {
|
|
const width = this._private__widthCache._internal_measureText(ctx, views[j]._internal_text());
|
|
if (width > tickMarkMaxWidth) {
|
|
tickMarkMaxWidth = width;
|
|
}
|
|
}
|
|
const firstValue = this._private__priceScale._internal_firstValue();
|
|
if (firstValue !== null &&
|
|
this._private__size !== null &&
|
|
priceScaleCrosshairLabelVisible(this._private__options.crosshair)) {
|
|
const topValue = this._private__priceScale._internal_coordinateToPrice(1, firstValue);
|
|
const bottomValue = this._private__priceScale._internal_coordinateToPrice(this._private__size.height - 2, firstValue);
|
|
tickMarkMaxWidth = Math.max(tickMarkMaxWidth, this._private__widthCache._internal_measureText(ctx, this._private__priceScale._internal_formatPrice(Math.floor(Math.min(topValue, bottomValue)) + 0.11111111111111, firstValue)), this._private__widthCache._internal_measureText(ctx, this._private__priceScale._internal_formatPrice(Math.ceil(Math.max(topValue, bottomValue)) - 0.11111111111111, firstValue)));
|
|
}
|
|
ctx.restore();
|
|
const resultTickMarksMaxWidth = tickMarkMaxWidth || 34 /* Constants.DefaultOptimalWidth */;
|
|
const res = Math.ceil(rendererOptions._internal_borderSize +
|
|
rendererOptions._internal_tickLength +
|
|
rendererOptions._internal_paddingInner +
|
|
rendererOptions._internal_paddingOuter +
|
|
5 /* Constants.LabelOffset */ +
|
|
resultTickMarksMaxWidth);
|
|
// make it even, remove this after migration to perfect fancy canvas
|
|
return suggestPriceScaleWidth(res);
|
|
}
|
|
_internal_setSize(newSize) {
|
|
if (this._private__size === null || !equalSizes(this._private__size, newSize)) {
|
|
this._private__size = newSize;
|
|
this._private__isSettingSize = true;
|
|
this._private__canvasBinding.resizeCanvasElement(newSize);
|
|
this._private__topCanvasBinding.resizeCanvasElement(newSize);
|
|
this._private__isSettingSize = false;
|
|
this._private__cell.style.width = `${newSize.width}px`;
|
|
this._private__cell.style.height = `${newSize.height}px`;
|
|
}
|
|
}
|
|
_internal_getWidth() {
|
|
return ensureNotNull(this._private__size).width;
|
|
}
|
|
_internal_setPriceScale(priceScale) {
|
|
if (this._private__priceScale === priceScale) {
|
|
return;
|
|
}
|
|
if (this._private__priceScale !== null) {
|
|
this._private__priceScale._internal_onMarksChanged()._internal_unsubscribeAll(this);
|
|
}
|
|
this._private__priceScale = priceScale;
|
|
priceScale._internal_onMarksChanged()._internal_subscribe(this._private__onMarksChanged.bind(this), this);
|
|
}
|
|
_internal_priceScale() {
|
|
return this._private__priceScale;
|
|
}
|
|
_internal_reset() {
|
|
const pane = this._private__pane._internal_state();
|
|
const model = this._private__pane._internal_chart()._internal_model();
|
|
model._internal_resetPriceScale(pane, ensureNotNull(this._internal_priceScale()));
|
|
}
|
|
_internal_paint(type) {
|
|
if (this._private__size === null) {
|
|
return;
|
|
}
|
|
const canvasOptions = {
|
|
colorSpace: this._private__pane._internal_chart()._internal_options().layout.colorSpace,
|
|
};
|
|
if (type !== 1 /* InvalidationLevel.Cursor */) {
|
|
this._private__alignLabels();
|
|
this._private__canvasBinding.applySuggestedBitmapSize();
|
|
const target = tryCreateCanvasRenderingTarget2D(this._private__canvasBinding, canvasOptions);
|
|
if (target !== null) {
|
|
target.useBitmapCoordinateSpace((scope) => {
|
|
this._private__drawBackground(scope);
|
|
this._private__drawBorder(scope);
|
|
});
|
|
this._private__pane._internal_drawAdditionalSources(target, this._private__sourceBottomPaneViews);
|
|
this._private__drawTickMarks(target);
|
|
this._private__pane._internal_drawAdditionalSources(target, this._private__sourcePaneViews);
|
|
this._private__drawBackLabels(target);
|
|
}
|
|
}
|
|
this._private__topCanvasBinding.applySuggestedBitmapSize();
|
|
const topTarget = tryCreateCanvasRenderingTarget2D(this._private__topCanvasBinding, canvasOptions);
|
|
if (topTarget !== null) {
|
|
topTarget.useBitmapCoordinateSpace(({ context: ctx, bitmapSize }) => {
|
|
ctx.clearRect(0, 0, bitmapSize.width, bitmapSize.height);
|
|
});
|
|
this._private__drawCrosshairLabel(topTarget);
|
|
this._private__pane._internal_drawAdditionalSources(topTarget, this._private__sourceTopPaneViews);
|
|
}
|
|
}
|
|
_internal_getBitmapSize() {
|
|
return this._private__canvasBinding.bitmapSize;
|
|
}
|
|
_internal_drawBitmap(ctx, x, y, addTopLayer) {
|
|
const bitmapSize = this._internal_getBitmapSize();
|
|
if (bitmapSize.width > 0 && bitmapSize.height > 0) {
|
|
ctx.drawImage(this._private__canvasBinding.canvasElement, x, y);
|
|
if (addTopLayer) {
|
|
const topLayer = this._private__topCanvasBinding.canvasElement;
|
|
ctx.drawImage(topLayer, x, y);
|
|
}
|
|
}
|
|
}
|
|
_internal_update() {
|
|
// this call has side-effect - it regenerates marks on the price scale
|
|
this._private__priceScale?._internal_marks();
|
|
}
|
|
_private__mouseDownEvent(e) {
|
|
if (this._private__priceScale === null || this._private__priceScale._internal_isEmpty() || !this._private__options['handleScale'].axisPressedMouseMove.price) {
|
|
return;
|
|
}
|
|
const model = this._private__pane._internal_chart()._internal_model();
|
|
const pane = this._private__pane._internal_state();
|
|
this._private__mousedown = true;
|
|
model._internal_startScalePrice(pane, this._private__priceScale, e.localY);
|
|
}
|
|
_private__pressedMouseMoveEvent(e) {
|
|
if (this._private__priceScale === null || !this._private__options['handleScale'].axisPressedMouseMove.price) {
|
|
return;
|
|
}
|
|
const model = this._private__pane._internal_chart()._internal_model();
|
|
const pane = this._private__pane._internal_state();
|
|
const priceScale = this._private__priceScale;
|
|
model._internal_scalePriceTo(pane, priceScale, e.localY);
|
|
}
|
|
_private__mouseDownOutsideEvent() {
|
|
if (this._private__priceScale === null || !this._private__options['handleScale'].axisPressedMouseMove.price) {
|
|
return;
|
|
}
|
|
const model = this._private__pane._internal_chart()._internal_model();
|
|
const pane = this._private__pane._internal_state();
|
|
const priceScale = this._private__priceScale;
|
|
if (this._private__mousedown) {
|
|
this._private__mousedown = false;
|
|
model._internal_endScalePrice(pane, priceScale);
|
|
}
|
|
}
|
|
_private__mouseUpEvent(e) {
|
|
if (this._private__priceScale === null || !this._private__options['handleScale'].axisPressedMouseMove.price) {
|
|
return;
|
|
}
|
|
const model = this._private__pane._internal_chart()._internal_model();
|
|
const pane = this._private__pane._internal_state();
|
|
this._private__mousedown = false;
|
|
model._internal_endScalePrice(pane, this._private__priceScale);
|
|
}
|
|
_private__mouseDoubleClickEvent(e) {
|
|
if (this._private__options['handleScale'].axisDoubleClickReset.price) {
|
|
this._internal_reset();
|
|
}
|
|
}
|
|
_private__mouseEnterEvent(e) {
|
|
if (this._private__priceScale === null) {
|
|
return;
|
|
}
|
|
const model = this._private__pane._internal_chart()._internal_model();
|
|
if (model._internal_options()['handleScale'].axisPressedMouseMove.price && !this._private__priceScale._internal_isPercentage() && !this._private__priceScale._internal_isIndexedTo100()) {
|
|
this._private__setCursor(1 /* CursorType.NsResize */);
|
|
}
|
|
}
|
|
_private__mouseLeaveEvent(e) {
|
|
this._private__setCursor(0 /* CursorType.Default */);
|
|
}
|
|
_private__backLabels() {
|
|
const res = [];
|
|
const priceScale = (this._private__priceScale === null) ? undefined : this._private__priceScale;
|
|
const addViewsForSources = (sources) => {
|
|
for (let i = 0; i < sources.length; ++i) {
|
|
const source = sources[i];
|
|
const views = source._internal_priceAxisViews(this._private__pane._internal_state(), priceScale);
|
|
for (let j = 0; j < views.length; j++) {
|
|
res.push(views[j]);
|
|
}
|
|
}
|
|
};
|
|
// calculate max and min coordinates for views on selection
|
|
// crosshair individually
|
|
addViewsForSources(this._private__pane._internal_state()._internal_orderedSources());
|
|
return res;
|
|
}
|
|
_private__drawBackground({ context: ctx, bitmapSize }) {
|
|
const { width, height } = bitmapSize;
|
|
const model = this._private__pane._internal_state()._internal_model();
|
|
const topColor = model._internal_backgroundTopColor();
|
|
const bottomColor = model._internal_backgroundBottomColor();
|
|
if (topColor === bottomColor) {
|
|
clearRect(ctx, 0, 0, width, height, topColor);
|
|
}
|
|
else {
|
|
clearRectWithGradient(ctx, 0, 0, width, height, topColor, bottomColor);
|
|
}
|
|
}
|
|
_private__drawBorder({ context: ctx, bitmapSize, horizontalPixelRatio }) {
|
|
if (this._private__size === null || this._private__priceScale === null || !this._private__priceScale._internal_options().borderVisible) {
|
|
return;
|
|
}
|
|
ctx.fillStyle = this._private__priceScale._internal_options().borderColor;
|
|
const borderSize = Math.max(1, Math.floor(this._internal_rendererOptions()._internal_borderSize * horizontalPixelRatio));
|
|
let left;
|
|
if (this._private__isLeft) {
|
|
left = bitmapSize.width - borderSize;
|
|
}
|
|
else {
|
|
left = 0;
|
|
}
|
|
ctx.fillRect(left, 0, borderSize, bitmapSize.height);
|
|
}
|
|
_private__drawTickMarks(target) {
|
|
if (this._private__size === null || this._private__priceScale === null) {
|
|
return;
|
|
}
|
|
const tickMarks = this._private__priceScale._internal_marks();
|
|
const priceScaleOptions = this._private__priceScale._internal_options();
|
|
const rendererOptions = this._internal_rendererOptions();
|
|
const tickMarkLeftX = this._private__isLeft ?
|
|
(this._private__size.width - rendererOptions._internal_tickLength) :
|
|
0;
|
|
if (priceScaleOptions.borderVisible && priceScaleOptions.ticksVisible) {
|
|
target.useBitmapCoordinateSpace(({ context: ctx, horizontalPixelRatio, verticalPixelRatio }) => {
|
|
ctx.fillStyle = priceScaleOptions.borderColor;
|
|
const tickHeight = Math.max(1, Math.floor(verticalPixelRatio));
|
|
const tickOffset = Math.floor(verticalPixelRatio * 0.5);
|
|
const tickLength = Math.round(rendererOptions._internal_tickLength * horizontalPixelRatio);
|
|
ctx.beginPath();
|
|
for (const tickMark of tickMarks) {
|
|
ctx.rect(Math.floor(tickMarkLeftX * horizontalPixelRatio), Math.round(tickMark._internal_coord * verticalPixelRatio) - tickOffset, tickLength, tickHeight);
|
|
}
|
|
ctx.fill();
|
|
});
|
|
}
|
|
target.useMediaCoordinateSpace(({ context: ctx }) => {
|
|
ctx.font = this._private__baseFont();
|
|
ctx.fillStyle = priceScaleOptions.textColor ?? this._private__layoutOptions.textColor;
|
|
ctx.textAlign = this._private__isLeft ? 'right' : 'left';
|
|
ctx.textBaseline = 'middle';
|
|
const textLeftX = this._private__isLeft ?
|
|
Math.round(tickMarkLeftX - rendererOptions._internal_paddingInner) :
|
|
Math.round(tickMarkLeftX + rendererOptions._internal_tickLength + rendererOptions._internal_paddingInner);
|
|
const yMidCorrections = tickMarks.map((mark) => this._private__widthCache._internal_yMidCorrection(ctx, mark._internal_label));
|
|
for (let i = tickMarks.length; i--;) {
|
|
const tickMark = tickMarks[i];
|
|
ctx.fillText(tickMark._internal_label, textLeftX, tickMark._internal_coord + yMidCorrections[i]);
|
|
}
|
|
});
|
|
}
|
|
_private__alignLabels() {
|
|
if (this._private__size === null || this._private__priceScale === null) {
|
|
return;
|
|
}
|
|
let center = this._private__size.height / 2;
|
|
const views = [];
|
|
const orderedSources = this._private__priceScale._internal_orderedSources().slice(); // Copy of array
|
|
const pane = this._private__pane;
|
|
const paneState = pane._internal_state();
|
|
const rendererOptions = this._internal_rendererOptions();
|
|
// if we are default price scale, append labels from no-scale
|
|
const isDefault = this._private__priceScale === paneState._internal_defaultVisiblePriceScale();
|
|
if (isDefault) {
|
|
this._private__pane._internal_state()._internal_orderedSources().forEach((source) => {
|
|
if (paneState._internal_isOverlay(source)) {
|
|
orderedSources.push(source);
|
|
}
|
|
});
|
|
}
|
|
// we can use any, but let's use the first source as "center" one
|
|
const centerSource = this._private__priceScale._internal_dataSources()[0];
|
|
const priceScale = this._private__priceScale;
|
|
const updateForSources = (sources) => {
|
|
sources.forEach((source) => {
|
|
const sourceViews = source._internal_priceAxisViews(paneState, priceScale);
|
|
// never align selected sources
|
|
sourceViews.forEach((view) => {
|
|
if (view._internal_isVisible() && view._internal_getFixedCoordinate() === null) {
|
|
view._internal_setRenderCoordinate(null); // Clear the previous render coordinate
|
|
views.push(view);
|
|
}
|
|
});
|
|
if (centerSource === source && sourceViews.length > 0) {
|
|
center = sourceViews[0]._internal_coordinate();
|
|
}
|
|
});
|
|
};
|
|
// crosshair individually
|
|
updateForSources(orderedSources);
|
|
const options = this._private__priceScale._internal_options();
|
|
if (!options.alignLabels) {
|
|
return;
|
|
}
|
|
this._private__fixLabelOverlap(views, rendererOptions, center);
|
|
}
|
|
_private__fixLabelOverlap(views, rendererOptions, center) {
|
|
if (this._private__size === null) {
|
|
return;
|
|
}
|
|
// split into two parts
|
|
const top = views.filter((view) => view._internal_coordinate() <= center);
|
|
const bottom = views.filter((view) => view._internal_coordinate() > center);
|
|
// sort top from center to top
|
|
top.sort((l, r) => r._internal_coordinate() - l._internal_coordinate());
|
|
// share center label
|
|
if (top.length && bottom.length) {
|
|
bottom.push(top[0]);
|
|
}
|
|
bottom.sort((l, r) => l._internal_coordinate() - r._internal_coordinate());
|
|
for (const view of views) {
|
|
const halfHeight = Math.floor(view._internal_height(rendererOptions) / 2);
|
|
const coordinate = view._internal_coordinate();
|
|
if (coordinate > -halfHeight && coordinate < halfHeight) {
|
|
view._internal_setRenderCoordinate(halfHeight);
|
|
}
|
|
if (coordinate > (this._private__size.height - halfHeight) && coordinate < this._private__size.height + halfHeight) {
|
|
view._internal_setRenderCoordinate(this._private__size.height - halfHeight);
|
|
}
|
|
}
|
|
recalculateOverlapping(top, 1, this._private__size.height, rendererOptions);
|
|
recalculateOverlapping(bottom, -1, this._private__size.height, rendererOptions);
|
|
}
|
|
_private__drawBackLabels(target) {
|
|
if (this._private__size === null) {
|
|
return;
|
|
}
|
|
const views = this._private__backLabels();
|
|
const rendererOptions = this._internal_rendererOptions();
|
|
const align = this._private__isLeft ? 'right' : 'left';
|
|
views.forEach((view) => {
|
|
if (view._internal_isAxisLabelVisible()) {
|
|
const renderer = view._internal_renderer(ensureNotNull(this._private__priceScale));
|
|
renderer._internal_draw(target, rendererOptions, this._private__widthCache, align);
|
|
}
|
|
});
|
|
}
|
|
_private__drawCrosshairLabel(target) {
|
|
if (this._private__size === null || this._private__priceScale === null) {
|
|
return;
|
|
}
|
|
const model = this._private__pane._internal_chart()._internal_model();
|
|
const views = []; // array of arrays
|
|
const pane = this._private__pane._internal_state();
|
|
const v = model._internal_crosshairSource()._internal_priceAxisViews(pane, this._private__priceScale);
|
|
if (v.length) {
|
|
views.push(v);
|
|
}
|
|
const ro = this._internal_rendererOptions();
|
|
const align = this._private__isLeft ? 'right' : 'left';
|
|
views.forEach((arr) => {
|
|
arr.forEach((view) => {
|
|
view._internal_renderer(ensureNotNull(this._private__priceScale))._internal_draw(target, ro, this._private__widthCache, align);
|
|
});
|
|
});
|
|
}
|
|
_private__setCursor(type) {
|
|
this._private__cell.style.cursor = type === 1 /* CursorType.NsResize */ ? 'ns-resize' : 'default';
|
|
}
|
|
_private__onMarksChanged() {
|
|
const width = this._internal_optimalWidth();
|
|
// avoid price scale is shrunk
|
|
// using < instead !== to avoid infinite changes
|
|
if (this._private__prevOptimalWidth < width) {
|
|
this._private__pane._internal_chart()._internal_model()._internal_fullUpdate();
|
|
}
|
|
this._private__prevOptimalWidth = width;
|
|
}
|
|
_private__baseFont() {
|
|
return makeFont(this._private__layoutOptions.fontSize, this._private__layoutOptions.fontFamily);
|
|
}
|
|
}
|
|
|
|
function sourceBottomPaneViews$1(source, pane) {
|
|
return source._internal_bottomPaneViews?.(pane) ?? [];
|
|
}
|
|
function sourcePaneViews$1(source, pane) {
|
|
return source._internal_paneViews?.(pane) ?? [];
|
|
}
|
|
function sourceLabelPaneViews(source, pane) {
|
|
return source._internal_labelPaneViews?.(pane) ?? [];
|
|
}
|
|
function sourceTopPaneViews$1(source, pane) {
|
|
return source._internal_topPaneViews?.(pane) ?? [];
|
|
}
|
|
class PaneWidget {
|
|
constructor(chart, state) {
|
|
this._private__size = size$1({ width: 0, height: 0 });
|
|
this._private__leftPriceAxisWidget = null;
|
|
this._private__rightPriceAxisWidget = null;
|
|
this._private__attributionLogoWidget = null;
|
|
this._private__startScrollingPos = null;
|
|
this._private__isScrolling = false;
|
|
this._private__clicked = new Delegate();
|
|
this._private__dblClicked = new Delegate();
|
|
this._private__prevPinchScale = 0;
|
|
this._private__longTap = false;
|
|
this._private__startTrackPoint = null;
|
|
this._private__exitTrackingModeOnNextTry = false;
|
|
this._private__initCrosshairPosition = null;
|
|
this._private__scrollXAnimation = null;
|
|
this._private__isSettingSize = false;
|
|
this._private__canvasSuggestedBitmapSizeChangedHandler = () => {
|
|
if (this._private__isSettingSize || this._private__state === null) {
|
|
return;
|
|
}
|
|
this._private__model()._internal_lightUpdate();
|
|
};
|
|
this._private__topCanvasSuggestedBitmapSizeChangedHandler = () => {
|
|
if (this._private__isSettingSize || this._private__state === null) {
|
|
return;
|
|
}
|
|
this._private__model()._internal_lightUpdate();
|
|
};
|
|
this._private__chart = chart;
|
|
this._private__state = state;
|
|
this._private__state._internal_onDestroyed()._internal_subscribe(this._private__onStateDestroyed.bind(this), this, true);
|
|
this._private__paneCell = document.createElement('td');
|
|
this._private__paneCell.style.padding = '0';
|
|
this._private__paneCell.style.position = 'relative';
|
|
const paneWrapper = document.createElement('div');
|
|
paneWrapper.style.width = '100%';
|
|
paneWrapper.style.height = '100%';
|
|
paneWrapper.style.position = 'relative';
|
|
paneWrapper.style.overflow = 'hidden';
|
|
this._private__leftAxisCell = document.createElement('td');
|
|
this._private__leftAxisCell.style.padding = '0';
|
|
this._private__rightAxisCell = document.createElement('td');
|
|
this._private__rightAxisCell.style.padding = '0';
|
|
this._private__paneCell.appendChild(paneWrapper);
|
|
this._private__canvasBinding = createBoundCanvas(paneWrapper, size$1({ width: 16, height: 16 }));
|
|
this._private__canvasBinding.subscribeSuggestedBitmapSizeChanged(this._private__canvasSuggestedBitmapSizeChangedHandler);
|
|
const canvas = this._private__canvasBinding.canvasElement;
|
|
canvas.style.position = 'absolute';
|
|
canvas.style.zIndex = '1';
|
|
canvas.style.left = '0';
|
|
canvas.style.top = '0';
|
|
this._private__topCanvasBinding = createBoundCanvas(paneWrapper, size$1({ width: 16, height: 16 }));
|
|
this._private__topCanvasBinding.subscribeSuggestedBitmapSizeChanged(this._private__topCanvasSuggestedBitmapSizeChangedHandler);
|
|
const topCanvas = this._private__topCanvasBinding.canvasElement;
|
|
topCanvas.style.position = 'absolute';
|
|
topCanvas.style.zIndex = '2';
|
|
topCanvas.style.left = '0';
|
|
topCanvas.style.top = '0';
|
|
this._private__rowElement = document.createElement('tr');
|
|
this._private__rowElement.appendChild(this._private__leftAxisCell);
|
|
this._private__rowElement.appendChild(this._private__paneCell);
|
|
this._private__rowElement.appendChild(this._private__rightAxisCell);
|
|
this._internal_updatePriceAxisWidgetsStates();
|
|
this._private__mouseEventHandler = new MouseEventHandler(this._private__topCanvasBinding.canvasElement, this, {
|
|
_internal_treatVertTouchDragAsPageScroll: () => this._private__startTrackPoint === null && !this._private__chart._internal_options()['handleScroll'].vertTouchDrag,
|
|
_internal_treatHorzTouchDragAsPageScroll: () => this._private__startTrackPoint === null && !this._private__chart._internal_options()['handleScroll'].horzTouchDrag,
|
|
});
|
|
}
|
|
_internal_destroy() {
|
|
if (this._private__leftPriceAxisWidget !== null) {
|
|
this._private__leftPriceAxisWidget._internal_destroy();
|
|
}
|
|
if (this._private__rightPriceAxisWidget !== null) {
|
|
this._private__rightPriceAxisWidget._internal_destroy();
|
|
}
|
|
this._private__attributionLogoWidget = null;
|
|
this._private__topCanvasBinding.unsubscribeSuggestedBitmapSizeChanged(this._private__topCanvasSuggestedBitmapSizeChangedHandler);
|
|
releaseCanvas(this._private__topCanvasBinding.canvasElement);
|
|
this._private__topCanvasBinding.dispose();
|
|
this._private__canvasBinding.unsubscribeSuggestedBitmapSizeChanged(this._private__canvasSuggestedBitmapSizeChangedHandler);
|
|
releaseCanvas(this._private__canvasBinding.canvasElement);
|
|
this._private__canvasBinding.dispose();
|
|
if (this._private__state !== null) {
|
|
this._private__state._internal_onDestroyed()._internal_unsubscribeAll(this);
|
|
this._private__state._internal_destroy();
|
|
}
|
|
this._private__mouseEventHandler._internal_destroy();
|
|
}
|
|
_internal_state() {
|
|
return ensureNotNull(this._private__state);
|
|
}
|
|
_internal_setState(pane) {
|
|
if (this._private__state !== null) {
|
|
this._private__state._internal_onDestroyed()._internal_unsubscribeAll(this);
|
|
}
|
|
this._private__state = pane;
|
|
if (this._private__state !== null) {
|
|
this._private__state._internal_onDestroyed()._internal_subscribe(PaneWidget.prototype._private__onStateDestroyed.bind(this), this, true);
|
|
}
|
|
this._internal_updatePriceAxisWidgetsStates();
|
|
if (this._private__chart._internal_paneWidgets().indexOf(this) === this._private__chart._internal_paneWidgets().length - 1) {
|
|
this._private__attributionLogoWidget = this._private__attributionLogoWidget ?? new AttributionLogoWidget(this._private__paneCell, this._private__chart);
|
|
this._private__attributionLogoWidget._internal_update();
|
|
}
|
|
else {
|
|
this._private__attributionLogoWidget?._internal_removeElement();
|
|
this._private__attributionLogoWidget = null;
|
|
}
|
|
}
|
|
_internal_chart() {
|
|
return this._private__chart;
|
|
}
|
|
_internal_getElement() {
|
|
return this._private__rowElement;
|
|
}
|
|
_internal_updatePriceAxisWidgetsStates() {
|
|
if (this._private__state === null) {
|
|
return;
|
|
}
|
|
this._private__recreatePriceAxisWidgets();
|
|
if (this._private__model()._internal_serieses().length === 0) {
|
|
return;
|
|
}
|
|
if (this._private__leftPriceAxisWidget !== null) {
|
|
const leftPriceScale = this._private__state._internal_leftPriceScale();
|
|
this._private__leftPriceAxisWidget._internal_setPriceScale(ensureNotNull(leftPriceScale));
|
|
}
|
|
if (this._private__rightPriceAxisWidget !== null) {
|
|
const rightPriceScale = this._private__state._internal_rightPriceScale();
|
|
this._private__rightPriceAxisWidget._internal_setPriceScale(ensureNotNull(rightPriceScale));
|
|
}
|
|
}
|
|
_internal_updatePriceAxisWidgets() {
|
|
if (this._private__leftPriceAxisWidget !== null) {
|
|
this._private__leftPriceAxisWidget._internal_update();
|
|
}
|
|
if (this._private__rightPriceAxisWidget !== null) {
|
|
this._private__rightPriceAxisWidget._internal_update();
|
|
}
|
|
}
|
|
_internal_stretchFactor() {
|
|
return this._private__state !== null ? this._private__state._internal_stretchFactor() : 0;
|
|
}
|
|
_internal_setStretchFactor(stretchFactor) {
|
|
if (this._private__state) {
|
|
this._private__state._internal_setStretchFactor(stretchFactor);
|
|
}
|
|
}
|
|
_internal_mouseEnterEvent(event) {
|
|
if (!this._private__state) {
|
|
return;
|
|
}
|
|
this._private__onMouseEvent();
|
|
const x = event.localX;
|
|
const y = event.localY;
|
|
this._private__setCrosshairPosition(x, y, event);
|
|
}
|
|
_internal_mouseDownEvent(event) {
|
|
this._private__onMouseEvent();
|
|
this._private__mouseTouchDownEvent();
|
|
this._private__setCrosshairPosition(event.localX, event.localY, event);
|
|
}
|
|
_internal_mouseMoveEvent(event) {
|
|
if (!this._private__state) {
|
|
return;
|
|
}
|
|
this._private__onMouseEvent();
|
|
const x = event.localX;
|
|
const y = event.localY;
|
|
this._private__setCrosshairPosition(x, y, event);
|
|
}
|
|
_internal_mouseClickEvent(event) {
|
|
if (this._private__state === null) {
|
|
return;
|
|
}
|
|
this._private__onMouseEvent();
|
|
this._private__setCrosshairPosition(event.localX, event.localY, event);
|
|
this._private__fireClickedDelegate(event);
|
|
}
|
|
_internal_mouseDoubleClickEvent(event) {
|
|
if (this._private__state === null) {
|
|
return;
|
|
}
|
|
this._private__fireMouseClickDelegate(this._private__dblClicked, event);
|
|
}
|
|
_internal_doubleTapEvent(event) {
|
|
this._internal_mouseDoubleClickEvent(event);
|
|
}
|
|
_internal_pressedMouseMoveEvent(event) {
|
|
this._private__onMouseEvent();
|
|
this._private__pressedMouseTouchMoveEvent(event);
|
|
this._private__setCrosshairPosition(event.localX, event.localY, event);
|
|
}
|
|
_internal_mouseUpEvent(event) {
|
|
if (this._private__state === null) {
|
|
return;
|
|
}
|
|
this._private__onMouseEvent();
|
|
this._private__longTap = false;
|
|
this._private__endScroll(event);
|
|
}
|
|
_internal_tapEvent(event) {
|
|
if (this._private__state === null) {
|
|
return;
|
|
}
|
|
this._private__fireClickedDelegate(event);
|
|
}
|
|
_internal_longTapEvent(event) {
|
|
this._private__longTap = true;
|
|
if (this._private__startTrackPoint === null) {
|
|
const point = { x: event.localX, y: event.localY };
|
|
this._private__startTrackingMode(point, point, event);
|
|
}
|
|
}
|
|
_internal_mouseLeaveEvent(event) {
|
|
if (this._private__state === null) {
|
|
return;
|
|
}
|
|
this._private__onMouseEvent();
|
|
this._private__state._internal_model()._internal_setHoveredSource(null);
|
|
this._private__clearCrosshairPosition();
|
|
}
|
|
_internal_clicked() {
|
|
return this._private__clicked;
|
|
}
|
|
_internal_dblClicked() {
|
|
return this._private__dblClicked;
|
|
}
|
|
_internal_pinchStartEvent() {
|
|
this._private__prevPinchScale = 1;
|
|
this._private__model()._internal_stopTimeScaleAnimation();
|
|
}
|
|
_internal_pinchEvent(middlePoint, scale) {
|
|
if (!this._private__chart._internal_options()['handleScale'].pinch) {
|
|
return;
|
|
}
|
|
const zoomScale = (scale - this._private__prevPinchScale) * 5;
|
|
this._private__prevPinchScale = scale;
|
|
this._private__model()._internal_zoomTime(middlePoint._internal_x, zoomScale);
|
|
}
|
|
_internal_touchStartEvent(event) {
|
|
this._private__longTap = false;
|
|
this._private__exitTrackingModeOnNextTry = this._private__startTrackPoint !== null;
|
|
this._private__mouseTouchDownEvent();
|
|
const crosshair = this._private__model()._internal_crosshairSource();
|
|
if (this._private__startTrackPoint !== null && crosshair._internal_visible()) {
|
|
this._private__initCrosshairPosition = { x: crosshair._internal_appliedX(), y: crosshair._internal_appliedY() };
|
|
this._private__startTrackPoint = { x: event.localX, y: event.localY };
|
|
}
|
|
}
|
|
_internal_touchMoveEvent(event) {
|
|
if (this._private__state === null) {
|
|
return;
|
|
}
|
|
const x = event.localX;
|
|
const y = event.localY;
|
|
if (this._private__startTrackPoint !== null) {
|
|
// tracking mode: move crosshair
|
|
this._private__exitTrackingModeOnNextTry = false;
|
|
const origPoint = ensureNotNull(this._private__initCrosshairPosition);
|
|
const newX = origPoint.x + (x - this._private__startTrackPoint.x);
|
|
const newY = origPoint.y + (y - this._private__startTrackPoint.y);
|
|
this._private__setCrosshairPosition(newX, newY, event);
|
|
return;
|
|
}
|
|
this._private__pressedMouseTouchMoveEvent(event);
|
|
}
|
|
_internal_touchEndEvent(event) {
|
|
if (this._internal_chart()._internal_options().trackingMode.exitMode === 0 /* TrackingModeExitMode.OnTouchEnd */) {
|
|
this._private__exitTrackingModeOnNextTry = true;
|
|
}
|
|
this._private__tryExitTrackingMode();
|
|
this._private__endScroll(event);
|
|
}
|
|
_internal_hitTest(x, y) {
|
|
const state = this._private__state;
|
|
if (state === null) {
|
|
return null;
|
|
}
|
|
return hitTestPane(state, x, y);
|
|
}
|
|
_internal_setPriceAxisSize(width, position) {
|
|
const priceAxisWidget = position === 'left' ? this._private__leftPriceAxisWidget : this._private__rightPriceAxisWidget;
|
|
ensureNotNull(priceAxisWidget)._internal_setSize(size$1({ width, height: this._private__size.height }));
|
|
}
|
|
_internal_getSize() {
|
|
return this._private__size;
|
|
}
|
|
_internal_setSize(newSize) {
|
|
if (equalSizes(this._private__size, newSize)) {
|
|
return;
|
|
}
|
|
this._private__size = newSize;
|
|
this._private__isSettingSize = true;
|
|
this._private__canvasBinding.resizeCanvasElement(newSize);
|
|
this._private__topCanvasBinding.resizeCanvasElement(newSize);
|
|
this._private__isSettingSize = false;
|
|
this._private__paneCell.style.width = newSize.width + 'px';
|
|
this._private__paneCell.style.height = newSize.height + 'px';
|
|
}
|
|
_internal_recalculatePriceScales() {
|
|
const pane = ensureNotNull(this._private__state);
|
|
pane._internal_recalculatePriceScale(pane._internal_leftPriceScale());
|
|
pane._internal_recalculatePriceScale(pane._internal_rightPriceScale());
|
|
for (const source of pane._internal_dataSources()) {
|
|
if (pane._internal_isOverlay(source)) {
|
|
const priceScale = source._internal_priceScale();
|
|
if (priceScale !== null) {
|
|
pane._internal_recalculatePriceScale(priceScale);
|
|
}
|
|
// for overlay drawings price scale is owner's price scale
|
|
// however owner's price scale could not contain ds
|
|
source._internal_updateAllViews();
|
|
}
|
|
}
|
|
for (const primitive of pane._internal_primitives()) {
|
|
primitive._internal_updateAllViews();
|
|
}
|
|
}
|
|
_internal_getBitmapSize() {
|
|
return this._private__canvasBinding.bitmapSize;
|
|
}
|
|
_internal_drawBitmap(ctx, x, y, addTopLayer) {
|
|
const bitmapSize = this._internal_getBitmapSize();
|
|
if (bitmapSize.width > 0 && bitmapSize.height > 0) {
|
|
ctx.drawImage(this._private__canvasBinding.canvasElement, x, y);
|
|
if (addTopLayer) {
|
|
const topLayer = this._private__topCanvasBinding.canvasElement;
|
|
if (ctx !== null) {
|
|
ctx.drawImage(topLayer, x, y);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
_internal_paint(type) {
|
|
if (type === 0 /* InvalidationLevel.None */) {
|
|
return;
|
|
}
|
|
if (this._private__state === null) {
|
|
return;
|
|
}
|
|
if (type > 1 /* InvalidationLevel.Cursor */) {
|
|
this._internal_recalculatePriceScales();
|
|
}
|
|
if (this._private__leftPriceAxisWidget !== null) {
|
|
this._private__leftPriceAxisWidget._internal_paint(type);
|
|
}
|
|
if (this._private__rightPriceAxisWidget !== null) {
|
|
this._private__rightPriceAxisWidget._internal_paint(type);
|
|
}
|
|
const canvasOptions = {
|
|
colorSpace: this._private__chart._internal_options().layout.colorSpace,
|
|
};
|
|
if (type !== 1 /* InvalidationLevel.Cursor */) {
|
|
this._private__canvasBinding.applySuggestedBitmapSize();
|
|
const target = tryCreateCanvasRenderingTarget2D(this._private__canvasBinding, canvasOptions);
|
|
if (target !== null) {
|
|
target.useBitmapCoordinateSpace((scope) => {
|
|
this._private__drawBackground(scope);
|
|
});
|
|
if (this._private__state) {
|
|
this._private__drawSources(target, sourceBottomPaneViews$1);
|
|
this._private__drawGrid(target);
|
|
this._private__drawSources(target, sourcePaneViews$1);
|
|
this._private__drawSources(target, sourceLabelPaneViews);
|
|
}
|
|
}
|
|
}
|
|
this._private__topCanvasBinding.applySuggestedBitmapSize();
|
|
const topTarget = tryCreateCanvasRenderingTarget2D(this._private__topCanvasBinding, canvasOptions);
|
|
if (topTarget !== null) {
|
|
topTarget.useBitmapCoordinateSpace(({ context: ctx, bitmapSize }) => {
|
|
ctx.clearRect(0, 0, bitmapSize.width, bitmapSize.height);
|
|
});
|
|
this._private__drawCrosshair(topTarget);
|
|
this._private__drawSources(topTarget, sourceTopPaneViews$1);
|
|
this._private__drawSources(topTarget, sourceLabelPaneViews);
|
|
}
|
|
}
|
|
_internal_leftPriceAxisWidget() {
|
|
return this._private__leftPriceAxisWidget;
|
|
}
|
|
_internal_rightPriceAxisWidget() {
|
|
return this._private__rightPriceAxisWidget;
|
|
}
|
|
_internal_drawAdditionalSources(target, paneViewsGetter) {
|
|
this._private__drawSources(target, paneViewsGetter);
|
|
}
|
|
_private__onStateDestroyed() {
|
|
if (this._private__state !== null) {
|
|
this._private__state._internal_onDestroyed()._internal_unsubscribeAll(this);
|
|
}
|
|
this._private__state = null;
|
|
}
|
|
_private__fireClickedDelegate(event) {
|
|
this._private__fireMouseClickDelegate(this._private__clicked, event);
|
|
}
|
|
_private__fireMouseClickDelegate(delegate, event) {
|
|
const x = event.localX;
|
|
const y = event.localY;
|
|
if (delegate._internal_hasListeners()) {
|
|
delegate._internal_fire(this._private__model()._internal_timeScale()._internal_coordinateToIndex(x), { x, y }, event);
|
|
}
|
|
}
|
|
_private__drawBackground({ context: ctx, bitmapSize }) {
|
|
const { width, height } = bitmapSize;
|
|
const model = this._private__model();
|
|
const topColor = model._internal_backgroundTopColor();
|
|
const bottomColor = model._internal_backgroundBottomColor();
|
|
if (topColor === bottomColor) {
|
|
clearRect(ctx, 0, 0, width, height, bottomColor);
|
|
}
|
|
else {
|
|
clearRectWithGradient(ctx, 0, 0, width, height, topColor, bottomColor);
|
|
}
|
|
}
|
|
_private__drawGrid(target) {
|
|
const state = ensureNotNull(this._private__state);
|
|
const paneView = state._internal_grid()._internal_paneView();
|
|
const renderer = paneView._internal_renderer(state);
|
|
if (renderer !== null) {
|
|
renderer._internal_draw(target, false);
|
|
}
|
|
}
|
|
_private__drawCrosshair(target) {
|
|
this._private__drawSourceImpl(target, sourcePaneViews$1, drawForeground, this._private__model()._internal_crosshairSource());
|
|
}
|
|
_private__drawSources(target, paneViewsGetter) {
|
|
const state = ensureNotNull(this._private__state);
|
|
const sources = state._internal_orderedSourcesForRendering();
|
|
const panePrimitives = state._internal_primitives();
|
|
for (const panePrimitive of panePrimitives) {
|
|
this._private__drawSourceImpl(target, paneViewsGetter, drawBackground, panePrimitive);
|
|
}
|
|
for (const source of sources) {
|
|
this._private__drawSourceImpl(target, paneViewsGetter, drawBackground, source);
|
|
}
|
|
for (const panePrimitive of panePrimitives) {
|
|
this._private__drawSourceImpl(target, paneViewsGetter, drawForeground, panePrimitive);
|
|
}
|
|
for (const source of sources) {
|
|
this._private__drawSourceImpl(target, paneViewsGetter, drawForeground, source);
|
|
}
|
|
}
|
|
_private__drawSourceImpl(target, paneViewsGetter, drawFn, source) {
|
|
const state = ensureNotNull(this._private__state);
|
|
const hoveredSource = state._internal_model()._internal_hoveredSource();
|
|
const isHovered = hoveredSource !== null && hoveredSource._internal_source === source;
|
|
const objecId = hoveredSource !== null && isHovered && hoveredSource._internal_object !== undefined
|
|
? hoveredSource._internal_object._internal_hitTestData
|
|
: undefined;
|
|
const drawRendererFn = (renderer) => drawFn(renderer, target, isHovered, objecId);
|
|
drawSourceViews(paneViewsGetter, drawRendererFn, source, state);
|
|
}
|
|
_private__recreatePriceAxisWidgets() {
|
|
if (this._private__state === null) {
|
|
return;
|
|
}
|
|
const chart = this._private__chart;
|
|
const leftAxisVisible = this._private__state._internal_leftPriceScale()._internal_options().visible;
|
|
const rightAxisVisible = this._private__state._internal_rightPriceScale()._internal_options().visible;
|
|
if (!leftAxisVisible && this._private__leftPriceAxisWidget !== null) {
|
|
this._private__leftAxisCell.removeChild(this._private__leftPriceAxisWidget._internal_getElement());
|
|
this._private__leftPriceAxisWidget._internal_destroy();
|
|
this._private__leftPriceAxisWidget = null;
|
|
}
|
|
if (!rightAxisVisible && this._private__rightPriceAxisWidget !== null) {
|
|
this._private__rightAxisCell.removeChild(this._private__rightPriceAxisWidget._internal_getElement());
|
|
this._private__rightPriceAxisWidget._internal_destroy();
|
|
this._private__rightPriceAxisWidget = null;
|
|
}
|
|
const rendererOptionsProvider = chart._internal_model()._internal_rendererOptionsProvider();
|
|
if (leftAxisVisible && this._private__leftPriceAxisWidget === null) {
|
|
this._private__leftPriceAxisWidget = new PriceAxisWidget(this, chart._internal_options(), rendererOptionsProvider, 'left');
|
|
this._private__leftAxisCell.appendChild(this._private__leftPriceAxisWidget._internal_getElement());
|
|
}
|
|
if (rightAxisVisible && this._private__rightPriceAxisWidget === null) {
|
|
this._private__rightPriceAxisWidget = new PriceAxisWidget(this, chart._internal_options(), rendererOptionsProvider, 'right');
|
|
this._private__rightAxisCell.appendChild(this._private__rightPriceAxisWidget._internal_getElement());
|
|
}
|
|
}
|
|
_private__preventScroll(event) {
|
|
return event._internal_isTouch && this._private__longTap || this._private__startTrackPoint !== null;
|
|
}
|
|
_private__setCrosshairPosition(x, y, event) {
|
|
x = Math.max(0, Math.min(x, this._private__size.width - 1));
|
|
y = Math.max(0, Math.min(y, this._private__size.height - 1));
|
|
this._private__model()._internal_setAndSaveCurrentPosition(x, y, event, ensureNotNull(this._private__state));
|
|
}
|
|
_private__clearCrosshairPosition() {
|
|
this._private__model()._internal_clearCurrentPosition();
|
|
}
|
|
_private__tryExitTrackingMode() {
|
|
if (this._private__exitTrackingModeOnNextTry) {
|
|
this._private__startTrackPoint = null;
|
|
this._private__clearCrosshairPosition();
|
|
}
|
|
}
|
|
_private__startTrackingMode(startTrackPoint, crossHairPosition, event) {
|
|
this._private__startTrackPoint = startTrackPoint;
|
|
this._private__exitTrackingModeOnNextTry = false;
|
|
this._private__setCrosshairPosition(crossHairPosition.x, crossHairPosition.y, event);
|
|
const crosshair = this._private__model()._internal_crosshairSource();
|
|
this._private__initCrosshairPosition = { x: crosshair._internal_appliedX(), y: crosshair._internal_appliedY() };
|
|
}
|
|
_private__model() {
|
|
return this._private__chart._internal_model();
|
|
}
|
|
_private__endScroll(event) {
|
|
if (!this._private__isScrolling) {
|
|
return;
|
|
}
|
|
const model = this._private__model();
|
|
const state = this._internal_state();
|
|
model._internal_endScrollPrice(state, state._internal_defaultPriceScale());
|
|
this._private__startScrollingPos = null;
|
|
this._private__isScrolling = false;
|
|
model._internal_endScrollTime();
|
|
if (this._private__scrollXAnimation !== null) {
|
|
const startAnimationTime = performance.now();
|
|
const timeScale = model._internal_timeScale();
|
|
this._private__scrollXAnimation._internal_start(timeScale._internal_rightOffset(), startAnimationTime);
|
|
if (!this._private__scrollXAnimation._internal_finished(startAnimationTime)) {
|
|
model._internal_setTimeScaleAnimation(this._private__scrollXAnimation);
|
|
}
|
|
}
|
|
}
|
|
_private__onMouseEvent() {
|
|
this._private__startTrackPoint = null;
|
|
}
|
|
_private__mouseTouchDownEvent() {
|
|
if (!this._private__state) {
|
|
return;
|
|
}
|
|
this._private__model()._internal_stopTimeScaleAnimation();
|
|
if (document.activeElement !== document.body && document.activeElement !== document.documentElement) {
|
|
// If any focusable element except the page itself is focused, remove the focus
|
|
ensureNotNull(document.activeElement).blur();
|
|
}
|
|
else {
|
|
// Clear selection
|
|
const selection = document.getSelection();
|
|
if (selection !== null) {
|
|
selection.removeAllRanges();
|
|
}
|
|
}
|
|
const priceScale = this._private__state._internal_defaultPriceScale();
|
|
if (priceScale._internal_isEmpty() || this._private__model()._internal_timeScale()._internal_isEmpty()) {
|
|
return;
|
|
}
|
|
}
|
|
// eslint-disable-next-line complexity
|
|
_private__pressedMouseTouchMoveEvent(event) {
|
|
if (this._private__state === null) {
|
|
return;
|
|
}
|
|
const model = this._private__model();
|
|
const timeScale = model._internal_timeScale();
|
|
if (timeScale._internal_isEmpty()) {
|
|
return;
|
|
}
|
|
const chartOptions = this._private__chart._internal_options();
|
|
const scrollOptions = chartOptions['handleScroll'];
|
|
const kineticScrollOptions = chartOptions.kineticScroll;
|
|
if ((!scrollOptions.pressedMouseMove || event._internal_isTouch) &&
|
|
(!scrollOptions.horzTouchDrag && !scrollOptions.vertTouchDrag || !event._internal_isTouch)) {
|
|
return;
|
|
}
|
|
const priceScale = this._private__state._internal_defaultPriceScale();
|
|
const now = performance.now();
|
|
if (this._private__startScrollingPos === null && !this._private__preventScroll(event)) {
|
|
this._private__startScrollingPos = {
|
|
x: event.clientX,
|
|
y: event.clientY,
|
|
_internal_timestamp: now,
|
|
_internal_localX: event.localX,
|
|
_internal_localY: event.localY,
|
|
};
|
|
}
|
|
if (this._private__startScrollingPos !== null &&
|
|
!this._private__isScrolling &&
|
|
(this._private__startScrollingPos.x !== event.clientX || this._private__startScrollingPos.y !== event.clientY)) {
|
|
if (event._internal_isTouch && kineticScrollOptions.touch || !event._internal_isTouch && kineticScrollOptions.mouse) {
|
|
const barSpacing = timeScale._internal_barSpacing();
|
|
this._private__scrollXAnimation = new KineticAnimation(0.2 /* KineticScrollConstants.MinScrollSpeed */ / barSpacing, 7 /* KineticScrollConstants.MaxScrollSpeed */ / barSpacing, 0.997 /* KineticScrollConstants.DumpingCoeff */, 15 /* KineticScrollConstants.ScrollMinMove */ / barSpacing);
|
|
this._private__scrollXAnimation._internal_addPosition(timeScale._internal_rightOffset(), this._private__startScrollingPos._internal_timestamp);
|
|
}
|
|
else {
|
|
this._private__scrollXAnimation = null;
|
|
}
|
|
if (!priceScale._internal_isEmpty()) {
|
|
model._internal_startScrollPrice(this._private__state, priceScale, event.localY);
|
|
}
|
|
model._internal_startScrollTime(event.localX);
|
|
this._private__isScrolling = true;
|
|
}
|
|
if (this._private__isScrolling) {
|
|
// this allows scrolling not default price scales
|
|
if (!priceScale._internal_isEmpty()) {
|
|
model._internal_scrollPriceTo(this._private__state, priceScale, event.localY);
|
|
}
|
|
model._internal_scrollTimeTo(event.localX);
|
|
if (this._private__scrollXAnimation !== null) {
|
|
this._private__scrollXAnimation._internal_addPosition(timeScale._internal_rightOffset(), now);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class PriceAxisStub {
|
|
constructor(side, options, params, borderVisible, bottomColor) {
|
|
this._private__invalidated = true;
|
|
this._private__size = size$1({ width: 0, height: 0 });
|
|
this._private__canvasSuggestedBitmapSizeChangedHandler = () => this._internal_paint(3 /* InvalidationLevel.Full */);
|
|
this._private__isLeft = side === 'left';
|
|
this._private__rendererOptionsProvider = params._internal_rendererOptionsProvider;
|
|
this._private__options = options;
|
|
this._private__borderVisible = borderVisible;
|
|
this._private__bottomColor = bottomColor;
|
|
this._private__cell = document.createElement('div');
|
|
this._private__cell.style.width = '25px';
|
|
this._private__cell.style.height = '100%';
|
|
this._private__cell.style.overflow = 'hidden';
|
|
this._private__canvasBinding = createBoundCanvas(this._private__cell, size$1({ width: 16, height: 16 }));
|
|
this._private__canvasBinding.subscribeSuggestedBitmapSizeChanged(this._private__canvasSuggestedBitmapSizeChangedHandler);
|
|
}
|
|
_internal_destroy() {
|
|
this._private__canvasBinding.unsubscribeSuggestedBitmapSizeChanged(this._private__canvasSuggestedBitmapSizeChangedHandler);
|
|
releaseCanvas(this._private__canvasBinding.canvasElement);
|
|
this._private__canvasBinding.dispose();
|
|
}
|
|
_internal_getElement() {
|
|
return this._private__cell;
|
|
}
|
|
_internal_getSize() {
|
|
return this._private__size;
|
|
}
|
|
_internal_setSize(newSize) {
|
|
if (!equalSizes(this._private__size, newSize)) {
|
|
this._private__size = newSize;
|
|
this._private__canvasBinding.resizeCanvasElement(newSize);
|
|
this._private__cell.style.width = `${newSize.width}px`;
|
|
this._private__cell.style.height = `${newSize.height}px`;
|
|
this._private__invalidated = true;
|
|
}
|
|
}
|
|
_internal_paint(type) {
|
|
if (type < 3 /* InvalidationLevel.Full */ && !this._private__invalidated) {
|
|
return;
|
|
}
|
|
if (this._private__size.width === 0 || this._private__size.height === 0) {
|
|
return;
|
|
}
|
|
this._private__invalidated = false;
|
|
this._private__canvasBinding.applySuggestedBitmapSize();
|
|
const target = tryCreateCanvasRenderingTarget2D(this._private__canvasBinding, {
|
|
colorSpace: this._private__options.layout.colorSpace,
|
|
});
|
|
if (target !== null) {
|
|
target.useBitmapCoordinateSpace((scope) => {
|
|
this._private__drawBackground(scope);
|
|
this._private__drawBorder(scope);
|
|
});
|
|
}
|
|
}
|
|
_internal_getBitmapSize() {
|
|
return this._private__canvasBinding.bitmapSize;
|
|
}
|
|
_internal_drawBitmap(ctx, x, y) {
|
|
const bitmapSize = this._internal_getBitmapSize();
|
|
if (bitmapSize.width > 0 && bitmapSize.height > 0) {
|
|
ctx.drawImage(this._private__canvasBinding.canvasElement, x, y);
|
|
}
|
|
}
|
|
_private__drawBorder({ context: ctx, bitmapSize, horizontalPixelRatio, verticalPixelRatio }) {
|
|
if (!this._private__borderVisible()) {
|
|
return;
|
|
}
|
|
ctx.fillStyle = this._private__options.timeScale.borderColor;
|
|
const horzBorderSize = Math.floor(this._private__rendererOptionsProvider._internal_options()._internal_borderSize * horizontalPixelRatio);
|
|
const vertBorderSize = Math.floor(this._private__rendererOptionsProvider._internal_options()._internal_borderSize * verticalPixelRatio);
|
|
const left = (this._private__isLeft) ? bitmapSize.width - horzBorderSize : 0;
|
|
ctx.fillRect(left, 0, horzBorderSize, vertBorderSize);
|
|
}
|
|
_private__drawBackground({ context: ctx, bitmapSize }) {
|
|
clearRect(ctx, 0, 0, bitmapSize.width, bitmapSize.height, this._private__bottomColor());
|
|
}
|
|
}
|
|
|
|
function buildTimeAxisViewsGetter(zOrder) {
|
|
return (source) => source._internal_timePaneViews?.(zOrder) ?? [];
|
|
}
|
|
const sourcePaneViews = buildTimeAxisViewsGetter('normal');
|
|
const sourceTopPaneViews = buildTimeAxisViewsGetter('top');
|
|
const sourceBottomPaneViews = buildTimeAxisViewsGetter('bottom');
|
|
class TimeAxisWidget {
|
|
constructor(chartWidget, horzScaleBehavior) {
|
|
this._private__leftStub = null;
|
|
this._private__rightStub = null;
|
|
this._private__rendererOptions = null;
|
|
this._private__mouseDown = false;
|
|
this._private__size = size$1({ width: 0, height: 0 });
|
|
this._private__sizeChanged = new Delegate();
|
|
this._private__widthCache = new TextWidthCache(5);
|
|
this._private__isSettingSize = false;
|
|
this._private__canvasSuggestedBitmapSizeChangedHandler = () => {
|
|
if (!this._private__isSettingSize) {
|
|
this._private__chart._internal_model()._internal_lightUpdate();
|
|
}
|
|
};
|
|
this._private__topCanvasSuggestedBitmapSizeChangedHandler = () => {
|
|
if (!this._private__isSettingSize) {
|
|
this._private__chart._internal_model()._internal_lightUpdate();
|
|
}
|
|
};
|
|
this._private__chart = chartWidget;
|
|
this._private__horzScaleBehavior = horzScaleBehavior;
|
|
this._private__options = chartWidget._internal_options()['layout'];
|
|
this._private__element = document.createElement('tr');
|
|
this._private__leftStubCell = document.createElement('td');
|
|
this._private__leftStubCell.style.padding = '0';
|
|
this._private__rightStubCell = document.createElement('td');
|
|
this._private__rightStubCell.style.padding = '0';
|
|
this._private__cell = document.createElement('td');
|
|
this._private__cell.style.height = '25px';
|
|
this._private__cell.style.padding = '0';
|
|
this._private__dv = document.createElement('div');
|
|
this._private__dv.style.width = '100%';
|
|
this._private__dv.style.height = '100%';
|
|
this._private__dv.style.position = 'relative';
|
|
this._private__dv.style.overflow = 'hidden';
|
|
this._private__cell.appendChild(this._private__dv);
|
|
this._private__canvasBinding = createBoundCanvas(this._private__dv, size$1({ width: 16, height: 16 }));
|
|
this._private__canvasBinding.subscribeSuggestedBitmapSizeChanged(this._private__canvasSuggestedBitmapSizeChangedHandler);
|
|
const canvas = this._private__canvasBinding.canvasElement;
|
|
canvas.style.position = 'absolute';
|
|
canvas.style.zIndex = '1';
|
|
canvas.style.left = '0';
|
|
canvas.style.top = '0';
|
|
this._private__topCanvasBinding = createBoundCanvas(this._private__dv, size$1({ width: 16, height: 16 }));
|
|
this._private__topCanvasBinding.subscribeSuggestedBitmapSizeChanged(this._private__topCanvasSuggestedBitmapSizeChangedHandler);
|
|
const topCanvas = this._private__topCanvasBinding.canvasElement;
|
|
topCanvas.style.position = 'absolute';
|
|
topCanvas.style.zIndex = '2';
|
|
topCanvas.style.left = '0';
|
|
topCanvas.style.top = '0';
|
|
this._private__element.appendChild(this._private__leftStubCell);
|
|
this._private__element.appendChild(this._private__cell);
|
|
this._private__element.appendChild(this._private__rightStubCell);
|
|
this._private__recreateStubs();
|
|
this._private__chart._internal_model()._internal_priceScalesOptionsChanged()._internal_subscribe(this._private__recreateStubs.bind(this), this);
|
|
this._private__mouseEventHandler = new MouseEventHandler(this._private__topCanvasBinding.canvasElement, this, {
|
|
_internal_treatVertTouchDragAsPageScroll: () => true,
|
|
_internal_treatHorzTouchDragAsPageScroll: () => !this._private__chart._internal_options()['handleScroll'].horzTouchDrag,
|
|
});
|
|
}
|
|
_internal_destroy() {
|
|
this._private__mouseEventHandler._internal_destroy();
|
|
if (this._private__leftStub !== null) {
|
|
this._private__leftStub._internal_destroy();
|
|
}
|
|
if (this._private__rightStub !== null) {
|
|
this._private__rightStub._internal_destroy();
|
|
}
|
|
this._private__topCanvasBinding.unsubscribeSuggestedBitmapSizeChanged(this._private__topCanvasSuggestedBitmapSizeChangedHandler);
|
|
releaseCanvas(this._private__topCanvasBinding.canvasElement);
|
|
this._private__topCanvasBinding.dispose();
|
|
this._private__canvasBinding.unsubscribeSuggestedBitmapSizeChanged(this._private__canvasSuggestedBitmapSizeChangedHandler);
|
|
releaseCanvas(this._private__canvasBinding.canvasElement);
|
|
this._private__canvasBinding.dispose();
|
|
}
|
|
_internal_getElement() {
|
|
return this._private__element;
|
|
}
|
|
_internal_leftStub() {
|
|
return this._private__leftStub;
|
|
}
|
|
_internal_rightStub() {
|
|
return this._private__rightStub;
|
|
}
|
|
_internal_mouseDownEvent(event) {
|
|
if (this._private__mouseDown) {
|
|
return;
|
|
}
|
|
this._private__mouseDown = true;
|
|
const model = this._private__chart._internal_model();
|
|
if (model._internal_timeScale()._internal_isEmpty() || !this._private__chart._internal_options()['handleScale'].axisPressedMouseMove.time) {
|
|
return;
|
|
}
|
|
model._internal_startScaleTime(event.localX);
|
|
}
|
|
_internal_touchStartEvent(event) {
|
|
this._internal_mouseDownEvent(event);
|
|
}
|
|
_internal_mouseDownOutsideEvent() {
|
|
const model = this._private__chart._internal_model();
|
|
if (!model._internal_timeScale()._internal_isEmpty() && this._private__mouseDown) {
|
|
this._private__mouseDown = false;
|
|
if (this._private__chart._internal_options()['handleScale'].axisPressedMouseMove.time) {
|
|
model._internal_endScaleTime();
|
|
}
|
|
}
|
|
}
|
|
_internal_pressedMouseMoveEvent(event) {
|
|
const model = this._private__chart._internal_model();
|
|
if (model._internal_timeScale()._internal_isEmpty() || !this._private__chart._internal_options()['handleScale'].axisPressedMouseMove.time) {
|
|
return;
|
|
}
|
|
model._internal_scaleTimeTo(event.localX);
|
|
}
|
|
_internal_touchMoveEvent(event) {
|
|
this._internal_pressedMouseMoveEvent(event);
|
|
}
|
|
_internal_mouseUpEvent() {
|
|
this._private__mouseDown = false;
|
|
const model = this._private__chart._internal_model();
|
|
if (model._internal_timeScale()._internal_isEmpty() && !this._private__chart._internal_options()['handleScale'].axisPressedMouseMove.time) {
|
|
return;
|
|
}
|
|
model._internal_endScaleTime();
|
|
}
|
|
_internal_touchEndEvent() {
|
|
this._internal_mouseUpEvent();
|
|
}
|
|
_internal_mouseDoubleClickEvent() {
|
|
if (this._private__chart._internal_options()['handleScale'].axisDoubleClickReset.time) {
|
|
this._private__chart._internal_model()._internal_resetTimeScale();
|
|
}
|
|
}
|
|
_internal_doubleTapEvent() {
|
|
this._internal_mouseDoubleClickEvent();
|
|
}
|
|
_internal_mouseEnterEvent() {
|
|
if (this._private__chart._internal_model()._internal_options()['handleScale'].axisPressedMouseMove.time) {
|
|
this._private__setCursor(1 /* CursorType.EwResize */);
|
|
}
|
|
}
|
|
_internal_mouseLeaveEvent() {
|
|
this._private__setCursor(0 /* CursorType.Default */);
|
|
}
|
|
_internal_getSize() {
|
|
return this._private__size;
|
|
}
|
|
_internal_sizeChanged() {
|
|
return this._private__sizeChanged;
|
|
}
|
|
_internal_setSizes(timeAxisSize, leftStubWidth, rightStubWidth) {
|
|
if (!equalSizes(this._private__size, timeAxisSize)) {
|
|
this._private__size = timeAxisSize;
|
|
this._private__isSettingSize = true;
|
|
this._private__canvasBinding.resizeCanvasElement(timeAxisSize);
|
|
this._private__topCanvasBinding.resizeCanvasElement(timeAxisSize);
|
|
this._private__isSettingSize = false;
|
|
this._private__cell.style.width = `${timeAxisSize.width}px`;
|
|
this._private__cell.style.height = `${timeAxisSize.height}px`;
|
|
this._private__sizeChanged._internal_fire(timeAxisSize);
|
|
}
|
|
if (this._private__leftStub !== null) {
|
|
this._private__leftStub._internal_setSize(size$1({ width: leftStubWidth, height: timeAxisSize.height }));
|
|
}
|
|
if (this._private__rightStub !== null) {
|
|
this._private__rightStub._internal_setSize(size$1({ width: rightStubWidth, height: timeAxisSize.height }));
|
|
}
|
|
}
|
|
_internal_optimalHeight() {
|
|
const rendererOptions = this._private__getRendererOptions();
|
|
return Math.ceil(
|
|
// rendererOptions.offsetSize +
|
|
rendererOptions._internal_borderSize +
|
|
rendererOptions._internal_tickLength +
|
|
rendererOptions._internal_fontSize +
|
|
rendererOptions._internal_paddingTop +
|
|
rendererOptions._internal_paddingBottom +
|
|
rendererOptions._internal_labelBottomOffset);
|
|
}
|
|
_internal_update() {
|
|
// this call has side-effect - it regenerates marks on the time scale
|
|
this._private__chart._internal_model()._internal_timeScale()._internal_marks();
|
|
}
|
|
_internal_getBitmapSize() {
|
|
return this._private__canvasBinding.bitmapSize;
|
|
}
|
|
_internal_drawBitmap(ctx, x, y, addTopLayer) {
|
|
const bitmapSize = this._internal_getBitmapSize();
|
|
if (bitmapSize.width > 0 && bitmapSize.height > 0) {
|
|
ctx.drawImage(this._private__canvasBinding.canvasElement, x, y);
|
|
if (addTopLayer) {
|
|
const topLayer = this._private__topCanvasBinding.canvasElement;
|
|
ctx.drawImage(topLayer, x, y);
|
|
}
|
|
}
|
|
}
|
|
_internal_paint(type) {
|
|
if (type === 0 /* InvalidationLevel.None */) {
|
|
return;
|
|
}
|
|
const canvasOptions = {
|
|
colorSpace: this._private__options.colorSpace,
|
|
};
|
|
if (type !== 1 /* InvalidationLevel.Cursor */) {
|
|
this._private__canvasBinding.applySuggestedBitmapSize();
|
|
const target = tryCreateCanvasRenderingTarget2D(this._private__canvasBinding, canvasOptions);
|
|
if (target !== null) {
|
|
target.useBitmapCoordinateSpace((scope) => {
|
|
this._private__drawBackground(scope);
|
|
this._private__drawBorder(scope);
|
|
this._private__drawAdditionalSources(target, sourceBottomPaneViews);
|
|
});
|
|
this._private__drawTickMarks(target);
|
|
this._private__drawAdditionalSources(target, sourcePaneViews);
|
|
// atm we don't have sources to be drawn on time axis except crosshair which is rendered on top level canvas
|
|
// so let's don't call this code at all for now
|
|
// this._drawLabels(this._chart.model().dataSources(), target);
|
|
}
|
|
if (this._private__leftStub !== null) {
|
|
this._private__leftStub._internal_paint(type);
|
|
}
|
|
if (this._private__rightStub !== null) {
|
|
this._private__rightStub._internal_paint(type);
|
|
}
|
|
}
|
|
this._private__topCanvasBinding.applySuggestedBitmapSize();
|
|
const topTarget = tryCreateCanvasRenderingTarget2D(this._private__topCanvasBinding, canvasOptions);
|
|
if (topTarget !== null) {
|
|
topTarget.useBitmapCoordinateSpace(({ context: ctx, bitmapSize }) => {
|
|
ctx.clearRect(0, 0, bitmapSize.width, bitmapSize.height);
|
|
});
|
|
this._private__drawLabels([...this._private__chart._internal_model()._internal_serieses(), this._private__chart._internal_model()._internal_crosshairSource()], topTarget);
|
|
this._private__drawAdditionalSources(topTarget, sourceTopPaneViews);
|
|
}
|
|
}
|
|
_private__drawAdditionalSources(target, axisViewsGetter) {
|
|
const sources = this._private__chart._internal_model()._internal_serieses();
|
|
for (const source of sources) {
|
|
drawSourceViews(axisViewsGetter, (renderer) => drawBackground(renderer, target, false, undefined), source, undefined);
|
|
}
|
|
for (const source of sources) {
|
|
drawSourceViews(axisViewsGetter, (renderer) => drawForeground(renderer, target, false, undefined), source, undefined);
|
|
}
|
|
}
|
|
_private__drawBackground({ context: ctx, bitmapSize }) {
|
|
clearRect(ctx, 0, 0, bitmapSize.width, bitmapSize.height, this._private__chart._internal_model()._internal_backgroundBottomColor());
|
|
}
|
|
_private__drawBorder({ context: ctx, bitmapSize, verticalPixelRatio }) {
|
|
if (this._private__chart._internal_options().timeScale.borderVisible) {
|
|
ctx.fillStyle = this._private__lineColor();
|
|
const borderSize = Math.max(1, Math.floor(this._private__getRendererOptions()._internal_borderSize * verticalPixelRatio));
|
|
ctx.fillRect(0, 0, bitmapSize.width, borderSize);
|
|
}
|
|
}
|
|
_private__drawTickMarks(target) {
|
|
const timeScale = this._private__chart._internal_model()._internal_timeScale();
|
|
const tickMarks = timeScale._internal_marks();
|
|
if (!tickMarks || tickMarks.length === 0) {
|
|
return;
|
|
}
|
|
const maxWeight = this._private__horzScaleBehavior.maxTickMarkWeight(tickMarks);
|
|
const rendererOptions = this._private__getRendererOptions();
|
|
const options = timeScale._internal_options();
|
|
if (options.borderVisible && options.ticksVisible) {
|
|
target.useBitmapCoordinateSpace(({ context: ctx, horizontalPixelRatio, verticalPixelRatio }) => {
|
|
ctx.strokeStyle = this._private__lineColor();
|
|
ctx.fillStyle = this._private__lineColor();
|
|
const tickWidth = Math.max(1, Math.floor(horizontalPixelRatio));
|
|
const tickOffset = Math.floor(horizontalPixelRatio * 0.5);
|
|
ctx.beginPath();
|
|
const tickLen = Math.round(rendererOptions._internal_tickLength * verticalPixelRatio);
|
|
for (let index = tickMarks.length; index--;) {
|
|
const x = Math.round(tickMarks[index].coord * horizontalPixelRatio);
|
|
ctx.rect(x - tickOffset, 0, tickWidth, tickLen);
|
|
}
|
|
ctx.fill();
|
|
});
|
|
}
|
|
target.useMediaCoordinateSpace(({ context: ctx }) => {
|
|
const yText = (rendererOptions._internal_borderSize +
|
|
rendererOptions._internal_tickLength +
|
|
rendererOptions._internal_paddingTop +
|
|
rendererOptions._internal_fontSize / 2);
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillStyle = this._private__textColor();
|
|
// draw base marks
|
|
ctx.font = this._private__baseFont();
|
|
for (const tickMark of tickMarks) {
|
|
if (tickMark.weight < maxWeight) {
|
|
const coordinate = tickMark.needAlignCoordinate ? this._private__alignTickMarkLabelCoordinate(ctx, tickMark.coord, tickMark.label) : tickMark.coord;
|
|
ctx.fillText(tickMark.label, coordinate, yText);
|
|
}
|
|
}
|
|
if (this._private__chart._internal_options().timeScale.allowBoldLabels) {
|
|
ctx.font = this._private__baseBoldFont();
|
|
}
|
|
for (const tickMark of tickMarks) {
|
|
if (tickMark.weight >= maxWeight) {
|
|
const coordinate = tickMark.needAlignCoordinate ? this._private__alignTickMarkLabelCoordinate(ctx, tickMark.coord, tickMark.label) : tickMark.coord;
|
|
ctx.fillText(tickMark.label, coordinate, yText);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
_private__alignTickMarkLabelCoordinate(ctx, coordinate, labelText) {
|
|
const labelWidth = this._private__widthCache._internal_measureText(ctx, labelText);
|
|
const labelWidthHalf = labelWidth / 2;
|
|
const leftTextCoordinate = Math.floor(coordinate - labelWidthHalf) + 0.5;
|
|
if (leftTextCoordinate < 0) {
|
|
coordinate = coordinate + Math.abs(0 - leftTextCoordinate);
|
|
}
|
|
else if (leftTextCoordinate + labelWidth > this._private__size.width) {
|
|
coordinate = coordinate - Math.abs(this._private__size.width - (leftTextCoordinate + labelWidth));
|
|
}
|
|
return coordinate;
|
|
}
|
|
_private__drawLabels(sources, target) {
|
|
const rendererOptions = this._private__getRendererOptions();
|
|
for (const source of sources) {
|
|
for (const view of source._internal_timeAxisViews()) {
|
|
view._internal_renderer()._internal_draw(target, rendererOptions);
|
|
}
|
|
}
|
|
}
|
|
_private__lineColor() {
|
|
return this._private__chart._internal_options().timeScale.borderColor;
|
|
}
|
|
_private__textColor() {
|
|
return this._private__options.textColor;
|
|
}
|
|
_private__fontSize() {
|
|
return this._private__options.fontSize;
|
|
}
|
|
_private__baseFont() {
|
|
return makeFont(this._private__fontSize(), this._private__options.fontFamily);
|
|
}
|
|
_private__baseBoldFont() {
|
|
return makeFont(this._private__fontSize(), this._private__options.fontFamily, 'bold');
|
|
}
|
|
_private__getRendererOptions() {
|
|
if (this._private__rendererOptions === null) {
|
|
this._private__rendererOptions = {
|
|
_internal_borderSize: 1 /* Constants.BorderSize */,
|
|
_internal_baselineOffset: NaN,
|
|
_internal_paddingTop: NaN,
|
|
_internal_paddingBottom: NaN,
|
|
_internal_paddingHorizontal: NaN,
|
|
_internal_tickLength: 5 /* Constants.TickLength */,
|
|
_internal_fontSize: NaN,
|
|
_internal_font: '',
|
|
_internal_widthCache: new TextWidthCache(),
|
|
_internal_labelBottomOffset: 0,
|
|
};
|
|
}
|
|
const rendererOptions = this._private__rendererOptions;
|
|
const newFont = this._private__baseFont();
|
|
if (rendererOptions._internal_font !== newFont) {
|
|
const fontSize = this._private__fontSize();
|
|
rendererOptions._internal_fontSize = fontSize;
|
|
rendererOptions._internal_font = newFont;
|
|
rendererOptions._internal_paddingTop = 3 * fontSize / 12;
|
|
rendererOptions._internal_paddingBottom = 3 * fontSize / 12;
|
|
rendererOptions._internal_paddingHorizontal = 9 * fontSize / 12;
|
|
rendererOptions._internal_baselineOffset = 0;
|
|
rendererOptions._internal_labelBottomOffset = 4 * fontSize / 12;
|
|
rendererOptions._internal_widthCache._internal_reset();
|
|
}
|
|
return this._private__rendererOptions;
|
|
}
|
|
_private__setCursor(type) {
|
|
this._private__cell.style.cursor = type === 1 /* CursorType.EwResize */ ? 'ew-resize' : 'default';
|
|
}
|
|
_private__recreateStubs() {
|
|
const model = this._private__chart._internal_model();
|
|
const options = model._internal_options();
|
|
if (!options.leftPriceScale.visible && this._private__leftStub !== null) {
|
|
this._private__leftStubCell.removeChild(this._private__leftStub._internal_getElement());
|
|
this._private__leftStub._internal_destroy();
|
|
this._private__leftStub = null;
|
|
}
|
|
if (!options.rightPriceScale.visible && this._private__rightStub !== null) {
|
|
this._private__rightStubCell.removeChild(this._private__rightStub._internal_getElement());
|
|
this._private__rightStub._internal_destroy();
|
|
this._private__rightStub = null;
|
|
}
|
|
const rendererOptionsProvider = this._private__chart._internal_model()._internal_rendererOptionsProvider();
|
|
const params = {
|
|
_internal_rendererOptionsProvider: rendererOptionsProvider,
|
|
};
|
|
const borderVisibleGetter = () => {
|
|
return options.leftPriceScale.borderVisible && model._internal_timeScale()._internal_options().borderVisible;
|
|
};
|
|
const bottomColorGetter = () => model._internal_backgroundBottomColor();
|
|
if (options.leftPriceScale.visible && this._private__leftStub === null) {
|
|
this._private__leftStub = new PriceAxisStub('left', options, params, borderVisibleGetter, bottomColorGetter);
|
|
this._private__leftStubCell.appendChild(this._private__leftStub._internal_getElement());
|
|
}
|
|
if (options.rightPriceScale.visible && this._private__rightStub === null) {
|
|
this._private__rightStub = new PriceAxisStub('right', options, params, borderVisibleGetter, bottomColorGetter);
|
|
this._private__rightStubCell.appendChild(this._private__rightStub._internal_getElement());
|
|
}
|
|
}
|
|
}
|
|
|
|
const windowsChrome = isChromiumBased() && isWindows();
|
|
class ChartWidget {
|
|
constructor(container, options, horzScaleBehavior) {
|
|
this._private__paneWidgets = [];
|
|
this._private__paneSeparators = [];
|
|
this._private__drawRafId = 0;
|
|
this._private__height = 0;
|
|
this._private__width = 0;
|
|
this._private__leftPriceAxisWidth = 0;
|
|
this._private__rightPriceAxisWidth = 0;
|
|
this._private__invalidateMask = null;
|
|
this._private__drawPlanned = false;
|
|
this._private__clicked = new Delegate();
|
|
this._private__dblClicked = new Delegate();
|
|
this._private__crosshairMoved = new Delegate();
|
|
this._private__observer = null;
|
|
this._private__cursorStyleOverride = null;
|
|
this._private__container = container;
|
|
this._private__options = options;
|
|
this._private__horzScaleBehavior = horzScaleBehavior;
|
|
this._private__element = document.createElement('div');
|
|
this._private__element.classList.add('tv-lightweight-charts');
|
|
this._private__element.style.overflow = 'hidden';
|
|
this._private__element.style.direction = 'ltr';
|
|
this._private__element.style.width = '100%';
|
|
this._private__element.style.height = '100%';
|
|
disableSelection(this._private__element);
|
|
this._private__tableElement = document.createElement('table');
|
|
this._private__tableElement.setAttribute('cellspacing', '0');
|
|
this._private__element.appendChild(this._private__tableElement);
|
|
this._private__onWheelBound = this._private__onMousewheel.bind(this);
|
|
if (shouldSubscribeMouseWheel(this._private__options)) {
|
|
this._private__setMouseWheelEventListener(true);
|
|
}
|
|
this._private__model = new ChartModel(this._private__invalidateHandler.bind(this), this._private__options, horzScaleBehavior);
|
|
this._internal_model()._internal_crosshairMoved()._internal_subscribe(this._private__onPaneWidgetCrosshairMoved.bind(this), this);
|
|
this._private__timeAxisWidget = new TimeAxisWidget(this, this._private__horzScaleBehavior);
|
|
this._private__tableElement.appendChild(this._private__timeAxisWidget._internal_getElement());
|
|
const usedObserver = options.autoSize && this._private__installObserver();
|
|
// observer could not fire event immediately for some cases
|
|
// so we have to set initial size manually
|
|
let width = this._private__options.width;
|
|
let height = this._private__options.height;
|
|
// ignore width/height options if observer has actually been used
|
|
// however respect options if installing resize observer failed
|
|
if (usedObserver || width === 0 || height === 0) {
|
|
const containerRect = container.getBoundingClientRect();
|
|
width = width || containerRect.width;
|
|
height = height || containerRect.height;
|
|
}
|
|
// BEWARE: resize must be called BEFORE _syncGuiWithModel (in constructor only)
|
|
// or after but with adjustSize to properly update time scale
|
|
this._internal_resize(width, height);
|
|
this._private__syncGuiWithModel();
|
|
container.appendChild(this._private__element);
|
|
this._private__updateTimeAxisVisibility();
|
|
this._private__model._internal_timeScale()._internal_optionsApplied()._internal_subscribe(this._private__model._internal_fullUpdate.bind(this._private__model), this);
|
|
this._private__model._internal_priceScalesOptionsChanged()._internal_subscribe(this._private__model._internal_fullUpdate.bind(this._private__model), this);
|
|
}
|
|
_internal_model() {
|
|
return this._private__model;
|
|
}
|
|
_internal_options() {
|
|
return this._private__options;
|
|
}
|
|
_internal_paneWidgets() {
|
|
return this._private__paneWidgets;
|
|
}
|
|
_internal_timeAxisWidget() {
|
|
return this._private__timeAxisWidget;
|
|
}
|
|
_internal_destroy() {
|
|
this._private__setMouseWheelEventListener(false);
|
|
if (this._private__drawRafId !== 0) {
|
|
window.cancelAnimationFrame(this._private__drawRafId);
|
|
}
|
|
this._private__model._internal_crosshairMoved()._internal_unsubscribeAll(this);
|
|
this._private__model._internal_timeScale()._internal_optionsApplied()._internal_unsubscribeAll(this);
|
|
this._private__model._internal_priceScalesOptionsChanged()._internal_unsubscribeAll(this);
|
|
this._private__model._internal_destroy();
|
|
for (const paneWidget of this._private__paneWidgets) {
|
|
this._private__tableElement.removeChild(paneWidget._internal_getElement());
|
|
paneWidget._internal_clicked()._internal_unsubscribeAll(this);
|
|
paneWidget._internal_dblClicked()._internal_unsubscribeAll(this);
|
|
paneWidget._internal_destroy();
|
|
}
|
|
this._private__paneWidgets = [];
|
|
for (const paneSeparator of this._private__paneSeparators) {
|
|
this._private__destroySeparator(paneSeparator);
|
|
}
|
|
this._private__paneSeparators = [];
|
|
ensureNotNull(this._private__timeAxisWidget)._internal_destroy();
|
|
if (this._private__element.parentElement !== null) {
|
|
this._private__element.parentElement.removeChild(this._private__element);
|
|
}
|
|
this._private__crosshairMoved._internal_destroy();
|
|
this._private__clicked._internal_destroy();
|
|
this._private__dblClicked._internal_destroy();
|
|
this._private__uninstallObserver();
|
|
}
|
|
_internal_resize(width, height, forceRepaint = false) {
|
|
if (this._private__height === height && this._private__width === width) {
|
|
return;
|
|
}
|
|
const sizeHint = suggestChartSize(size$1({ width, height }));
|
|
this._private__height = sizeHint.height;
|
|
this._private__width = sizeHint.width;
|
|
const heightStr = this._private__height + 'px';
|
|
const widthStr = this._private__width + 'px';
|
|
// When autoSize is enabled, keep outer element at 100%/100% to avoid jitter
|
|
// from pixel rounding differences between container and chart size
|
|
if (!this._internal_autoSizeActive()) {
|
|
ensureNotNull(this._private__element).style.height = heightStr;
|
|
ensureNotNull(this._private__element).style.width = widthStr;
|
|
}
|
|
this._private__tableElement.style.height = heightStr;
|
|
this._private__tableElement.style.width = widthStr;
|
|
if (forceRepaint) {
|
|
// Cancel any pending animation frame since we're doing a synchronous paint
|
|
if (this._private__drawRafId !== 0) {
|
|
window.cancelAnimationFrame(this._private__drawRafId);
|
|
this._private__drawRafId = 0;
|
|
}
|
|
this._private__drawPlanned = false;
|
|
// Merge any pending invalidations and clear them
|
|
const mask = InvalidateMask._internal_full();
|
|
if (this._private__invalidateMask !== null) {
|
|
mask._internal_merge(this._private__invalidateMask);
|
|
this._private__invalidateMask = null;
|
|
}
|
|
this._private__drawImpl(mask, performance.now());
|
|
}
|
|
else {
|
|
this._private__model._internal_fullUpdate();
|
|
}
|
|
}
|
|
_internal_paint(invalidateMask) {
|
|
if (invalidateMask === undefined) {
|
|
invalidateMask = InvalidateMask._internal_full();
|
|
}
|
|
for (let i = 0; i < this._private__paneWidgets.length; i++) {
|
|
this._private__paneWidgets[i]._internal_paint(invalidateMask._internal_invalidateForPane(i)._internal_level);
|
|
}
|
|
if (this._private__options.timeScale.visible) {
|
|
this._private__timeAxisWidget._internal_paint(invalidateMask._internal_fullInvalidation());
|
|
}
|
|
}
|
|
_internal_applyOptions(options) {
|
|
const currentlyHasMouseWheelListener = shouldSubscribeMouseWheel(this._private__options);
|
|
// we don't need to merge options here because it's done in chart model
|
|
// and since both model and widget share the same object it will be done automatically for widget as well
|
|
// not ideal solution for sure, but it work's for now ¯\_(ツ)_/¯
|
|
this._private__model._internal_applyOptions(options);
|
|
const shouldHaveMouseWheelListener = shouldSubscribeMouseWheel(this._private__options);
|
|
if (shouldHaveMouseWheelListener !== currentlyHasMouseWheelListener) {
|
|
this._private__setMouseWheelEventListener(shouldHaveMouseWheelListener);
|
|
}
|
|
if (options['layout']?.panes) {
|
|
this._private__applyPanesOptions();
|
|
}
|
|
this._private__updateTimeAxisVisibility();
|
|
this._private__applyAutoSizeOptions(options);
|
|
}
|
|
_internal_clicked() {
|
|
return this._private__clicked;
|
|
}
|
|
_internal_dblClicked() {
|
|
return this._private__dblClicked;
|
|
}
|
|
_internal_crosshairMoved() {
|
|
return this._private__crosshairMoved;
|
|
}
|
|
_internal_takeScreenshot(addTopLayer = false) {
|
|
if (this._private__invalidateMask !== null) {
|
|
this._private__drawImpl(this._private__invalidateMask, performance.now());
|
|
this._private__invalidateMask = null;
|
|
}
|
|
const screeshotBitmapSize = this._private__traverseLayout(null);
|
|
const screenshotCanvas = document.createElement('canvas');
|
|
screenshotCanvas.width = screeshotBitmapSize.width;
|
|
screenshotCanvas.height = screeshotBitmapSize.height;
|
|
const ctx = ensureNotNull(screenshotCanvas.getContext('2d'));
|
|
this._private__traverseLayout(ctx, addTopLayer);
|
|
return screenshotCanvas;
|
|
}
|
|
_internal_getPriceAxisWidth(position) {
|
|
if (position === "left" /* DefaultPriceScaleId.Left */ && !this._private__isLeftAxisVisible()) {
|
|
return 0;
|
|
}
|
|
if (position === "right" /* DefaultPriceScaleId.Right */ && !this._private__isRightAxisVisible()) {
|
|
return 0;
|
|
}
|
|
if (this._private__paneWidgets.length === 0) {
|
|
return 0;
|
|
}
|
|
// we don't need to worry about exactly pane widget here
|
|
// because all pane widgets have the same width of price axis widget
|
|
// see _adjustSizeImpl
|
|
const priceAxisWidget = position === "left" /* DefaultPriceScaleId.Left */
|
|
? this._private__paneWidgets[0]._internal_leftPriceAxisWidget()
|
|
: this._private__paneWidgets[0]._internal_rightPriceAxisWidget();
|
|
return ensureNotNull(priceAxisWidget)._internal_getWidth();
|
|
}
|
|
_internal_autoSizeActive() {
|
|
return this._private__options.autoSize && this._private__observer !== null;
|
|
}
|
|
_internal_element() {
|
|
return this._private__element;
|
|
}
|
|
_internal_setCursorStyle(style) {
|
|
this._private__cursorStyleOverride = style;
|
|
if (this._private__cursorStyleOverride) {
|
|
this._internal_element().style.setProperty('cursor', style);
|
|
}
|
|
else {
|
|
this._internal_element().style.removeProperty('cursor');
|
|
}
|
|
}
|
|
_internal_getCursorOverrideStyle() {
|
|
return this._private__cursorStyleOverride;
|
|
}
|
|
_internal_paneSize(paneIndex) {
|
|
return ensureDefined(this._private__paneWidgets[paneIndex])._internal_getSize();
|
|
}
|
|
_private__applyPanesOptions() {
|
|
this._private__paneSeparators.forEach((separator) => {
|
|
separator._internal_update();
|
|
});
|
|
}
|
|
// eslint-disable-next-line complexity
|
|
_private__applyAutoSizeOptions(options) {
|
|
if (options.autoSize === undefined && this._private__observer && (options.width !== undefined || options.height !== undefined)) {
|
|
warn(`You should turn autoSize off explicitly before specifying sizes; try adding options.autoSize: false to new options`);
|
|
return;
|
|
}
|
|
if (options.autoSize && !this._private__observer) {
|
|
// installing observer will override resize if successful
|
|
this._private__installObserver();
|
|
}
|
|
if (options.autoSize === false && this._private__observer !== null) {
|
|
this._private__uninstallObserver();
|
|
}
|
|
if (!options.autoSize && (options.width !== undefined || options.height !== undefined)) {
|
|
this._internal_resize(options.width || this._private__width, options.height || this._private__height);
|
|
}
|
|
}
|
|
/**
|
|
* Traverses the widget's layout (pane and axis child widgets),
|
|
* draws the screenshot (if rendering context is passed) and returns the screenshot bitmap size
|
|
*
|
|
* @param ctx - if passed, used to draw the screenshot of widget
|
|
* @param addTopLayer - if true, the top layer with crosshair and primitives will be drawn
|
|
* @returns screenshot bitmap size
|
|
*/
|
|
_private__traverseLayout(ctx, addTopLayer) {
|
|
let totalWidth = 0;
|
|
let totalHeight = 0;
|
|
const firstPane = this._private__paneWidgets[0];
|
|
const drawPriceAxises = (position, targetX) => {
|
|
let targetY = 0;
|
|
for (let paneIndex = 0; paneIndex < this._private__paneWidgets.length; paneIndex++) {
|
|
const paneWidget = this._private__paneWidgets[paneIndex];
|
|
const priceAxisWidget = ensureNotNull(position === 'left' ? paneWidget._internal_leftPriceAxisWidget() : paneWidget._internal_rightPriceAxisWidget());
|
|
const bitmapSize = priceAxisWidget._internal_getBitmapSize();
|
|
if (ctx !== null) {
|
|
priceAxisWidget._internal_drawBitmap(ctx, targetX, targetY, addTopLayer);
|
|
}
|
|
targetY += bitmapSize.height;
|
|
if (paneIndex < this._private__paneWidgets.length - 1) {
|
|
const separator = this._private__paneSeparators[paneIndex];
|
|
const separatorBitmapSize = separator._internal_getBitmapSize();
|
|
if (ctx !== null) {
|
|
separator._internal_drawBitmap(ctx, targetX, targetY);
|
|
}
|
|
targetY += separatorBitmapSize.height;
|
|
}
|
|
}
|
|
};
|
|
// draw left price scale if exists
|
|
if (this._private__isLeftAxisVisible()) {
|
|
drawPriceAxises('left', 0);
|
|
const leftAxisBitmapWidth = ensureNotNull(firstPane._internal_leftPriceAxisWidget())._internal_getBitmapSize().width;
|
|
totalWidth += leftAxisBitmapWidth;
|
|
}
|
|
for (let paneIndex = 0; paneIndex < this._private__paneWidgets.length; paneIndex++) {
|
|
const paneWidget = this._private__paneWidgets[paneIndex];
|
|
const bitmapSize = paneWidget._internal_getBitmapSize();
|
|
if (ctx !== null) {
|
|
paneWidget._internal_drawBitmap(ctx, totalWidth, totalHeight, addTopLayer);
|
|
}
|
|
totalHeight += bitmapSize.height;
|
|
if (paneIndex < this._private__paneWidgets.length - 1) {
|
|
const separator = this._private__paneSeparators[paneIndex];
|
|
const separatorBitmapSize = separator._internal_getBitmapSize();
|
|
if (ctx !== null) {
|
|
separator._internal_drawBitmap(ctx, totalWidth, totalHeight);
|
|
}
|
|
totalHeight += separatorBitmapSize.height;
|
|
}
|
|
}
|
|
const firstPaneBitmapWidth = firstPane._internal_getBitmapSize().width;
|
|
totalWidth += firstPaneBitmapWidth;
|
|
// draw right price scale if exists
|
|
if (this._private__isRightAxisVisible()) {
|
|
drawPriceAxises('right', totalWidth);
|
|
const rightAxisBitmapWidth = ensureNotNull(firstPane._internal_rightPriceAxisWidget())._internal_getBitmapSize().width;
|
|
totalWidth += rightAxisBitmapWidth;
|
|
}
|
|
const drawStub = (position, targetX, targetY) => {
|
|
const stub = ensureNotNull(position === 'left' ? this._private__timeAxisWidget._internal_leftStub() : this._private__timeAxisWidget._internal_rightStub());
|
|
stub._internal_drawBitmap(ensureNotNull(ctx), targetX, targetY);
|
|
};
|
|
// draw time scale and stubs
|
|
if (this._private__options.timeScale.visible) {
|
|
const timeAxisBitmapSize = this._private__timeAxisWidget._internal_getBitmapSize();
|
|
if (ctx !== null) {
|
|
let targetX = 0;
|
|
if (this._private__isLeftAxisVisible()) {
|
|
drawStub('left', targetX, totalHeight);
|
|
targetX = ensureNotNull(firstPane._internal_leftPriceAxisWidget())._internal_getBitmapSize().width;
|
|
}
|
|
this._private__timeAxisWidget._internal_drawBitmap(ctx, targetX, totalHeight, addTopLayer);
|
|
targetX += timeAxisBitmapSize.width;
|
|
if (this._private__isRightAxisVisible()) {
|
|
drawStub('right', targetX, totalHeight);
|
|
}
|
|
}
|
|
totalHeight += timeAxisBitmapSize.height;
|
|
}
|
|
return size$1({
|
|
width: totalWidth,
|
|
height: totalHeight,
|
|
});
|
|
}
|
|
// eslint-disable-next-line complexity
|
|
_private__adjustSizeImpl() {
|
|
let totalStretch = 0;
|
|
let leftPriceAxisWidth = 0;
|
|
let rightPriceAxisWidth = 0;
|
|
for (const paneWidget of this._private__paneWidgets) {
|
|
if (this._private__isLeftAxisVisible()) {
|
|
leftPriceAxisWidth = Math.max(leftPriceAxisWidth, ensureNotNull(paneWidget._internal_leftPriceAxisWidget())._internal_optimalWidth(), this._private__options.leftPriceScale.minimumWidth);
|
|
}
|
|
if (this._private__isRightAxisVisible()) {
|
|
rightPriceAxisWidth = Math.max(rightPriceAxisWidth, ensureNotNull(paneWidget._internal_rightPriceAxisWidget())._internal_optimalWidth(), this._private__options.rightPriceScale.minimumWidth);
|
|
}
|
|
totalStretch += paneWidget._internal_stretchFactor();
|
|
}
|
|
leftPriceAxisWidth = suggestPriceScaleWidth(leftPriceAxisWidth);
|
|
rightPriceAxisWidth = suggestPriceScaleWidth(rightPriceAxisWidth);
|
|
const width = this._private__width;
|
|
const height = this._private__height;
|
|
const paneWidth = Math.max(width - leftPriceAxisWidth - rightPriceAxisWidth, 0);
|
|
const separatorCount = this._private__paneSeparators.length;
|
|
const separatorHeight = 1 /* SeparatorConstants.SeparatorHeight */;
|
|
const separatorsHeight = separatorHeight * separatorCount;
|
|
const timeAxisVisible = this._private__options.timeScale.visible;
|
|
let timeAxisHeight = timeAxisVisible ? Math.max(this._private__timeAxisWidget._internal_optimalHeight(), this._private__options.timeScale.minimumHeight) : 0;
|
|
timeAxisHeight = suggestTimeScaleHeight(timeAxisHeight);
|
|
const otherWidgetHeight = separatorsHeight + timeAxisHeight;
|
|
const totalPaneHeight = height < otherWidgetHeight ? 0 : height - otherWidgetHeight;
|
|
const stretchPixels = totalPaneHeight / totalStretch;
|
|
let accumulatedHeight = 0;
|
|
const pixelRatio = window.devicePixelRatio || 1;
|
|
for (let paneIndex = 0; paneIndex < this._private__paneWidgets.length; ++paneIndex) {
|
|
const paneWidget = this._private__paneWidgets[paneIndex];
|
|
paneWidget._internal_setState(this._private__model._internal_panes()[paneIndex]);
|
|
let paneHeight = 0;
|
|
let calculatePaneHeight = 0;
|
|
if (paneIndex === this._private__paneWidgets.length - 1) {
|
|
calculatePaneHeight = Math.ceil((totalPaneHeight - accumulatedHeight) * pixelRatio) / pixelRatio;
|
|
}
|
|
else {
|
|
calculatePaneHeight = Math.round(paneWidget._internal_stretchFactor() * stretchPixels * pixelRatio) / pixelRatio;
|
|
}
|
|
paneHeight = Math.max(calculatePaneHeight, 2);
|
|
accumulatedHeight += paneHeight;
|
|
paneWidget._internal_setSize(size$1({ width: paneWidth, height: paneHeight }));
|
|
if (this._private__isLeftAxisVisible()) {
|
|
paneWidget._internal_setPriceAxisSize(leftPriceAxisWidth, 'left');
|
|
}
|
|
if (this._private__isRightAxisVisible()) {
|
|
paneWidget._internal_setPriceAxisSize(rightPriceAxisWidth, 'right');
|
|
}
|
|
if (paneWidget._internal_state()) {
|
|
this._private__model._internal_setPaneHeight(paneWidget._internal_state(), paneHeight);
|
|
}
|
|
}
|
|
this._private__timeAxisWidget._internal_setSizes(size$1({ width: timeAxisVisible ? paneWidth : 0, height: timeAxisHeight }), timeAxisVisible ? leftPriceAxisWidth : 0, timeAxisVisible ? rightPriceAxisWidth : 0);
|
|
this._private__model._internal_setWidth(paneWidth);
|
|
if (this._private__leftPriceAxisWidth !== leftPriceAxisWidth) {
|
|
this._private__leftPriceAxisWidth = leftPriceAxisWidth;
|
|
}
|
|
if (this._private__rightPriceAxisWidth !== rightPriceAxisWidth) {
|
|
this._private__rightPriceAxisWidth = rightPriceAxisWidth;
|
|
}
|
|
}
|
|
_private__setMouseWheelEventListener(add) {
|
|
if (add) {
|
|
this._private__element.addEventListener('wheel', this._private__onWheelBound, { passive: false });
|
|
return;
|
|
}
|
|
this._private__element.removeEventListener('wheel', this._private__onWheelBound);
|
|
}
|
|
_private__determineWheelSpeedAdjustment(event) {
|
|
switch (event.deltaMode) {
|
|
case event.DOM_DELTA_PAGE:
|
|
// one screen at time scroll mode
|
|
return 120;
|
|
case event.DOM_DELTA_LINE:
|
|
// one line at time scroll mode
|
|
return 32;
|
|
}
|
|
if (!windowsChrome) {
|
|
return 1;
|
|
}
|
|
// Chromium on Windows has a bug where the scroll speed isn't correctly
|
|
// adjusted for high density displays. We need to correct for this so that
|
|
// scroll speed is consistent between browsers.
|
|
// https://bugs.chromium.org/p/chromium/issues/detail?id=1001735
|
|
// https://bugs.chromium.org/p/chromium/issues/detail?id=1207308
|
|
return (1 / window.devicePixelRatio);
|
|
}
|
|
_private__onMousewheel(event) {
|
|
if ((event.deltaX === 0 || !this._private__options['handleScroll'].mouseWheel) &&
|
|
(event.deltaY === 0 || !this._private__options['handleScale'].mouseWheel)) {
|
|
return;
|
|
}
|
|
const scrollSpeedAdjustment = this._private__determineWheelSpeedAdjustment(event);
|
|
const deltaX = scrollSpeedAdjustment * event.deltaX / 100;
|
|
const deltaY = -(scrollSpeedAdjustment * event.deltaY / 100);
|
|
if (event.cancelable) {
|
|
event.preventDefault();
|
|
}
|
|
if (deltaY !== 0 && this._private__options['handleScale'].mouseWheel) {
|
|
const zoomScale = Math.sign(deltaY) * Math.min(1, Math.abs(deltaY));
|
|
const scrollPosition = event.clientX - this._private__element.getBoundingClientRect().left;
|
|
this._internal_model()._internal_zoomTime(scrollPosition, zoomScale);
|
|
}
|
|
if (deltaX !== 0 && this._private__options['handleScroll'].mouseWheel) {
|
|
this._internal_model()._internal_scrollChart(deltaX * -80); // 80 is a made up coefficient, and minus is for the "natural" scroll
|
|
}
|
|
}
|
|
_private__drawImpl(invalidateMask, time) {
|
|
const invalidationType = invalidateMask._internal_fullInvalidation();
|
|
// actions for full invalidation ONLY (not shared with light)
|
|
if (invalidationType === 3 /* InvalidationLevel.Full */) {
|
|
this._private__updateGui();
|
|
}
|
|
// light or full invalidate actions
|
|
if (invalidationType === 3 /* InvalidationLevel.Full */ ||
|
|
invalidationType === 2 /* InvalidationLevel.Light */) {
|
|
this._private__applyMomentaryAutoScale(invalidateMask);
|
|
this._private__applyTimeScaleInvalidations(invalidateMask, time);
|
|
this._private__timeAxisWidget._internal_update();
|
|
this._private__paneWidgets.forEach((pane) => {
|
|
pane._internal_updatePriceAxisWidgets();
|
|
});
|
|
// In the case a full invalidation has been postponed during the draw, reapply
|
|
// the timescale invalidations. A full invalidation would mean there is a change
|
|
// in the timescale width (caused by price scale changes) that needs to be drawn
|
|
// right away to avoid flickering.
|
|
if (this._private__invalidateMask?._internal_fullInvalidation() === 3 /* InvalidationLevel.Full */) {
|
|
this._private__invalidateMask._internal_merge(invalidateMask);
|
|
this._private__updateGui();
|
|
this._private__applyMomentaryAutoScale(this._private__invalidateMask);
|
|
this._private__applyTimeScaleInvalidations(this._private__invalidateMask, time);
|
|
invalidateMask = this._private__invalidateMask;
|
|
this._private__invalidateMask = null;
|
|
}
|
|
}
|
|
this._internal_paint(invalidateMask);
|
|
}
|
|
_private__applyTimeScaleInvalidations(invalidateMask, time) {
|
|
for (const tsInvalidation of invalidateMask._internal_timeScaleInvalidations()) {
|
|
this._private__applyTimeScaleInvalidation(tsInvalidation, time);
|
|
}
|
|
}
|
|
_private__applyMomentaryAutoScale(invalidateMask) {
|
|
const panes = this._private__model._internal_panes();
|
|
for (let i = 0; i < panes.length; i++) {
|
|
if (invalidateMask._internal_invalidateForPane(i)._internal_autoScale) {
|
|
panes[i]._internal_momentaryAutoScale();
|
|
}
|
|
}
|
|
}
|
|
_private__applyTimeScaleInvalidation(invalidation, time) {
|
|
const timeScale = this._private__model._internal_timeScale();
|
|
switch (invalidation._internal_type) {
|
|
case 0 /* TimeScaleInvalidationType.FitContent */:
|
|
timeScale._internal_fitContent();
|
|
break;
|
|
case 1 /* TimeScaleInvalidationType.ApplyRange */:
|
|
timeScale._internal_setLogicalRange(invalidation._internal_value);
|
|
break;
|
|
case 2 /* TimeScaleInvalidationType.ApplyBarSpacing */:
|
|
timeScale._internal_setBarSpacing(invalidation._internal_value);
|
|
break;
|
|
case 3 /* TimeScaleInvalidationType.ApplyRightOffset */:
|
|
timeScale._internal_setRightOffset(invalidation._internal_value);
|
|
break;
|
|
case 4 /* TimeScaleInvalidationType.Reset */:
|
|
timeScale._internal_restoreDefault();
|
|
break;
|
|
case 5 /* TimeScaleInvalidationType.Animation */:
|
|
if (!invalidation._internal_value._internal_finished(time)) {
|
|
timeScale._internal_setRightOffset(invalidation._internal_value._internal_getPosition(time));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
_private__invalidateHandler(invalidateMask) {
|
|
if (this._private__invalidateMask !== null) {
|
|
this._private__invalidateMask._internal_merge(invalidateMask);
|
|
}
|
|
else {
|
|
this._private__invalidateMask = invalidateMask;
|
|
}
|
|
if (!this._private__drawPlanned) {
|
|
this._private__drawPlanned = true;
|
|
this._private__drawRafId = window.requestAnimationFrame((time) => {
|
|
this._private__drawPlanned = false;
|
|
this._private__drawRafId = 0;
|
|
if (this._private__invalidateMask !== null) {
|
|
const mask = this._private__invalidateMask;
|
|
this._private__invalidateMask = null;
|
|
this._private__drawImpl(mask, time);
|
|
for (const tsInvalidation of mask._internal_timeScaleInvalidations()) {
|
|
if (tsInvalidation._internal_type === 5 /* TimeScaleInvalidationType.Animation */ && !tsInvalidation._internal_value._internal_finished(time)) {
|
|
this._internal_model()._internal_setTimeScaleAnimation(tsInvalidation._internal_value);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
_private__updateGui() {
|
|
this._private__syncGuiWithModel();
|
|
}
|
|
_private__destroySeparator(separator) {
|
|
this._private__tableElement.removeChild(separator._internal_getElement());
|
|
separator._internal_destroy();
|
|
}
|
|
_private__syncGuiWithModel() {
|
|
const panes = this._private__model._internal_panes();
|
|
const targetPaneWidgetsCount = panes.length;
|
|
const actualPaneWidgetsCount = this._private__paneWidgets.length;
|
|
// Remove (if needed) pane widgets and separators
|
|
for (let i = targetPaneWidgetsCount; i < actualPaneWidgetsCount; i++) {
|
|
const paneWidget = ensureDefined(this._private__paneWidgets.pop());
|
|
this._private__tableElement.removeChild(paneWidget._internal_getElement());
|
|
paneWidget._internal_clicked()._internal_unsubscribeAll(this);
|
|
paneWidget._internal_dblClicked()._internal_unsubscribeAll(this);
|
|
paneWidget._internal_destroy();
|
|
const paneSeparator = this._private__paneSeparators.pop();
|
|
if (paneSeparator !== undefined) {
|
|
this._private__destroySeparator(paneSeparator);
|
|
}
|
|
}
|
|
// Create (if needed) new pane widgets and separators
|
|
for (let i = actualPaneWidgetsCount; i < targetPaneWidgetsCount; i++) {
|
|
const paneWidget = new PaneWidget(this, panes[i]);
|
|
paneWidget._internal_clicked()._internal_subscribe(this._private__onPaneWidgetClicked.bind(this, paneWidget), this);
|
|
paneWidget._internal_dblClicked()._internal_subscribe(this._private__onPaneWidgetDblClicked.bind(this, paneWidget), this);
|
|
this._private__paneWidgets.push(paneWidget);
|
|
// create and insert separator
|
|
if (i > 0) {
|
|
const paneSeparator = new PaneSeparator(this, i - 1, i);
|
|
this._private__paneSeparators.push(paneSeparator);
|
|
this._private__tableElement.insertBefore(paneSeparator._internal_getElement(), this._private__timeAxisWidget._internal_getElement());
|
|
}
|
|
// insert paneWidget
|
|
this._private__tableElement.insertBefore(paneWidget._internal_getElement(), this._private__timeAxisWidget._internal_getElement());
|
|
}
|
|
for (let i = 0; i < targetPaneWidgetsCount; i++) {
|
|
const state = panes[i];
|
|
const paneWidget = this._private__paneWidgets[i];
|
|
if (paneWidget._internal_state() !== state) {
|
|
paneWidget._internal_setState(state);
|
|
}
|
|
else {
|
|
paneWidget._internal_updatePriceAxisWidgetsStates();
|
|
}
|
|
}
|
|
this._private__updateTimeAxisVisibility();
|
|
this._private__adjustSizeImpl();
|
|
}
|
|
_private__getMouseEventParamsImpl(index, point, event, pane) {
|
|
const seriesData = new Map();
|
|
if (index !== null) {
|
|
const serieses = this._private__model._internal_serieses();
|
|
serieses.forEach((s) => {
|
|
// TODO: replace with search left
|
|
const data = s._internal_bars()._internal_search(index);
|
|
if (data !== null) {
|
|
seriesData.set(s, data);
|
|
}
|
|
});
|
|
}
|
|
let clientTime;
|
|
if (index !== null) {
|
|
const timePoint = this._private__model._internal_timeScale()._internal_indexToTimeScalePoint(index)?.originalTime;
|
|
if (timePoint !== undefined) {
|
|
clientTime = timePoint;
|
|
}
|
|
}
|
|
const hoveredSource = this._internal_model()._internal_hoveredSource();
|
|
const paneIndex = this._private__getPaneIndex(pane);
|
|
const hoveredInfo = buildHoveredEventInfo(hoveredSource, paneIndex);
|
|
return {
|
|
_internal_originalTime: clientTime,
|
|
_internal_index: index ?? undefined,
|
|
_internal_point: point ?? undefined,
|
|
_internal_paneIndex: paneIndex !== -1 ? paneIndex : undefined,
|
|
_internal_hoveredSeries: hoveredInfo._internal_hoveredSeries,
|
|
_internal_seriesData: seriesData,
|
|
_internal_hoveredObject: hoveredInfo._internal_hoveredObject,
|
|
_internal_hoveredInfo: hoveredInfo._internal_hoveredInfo,
|
|
_internal_touchMouseEventData: event ?? undefined,
|
|
};
|
|
}
|
|
_private__getPaneIndex(pane) {
|
|
let paneIndex = -1;
|
|
if (pane) {
|
|
paneIndex = this._private__paneWidgets.indexOf(pane);
|
|
}
|
|
else {
|
|
const crosshairPane = this._internal_model()._internal_crosshairSource()._internal_pane();
|
|
if (crosshairPane !== null) {
|
|
paneIndex = this._internal_model()._internal_panes().indexOf(crosshairPane);
|
|
}
|
|
}
|
|
return paneIndex;
|
|
}
|
|
_private__onPaneWidgetClicked(pane, time, point, event) {
|
|
this._private__clicked._internal_fire(() => this._private__getMouseEventParamsImpl(time, point, event, pane));
|
|
}
|
|
_private__onPaneWidgetDblClicked(pane, time, point, event) {
|
|
this._private__dblClicked._internal_fire(() => this._private__getMouseEventParamsImpl(time, point, event, pane));
|
|
}
|
|
_private__onPaneWidgetCrosshairMoved(time, point, event) {
|
|
this._internal_setCursorStyle(this._internal_model()._internal_hoveredSource()?._internal_cursorStyle ?? null);
|
|
this._private__crosshairMoved._internal_fire(() => this._private__getMouseEventParamsImpl(time, point, event));
|
|
}
|
|
_private__updateTimeAxisVisibility() {
|
|
const display = this._private__options.timeScale.visible ? '' : 'none';
|
|
this._private__timeAxisWidget._internal_getElement().style.display = display;
|
|
}
|
|
_private__isLeftAxisVisible() {
|
|
return this._private__paneWidgets[0]._internal_state()._internal_leftPriceScale()._internal_options().visible;
|
|
}
|
|
_private__isRightAxisVisible() {
|
|
return this._private__paneWidgets[0]._internal_state()._internal_rightPriceScale()._internal_options().visible;
|
|
}
|
|
_private__installObserver() {
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
if (!('ResizeObserver' in window)) {
|
|
warn('Options contains "autoSize" flag, but the browser does not support ResizeObserver feature. Please provide polyfill.');
|
|
return false;
|
|
}
|
|
else {
|
|
this._private__observer = new ResizeObserver((entries) => {
|
|
// There is no need to check if entry.target === this._container since there is only
|
|
// a single element being observed.
|
|
// and we want to use the last entry (if multiple) because it would be most up to date
|
|
// (since the browser may batch multiple updates).
|
|
const containerEntry = entries[entries.length - 1];
|
|
if (!containerEntry) {
|
|
// this may be undefined if the entries array was empty.
|
|
return;
|
|
}
|
|
const newW = containerEntry.contentRect.width;
|
|
const newH = containerEntry.contentRect.height;
|
|
// Use forceRepaint=true to paint synchronously within this callback,
|
|
// rather than scheduling to the next animation frame. This prevents
|
|
// the visual "jitter" that occurs when the container has resized but
|
|
// the canvas hasn't repainted yet.
|
|
this._internal_resize(newW, newH, true);
|
|
});
|
|
this._private__observer.observe(this._private__container, { box: 'border-box' });
|
|
return true;
|
|
}
|
|
}
|
|
_private__uninstallObserver() {
|
|
if (this._private__observer !== null) {
|
|
this._private__observer.disconnect();
|
|
}
|
|
this._private__observer = null;
|
|
}
|
|
}
|
|
function disableSelection(element) {
|
|
element.style.userSelect = 'none';
|
|
// eslint-disable-next-line deprecation/deprecation
|
|
element.style.webkitUserSelect = 'none';
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-member-access
|
|
element.style.msUserSelect = 'none';
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-member-access
|
|
element.style.MozUserSelect = 'none';
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-member-access
|
|
element.style.webkitTapHighlightColor = 'transparent';
|
|
}
|
|
function shouldSubscribeMouseWheel(options) {
|
|
return Boolean(options['handleScroll'].mouseWheel || options['handleScale'].mouseWheel);
|
|
}
|
|
|
|
function isWhitespaceData(data) {
|
|
return data.open === undefined && data.value === undefined;
|
|
}
|
|
function isFulfilledData(data) {
|
|
return isFulfilledBarData(data) || isFulfilledLineData(data);
|
|
}
|
|
function isFulfilledBarData(data) {
|
|
return data.open !== undefined;
|
|
}
|
|
function isFulfilledLineData(data) {
|
|
return data.value !== undefined;
|
|
}
|
|
|
|
function getColoredLineBasedSeriesPlotRow(time, index, item, originalTime) {
|
|
const val = item.value;
|
|
const res = { _internal_index: index, _internal_time: time, _internal_value: [val, val, val, val], _internal_originalTime: originalTime };
|
|
if (item.color !== undefined) {
|
|
res._internal_color = item.color;
|
|
}
|
|
return res;
|
|
}
|
|
function getAreaSeriesPlotRow(time, index, item, originalTime) {
|
|
const val = item.value;
|
|
const res = { _internal_index: index, _internal_time: time, _internal_value: [val, val, val, val], _internal_originalTime: originalTime };
|
|
if (item.lineColor !== undefined) {
|
|
res._internal_lineColor = item.lineColor;
|
|
}
|
|
if (item.topColor !== undefined) {
|
|
res._internal_topColor = item.topColor;
|
|
}
|
|
if (item.bottomColor !== undefined) {
|
|
res._internal_bottomColor = item.bottomColor;
|
|
}
|
|
return res;
|
|
}
|
|
function getBaselineSeriesPlotRow(time, index, item, originalTime) {
|
|
const val = item.value;
|
|
const res = { _internal_index: index, _internal_time: time, _internal_value: [val, val, val, val], _internal_originalTime: originalTime };
|
|
if (item.topLineColor !== undefined) {
|
|
res._internal_topLineColor = item.topLineColor;
|
|
}
|
|
if (item.bottomLineColor !== undefined) {
|
|
res._internal_bottomLineColor = item.bottomLineColor;
|
|
}
|
|
if (item.topFillColor1 !== undefined) {
|
|
res._internal_topFillColor1 = item.topFillColor1;
|
|
}
|
|
if (item.topFillColor2 !== undefined) {
|
|
res._internal_topFillColor2 = item.topFillColor2;
|
|
}
|
|
if (item.bottomFillColor1 !== undefined) {
|
|
res._internal_bottomFillColor1 = item.bottomFillColor1;
|
|
}
|
|
if (item.bottomFillColor2 !== undefined) {
|
|
res._internal_bottomFillColor2 = item.bottomFillColor2;
|
|
}
|
|
return res;
|
|
}
|
|
function getBarSeriesPlotRow(time, index, item, originalTime) {
|
|
const res = { _internal_index: index, _internal_time: time, _internal_value: [item.open, item.high, item.low, item.close], _internal_originalTime: originalTime };
|
|
if (item.color !== undefined) {
|
|
res._internal_color = item.color;
|
|
}
|
|
return res;
|
|
}
|
|
function getCandlestickSeriesPlotRow(time, index, item, originalTime) {
|
|
const res = { _internal_index: index, _internal_time: time, _internal_value: [item.open, item.high, item.low, item.close], _internal_originalTime: originalTime };
|
|
if (item.color !== undefined) {
|
|
res._internal_color = item.color;
|
|
}
|
|
if (item.borderColor !== undefined) {
|
|
res._internal_borderColor = item.borderColor;
|
|
}
|
|
if (item.wickColor !== undefined) {
|
|
res._internal_wickColor = item.wickColor;
|
|
}
|
|
return res;
|
|
}
|
|
function getCustomSeriesPlotRow(time, index, item, originalTime, dataToPlotRow) {
|
|
const values = ensureDefined(dataToPlotRow)(item);
|
|
const max = Math.max(...values);
|
|
const min = Math.min(...values);
|
|
const last = values[values.length - 1];
|
|
const value = [last, max, min, last];
|
|
const { time: excludedTime, color, ...data } = item;
|
|
return { _internal_index: index, _internal_time: time, _internal_value: value, _internal_originalTime: originalTime, _internal_data: data, _internal_color: color };
|
|
}
|
|
function isSeriesPlotRow(row) {
|
|
return row._internal_value !== undefined;
|
|
}
|
|
function wrapCustomValues(plotRow, bar) {
|
|
if (bar.customValues !== undefined) {
|
|
plotRow._internal_customValues = bar.customValues;
|
|
}
|
|
return plotRow;
|
|
}
|
|
function isWhitespaceDataWithCustomCheck(bar, customIsWhitespace) {
|
|
if (customIsWhitespace) {
|
|
return customIsWhitespace(bar);
|
|
}
|
|
return isWhitespaceData(bar);
|
|
}
|
|
function wrapWhitespaceData(createPlotRowFn) {
|
|
return (time, index, bar, originalTime, dataToPlotRow, customIsWhitespace) => {
|
|
if (isWhitespaceDataWithCustomCheck(bar, customIsWhitespace)) {
|
|
return wrapCustomValues({ _internal_time: time, _internal_index: index, _internal_originalTime: originalTime }, bar);
|
|
}
|
|
return wrapCustomValues(createPlotRowFn(time, index, bar, originalTime, dataToPlotRow), bar);
|
|
};
|
|
}
|
|
function getSeriesPlotRowCreator(seriesType) {
|
|
const seriesPlotRowFnMap = {
|
|
Candlestick: wrapWhitespaceData(getCandlestickSeriesPlotRow),
|
|
Bar: wrapWhitespaceData(getBarSeriesPlotRow),
|
|
Area: wrapWhitespaceData(getAreaSeriesPlotRow),
|
|
Baseline: wrapWhitespaceData(getBaselineSeriesPlotRow),
|
|
Histogram: wrapWhitespaceData(getColoredLineBasedSeriesPlotRow),
|
|
Line: wrapWhitespaceData(getColoredLineBasedSeriesPlotRow),
|
|
Custom: wrapWhitespaceData(getCustomSeriesPlotRow),
|
|
};
|
|
return seriesPlotRowFnMap[seriesType];
|
|
}
|
|
|
|
/// <reference types="_build-time-constants" />
|
|
function createEmptyTimePointData(timePoint) {
|
|
return { _internal_index: 0, _internal_mapping: new Map(), _internal_timePoint: timePoint };
|
|
}
|
|
function seriesRowsFirstAndLastTime(seriesRows, bh) {
|
|
if (seriesRows === undefined || seriesRows.length === 0) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
_internal_firstTime: bh.key(seriesRows[0]._internal_time),
|
|
_internal_lastTime: bh.key(seriesRows[seriesRows.length - 1]._internal_time),
|
|
};
|
|
}
|
|
function seriesUpdateInfo(seriesRows, prevSeriesRows, bh) {
|
|
const firstAndLastTime = seriesRowsFirstAndLastTime(seriesRows, bh);
|
|
const prevFirstAndLastTime = seriesRowsFirstAndLastTime(prevSeriesRows, bh);
|
|
if (firstAndLastTime !== undefined && prevFirstAndLastTime !== undefined) {
|
|
return {
|
|
_internal_historicalUpdate: false,
|
|
_internal_lastBarUpdatedOrNewBarsAddedToTheRight: firstAndLastTime._internal_lastTime >= prevFirstAndLastTime._internal_lastTime &&
|
|
firstAndLastTime._internal_firstTime >= prevFirstAndLastTime._internal_firstTime,
|
|
};
|
|
}
|
|
return undefined;
|
|
}
|
|
function timeScalePointTime(mergedPointData) {
|
|
let result;
|
|
mergedPointData.forEach((v) => {
|
|
if (result === undefined) {
|
|
result = v._internal_originalTime;
|
|
}
|
|
});
|
|
return ensureDefined(result);
|
|
}
|
|
function saveOriginalTime(data) {
|
|
if (data._internal_originalTime === undefined) {
|
|
data._internal_originalTime = data.time;
|
|
}
|
|
}
|
|
class DataLayer {
|
|
constructor(horzScaleBehavior) {
|
|
// note that _pointDataByTimePoint and _seriesRowsBySeries shares THE SAME objects in their values between each other
|
|
// it's just different kind of maps to make usages/perf better
|
|
this._private__pointDataByTimePoint = new Map();
|
|
this._private__seriesRowsBySeries = new Map();
|
|
this._private__seriesLastTimePoint = new Map();
|
|
// this is kind of "dest" values (in opposite to "source" ones) - we don't need to modify it manually, the only by calling _updateTimeScalePoints or updateSeriesData methods
|
|
this._private__sortedTimePoints = [];
|
|
this._private__horzScaleBehavior = horzScaleBehavior;
|
|
}
|
|
_internal_destroy() {
|
|
this._private__pointDataByTimePoint.clear();
|
|
this._private__seriesRowsBySeries.clear();
|
|
this._private__seriesLastTimePoint.clear();
|
|
this._private__sortedTimePoints = [];
|
|
}
|
|
_internal_setSeriesData(series, data) {
|
|
let needCleanupPoints = this._private__pointDataByTimePoint.size !== 0;
|
|
let isTimeScaleAffected = false;
|
|
// save previous series rows data before it's replaced inside this._setRowsToSeries
|
|
const prevSeriesRows = this._private__seriesRowsBySeries.get(series);
|
|
if (prevSeriesRows !== undefined) {
|
|
if (this._private__seriesRowsBySeries.size === 1) {
|
|
needCleanupPoints = false;
|
|
isTimeScaleAffected = true;
|
|
// perf optimization - if there is only 1 series, then we can just clear and fill everything from scratch
|
|
this._private__pointDataByTimePoint.clear();
|
|
}
|
|
else {
|
|
// perf optimization - actually we have to use this._pointDataByTimePoint for going through here
|
|
// but as soon as this._sortedTimePoints is just a different form of _pointDataByTimePoint we can use it as well
|
|
for (const point of this._private__sortedTimePoints) {
|
|
if (point.pointData._internal_mapping.delete(series)) {
|
|
isTimeScaleAffected = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let seriesRows = [];
|
|
if (data.length !== 0) {
|
|
const originalTimes = data.map((d) => d.time);
|
|
const timeConverter = this._private__horzScaleBehavior.createConverterToInternalObj(data);
|
|
const createPlotRow = getSeriesPlotRowCreator(series._internal_seriesType());
|
|
const dataToPlotRow = series._internal_customSeriesPlotValuesBuilder();
|
|
const customWhitespaceChecker = series._internal_customSeriesWhitespaceCheck();
|
|
seriesRows = data.map((item, index) => {
|
|
const time = timeConverter(item.time);
|
|
const horzItemKey = this._private__horzScaleBehavior.key(time);
|
|
let timePointData = this._private__pointDataByTimePoint.get(horzItemKey);
|
|
if (timePointData === undefined) {
|
|
// the indexes will be sync later
|
|
timePointData = createEmptyTimePointData(time);
|
|
this._private__pointDataByTimePoint.set(horzItemKey, timePointData);
|
|
isTimeScaleAffected = true;
|
|
}
|
|
const row = createPlotRow(time, timePointData._internal_index, item, originalTimes[index], dataToPlotRow, customWhitespaceChecker);
|
|
timePointData._internal_mapping.set(series, row);
|
|
return row;
|
|
});
|
|
}
|
|
if (needCleanupPoints) {
|
|
// we deleted the old data from mapping and added the new ones
|
|
// so there might be empty points now, let's remove them first
|
|
this._private__cleanupPointsData();
|
|
}
|
|
this._private__setRowsToSeries(series, seriesRows);
|
|
let firstChangedPointIndex = -1;
|
|
if (isTimeScaleAffected) {
|
|
// then generate the time scale points
|
|
// timeWeight will be updates in _updateTimeScalePoints later
|
|
const newTimeScalePoints = [];
|
|
this._private__pointDataByTimePoint.forEach((pointData) => {
|
|
newTimeScalePoints.push({
|
|
timeWeight: 0,
|
|
time: pointData._internal_timePoint,
|
|
pointData,
|
|
originalTime: timeScalePointTime(pointData._internal_mapping),
|
|
});
|
|
});
|
|
newTimeScalePoints.sort((t1, t2) => this._private__horzScaleBehavior.key(t1.time) - this._private__horzScaleBehavior.key(t2.time));
|
|
firstChangedPointIndex = this._private__replaceTimeScalePoints(newTimeScalePoints);
|
|
}
|
|
return this._private__getUpdateResponse(series, firstChangedPointIndex, seriesUpdateInfo(this._private__seriesRowsBySeries.get(series), prevSeriesRows, this._private__horzScaleBehavior));
|
|
}
|
|
_internal_removeSeries(series) {
|
|
return this._internal_setSeriesData(series, []);
|
|
}
|
|
// eslint-disable-next-line complexity
|
|
_internal_updateSeriesData(series, data, historicalUpdate) {
|
|
// check if conflation is enabled and this is a historical update
|
|
if (historicalUpdate && series._internal_isConflationEnabled()) {
|
|
throw new Error('Historical updates are not supported when conflation is enabled. Conflation requires data to be processed in order.');
|
|
}
|
|
const extendedData = data;
|
|
saveOriginalTime(extendedData);
|
|
// convertStringToBusinessDay(data);
|
|
this._private__horzScaleBehavior.preprocessData(data);
|
|
const timeConverter = this._private__horzScaleBehavior.createConverterToInternalObj([data]);
|
|
const time = timeConverter(data.time);
|
|
const lastSeriesTime = this._private__seriesLastTimePoint.get(series);
|
|
if (!historicalUpdate && lastSeriesTime !== undefined && this._private__horzScaleBehavior.key(time) < this._private__horzScaleBehavior.key(lastSeriesTime)) {
|
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
throw new Error(`Cannot update oldest data, last time=${lastSeriesTime}, new time=${time}`);
|
|
}
|
|
let pointDataAtTime = this._private__pointDataByTimePoint.get(this._private__horzScaleBehavior.key(time));
|
|
if (historicalUpdate && pointDataAtTime === undefined) {
|
|
throw new Error('Cannot update non-existing data point when historicalUpdate is true');
|
|
}
|
|
// if no point data found for the new data item
|
|
// that means that we need to update scale
|
|
const affectsTimeScale = pointDataAtTime === undefined;
|
|
if (pointDataAtTime === undefined) {
|
|
// the indexes will be sync later
|
|
pointDataAtTime = createEmptyTimePointData(time);
|
|
this._private__pointDataByTimePoint.set(this._private__horzScaleBehavior.key(time), pointDataAtTime);
|
|
}
|
|
const createPlotRow = getSeriesPlotRowCreator(series._internal_seriesType());
|
|
const dataToPlotRow = series._internal_customSeriesPlotValuesBuilder();
|
|
const customWhitespaceChecker = series._internal_customSeriesWhitespaceCheck();
|
|
const plotRow = createPlotRow(time, pointDataAtTime._internal_index, data, extendedData._internal_originalTime, dataToPlotRow, customWhitespaceChecker);
|
|
// For efficient conflation updates, check if this is just updating the last bar
|
|
const isLastBarUpdate = !historicalUpdate &&
|
|
!affectsTimeScale &&
|
|
lastSeriesTime !== undefined &&
|
|
this._private__horzScaleBehavior.key(time) === this._private__horzScaleBehavior.key(lastSeriesTime);
|
|
pointDataAtTime._internal_mapping.set(series, plotRow);
|
|
if (historicalUpdate) {
|
|
this._private__updateHistoricalSeriesRow(series, plotRow, pointDataAtTime._internal_index);
|
|
}
|
|
else if (isLastBarUpdate && series._internal_isConflationEnabled() && isSeriesPlotRow(plotRow)) {
|
|
series._internal_updateLastConflatedChunk(plotRow);
|
|
this._private__updateLastSeriesRow(series, plotRow);
|
|
}
|
|
else {
|
|
this._private__updateLastSeriesRow(series, plotRow);
|
|
}
|
|
const info = {
|
|
_internal_lastBarUpdatedOrNewBarsAddedToTheRight: isSeriesPlotRow(plotRow),
|
|
_internal_historicalUpdate: historicalUpdate,
|
|
};
|
|
// if point already exist on the time scale - we don't need to make a full update and just make an incremental one
|
|
if (!affectsTimeScale) {
|
|
return this._private__getUpdateResponse(series, -1, info);
|
|
}
|
|
const newPoint = {
|
|
timeWeight: 0,
|
|
time: pointDataAtTime._internal_timePoint,
|
|
pointData: pointDataAtTime,
|
|
originalTime: timeScalePointTime(pointDataAtTime._internal_mapping),
|
|
};
|
|
const insertIndex = lowerBound(this._private__sortedTimePoints, this._private__horzScaleBehavior.key(newPoint.time), (a, b) => this._private__horzScaleBehavior.key(a.time) < b);
|
|
// yes, I know that this array is readonly and this change is intended to make it performative
|
|
// we marked _sortedTimePoints array as readonly to avoid modifying this array anywhere else
|
|
// but this place is exceptional case due performance reasons, sorry
|
|
this._private__sortedTimePoints.splice(insertIndex, 0, newPoint);
|
|
for (let index = insertIndex; index < this._private__sortedTimePoints.length; ++index) {
|
|
assignIndexToPointData(this._private__sortedTimePoints[index].pointData, index);
|
|
}
|
|
this._private__horzScaleBehavior.fillWeightsForPoints(this._private__sortedTimePoints, insertIndex);
|
|
return this._private__getUpdateResponse(series, insertIndex, info);
|
|
}
|
|
_internal_popSeriesData(series, count) {
|
|
const seriesData = this._private__seriesRowsBySeries.get(series);
|
|
if (seriesData === undefined || count <= 0) {
|
|
return [[], this._private__emptyUpdateResponse()];
|
|
}
|
|
count = Math.min(count, seriesData.length);
|
|
const poppedData = seriesData.splice(-count).reverse();
|
|
if (seriesData.length === 0) {
|
|
this._private__seriesLastTimePoint.delete(series);
|
|
}
|
|
else {
|
|
this._private__seriesLastTimePoint.set(series, seriesData[seriesData.length - 1]._internal_time);
|
|
}
|
|
for (const data of poppedData) {
|
|
const pointData = this._private__pointDataByTimePoint.get(this._private__horzScaleBehavior.key(data._internal_time));
|
|
if (!pointData) {
|
|
continue;
|
|
}
|
|
pointData._internal_mapping.delete(series);
|
|
if (pointData._internal_mapping.size !== 0) {
|
|
continue;
|
|
}
|
|
// No remaining data at this timepoint therefore we need to remove the internal
|
|
// timescale point and update the indices.
|
|
this._private__pointDataByTimePoint.delete(this._private__horzScaleBehavior.key(pointData._internal_timePoint));
|
|
this._private__sortedTimePoints.splice(pointData._internal_index, 1);
|
|
for (let index = pointData._internal_index; index < this._private__sortedTimePoints.length; ++index) {
|
|
assignIndexToPointData(this._private__sortedTimePoints[index].pointData, index);
|
|
}
|
|
}
|
|
const info = {
|
|
_internal_historicalUpdate: false,
|
|
_internal_lastBarUpdatedOrNewBarsAddedToTheRight: false,
|
|
};
|
|
return [poppedData, this._private__getUpdateResponse(series, this._private__sortedTimePoints.length - 1, info)];
|
|
}
|
|
_private__updateLastSeriesRow(series, plotRow) {
|
|
let seriesData = this._private__seriesRowsBySeries.get(series);
|
|
if (seriesData === undefined) {
|
|
seriesData = [];
|
|
this._private__seriesRowsBySeries.set(series, seriesData);
|
|
}
|
|
const lastSeriesRow = seriesData.length !== 0 ? seriesData[seriesData.length - 1] : null;
|
|
if (lastSeriesRow === null || this._private__horzScaleBehavior.key(plotRow._internal_time) > this._private__horzScaleBehavior.key(lastSeriesRow._internal_time)) {
|
|
if (isSeriesPlotRow(plotRow)) {
|
|
seriesData.push(plotRow);
|
|
}
|
|
}
|
|
else {
|
|
if (isSeriesPlotRow(plotRow)) {
|
|
seriesData[seriesData.length - 1] = plotRow;
|
|
}
|
|
else {
|
|
seriesData.splice(-1, 1);
|
|
}
|
|
}
|
|
this._private__seriesLastTimePoint.set(series, plotRow._internal_time);
|
|
}
|
|
_private__updateHistoricalSeriesRow(series, plotRow, pointDataIndex) {
|
|
const seriesData = this._private__seriesRowsBySeries.get(series);
|
|
if (seriesData === undefined) {
|
|
return;
|
|
}
|
|
// binary search for actual index in array.
|
|
const index = lowerBound(seriesData, pointDataIndex, (row, currentIndex) => row._internal_index < currentIndex);
|
|
if (isSeriesPlotRow(plotRow)) {
|
|
seriesData[index] = plotRow;
|
|
}
|
|
else {
|
|
seriesData.splice(index, 1);
|
|
}
|
|
}
|
|
_private__setRowsToSeries(series, seriesRows) {
|
|
if (seriesRows.length !== 0) {
|
|
this._private__seriesRowsBySeries.set(series, seriesRows.filter(isSeriesPlotRow));
|
|
this._private__seriesLastTimePoint.set(series, seriesRows[seriesRows.length - 1]._internal_time);
|
|
}
|
|
else {
|
|
this._private__seriesRowsBySeries.delete(series);
|
|
this._private__seriesLastTimePoint.delete(series);
|
|
}
|
|
}
|
|
_private__cleanupPointsData() {
|
|
// let's treat all current points as "potentially removed"
|
|
// we could create an array with actually potentially removed points
|
|
// but most likely this array will be similar to _sortedTimePoints so let's avoid using additional memory
|
|
// note that we can use _sortedTimePoints here since a point might be removed only it was here previously
|
|
for (const point of this._private__sortedTimePoints) {
|
|
if (point.pointData._internal_mapping.size === 0) {
|
|
this._private__pointDataByTimePoint.delete(this._private__horzScaleBehavior.key(point.time));
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Sets new time scale and make indexes valid for all series
|
|
*
|
|
* @returns The index of the first changed point or `-1` if there is no change.
|
|
*/
|
|
_private__replaceTimeScalePoints(newTimePoints) {
|
|
let firstChangedPointIndex = -1;
|
|
// search the first different point and "syncing" time weight by the way
|
|
for (let index = 0; index < this._private__sortedTimePoints.length && index < newTimePoints.length; ++index) {
|
|
const oldPoint = this._private__sortedTimePoints[index];
|
|
const newPoint = newTimePoints[index];
|
|
if (this._private__horzScaleBehavior.key(oldPoint.time) !== this._private__horzScaleBehavior.key(newPoint.time)) {
|
|
firstChangedPointIndex = index;
|
|
break;
|
|
}
|
|
// re-assign point's time weight for points if time is the same (and all prior times was the same)
|
|
newPoint.timeWeight = oldPoint.timeWeight;
|
|
assignIndexToPointData(newPoint.pointData, index);
|
|
}
|
|
if (firstChangedPointIndex === -1 && this._private__sortedTimePoints.length !== newTimePoints.length) {
|
|
// the common part of the prev and the new points are the same
|
|
// so the first changed point is the next after the common part
|
|
firstChangedPointIndex = Math.min(this._private__sortedTimePoints.length, newTimePoints.length);
|
|
}
|
|
if (firstChangedPointIndex === -1) {
|
|
// if no time scale changed, then do nothing
|
|
return -1;
|
|
}
|
|
// if time scale points are changed that means that we need to make full update to all series (with clearing points)
|
|
// but first we need to synchronize indexes and re-fill time weights
|
|
for (let index = firstChangedPointIndex; index < newTimePoints.length; ++index) {
|
|
assignIndexToPointData(newTimePoints[index].pointData, index);
|
|
}
|
|
// re-fill time weights for point after the first changed one
|
|
this._private__horzScaleBehavior.fillWeightsForPoints(newTimePoints, firstChangedPointIndex);
|
|
this._private__sortedTimePoints = newTimePoints;
|
|
return firstChangedPointIndex;
|
|
}
|
|
_private__getBaseIndex() {
|
|
if (this._private__seriesRowsBySeries.size === 0) {
|
|
// if we have no data then 'reset' the base index to null
|
|
return null;
|
|
}
|
|
let baseIndex = 0;
|
|
this._private__seriesRowsBySeries.forEach((data) => {
|
|
if (data.length !== 0) {
|
|
baseIndex = Math.max(baseIndex, data[data.length - 1]._internal_index);
|
|
}
|
|
});
|
|
return baseIndex;
|
|
}
|
|
_private__getUpdateResponse(updatedSeries, firstChangedPointIndex, info) {
|
|
const dataUpdateResponse = this._private__emptyUpdateResponse();
|
|
if (firstChangedPointIndex !== -1) {
|
|
// TODO: it's possible to make perf improvements by checking what series has data after firstChangedPointIndex
|
|
// but let's skip for now
|
|
this._private__seriesRowsBySeries.forEach((data, s) => {
|
|
dataUpdateResponse._internal_series.set(s, {
|
|
_internal_data: data,
|
|
_internal_info: s === updatedSeries ? info : undefined,
|
|
});
|
|
});
|
|
// if the series data was set to [] it will have already been removed from _seriesRowBySeries
|
|
// meaning the forEach above won't add the series to the data update response
|
|
// so we handle that case here
|
|
if (!this._private__seriesRowsBySeries.has(updatedSeries)) {
|
|
dataUpdateResponse._internal_series.set(updatedSeries, { _internal_data: [], _internal_info: info });
|
|
}
|
|
dataUpdateResponse._internal_timeScale._internal_points = this._private__sortedTimePoints;
|
|
dataUpdateResponse._internal_timeScale._internal_firstChangedPointIndex = firstChangedPointIndex;
|
|
}
|
|
else {
|
|
const seriesData = this._private__seriesRowsBySeries.get(updatedSeries);
|
|
// if no seriesData found that means that we just removed the series
|
|
dataUpdateResponse._internal_series.set(updatedSeries, { _internal_data: seriesData || [], _internal_info: info });
|
|
}
|
|
return dataUpdateResponse;
|
|
}
|
|
_private__emptyUpdateResponse() {
|
|
return {
|
|
_internal_series: new Map(),
|
|
_internal_timeScale: {
|
|
_internal_baseIndex: this._private__getBaseIndex(),
|
|
},
|
|
};
|
|
}
|
|
}
|
|
function assignIndexToPointData(pointData, index) {
|
|
// first, nevertheless update index of point data ("make it valid")
|
|
pointData._internal_index = index;
|
|
// and then we need to sync indexes for all series
|
|
pointData._internal_mapping.forEach((seriesRow) => {
|
|
seriesRow._internal_index = index;
|
|
});
|
|
}
|
|
|
|
function lowerBoundByCoordinate(item, value) {
|
|
return item._internal_x < value;
|
|
}
|
|
function upperBoundByCoordinate(item, value) {
|
|
return value < item._internal_x;
|
|
}
|
|
function lowerBoundByX(items, value, from, to) {
|
|
return lowerBound(items, value, lowerBoundByCoordinate, from, to);
|
|
}
|
|
function upperBoundByX(items, value, from, to) {
|
|
return upperBound(items, value, upperBoundByCoordinate, from, to);
|
|
}
|
|
function hoveredSeriesHitTestResult(distance, priority, itemType) {
|
|
return { _internal_distance: distance, _internal_priority: priority, _internal_itemType: itemType };
|
|
}
|
|
function isWithinHorizontalSweep(x, left, right, radius) {
|
|
return x >= left - radius && x <= right + radius;
|
|
}
|
|
function distanceToSegment(x, y, x1, y1, x2, y2) {
|
|
const deltaX = x2 - x1;
|
|
const deltaY = y2 - y1;
|
|
if (deltaX === 0 && deltaY === 0) {
|
|
return Math.hypot(x - x1, y - y1);
|
|
}
|
|
const projection = ((x - x1) * deltaX + (y - y1) * deltaY) / (deltaX * deltaX + deltaY * deltaY);
|
|
const clampedProjection = Math.max(0, Math.min(1, projection));
|
|
const closestX = x1 + deltaX * clampedProjection;
|
|
const closestY = y1 + deltaY * clampedProjection;
|
|
return Math.hypot(x - closestX, y - closestY);
|
|
}
|
|
const rangePair = [0, 0];
|
|
|
|
function slotStart(item, previousItem, barSpacing) {
|
|
if (previousItem === undefined || previousItem._internal_time !== item._internal_time - 1) {
|
|
return item._internal_x - barSpacing / 2;
|
|
}
|
|
return (previousItem._internal_x + item._internal_x) / 2;
|
|
}
|
|
function slotEnd(item, nextItem, barSpacing) {
|
|
if (nextItem === undefined || nextItem._internal_time !== item._internal_time + 1) {
|
|
return item._internal_x + barSpacing / 2;
|
|
}
|
|
return (item._internal_x + nextItem._internal_x) / 2;
|
|
}
|
|
// eslint-disable-next-line max-params, complexity
|
|
function hitTestSeriesRange(items, visibleRange, x, y, barSpacing, hitTestTolerance, rangeProvider) {
|
|
if (visibleRange === null || visibleRange.from >= visibleRange.to || items.length === 0) {
|
|
return null;
|
|
}
|
|
const horizontalRadius = barSpacing / 2 + hitTestTolerance;
|
|
const candidateFrom = lowerBoundByX(items, x - horizontalRadius, visibleRange.from, visibleRange.to);
|
|
const candidateTo = upperBoundByX(items, x + horizontalRadius, candidateFrom, visibleRange.to);
|
|
if (candidateFrom >= candidateTo) {
|
|
return null;
|
|
}
|
|
let minDistance = Number.POSITIVE_INFINITY;
|
|
for (let itemIndex = candidateFrom; itemIndex < candidateTo; itemIndex++) {
|
|
const item = items[itemIndex];
|
|
const previousItem = itemIndex > visibleRange.from ? items[itemIndex - 1] : undefined;
|
|
const nextItem = itemIndex < visibleRange.to - 1 ? items[itemIndex + 1] : undefined;
|
|
const leftBoundary = slotStart(item, previousItem, barSpacing) - hitTestTolerance;
|
|
const rightBoundary = slotEnd(item, nextItem, barSpacing) + hitTestTolerance;
|
|
if (x < leftBoundary || x > rightBoundary) {
|
|
continue;
|
|
}
|
|
rangeProvider(item, rangePair);
|
|
const rangeStart = rangePair[0];
|
|
const rangeEnd = rangePair[1];
|
|
const actualTop = Math.min(rangeStart, rangeEnd);
|
|
const actualBottom = Math.max(rangeStart, rangeEnd);
|
|
const top = actualTop - hitTestTolerance;
|
|
const bottom = actualBottom + hitTestTolerance;
|
|
if (y >= actualTop && y <= actualBottom) {
|
|
minDistance = Math.min(minDistance, 0);
|
|
continue;
|
|
}
|
|
if (y >= top && y <= bottom) {
|
|
const distance = Math.min(Math.abs(y - actualTop), Math.abs(actualBottom - y));
|
|
minDistance = Math.min(minDistance, distance);
|
|
}
|
|
}
|
|
return Number.isFinite(minDistance) ? hoveredSeriesHitTestResult(minDistance, 0 /* HitTestPriority.Range */, 'series-range') : null;
|
|
}
|
|
|
|
function lowerBoundItemsCompare(item, time) {
|
|
return item._internal_time < time;
|
|
}
|
|
function upperBoundItemsCompare(item, time) {
|
|
return time < item._internal_time;
|
|
}
|
|
function visibleTimedValues(items, range, extendedRange) {
|
|
const firstBar = range._internal_left();
|
|
const lastBar = range._internal_right();
|
|
const from = lowerBound(items, firstBar, lowerBoundItemsCompare);
|
|
const to = upperBound(items, lastBar, upperBoundItemsCompare);
|
|
if (!extendedRange) {
|
|
return { from, to };
|
|
}
|
|
let extendedFrom = from;
|
|
let extendedTo = to;
|
|
if (from > 0 && from < items.length && items[from]._internal_time >= firstBar) {
|
|
extendedFrom = from - 1;
|
|
}
|
|
if (to > 0 && to < items.length && items[to - 1]._internal_time <= lastBar) {
|
|
extendedTo = to + 1;
|
|
}
|
|
return { from: extendedFrom, to: extendedTo };
|
|
}
|
|
|
|
class SeriesPaneViewBase {
|
|
constructor(series, model, extendedVisibleRange) {
|
|
this._internal__invalidated = true;
|
|
this._internal__dataInvalidated = true;
|
|
this._internal__optionsInvalidated = true;
|
|
this._internal__items = [];
|
|
this._internal__itemsVisibleRange = null;
|
|
this._private__lastConflationKey = -1;
|
|
this._internal__series = series;
|
|
this._internal__model = model;
|
|
this._private__extendedVisibleRange = extendedVisibleRange;
|
|
}
|
|
_internal_update(updateType) {
|
|
this._internal__invalidated = true;
|
|
if (updateType === 'data') {
|
|
this._internal__dataInvalidated = true;
|
|
}
|
|
if (updateType === 'options') {
|
|
this._internal__optionsInvalidated = true;
|
|
}
|
|
}
|
|
_internal_renderer() {
|
|
if (!this._internal__series._internal_visible()) {
|
|
return null;
|
|
}
|
|
this._internal__makeValid();
|
|
return this._internal__itemsVisibleRange === null ? null : this._internal__renderer;
|
|
}
|
|
_internal_hitTest(x, y) {
|
|
if (!this._internal__series._internal_visible()) {
|
|
return null;
|
|
}
|
|
this._internal__makeValid();
|
|
if (this._internal__itemsVisibleRange === null) {
|
|
return null;
|
|
}
|
|
return this._internal__hitTestImpl(x, y);
|
|
}
|
|
_internal__hitTestImpl(x, y) {
|
|
return null;
|
|
}
|
|
_internal__updateOptions() {
|
|
this._internal__items = this._internal__items.map((item) => ({
|
|
...item,
|
|
...this._internal__series._internal_barColorer()._internal_barStyle(item._internal_time),
|
|
}));
|
|
}
|
|
_internal__clearVisibleRange() {
|
|
this._internal__itemsVisibleRange = null;
|
|
}
|
|
_internal__makeValid() {
|
|
// If the conflation setting or factor changed (due to zoom/barSpacing),
|
|
// we must rebuild raw items from series data.
|
|
const timeScale = this._internal__model._internal_timeScale();
|
|
const conflationEnabled = timeScale._internal_options().enableConflation;
|
|
const currentConflationKey = conflationEnabled ? timeScale._internal_conflationFactor() : 0;
|
|
if (currentConflationKey !== this._private__lastConflationKey) {
|
|
this._internal__dataInvalidated = true;
|
|
this._private__lastConflationKey = currentConflationKey;
|
|
}
|
|
if (this._internal__dataInvalidated) {
|
|
this._internal__fillRawPoints();
|
|
this._internal__dataInvalidated = false;
|
|
}
|
|
if (this._internal__optionsInvalidated) {
|
|
this._internal__updateOptions();
|
|
this._internal__optionsInvalidated = false;
|
|
}
|
|
if (this._internal__invalidated) {
|
|
this._private__makeValidImpl();
|
|
this._internal__invalidated = false;
|
|
}
|
|
}
|
|
_private__makeValidImpl() {
|
|
const priceScale = this._internal__series._internal_priceScale();
|
|
const timeScale = this._internal__model._internal_timeScale();
|
|
this._internal__clearVisibleRange();
|
|
if (timeScale._internal_isEmpty() || priceScale._internal_isEmpty()) {
|
|
return;
|
|
}
|
|
const visibleBars = timeScale._internal_visibleStrictRange();
|
|
if (visibleBars === null) {
|
|
return;
|
|
}
|
|
if (this._internal__series._internal_bars()._internal_size() === 0) {
|
|
return;
|
|
}
|
|
const firstValue = this._internal__series._internal_firstValue();
|
|
if (firstValue === null) {
|
|
return;
|
|
}
|
|
this._internal__itemsVisibleRange = visibleTimedValues(this._internal__items, visibleBars, this._private__extendedVisibleRange);
|
|
this._internal__convertToCoordinates(priceScale, timeScale, firstValue._internal_value);
|
|
this._internal__prepareRendererData();
|
|
}
|
|
}
|
|
|
|
class CustomSeriesPaneRendererWrapper {
|
|
constructor(sourceRenderer, priceScale) {
|
|
this._private__sourceRenderer = sourceRenderer;
|
|
this._private__priceScale = priceScale;
|
|
}
|
|
_internal_draw(target, isHovered, hitTestData) {
|
|
this._private__sourceRenderer.draw(target, this._private__priceScale, isHovered, hitTestData);
|
|
}
|
|
}
|
|
function customHitPriority(type) {
|
|
switch (type) {
|
|
case 'point':
|
|
return 2 /* HitTestPriority.Point */;
|
|
case 'range':
|
|
return 0 /* HitTestPriority.Range */;
|
|
case 'line':
|
|
case 'custom':
|
|
default:
|
|
return 1 /* HitTestPriority.Line */;
|
|
}
|
|
}
|
|
function normalizeCustomHit(result) {
|
|
return {
|
|
_internal_distance: result.distance,
|
|
_internal_priority: customHitPriority(result.type),
|
|
_internal_itemType: 'custom',
|
|
_internal_cursorStyle: result.cursorStyle,
|
|
_internal_externalId: result.objectId,
|
|
_internal_hitTestData: result.hitTestData,
|
|
};
|
|
}
|
|
class SeriesCustomPaneView extends SeriesPaneViewBase {
|
|
constructor(series, model, paneView) {
|
|
super(series, model, false);
|
|
this._private__paneView = paneView;
|
|
this._private__sourceRenderer = this._private__paneView.renderer();
|
|
this._internal__renderer = new CustomSeriesPaneRendererWrapper(this._private__sourceRenderer, (price) => this._private__rendererPriceCoordinate(price));
|
|
}
|
|
get _internal_conflationReducer() {
|
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
return this._private__paneView.conflationReducer;
|
|
}
|
|
_internal_priceValueBuilder(plotRow) {
|
|
return this._private__paneView.priceValueBuilder(plotRow);
|
|
}
|
|
_internal_isWhitespace(data) {
|
|
return this._private__paneView.isWhitespace(data);
|
|
}
|
|
_internal__hitTestImpl(x, y) {
|
|
const customHit = this._private__sourceRenderer.hitTest?.(x, y, (price) => this._private__rendererPriceCoordinate(price));
|
|
if (customHit !== null && customHit !== undefined) {
|
|
return normalizeCustomHit(customHit);
|
|
}
|
|
const fallbackHit = hitTestSeriesRange(this._internal__items, this._internal__itemsVisibleRange, x, y, this._internal__model._internal_timeScale()._internal_barSpacing(), this._internal__series._internal_options().hitTestTolerance, (bar, out) => {
|
|
const originalData = bar._internal_originalData;
|
|
let top = NaN;
|
|
let bottom = NaN;
|
|
if (originalData !== undefined && !this._private__paneView.isWhitespace(originalData)) {
|
|
for (const price of this._private__paneView.priceValueBuilder(originalData)) {
|
|
const coordinate = this._private__rendererPriceCoordinate(price);
|
|
if (coordinate === null) {
|
|
continue;
|
|
}
|
|
top = Number.isNaN(top) ? coordinate : Math.min(top, coordinate);
|
|
bottom = Number.isNaN(bottom) ? coordinate : Math.max(bottom, coordinate);
|
|
}
|
|
}
|
|
out[0] = top;
|
|
out[1] = bottom;
|
|
});
|
|
return fallbackHit === null ? null : {
|
|
...fallbackHit,
|
|
_internal_itemType: 'custom',
|
|
};
|
|
}
|
|
_internal__fillRawPoints() {
|
|
const colorer = this._internal__series._internal_barColorer();
|
|
this._internal__items = this._internal__series._internal_conflatedBars()._internal_rows()
|
|
.map((row) => {
|
|
return {
|
|
_internal_time: row._internal_index,
|
|
_internal_x: NaN,
|
|
...colorer._internal_barStyle(row._internal_index),
|
|
_internal_originalData: row._internal_data,
|
|
};
|
|
});
|
|
}
|
|
_internal__convertToCoordinates(priceScale, timeScale) {
|
|
timeScale._internal_indexesToCoordinates(this._internal__items, undefinedIfNull(this._internal__itemsVisibleRange));
|
|
}
|
|
_internal__prepareRendererData() {
|
|
this._private__paneView.update({
|
|
bars: this._internal__items.map(unwrapItemData),
|
|
barSpacing: this._internal__model._internal_timeScale()._internal_barSpacing(),
|
|
visibleRange: this._internal__itemsVisibleRange,
|
|
conflationFactor: this._internal__model._internal_timeScale()._internal_conflationFactor(),
|
|
}, this._internal__series._internal_options());
|
|
}
|
|
_private__rendererPriceCoordinate(price) {
|
|
const firstValue = this._internal__series._internal_firstValue();
|
|
if (firstValue === null) {
|
|
return null;
|
|
}
|
|
return this._internal__series._internal_priceScale()._internal_priceToCoordinate(price, firstValue._internal_value);
|
|
}
|
|
}
|
|
function unwrapItemData(item) {
|
|
return {
|
|
x: item._internal_x,
|
|
time: item._internal_time,
|
|
originalData: item._internal_originalData,
|
|
barColor: item._internal_barColor,
|
|
};
|
|
}
|
|
|
|
const customStyleDefaults = {
|
|
color: '#2196f3',
|
|
};
|
|
const createPaneView$6 = (series, model, customPaneView) => {
|
|
const paneView = ensure(customPaneView);
|
|
return new SeriesCustomPaneView(series, model, paneView);
|
|
};
|
|
const createCustomSeriesDefinition = (paneView) => {
|
|
const definition = {
|
|
type: 'Custom',
|
|
isBuiltIn: false,
|
|
defaultOptions: { ...customStyleDefaults, ...paneView.defaultOptions() },
|
|
/**
|
|
* @internal
|
|
*/
|
|
_internal_createPaneView: createPaneView$6,
|
|
_internal_customPaneView: paneView,
|
|
};
|
|
return definition;
|
|
};
|
|
|
|
const isSeriesDefinition = (value) => {
|
|
return value._internal_createPaneView !== undefined;
|
|
};
|
|
|
|
function singleValueData(plotRow) {
|
|
const data = {
|
|
value: plotRow._internal_value[3 /* PlotRowValueIndex.Close */],
|
|
time: plotRow._internal_originalTime,
|
|
};
|
|
if (plotRow._internal_customValues !== undefined) {
|
|
data.customValues = plotRow._internal_customValues;
|
|
}
|
|
return data;
|
|
}
|
|
function lineData(plotRow) {
|
|
const result = singleValueData(plotRow);
|
|
if (plotRow._internal_color !== undefined) {
|
|
result.color = plotRow._internal_color;
|
|
}
|
|
return result;
|
|
}
|
|
function areaData(plotRow) {
|
|
const result = singleValueData(plotRow);
|
|
if (plotRow._internal_lineColor !== undefined) {
|
|
result.lineColor = plotRow._internal_lineColor;
|
|
}
|
|
if (plotRow._internal_topColor !== undefined) {
|
|
result.topColor = plotRow._internal_topColor;
|
|
}
|
|
if (plotRow._internal_bottomColor !== undefined) {
|
|
result.bottomColor = plotRow._internal_bottomColor;
|
|
}
|
|
return result;
|
|
}
|
|
function baselineData(plotRow) {
|
|
const result = singleValueData(plotRow);
|
|
if (plotRow._internal_topLineColor !== undefined) {
|
|
result.topLineColor = plotRow._internal_topLineColor;
|
|
}
|
|
if (plotRow._internal_bottomLineColor !== undefined) {
|
|
result.bottomLineColor = plotRow._internal_bottomLineColor;
|
|
}
|
|
if (plotRow._internal_topFillColor1 !== undefined) {
|
|
result.topFillColor1 = plotRow._internal_topFillColor1;
|
|
}
|
|
if (plotRow._internal_topFillColor2 !== undefined) {
|
|
result.topFillColor2 = plotRow._internal_topFillColor2;
|
|
}
|
|
if (plotRow._internal_bottomFillColor1 !== undefined) {
|
|
result.bottomFillColor1 = plotRow._internal_bottomFillColor1;
|
|
}
|
|
if (plotRow._internal_bottomFillColor2 !== undefined) {
|
|
result.bottomFillColor2 = plotRow._internal_bottomFillColor2;
|
|
}
|
|
return result;
|
|
}
|
|
function ohlcData(plotRow) {
|
|
const data = {
|
|
open: plotRow._internal_value[0 /* PlotRowValueIndex.Open */],
|
|
high: plotRow._internal_value[1 /* PlotRowValueIndex.High */],
|
|
low: plotRow._internal_value[2 /* PlotRowValueIndex.Low */],
|
|
close: plotRow._internal_value[3 /* PlotRowValueIndex.Close */],
|
|
time: plotRow._internal_originalTime,
|
|
};
|
|
if (plotRow._internal_customValues !== undefined) {
|
|
data.customValues = plotRow._internal_customValues;
|
|
}
|
|
return data;
|
|
}
|
|
function barData(plotRow) {
|
|
const result = ohlcData(plotRow);
|
|
if (plotRow._internal_color !== undefined) {
|
|
result.color = plotRow._internal_color;
|
|
}
|
|
return result;
|
|
}
|
|
function candlestickData(plotRow) {
|
|
const result = ohlcData(plotRow);
|
|
const { _internal_color: color, _internal_borderColor: borderColor, _internal_wickColor: wickColor } = plotRow;
|
|
if (color !== undefined) {
|
|
result.color = color;
|
|
}
|
|
if (borderColor !== undefined) {
|
|
result.borderColor = borderColor;
|
|
}
|
|
if (wickColor !== undefined) {
|
|
result.wickColor = wickColor;
|
|
}
|
|
return result;
|
|
}
|
|
function getSeriesDataCreator(seriesType) {
|
|
const seriesPlotRowToDataMap = {
|
|
Area: (areaData),
|
|
Line: (lineData),
|
|
Baseline: (baselineData),
|
|
Histogram: (lineData),
|
|
Bar: (barData),
|
|
Candlestick: (candlestickData),
|
|
Custom: (customData),
|
|
};
|
|
return seriesPlotRowToDataMap[seriesType];
|
|
}
|
|
function customData(plotRow) {
|
|
const time = plotRow._internal_originalTime;
|
|
return {
|
|
...plotRow._internal_data,
|
|
time,
|
|
};
|
|
}
|
|
|
|
const crosshairOptionsDefaults = {
|
|
vertLine: {
|
|
color: '#9598A1',
|
|
width: 1,
|
|
style: 3 /* LineStyle.LargeDashed */,
|
|
visible: true,
|
|
labelVisible: true,
|
|
labelBackgroundColor: '#131722',
|
|
},
|
|
horzLine: {
|
|
color: '#9598A1',
|
|
width: 1,
|
|
style: 3 /* LineStyle.LargeDashed */,
|
|
visible: true,
|
|
labelVisible: true,
|
|
labelBackgroundColor: '#131722',
|
|
},
|
|
mode: 1 /* CrosshairMode.Magnet */,
|
|
doNotSnapToHiddenSeriesIndices: false,
|
|
};
|
|
|
|
const gridOptionsDefaults = {
|
|
vertLines: {
|
|
color: '#D6DCDE',
|
|
style: 0 /* LineStyle.Solid */,
|
|
visible: true,
|
|
},
|
|
horzLines: {
|
|
color: '#D6DCDE',
|
|
style: 0 /* LineStyle.Solid */,
|
|
visible: true,
|
|
},
|
|
};
|
|
|
|
const layoutOptionsDefaults = {
|
|
background: {
|
|
type: "solid" /* ColorType.Solid */,
|
|
color: '#FFFFFF',
|
|
},
|
|
textColor: '#191919',
|
|
fontSize: 12,
|
|
fontFamily: defaultFontFamily,
|
|
panes: {
|
|
enableResize: true,
|
|
separatorColor: '#E0E3EB',
|
|
separatorHoverColor: 'rgba(178, 181, 189, 0.2)',
|
|
},
|
|
attributionLogo: true,
|
|
colorSpace: 'srgb',
|
|
colorParsers: [],
|
|
};
|
|
|
|
const priceScaleOptionsDefaults = {
|
|
autoScale: true,
|
|
mode: 0 /* PriceScaleMode.Normal */,
|
|
invertScale: false,
|
|
alignLabels: true,
|
|
borderVisible: true,
|
|
borderColor: '#2B2B43',
|
|
entireTextOnly: false,
|
|
visible: false,
|
|
ticksVisible: false,
|
|
scaleMargins: {
|
|
bottom: 0.1,
|
|
top: 0.2,
|
|
},
|
|
minimumWidth: 0,
|
|
ensureEdgeTickMarksVisible: false,
|
|
tickMarkDensity: 2.5,
|
|
};
|
|
|
|
const timeScaleOptionsDefaults = {
|
|
rightOffset: 0,
|
|
barSpacing: 6,
|
|
minBarSpacing: 0.5,
|
|
maxBarSpacing: 0,
|
|
fixLeftEdge: false,
|
|
fixRightEdge: false,
|
|
lockVisibleTimeRangeOnResize: false,
|
|
rightBarStaysOnScroll: false,
|
|
borderVisible: true,
|
|
borderColor: '#2B2B43',
|
|
visible: true,
|
|
timeVisible: false,
|
|
secondsVisible: true,
|
|
shiftVisibleRangeOnNewBar: true,
|
|
allowShiftVisibleRangeOnWhitespaceReplacement: false,
|
|
ticksVisible: false,
|
|
uniformDistribution: false,
|
|
minimumHeight: 0,
|
|
allowBoldLabels: true,
|
|
ignoreWhitespaceIndices: false,
|
|
enableConflation: false,
|
|
conflationThresholdFactor: 1,
|
|
precomputeConflationOnInit: false,
|
|
precomputeConflationPriority: 'background',
|
|
};
|
|
|
|
function chartOptionsDefaults() {
|
|
return {
|
|
addDefaultPane: true,
|
|
hoveredSeriesOnTop: true,
|
|
width: 0,
|
|
height: 0,
|
|
autoSize: false,
|
|
layout: layoutOptionsDefaults,
|
|
crosshair: crosshairOptionsDefaults,
|
|
grid: gridOptionsDefaults,
|
|
overlayPriceScales: {
|
|
...priceScaleOptionsDefaults,
|
|
},
|
|
leftPriceScale: {
|
|
...priceScaleOptionsDefaults,
|
|
visible: false,
|
|
},
|
|
rightPriceScale: {
|
|
...priceScaleOptionsDefaults,
|
|
visible: true,
|
|
},
|
|
defaultVisiblePriceScaleId: 'right',
|
|
timeScale: timeScaleOptionsDefaults,
|
|
localization: {
|
|
locale: isRunningOnClientSide ? navigator.language : '',
|
|
dateFormat: 'dd MMM \'yy',
|
|
},
|
|
handleScroll: {
|
|
mouseWheel: true,
|
|
pressedMouseMove: true,
|
|
horzTouchDrag: true,
|
|
vertTouchDrag: true,
|
|
},
|
|
handleScale: {
|
|
axisPressedMouseMove: {
|
|
time: true,
|
|
price: true,
|
|
},
|
|
axisDoubleClickReset: {
|
|
time: true,
|
|
price: true,
|
|
},
|
|
mouseWheel: true,
|
|
pinch: true,
|
|
},
|
|
kineticScroll: {
|
|
mouse: false,
|
|
touch: true,
|
|
},
|
|
trackingMode: {
|
|
exitMode: 1 /* TrackingModeExitMode.OnNextTap */,
|
|
},
|
|
};
|
|
}
|
|
|
|
class PriceScaleApi {
|
|
constructor(chartWidget, priceScaleId, paneIndex) {
|
|
this._private__chartWidget = chartWidget;
|
|
this._private__priceScaleId = priceScaleId;
|
|
this._private__paneIndex = paneIndex ?? 0;
|
|
}
|
|
applyOptions(options) {
|
|
this._private__chartWidget._internal_model()._internal_applyPriceScaleOptions(this._private__priceScaleId, options, this._private__paneIndex);
|
|
}
|
|
options() {
|
|
return this._private__priceScale()._internal_options();
|
|
}
|
|
width() {
|
|
if (!isDefaultPriceScale(this._private__priceScaleId)) {
|
|
return 0;
|
|
}
|
|
return this._private__chartWidget._internal_getPriceAxisWidth(this._private__priceScaleId);
|
|
}
|
|
setVisibleRange(range) {
|
|
this.setAutoScale(false);
|
|
this._private__priceScale()._internal_setCustomPriceRange(new PriceRangeImpl(range.from, range.to));
|
|
}
|
|
getVisibleRange() {
|
|
let range = this._private__priceScale()._internal_priceRange();
|
|
if (range === null) {
|
|
return null;
|
|
}
|
|
let from;
|
|
let to;
|
|
if (this._private__priceScale()._internal_isLog()) {
|
|
const minMove = this._private__priceScale()._internal_minMove();
|
|
const minMovePrecision = precisionByMinMove(minMove);
|
|
range = convertPriceRangeFromLog(range, this._private__priceScale()._internal_getLogFormula());
|
|
from = Number((Math.round(range._internal_minValue() / minMove) * minMove).toFixed(minMovePrecision));
|
|
to = Number((Math.round(range._internal_maxValue() / minMove) * minMove).toFixed(minMovePrecision));
|
|
}
|
|
else {
|
|
from = range._internal_minValue();
|
|
to = range._internal_maxValue();
|
|
}
|
|
return {
|
|
from,
|
|
to,
|
|
};
|
|
}
|
|
setAutoScale(on) {
|
|
this.applyOptions({ autoScale: on });
|
|
}
|
|
_private__priceScale() {
|
|
return ensureNotNull(this._private__chartWidget._internal_model()._internal_findPriceScale(this._private__priceScaleId, this._private__paneIndex))._internal_priceScale;
|
|
}
|
|
}
|
|
|
|
class PaneApi {
|
|
constructor(chartWidget, seriesApiGetter, pane, chartApi) {
|
|
this._private__chartWidget = chartWidget;
|
|
this._private__pane = pane;
|
|
this._private__seriesApiGetter = seriesApiGetter;
|
|
this._internal__chartApi = chartApi;
|
|
}
|
|
getHeight() {
|
|
return this._private__pane._internal_height();
|
|
}
|
|
setHeight(height) {
|
|
const chartModel = this._private__chartWidget._internal_model();
|
|
const paneIndex = chartModel._internal_getPaneIndex(this._private__pane);
|
|
chartModel._internal_changePanesHeight(paneIndex, height);
|
|
}
|
|
getStretchFactor() {
|
|
return this._private__pane._internal_stretchFactor();
|
|
}
|
|
setStretchFactor(stretchFactor) {
|
|
this._private__pane._internal_setStretchFactor(stretchFactor);
|
|
this._private__chartWidget._internal_model()._internal_fullUpdate();
|
|
}
|
|
paneIndex() {
|
|
return this._private__chartWidget._internal_model()._internal_getPaneIndex(this._private__pane);
|
|
}
|
|
moveTo(paneIndex) {
|
|
const currentIndex = this.paneIndex();
|
|
if (currentIndex === paneIndex) {
|
|
return;
|
|
}
|
|
assert(paneIndex >= 0 && paneIndex < this._private__chartWidget._internal_paneWidgets().length, 'Invalid pane index');
|
|
this._private__chartWidget._internal_model()._internal_movePane(currentIndex, paneIndex);
|
|
}
|
|
getSeries() {
|
|
return this._private__pane._internal_series().map((source) => this._private__seriesApiGetter(source)) ?? [];
|
|
}
|
|
getHTMLElement() {
|
|
const widgets = this._private__chartWidget._internal_paneWidgets();
|
|
if (!widgets || widgets.length === 0 || !widgets[this.paneIndex()]) {
|
|
return null;
|
|
}
|
|
return widgets[this.paneIndex()]._internal_getElement();
|
|
}
|
|
attachPrimitive(primitive) {
|
|
this._private__pane._internal_attachPrimitive(primitive);
|
|
if (primitive.attached) {
|
|
primitive.attached({
|
|
chart: this._internal__chartApi,
|
|
requestUpdate: () => this._private__pane._internal_model()._internal_fullUpdate(),
|
|
});
|
|
}
|
|
}
|
|
detachPrimitive(primitive) {
|
|
this._private__pane._internal_detachPrimitive(primitive);
|
|
}
|
|
priceScale(priceScaleId) {
|
|
const priceScale = this._private__pane._internal_priceScaleById(priceScaleId);
|
|
if (priceScale === null) {
|
|
throw new Error(`Cannot find price scale with id: ${priceScaleId}`);
|
|
}
|
|
return new PriceScaleApi(this._private__chartWidget, priceScaleId, this.paneIndex());
|
|
}
|
|
setPreserveEmptyPane(preserve) {
|
|
this._private__pane._internal_setPreserveEmptyPane(preserve);
|
|
}
|
|
preserveEmptyPane() {
|
|
return this._private__pane._internal_preserveEmptyPane();
|
|
}
|
|
addCustomSeries(customPaneView, options = {}, paneIndex = 0) {
|
|
return this._internal__chartApi.addCustomSeries(customPaneView, options, paneIndex);
|
|
}
|
|
addSeries(definition, options = {}) {
|
|
return this._internal__chartApi.addSeries(definition, options, this.paneIndex());
|
|
}
|
|
}
|
|
|
|
/// <reference types="_build-time-constants" />
|
|
function checkPriceLineOptions(options) {
|
|
assert(typeof options.price === 'number', `the type of 'price' price line's property must be a number, got '${typeof options.price}'`);
|
|
}
|
|
function checkItemsAreOrdered(data, bh, allowDuplicates = false) {
|
|
if (data.length === 0) {
|
|
return;
|
|
}
|
|
let prevTime = bh.key(data[0].time);
|
|
for (let i = 1; i < data.length; ++i) {
|
|
const currentTime = bh.key(data[i].time);
|
|
const checkResult = allowDuplicates ? prevTime <= currentTime : prevTime < currentTime;
|
|
assert(checkResult, `data must be asc ordered by time, index=${i}, time=${currentTime}, prev time=${prevTime}`);
|
|
prevTime = currentTime;
|
|
}
|
|
}
|
|
function checkSeriesValuesType(type, data) {
|
|
data.forEach(getChecker(type));
|
|
}
|
|
function getChecker(type) {
|
|
switch (type) {
|
|
case 'Bar':
|
|
case 'Candlestick':
|
|
return checkBarItem.bind(null, type);
|
|
case 'Area':
|
|
case 'Baseline':
|
|
case 'Line':
|
|
case 'Histogram':
|
|
return checkLineItem.bind(null, type);
|
|
case 'Custom':
|
|
return checkCustomItem.bind(null);
|
|
}
|
|
}
|
|
function checkBarItem(type, barItem) {
|
|
if (!isFulfilledData(barItem)) {
|
|
return;
|
|
}
|
|
['open', 'high', 'low', 'close'].forEach((key) => {
|
|
assert(typeof barItem[key] === 'number', `${type} series item data value of ${key} must be a number, got=${typeof barItem[key]}, value=${barItem[key]}`);
|
|
assert(isSafeValue(barItem[key]), `${type} series item data value of ${key} must be between ${MIN_SAFE_VALUE.toPrecision(16)} and ${MAX_SAFE_VALUE.toPrecision(16)}, got=${typeof barItem[key]}, value=${barItem[key]}`);
|
|
});
|
|
}
|
|
function checkLineItem(type, lineItem) {
|
|
if (!isFulfilledData(lineItem)) {
|
|
return;
|
|
}
|
|
assert(typeof lineItem.value === 'number', `${type} series item data value must be a number, got=${typeof lineItem.value}, value=${lineItem.value}`);
|
|
assert(isSafeValue(lineItem.value), `${type} series item data value must be between ${MIN_SAFE_VALUE.toPrecision(16)} and ${MAX_SAFE_VALUE.toPrecision(16)}, got=${typeof lineItem.value}, value=${lineItem.value}`);
|
|
}
|
|
function checkCustomItem(
|
|
// type: 'Custom',
|
|
// customItem: SeriesDataItemTypeMap[typeof type]
|
|
) {
|
|
// Nothing to check yet...
|
|
return;
|
|
}
|
|
const MIN_SAFE_VALUE = Number.MIN_SAFE_INTEGER / 100;
|
|
const MAX_SAFE_VALUE = Number.MAX_SAFE_INTEGER / 100;
|
|
function isSafeValue(value) {
|
|
return value >= MIN_SAFE_VALUE && value <= MAX_SAFE_VALUE;
|
|
}
|
|
|
|
const priceLineOptionsDefaults = {
|
|
color: '#FF0000',
|
|
price: 0,
|
|
lineStyle: 2 /* LineStyle.Dashed */,
|
|
lineWidth: 1,
|
|
lineVisible: true,
|
|
axisLabelVisible: true,
|
|
title: '',
|
|
axisLabelColor: '',
|
|
axisLabelTextColor: '',
|
|
};
|
|
|
|
class PriceLine {
|
|
constructor(priceLine) {
|
|
this._private__priceLine = priceLine;
|
|
}
|
|
applyOptions(options) {
|
|
this._private__priceLine._internal_applyOptions(options);
|
|
}
|
|
options() {
|
|
return this._private__priceLine._internal_options();
|
|
}
|
|
_internal_priceLine() {
|
|
return this._private__priceLine;
|
|
}
|
|
}
|
|
|
|
class SeriesApi {
|
|
constructor(series, dataUpdatesConsumer, priceScaleApiProvider, chartApi, horzScaleBehavior, paneApiGetter) {
|
|
this._private__dataChangedDelegate = new Delegate();
|
|
this._internal__series = series;
|
|
this._internal__dataUpdatesConsumer = dataUpdatesConsumer;
|
|
this._private__priceScaleApiProvider = priceScaleApiProvider;
|
|
this._private__horzScaleBehavior = horzScaleBehavior;
|
|
this._internal__chartApi = chartApi;
|
|
this._private__paneApiGetter = paneApiGetter;
|
|
}
|
|
_internal_destroy() {
|
|
this._private__dataChangedDelegate._internal_destroy();
|
|
}
|
|
priceFormatter() {
|
|
return this._internal__series._internal_formatter();
|
|
}
|
|
priceToCoordinate(price) {
|
|
const firstValue = this._internal__series._internal_firstValue();
|
|
if (firstValue === null) {
|
|
return null;
|
|
}
|
|
return this._internal__series._internal_priceScale()._internal_priceToCoordinate(price, firstValue._internal_value);
|
|
}
|
|
coordinateToPrice(coordinate) {
|
|
const firstValue = this._internal__series._internal_firstValue();
|
|
if (firstValue === null) {
|
|
return null;
|
|
}
|
|
return this._internal__series._internal_priceScale()._internal_coordinateToPrice(coordinate, firstValue._internal_value);
|
|
}
|
|
barsInLogicalRange(range) {
|
|
if (range === null) {
|
|
return null;
|
|
}
|
|
// we use TimeScaleVisibleRange here to convert LogicalRange to strict range properly
|
|
const correctedRange = new TimeScaleVisibleRange(new RangeImpl(range.from, range.to))._internal_strictRange();
|
|
const bars = this._internal__series._internal_bars();
|
|
if (bars._internal_isEmpty()) {
|
|
return null;
|
|
}
|
|
const dataFirstBarInRange = bars._internal_search(correctedRange._internal_left(), 1 /* MismatchDirection.NearestRight */);
|
|
const dataLastBarInRange = bars._internal_search(correctedRange._internal_right(), -1 /* MismatchDirection.NearestLeft */);
|
|
const dataFirstIndex = ensureNotNull(bars._internal_firstIndex());
|
|
const dataLastIndex = ensureNotNull(bars._internal_lastIndex());
|
|
// this means that we request data in the data gap
|
|
// e.g. let's say we have series with data [0..10, 30..60]
|
|
// and we request bars info in range [15, 25]
|
|
// thus, dataFirstBarInRange will be with index 30 and dataLastBarInRange with 10
|
|
if (dataFirstBarInRange !== null && dataLastBarInRange !== null && dataFirstBarInRange._internal_index > dataLastBarInRange._internal_index) {
|
|
return {
|
|
barsBefore: range.from - dataFirstIndex,
|
|
barsAfter: dataLastIndex - range.to,
|
|
};
|
|
}
|
|
const barsBefore = (dataFirstBarInRange === null || dataFirstBarInRange._internal_index === dataFirstIndex)
|
|
? range.from - dataFirstIndex
|
|
: dataFirstBarInRange._internal_index - dataFirstIndex;
|
|
const barsAfter = (dataLastBarInRange === null || dataLastBarInRange._internal_index === dataLastIndex)
|
|
? dataLastIndex - range.to
|
|
: dataLastIndex - dataLastBarInRange._internal_index;
|
|
const result = { barsBefore, barsAfter };
|
|
// actually they can't exist separately
|
|
if (dataFirstBarInRange !== null && dataLastBarInRange !== null) {
|
|
result.from = dataFirstBarInRange._internal_originalTime;
|
|
result.to = dataLastBarInRange._internal_originalTime;
|
|
}
|
|
return result;
|
|
}
|
|
setData(data) {
|
|
checkItemsAreOrdered(data, this._private__horzScaleBehavior);
|
|
checkSeriesValuesType(this._internal__series._internal_seriesType(), data);
|
|
this._internal__dataUpdatesConsumer._internal_applyNewData(this._internal__series, data);
|
|
this._private__onDataChanged('full');
|
|
}
|
|
update(bar, historicalUpdate = false) {
|
|
checkSeriesValuesType(this._internal__series._internal_seriesType(), [bar]);
|
|
this._internal__dataUpdatesConsumer._internal_updateData(this._internal__series, bar, historicalUpdate);
|
|
this._private__onDataChanged('update');
|
|
}
|
|
pop(count = 1) {
|
|
const poppedRows = this._internal__dataUpdatesConsumer._internal_popData(this._internal__series, count);
|
|
if (poppedRows.length !== 0) {
|
|
this._private__onDataChanged('update');
|
|
}
|
|
const creator = getSeriesDataCreator(this.seriesType());
|
|
return poppedRows.map((row) => creator(row));
|
|
}
|
|
dataByIndex(logicalIndex, mismatchDirection) {
|
|
const data = this._internal__series._internal_bars()._internal_search(logicalIndex, mismatchDirection);
|
|
if (data === null) {
|
|
// actually it can be a whitespace
|
|
return null;
|
|
}
|
|
const creator = getSeriesDataCreator(this.seriesType());
|
|
return creator(data);
|
|
}
|
|
data() {
|
|
const seriesCreator = getSeriesDataCreator(this.seriesType());
|
|
const rows = this._internal__series._internal_bars()._internal_rows();
|
|
return rows.map((row) => seriesCreator(row));
|
|
}
|
|
subscribeDataChanged(handler) {
|
|
this._private__dataChangedDelegate._internal_subscribe(handler);
|
|
}
|
|
unsubscribeDataChanged(handler) {
|
|
this._private__dataChangedDelegate._internal_unsubscribe(handler);
|
|
}
|
|
applyOptions(options) {
|
|
this._internal__series._internal_applyOptions(options);
|
|
}
|
|
options() {
|
|
return clone(this._internal__series._internal_options());
|
|
}
|
|
priceScale() {
|
|
return this._private__priceScaleApiProvider.priceScale(this._internal__series._internal_priceScale()._internal_id(), this.getPane().paneIndex());
|
|
}
|
|
createPriceLine(options) {
|
|
checkPriceLineOptions(options);
|
|
const strictOptions = merge(clone(priceLineOptionsDefaults), options);
|
|
const priceLine = this._internal__series._internal_createPriceLine(strictOptions);
|
|
return new PriceLine(priceLine);
|
|
}
|
|
removePriceLine(line) {
|
|
this._internal__series._internal_removePriceLine(line._internal_priceLine());
|
|
}
|
|
priceLines() {
|
|
return this._internal__series._internal_priceLines().map((priceLine) => new PriceLine(priceLine));
|
|
}
|
|
seriesType() {
|
|
return this._internal__series._internal_seriesType();
|
|
}
|
|
lastValueData(globalLast) {
|
|
const result = this._internal__series._internal_lastValueData(globalLast);
|
|
if (result._internal_noData) {
|
|
return {
|
|
noData: true,
|
|
};
|
|
}
|
|
return {
|
|
noData: false,
|
|
price: result._internal_price,
|
|
color: result._internal_color,
|
|
};
|
|
}
|
|
attachPrimitive(primitive) {
|
|
// at this point we cast the generic to unknown because we
|
|
// don't want the model to know the types of the API (◑_◑)
|
|
this._internal__series._internal_attachPrimitive(primitive);
|
|
if (primitive.attached) {
|
|
primitive.attached({
|
|
chart: this._internal__chartApi,
|
|
series: this,
|
|
requestUpdate: () => this._internal__series._internal_model()._internal_fullUpdate(),
|
|
horzScaleBehavior: this._private__horzScaleBehavior,
|
|
});
|
|
}
|
|
}
|
|
detachPrimitive(primitive) {
|
|
this._internal__series._internal_detachPrimitive(primitive);
|
|
if (primitive.detached) {
|
|
primitive.detached();
|
|
}
|
|
this._internal__series._internal_model()._internal_fullUpdate();
|
|
}
|
|
getPane() {
|
|
const series = this._internal__series;
|
|
const pane = ensureNotNull(this._internal__series._internal_model()._internal_paneForSource(series));
|
|
return this._private__paneApiGetter(pane);
|
|
}
|
|
moveToPane(paneIndex) {
|
|
this._internal__series._internal_model()._internal_moveSeriesToPane(this._internal__series, paneIndex);
|
|
}
|
|
seriesOrder() {
|
|
const pane = this._internal__series._internal_model()._internal_paneForSource(this._internal__series);
|
|
if (pane === null) {
|
|
return -1;
|
|
}
|
|
return pane._internal_series().indexOf(this._internal__series);
|
|
}
|
|
setSeriesOrder(order) {
|
|
const pane = this._internal__series._internal_model()._internal_paneForSource(this._internal__series);
|
|
if (pane === null) {
|
|
return;
|
|
}
|
|
pane._internal_setSeriesOrder(this._internal__series, order);
|
|
}
|
|
_private__onDataChanged(scope) {
|
|
if (this._private__dataChangedDelegate._internal_hasListeners()) {
|
|
this._private__dataChangedDelegate._internal_fire(scope);
|
|
}
|
|
}
|
|
}
|
|
|
|
class TimeScaleApi {
|
|
constructor(model, timeAxisWidget, horzScaleBehavior) {
|
|
this._private__timeRangeChanged = new Delegate();
|
|
this._private__logicalRangeChanged = new Delegate();
|
|
this._private__sizeChanged = new Delegate();
|
|
this._private__model = model;
|
|
this._private__timeScale = model._internal_timeScale();
|
|
this._private__timeAxisWidget = timeAxisWidget;
|
|
this._private__timeScale._internal_visibleBarsChanged()._internal_subscribe(this._private__onVisibleBarsChanged.bind(this));
|
|
this._private__timeScale._internal_logicalRangeChanged()._internal_subscribe(this._private__onVisibleLogicalRangeChanged.bind(this));
|
|
this._private__timeAxisWidget._internal_sizeChanged()._internal_subscribe(this._private__onSizeChanged.bind(this));
|
|
this._private__horzScaleBehavior = horzScaleBehavior;
|
|
}
|
|
_internal_destroy() {
|
|
this._private__timeScale._internal_visibleBarsChanged()._internal_unsubscribeAll(this);
|
|
this._private__timeScale._internal_logicalRangeChanged()._internal_unsubscribeAll(this);
|
|
this._private__timeAxisWidget._internal_sizeChanged()._internal_unsubscribeAll(this);
|
|
this._private__timeRangeChanged._internal_destroy();
|
|
this._private__logicalRangeChanged._internal_destroy();
|
|
this._private__sizeChanged._internal_destroy();
|
|
}
|
|
scrollPosition() {
|
|
return this._private__timeScale._internal_rightOffset();
|
|
}
|
|
scrollToPosition(position, animated) {
|
|
if (!animated) {
|
|
this._private__model._internal_setRightOffset(position);
|
|
return;
|
|
}
|
|
this._private__timeScale._internal_scrollToOffsetAnimated(position, 1000 /* Constants.AnimationDurationMs */);
|
|
}
|
|
scrollToRealTime() {
|
|
this._private__timeScale._internal_scrollToRealTime();
|
|
}
|
|
getVisibleRange() {
|
|
const timeRange = this._private__timeScale._internal_visibleTimeRange();
|
|
if (timeRange === null) {
|
|
return null;
|
|
}
|
|
return {
|
|
from: timeRange.from.originalTime,
|
|
to: timeRange.to.originalTime,
|
|
};
|
|
}
|
|
setVisibleRange(range) {
|
|
const convertedRange = {
|
|
from: this._private__horzScaleBehavior.convertHorzItemToInternal(range.from),
|
|
to: this._private__horzScaleBehavior.convertHorzItemToInternal(range.to),
|
|
};
|
|
const logicalRange = this._private__timeScale._internal_logicalRangeForTimeRange(convertedRange);
|
|
this._private__model._internal_setTargetLogicalRange(logicalRange);
|
|
}
|
|
getVisibleLogicalRange() {
|
|
const logicalRange = this._private__timeScale._internal_visibleLogicalRange();
|
|
if (logicalRange === null) {
|
|
return null;
|
|
}
|
|
return {
|
|
from: logicalRange._internal_left(),
|
|
to: logicalRange._internal_right(),
|
|
};
|
|
}
|
|
setVisibleLogicalRange(range) {
|
|
assert(range.from <= range.to, 'The from index cannot be after the to index.');
|
|
this._private__model._internal_setTargetLogicalRange(range);
|
|
}
|
|
resetTimeScale() {
|
|
this._private__model._internal_resetTimeScale();
|
|
}
|
|
fitContent() {
|
|
this._private__model._internal_fitContent();
|
|
}
|
|
logicalToCoordinate(logical) {
|
|
const timeScale = this._private__model._internal_timeScale();
|
|
if (timeScale._internal_isEmpty()) {
|
|
return null;
|
|
}
|
|
else {
|
|
return timeScale._internal_indexToCoordinate(logical);
|
|
}
|
|
}
|
|
coordinateToLogical(x) {
|
|
if (this._private__timeScale._internal_isEmpty()) {
|
|
return null;
|
|
}
|
|
else {
|
|
return this._private__timeScale._internal_coordinateToIndex(x);
|
|
}
|
|
}
|
|
timeToIndex(time, findNearest) {
|
|
const timePoint = this._private__horzScaleBehavior.convertHorzItemToInternal(time);
|
|
return this._private__timeScale._internal_timeToIndex(timePoint, findNearest);
|
|
}
|
|
timeToCoordinate(time) {
|
|
const timePointIndex = this.timeToIndex(time, false);
|
|
if (timePointIndex === null) {
|
|
return null;
|
|
}
|
|
return this._private__timeScale._internal_indexToCoordinate(timePointIndex);
|
|
}
|
|
coordinateToTime(x) {
|
|
const timeScale = this._private__model._internal_timeScale();
|
|
const timePointIndex = timeScale._internal_coordinateToIndex(x);
|
|
const timePoint = timeScale._internal_indexToTimeScalePoint(timePointIndex);
|
|
if (timePoint === null) {
|
|
return null;
|
|
}
|
|
return timePoint.originalTime;
|
|
}
|
|
width() {
|
|
return this._private__timeAxisWidget._internal_getSize().width;
|
|
}
|
|
height() {
|
|
return this._private__timeAxisWidget._internal_getSize().height;
|
|
}
|
|
subscribeVisibleTimeRangeChange(handler) {
|
|
this._private__timeRangeChanged._internal_subscribe(handler);
|
|
}
|
|
unsubscribeVisibleTimeRangeChange(handler) {
|
|
this._private__timeRangeChanged._internal_unsubscribe(handler);
|
|
}
|
|
subscribeVisibleLogicalRangeChange(handler) {
|
|
this._private__logicalRangeChanged._internal_subscribe(handler);
|
|
}
|
|
unsubscribeVisibleLogicalRangeChange(handler) {
|
|
this._private__logicalRangeChanged._internal_unsubscribe(handler);
|
|
}
|
|
subscribeSizeChange(handler) {
|
|
this._private__sizeChanged._internal_subscribe(handler);
|
|
}
|
|
unsubscribeSizeChange(handler) {
|
|
this._private__sizeChanged._internal_unsubscribe(handler);
|
|
}
|
|
applyOptions(options) {
|
|
this._private__timeScale._internal_applyOptions(options);
|
|
}
|
|
options() {
|
|
return {
|
|
...clone(this._private__timeScale._internal_options()),
|
|
barSpacing: this._private__timeScale._internal_barSpacing(),
|
|
};
|
|
}
|
|
_private__onVisibleBarsChanged() {
|
|
if (this._private__timeRangeChanged._internal_hasListeners()) {
|
|
this._private__timeRangeChanged._internal_fire(this.getVisibleRange());
|
|
}
|
|
}
|
|
_private__onVisibleLogicalRangeChanged() {
|
|
if (this._private__logicalRangeChanged._internal_hasListeners()) {
|
|
this._private__logicalRangeChanged._internal_fire(this.getVisibleLogicalRange());
|
|
}
|
|
}
|
|
_private__onSizeChanged(size) {
|
|
this._private__sizeChanged._internal_fire(size.width, size.height);
|
|
}
|
|
}
|
|
|
|
/// <reference types="_build-time-constants" />
|
|
function patchPriceFormat(priceFormat) {
|
|
if (priceFormat === undefined || priceFormat.type === 'custom') {
|
|
return;
|
|
}
|
|
const priceFormatBuiltIn = priceFormat;
|
|
if (priceFormatBuiltIn.minMove !== undefined && priceFormatBuiltIn.precision === undefined) {
|
|
priceFormatBuiltIn.precision = precisionByMinMove(priceFormatBuiltIn.minMove);
|
|
}
|
|
}
|
|
function migrateHandleScaleScrollOptions(options) {
|
|
if (isBoolean(options['handleScale'])) {
|
|
const handleScale = options['handleScale'];
|
|
options['handleScale'] = {
|
|
axisDoubleClickReset: {
|
|
time: handleScale,
|
|
price: handleScale,
|
|
},
|
|
axisPressedMouseMove: {
|
|
time: handleScale,
|
|
price: handleScale,
|
|
},
|
|
mouseWheel: handleScale,
|
|
pinch: handleScale,
|
|
};
|
|
}
|
|
else if (options['handleScale'] !== undefined) {
|
|
const { axisPressedMouseMove, axisDoubleClickReset } = options['handleScale'];
|
|
if (isBoolean(axisPressedMouseMove)) {
|
|
options['handleScale'].axisPressedMouseMove = {
|
|
time: axisPressedMouseMove,
|
|
price: axisPressedMouseMove,
|
|
};
|
|
}
|
|
if (isBoolean(axisDoubleClickReset)) {
|
|
options['handleScale'].axisDoubleClickReset = {
|
|
time: axisDoubleClickReset,
|
|
price: axisDoubleClickReset,
|
|
};
|
|
}
|
|
}
|
|
const handleScroll = options['handleScroll'];
|
|
if (isBoolean(handleScroll)) {
|
|
options['handleScroll'] = {
|
|
horzTouchDrag: handleScroll,
|
|
vertTouchDrag: handleScroll,
|
|
mouseWheel: handleScroll,
|
|
pressedMouseMove: handleScroll,
|
|
};
|
|
}
|
|
}
|
|
function toInternalOptions(options) {
|
|
migrateHandleScaleScrollOptions(options);
|
|
return options;
|
|
}
|
|
class ChartApi {
|
|
constructor(container, horzScaleBehavior, options) {
|
|
this._private__seriesMap = new Map();
|
|
this._private__seriesMapReversed = new Map();
|
|
this._private__clickedDelegate = new Delegate();
|
|
this._private__dblClickedDelegate = new Delegate();
|
|
this._private__crosshairMovedDelegate = new Delegate();
|
|
this._private__panes = new WeakMap();
|
|
this._private__dataLayer = new DataLayer(horzScaleBehavior);
|
|
const internalOptions = (options === undefined) ?
|
|
clone(chartOptionsDefaults()) :
|
|
merge(clone(chartOptionsDefaults()), toInternalOptions(options));
|
|
this._internal__horzScaleBehavior = horzScaleBehavior;
|
|
this._private__chartWidget = new ChartWidget(container, internalOptions, horzScaleBehavior);
|
|
this._private__chartWidget._internal_clicked()._internal_subscribe((paramSupplier) => {
|
|
if (this._private__clickedDelegate._internal_hasListeners()) {
|
|
this._private__clickedDelegate._internal_fire(this._private__convertMouseParams(paramSupplier()));
|
|
}
|
|
}, this);
|
|
this._private__chartWidget._internal_dblClicked()._internal_subscribe((paramSupplier) => {
|
|
if (this._private__dblClickedDelegate._internal_hasListeners()) {
|
|
this._private__dblClickedDelegate._internal_fire(this._private__convertMouseParams(paramSupplier()));
|
|
}
|
|
}, this);
|
|
this._private__chartWidget._internal_crosshairMoved()._internal_subscribe((paramSupplier) => {
|
|
if (this._private__crosshairMovedDelegate._internal_hasListeners()) {
|
|
this._private__crosshairMovedDelegate._internal_fire(this._private__convertMouseParams(paramSupplier()));
|
|
}
|
|
}, this);
|
|
const model = this._private__chartWidget._internal_model();
|
|
this._private__timeScaleApi = new TimeScaleApi(model, this._private__chartWidget._internal_timeAxisWidget(), this._internal__horzScaleBehavior);
|
|
}
|
|
remove() {
|
|
this._private__chartWidget._internal_clicked()._internal_unsubscribeAll(this);
|
|
this._private__chartWidget._internal_dblClicked()._internal_unsubscribeAll(this);
|
|
this._private__chartWidget._internal_crosshairMoved()._internal_unsubscribeAll(this);
|
|
this._private__timeScaleApi._internal_destroy();
|
|
this._private__chartWidget._internal_destroy();
|
|
this._private__seriesMap.clear();
|
|
this._private__seriesMapReversed.clear();
|
|
this._private__clickedDelegate._internal_destroy();
|
|
this._private__dblClickedDelegate._internal_destroy();
|
|
this._private__crosshairMovedDelegate._internal_destroy();
|
|
this._private__dataLayer._internal_destroy();
|
|
}
|
|
resize(width, height, forceRepaint) {
|
|
if (this.autoSizeActive()) {
|
|
// We return early here instead of checking this within the actual _chartWidget.resize method
|
|
// because this should only apply to external resize requests.
|
|
warn(`Height and width values ignored because 'autoSize' option is enabled.`);
|
|
return;
|
|
}
|
|
this._private__chartWidget._internal_resize(width, height, forceRepaint);
|
|
}
|
|
addCustomSeries(customPaneView, options = {}, paneIndex = 0) {
|
|
const paneView = ensure(customPaneView);
|
|
const definition = createCustomSeriesDefinition(paneView);
|
|
return this._private__addSeriesImpl(definition, options, paneIndex);
|
|
}
|
|
addSeries(definition, options = {}, paneIndex = 0) {
|
|
return this._private__addSeriesImpl(definition, options, paneIndex);
|
|
}
|
|
removeSeries(seriesApi) {
|
|
const series = ensureDefined(this._private__seriesMap.get(seriesApi));
|
|
const update = this._private__dataLayer._internal_removeSeries(series);
|
|
const model = this._private__chartWidget._internal_model();
|
|
model._internal_removeSeries(series);
|
|
this._private__sendUpdateToChart(update);
|
|
this._private__seriesMap.delete(seriesApi);
|
|
this._private__seriesMapReversed.delete(series);
|
|
}
|
|
_internal_applyNewData(series, data) {
|
|
this._private__sendUpdateToChart(this._private__dataLayer._internal_setSeriesData(series, data));
|
|
}
|
|
_internal_updateData(series, data, historicalUpdate) {
|
|
this._private__sendUpdateToChart(this._private__dataLayer._internal_updateSeriesData(series, data, historicalUpdate));
|
|
}
|
|
_internal_popData(series, count) {
|
|
const [poppedData, update] = this._private__dataLayer._internal_popSeriesData(series, count);
|
|
if (poppedData.length !== 0) {
|
|
this._private__sendUpdateToChart(update);
|
|
}
|
|
return poppedData;
|
|
}
|
|
subscribeClick(handler) {
|
|
this._private__clickedDelegate._internal_subscribe(handler);
|
|
}
|
|
unsubscribeClick(handler) {
|
|
this._private__clickedDelegate._internal_unsubscribe(handler);
|
|
}
|
|
subscribeCrosshairMove(handler) {
|
|
this._private__crosshairMovedDelegate._internal_subscribe(handler);
|
|
}
|
|
unsubscribeCrosshairMove(handler) {
|
|
this._private__crosshairMovedDelegate._internal_unsubscribe(handler);
|
|
}
|
|
subscribeDblClick(handler) {
|
|
this._private__dblClickedDelegate._internal_subscribe(handler);
|
|
}
|
|
unsubscribeDblClick(handler) {
|
|
this._private__dblClickedDelegate._internal_unsubscribe(handler);
|
|
}
|
|
priceScale(priceScaleId, paneIndex = 0) {
|
|
return new PriceScaleApi(this._private__chartWidget, priceScaleId, paneIndex);
|
|
}
|
|
timeScale() {
|
|
return this._private__timeScaleApi;
|
|
}
|
|
applyOptions(options) {
|
|
{
|
|
const colorSpace = options.layout?.colorSpace;
|
|
if (colorSpace !== undefined && colorSpace !== this.options().layout.colorSpace) {
|
|
throw new Error(`colorSpace option should not be changed once the chart has been created.`);
|
|
}
|
|
const colorParsers = options.layout?.colorParsers;
|
|
if (colorParsers !== undefined && colorParsers !== this.options().layout.colorParsers) {
|
|
throw new Error(`colorParsers option should not be changed once the chart has been created.`);
|
|
}
|
|
}
|
|
this._private__chartWidget._internal_applyOptions(toInternalOptions(options));
|
|
}
|
|
options() {
|
|
return this._private__chartWidget._internal_options();
|
|
}
|
|
takeScreenshot(addTopLayer = false, includeCrosshair = false) {
|
|
let crosshairMode;
|
|
let screenshotCanvas;
|
|
try {
|
|
if (!includeCrosshair) {
|
|
crosshairMode = this._private__chartWidget._internal_model()._internal_options().crosshair.mode;
|
|
this._private__chartWidget._internal_applyOptions({
|
|
crosshair: {
|
|
mode: 2 /* CrosshairMode.Hidden */,
|
|
},
|
|
});
|
|
}
|
|
screenshotCanvas = this._private__chartWidget._internal_takeScreenshot(addTopLayer);
|
|
}
|
|
finally {
|
|
if (!includeCrosshair && crosshairMode !== undefined) {
|
|
this._private__chartWidget._internal_model()._internal_applyOptions({
|
|
crosshair: {
|
|
mode: crosshairMode,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
return screenshotCanvas;
|
|
}
|
|
addPane(preserveEmptyPane = false) {
|
|
const pane = this._private__chartWidget._internal_model()._internal_addPane();
|
|
pane._internal_setPreserveEmptyPane(preserveEmptyPane);
|
|
return this._private__getPaneApi(pane);
|
|
}
|
|
removePane(index) {
|
|
this._private__chartWidget._internal_model()._internal_removePane(index);
|
|
}
|
|
swapPanes(first, second) {
|
|
this._private__chartWidget._internal_model()._internal_swapPanes(first, second);
|
|
}
|
|
autoSizeActive() {
|
|
return this._private__chartWidget._internal_autoSizeActive();
|
|
}
|
|
chartElement() {
|
|
return this._private__chartWidget._internal_element();
|
|
}
|
|
panes() {
|
|
return this._private__chartWidget._internal_model()._internal_panes().map((pane) => this._private__getPaneApi(pane));
|
|
}
|
|
paneSize(paneIndex = 0) {
|
|
const size = this._private__chartWidget._internal_paneSize(paneIndex);
|
|
return {
|
|
height: size.height,
|
|
width: size.width,
|
|
};
|
|
}
|
|
setCrosshairPosition(price, horizontalPosition, seriesApi) {
|
|
const series = this._private__seriesMap.get(seriesApi);
|
|
if (series === undefined) {
|
|
return;
|
|
}
|
|
const pane = this._private__chartWidget._internal_model()._internal_paneForSource(series);
|
|
if (pane === null) {
|
|
return;
|
|
}
|
|
this._private__chartWidget._internal_model()._internal_setAndSaveSyntheticPosition(price, horizontalPosition, pane);
|
|
}
|
|
clearCrosshairPosition() {
|
|
this._private__chartWidget._internal_model()._internal_clearCurrentPosition(true);
|
|
}
|
|
horzBehaviour() {
|
|
return this._internal__horzScaleBehavior;
|
|
}
|
|
_private__addSeriesImpl(definition, options = {}, paneIndex = 0) {
|
|
assert(isSeriesDefinition(definition));
|
|
patchPriceFormat(options.priceFormat);
|
|
if (definition.type === 'Candlestick') {
|
|
fillUpDownCandlesticksColors(options);
|
|
}
|
|
const strictOptions = merge(clone(seriesOptionsDefaults), clone(definition.defaultOptions), options);
|
|
const createPaneView = definition._internal_createPaneView;
|
|
const series = new Series(this._private__chartWidget._internal_model(), definition.type, strictOptions, createPaneView, definition._internal_customPaneView);
|
|
this._private__chartWidget._internal_model()._internal_addSeriesToPane(series, paneIndex);
|
|
const res = new SeriesApi(series, this, this, this, this._internal__horzScaleBehavior, (pane) => this._private__getPaneApi(pane));
|
|
this._private__seriesMap.set(res, series);
|
|
this._private__seriesMapReversed.set(series, res);
|
|
return res;
|
|
}
|
|
_private__sendUpdateToChart(update) {
|
|
const model = this._private__chartWidget._internal_model();
|
|
model._internal_updateTimeScale(update._internal_timeScale._internal_baseIndex, update._internal_timeScale._internal_points, update._internal_timeScale._internal_firstChangedPointIndex);
|
|
update._internal_series.forEach((value, series) => series._internal_setData(value._internal_data, value._internal_info));
|
|
model._internal_timeScale()._internal_recalculateIndicesWithData();
|
|
model._internal_recalculateAllPanes();
|
|
}
|
|
_private__mapSeriesToApi(series) {
|
|
return ensureDefined(this._private__seriesMapReversed.get(series));
|
|
}
|
|
_private__resolveSeriesApi(series) {
|
|
return series !== undefined && this._private__seriesMapReversed.has(series) ? this._private__mapSeriesToApi(series) : undefined;
|
|
}
|
|
_private__convertMouseParams(param) {
|
|
const seriesData = new Map();
|
|
param._internal_seriesData.forEach((plotRow, series) => {
|
|
const seriesType = series._internal_seriesType();
|
|
const data = getSeriesDataCreator(seriesType)(plotRow);
|
|
if (seriesType !== 'Custom') {
|
|
assert(isFulfilledData(data));
|
|
}
|
|
else {
|
|
const customWhitespaceChecker = series._internal_customSeriesWhitespaceCheck();
|
|
assert(!customWhitespaceChecker || customWhitespaceChecker(data) === false);
|
|
}
|
|
seriesData.set(this._private__mapSeriesToApi(series), data);
|
|
});
|
|
const hoveredSeries = this._private__resolveSeriesApi(param._internal_hoveredSeries);
|
|
const hoveredInfo = param._internal_hoveredInfo === undefined
|
|
? undefined
|
|
: {
|
|
type: param._internal_hoveredInfo._internal_type,
|
|
sourceKind: param._internal_hoveredInfo._internal_sourceKind,
|
|
objectKind: param._internal_hoveredInfo._internal_objectKind,
|
|
series: this._private__resolveSeriesApi(param._internal_hoveredInfo._internal_series),
|
|
objectId: param._internal_hoveredInfo._internal_objectId,
|
|
paneIndex: param._internal_hoveredInfo._internal_paneIndex,
|
|
};
|
|
return {
|
|
time: param._internal_originalTime,
|
|
logical: param._internal_index,
|
|
point: param._internal_point,
|
|
paneIndex: param._internal_paneIndex,
|
|
hoveredInfo,
|
|
hoveredSeries,
|
|
hoveredObjectId: param._internal_hoveredObject,
|
|
seriesData,
|
|
sourceEvent: param._internal_touchMouseEventData,
|
|
};
|
|
}
|
|
_private__getPaneApi(pane) {
|
|
let result = this._private__panes.get(pane);
|
|
if (!result) {
|
|
result = new PaneApi(this._private__chartWidget, (series) => this._private__mapSeriesToApi(series), pane, this);
|
|
this._private__panes.set(pane, result);
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
function fetchHtmlElement(container) {
|
|
if (isString(container)) {
|
|
const element = document.getElementById(container);
|
|
assert(element !== null, `Cannot find element in DOM with id=${container}`);
|
|
return element;
|
|
}
|
|
return container;
|
|
}
|
|
/**
|
|
* This function is the main entry point of the Lightweight Charting Library. If you are using time values
|
|
* for the horizontal scale then it is recommended that you rather use the {@link createChart} function.
|
|
*
|
|
* @template HorzScaleItem - type of points on the horizontal scale
|
|
* @template THorzScaleBehavior - type of horizontal axis strategy that encapsulate all the specific behaviors of the horizontal scale type
|
|
*
|
|
* @param container - ID of HTML element or element itself
|
|
* @param horzScaleBehavior - Horizontal scale behavior
|
|
* @param options - Any subset of options to be applied at start.
|
|
* @returns An interface to the created chart
|
|
*/
|
|
function createChartEx(container, horzScaleBehavior, options) {
|
|
const htmlElement = fetchHtmlElement(container);
|
|
const res = new ChartApi(htmlElement, horzScaleBehavior, options);
|
|
horzScaleBehavior.setOptions(res.options());
|
|
return res;
|
|
}
|
|
/**
|
|
* This function is the simplified main entry point of the Lightweight Charting Library with time points for the horizontal scale.
|
|
*
|
|
* @param container - ID of HTML element or element itself
|
|
* @param options - Any subset of options to be applied at start.
|
|
* @returns An interface to the created chart
|
|
*/
|
|
function createChart(container, options) {
|
|
return createChartEx(container, new HorzScaleBehaviorTime(), HorzScaleBehaviorTime._internal_applyDefaults(options));
|
|
}
|
|
/**
|
|
* Provides the default implementation of the horizontal scale (time-based) that can be used as a base for extending the horizontal scale with custom behavior.
|
|
* This allows for the introduction of custom functionality without re-implementing the entire {@link IHorzScaleBehavior}<{@link Time}> interface.
|
|
*
|
|
* For further details, refer to the {@link createChartEx} chart constructor method.
|
|
*
|
|
* @returns An uninitialized class implementing the {@link IHorzScaleBehavior}<{@link Time}> interface
|
|
*/
|
|
function defaultHorzScaleBehavior() {
|
|
return HorzScaleBehaviorTime;
|
|
}
|
|
|
|
function drawSeriesPointMarkers(renderingScope, items, pointMarkersRadius, visibleRange,
|
|
// the values returned by styleGetter are compared using the operator !==,
|
|
// so if styleGetter returns objects, then styleGetter should return the same object for equal styles
|
|
styleGetter) {
|
|
if (visibleRange.to - visibleRange.from <= 0) {
|
|
return;
|
|
}
|
|
const { horizontalPixelRatio, verticalPixelRatio, context } = renderingScope;
|
|
let prevStyle = null;
|
|
const tickWidth = Math.max(1, Math.floor(horizontalPixelRatio));
|
|
const correction = (tickWidth % 2) / 2;
|
|
const radius = pointMarkersRadius * verticalPixelRatio + correction;
|
|
for (let i = visibleRange.to - 1; i >= visibleRange.from; --i) {
|
|
const point = items[i];
|
|
if (point) {
|
|
const style = styleGetter(renderingScope, point);
|
|
if (style !== prevStyle) {
|
|
if (prevStyle !== null) {
|
|
context.fill();
|
|
}
|
|
context.beginPath();
|
|
context.fillStyle = style;
|
|
prevStyle = style;
|
|
}
|
|
const centerX = Math.round(point._internal_x * horizontalPixelRatio) + correction; // correct x coordinate only
|
|
const centerY = point._internal_y * verticalPixelRatio;
|
|
context.moveTo(centerX, centerY);
|
|
context.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
|
}
|
|
}
|
|
context.fill();
|
|
}
|
|
|
|
function distanceByCoordinates(p1x, p1y, p2x, p2y) {
|
|
return Math.hypot(p2x - p1x, p2y - p1y);
|
|
}
|
|
// eslint-disable-next-line max-params, complexity
|
|
function walkLine(renderingScope, items, lineType, visibleRange, barWidth,
|
|
// the values returned by styleGetter are compared using the operator !==,
|
|
// so if styleGetter returns objects, then styleGetter should return the same object for equal styles
|
|
styleGetter, finishStyledArea, dashPatternLength = 0) {
|
|
if (items.length === 0 || visibleRange.from >= items.length || visibleRange.to <= 0) {
|
|
return;
|
|
}
|
|
const { context: ctx, horizontalPixelRatio, verticalPixelRatio } = renderingScope;
|
|
const firstItem = items[visibleRange.from];
|
|
let currentStyle = styleGetter(renderingScope, firstItem);
|
|
let currentStyleFirstItem = firstItem;
|
|
if (visibleRange.to - visibleRange.from < 2) {
|
|
const halfBarWidth = barWidth / 2;
|
|
ctx.beginPath();
|
|
const item1 = { _internal_x: firstItem._internal_x - halfBarWidth, _internal_y: firstItem._internal_y };
|
|
const item2 = { _internal_x: firstItem._internal_x + halfBarWidth, _internal_y: firstItem._internal_y };
|
|
ctx.moveTo(item1._internal_x * horizontalPixelRatio, item1._internal_y * verticalPixelRatio);
|
|
ctx.lineTo(item2._internal_x * horizontalPixelRatio, item2._internal_y * verticalPixelRatio);
|
|
finishStyledArea(renderingScope, currentStyle, item1, item2);
|
|
}
|
|
else {
|
|
const shouldTrackDashOffset = dashPatternLength > 0;
|
|
let accumulatedDistance = 0;
|
|
const changeStyle = (newStyle, currentItem) => {
|
|
finishStyledArea(renderingScope, currentStyle, currentStyleFirstItem, currentItem);
|
|
ctx.beginPath();
|
|
currentStyle = newStyle;
|
|
currentStyleFirstItem = currentItem;
|
|
if (shouldTrackDashOffset) {
|
|
const offset = accumulatedDistance % dashPatternLength;
|
|
ctx.lineDashOffset = offset;
|
|
// reset to the remainder to avoid floating-point precision drift over very long series.
|
|
accumulatedDistance = offset;
|
|
}
|
|
};
|
|
let currentItem = currentStyleFirstItem;
|
|
ctx.beginPath();
|
|
ctx.moveTo(firstItem._internal_x * horizontalPixelRatio, firstItem._internal_y * verticalPixelRatio);
|
|
for (let i = visibleRange.from + 1; i < visibleRange.to; ++i) {
|
|
currentItem = items[i];
|
|
const currentX = currentItem._internal_x * horizontalPixelRatio;
|
|
const currentY = currentItem._internal_y * verticalPixelRatio;
|
|
const itemStyle = styleGetter(renderingScope, currentItem);
|
|
switch (lineType) {
|
|
case 0 /* LineType.Simple */: {
|
|
ctx.lineTo(currentX, currentY);
|
|
if (shouldTrackDashOffset) {
|
|
const prevItem = items[i - 1];
|
|
const prevX = prevItem._internal_x * horizontalPixelRatio;
|
|
const prevY = prevItem._internal_y * verticalPixelRatio;
|
|
accumulatedDistance += distanceByCoordinates(prevX, prevY, currentX, currentY);
|
|
}
|
|
break;
|
|
}
|
|
case 1 /* LineType.WithSteps */: {
|
|
const prevItem = items[i - 1];
|
|
const prevY = prevItem._internal_y * verticalPixelRatio;
|
|
ctx.lineTo(currentX, prevY);
|
|
if (shouldTrackDashOffset) {
|
|
accumulatedDistance += Math.abs(currentItem._internal_x - prevItem._internal_x) * horizontalPixelRatio;
|
|
}
|
|
if (itemStyle !== currentStyle) {
|
|
changeStyle(itemStyle, currentItem);
|
|
ctx.lineTo(currentX, prevY);
|
|
}
|
|
ctx.lineTo(currentX, currentY);
|
|
if (shouldTrackDashOffset) {
|
|
accumulatedDistance += Math.abs(currentItem._internal_y - prevItem._internal_y) * verticalPixelRatio;
|
|
}
|
|
break;
|
|
}
|
|
case 2 /* LineType.Curved */: {
|
|
const [cp1, cp2] = getControlPoints(items, i - 1, i);
|
|
const cp1x = cp1._internal_x * horizontalPixelRatio;
|
|
const cp1y = cp1._internal_y * verticalPixelRatio;
|
|
const cp2x = cp2._internal_x * horizontalPixelRatio;
|
|
const cp2y = cp2._internal_y * verticalPixelRatio;
|
|
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, currentX, currentY);
|
|
if (shouldTrackDashOffset) {
|
|
const prevItem = items[i - 1];
|
|
const prevX = prevItem._internal_x * horizontalPixelRatio;
|
|
const prevY = prevItem._internal_y * verticalPixelRatio;
|
|
const chord = distanceByCoordinates(prevX, prevY, currentX, currentY);
|
|
const controlPolygon = distanceByCoordinates(prevX, prevY, cp1x, cp1y) +
|
|
distanceByCoordinates(cp1x, cp1y, cp2x, cp2y) +
|
|
distanceByCoordinates(cp2x, cp2y, currentX, currentY);
|
|
accumulatedDistance += (chord + controlPolygon) / 2;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if (lineType !== 1 /* LineType.WithSteps */ && itemStyle !== currentStyle) {
|
|
changeStyle(itemStyle, currentItem);
|
|
ctx.moveTo(currentX, currentY);
|
|
}
|
|
}
|
|
if (currentStyleFirstItem !== currentItem || currentStyleFirstItem === currentItem && lineType === 1 /* LineType.WithSteps */) {
|
|
finishStyledArea(renderingScope, currentStyle, currentStyleFirstItem, currentItem);
|
|
}
|
|
if (shouldTrackDashOffset) {
|
|
ctx.lineDashOffset = 0;
|
|
}
|
|
}
|
|
}
|
|
const curveTension = 6;
|
|
function subtract(p1, p2) {
|
|
return { _internal_x: p1._internal_x - p2._internal_x, _internal_y: p1._internal_y - p2._internal_y };
|
|
}
|
|
function add(p1, p2) {
|
|
return { _internal_x: p1._internal_x + p2._internal_x, _internal_y: p1._internal_y + p2._internal_y };
|
|
}
|
|
function divide(p1, n) {
|
|
return { _internal_x: p1._internal_x / n, _internal_y: p1._internal_y / n };
|
|
}
|
|
/**
|
|
* @returns Two control points that can be used as arguments to {@link CanvasRenderingContext2D.bezierCurveTo} to draw a curved line between `points[fromPointIndex]` and `points[toPointIndex]`.
|
|
*/
|
|
function getControlPoints(points, fromPointIndex, toPointIndex) {
|
|
const beforeFromPointIndex = Math.max(0, fromPointIndex - 1);
|
|
const afterToPointIndex = Math.min(points.length - 1, toPointIndex + 1);
|
|
const cp1 = add(points[fromPointIndex], divide(subtract(points[toPointIndex], points[beforeFromPointIndex]), curveTension));
|
|
const cp2 = subtract(points[toPointIndex], divide(subtract(points[afterToPointIndex], points[fromPointIndex]), curveTension));
|
|
return [cp1, cp2];
|
|
}
|
|
|
|
function finishStyledArea$1(scope, style) {
|
|
const ctx = scope.context;
|
|
ctx.strokeStyle = style;
|
|
ctx.stroke();
|
|
}
|
|
class PaneRendererLineBase extends BitmapCoordinatesPaneRenderer {
|
|
constructor() {
|
|
super(...arguments);
|
|
this._internal__data = null;
|
|
}
|
|
_internal_setData(data) {
|
|
this._internal__data = data;
|
|
}
|
|
_internal__drawImpl(renderingScope) {
|
|
if (this._internal__data === null) {
|
|
return;
|
|
}
|
|
const { _internal_items: items, _internal_visibleRange: visibleRange, _internal_barWidth: barWidth, _internal_lineType: lineType, _internal_lineWidth: lineWidth, _internal_lineStyle: lineStyle, _internal_pointMarkersRadius: pointMarkersRadius } = this._internal__data;
|
|
if (visibleRange === null) {
|
|
return;
|
|
}
|
|
const ctx = renderingScope.context;
|
|
ctx.lineCap = 'butt';
|
|
ctx.lineWidth = lineWidth * renderingScope.verticalPixelRatio;
|
|
const dashPattern = setLineStyle(ctx, lineStyle);
|
|
ctx.lineJoin = 'round';
|
|
const styleGetter = this._internal__strokeStyle.bind(this);
|
|
const dashPatternLength = getDashPatternLength(dashPattern);
|
|
if (lineType !== undefined) {
|
|
walkLine(renderingScope, items, lineType, visibleRange, barWidth, styleGetter, finishStyledArea$1, dashPatternLength);
|
|
}
|
|
if (pointMarkersRadius) {
|
|
drawSeriesPointMarkers(renderingScope, items, pointMarkersRadius, visibleRange, styleGetter);
|
|
}
|
|
}
|
|
}
|
|
|
|
class PaneRendererLine extends PaneRendererLineBase {
|
|
_internal__strokeStyle(renderingScope, item) {
|
|
return item._internal_lineColor;
|
|
}
|
|
}
|
|
|
|
const BEZIER_APPROXIMATION_STEPS = 12;
|
|
function cubicBezierPoint(p0, p1, p2, p3, t) {
|
|
const u = 1 - t;
|
|
return u * u * u * p0
|
|
+ 3 * u * u * t * p1
|
|
+ 3 * u * t * t * p2
|
|
+ t * t * t * p3;
|
|
}
|
|
function distanceToBezierCurve(x, y, points) {
|
|
let minDistance = Number.POSITIVE_INFINITY;
|
|
let previousPoint = points[0];
|
|
for (let step = 1; step <= BEZIER_APPROXIMATION_STEPS; step++) {
|
|
const t = step / BEZIER_APPROXIMATION_STEPS;
|
|
const currentPoint = {
|
|
_internal_x: cubicBezierPoint(points[0]._internal_x, points[1]._internal_x, points[2]._internal_x, points[3]._internal_x, t),
|
|
_internal_y: cubicBezierPoint(points[0]._internal_y, points[1]._internal_y, points[2]._internal_y, points[3]._internal_y, t),
|
|
};
|
|
minDistance = Math.min(minDistance, distanceToSegment(x, y, previousPoint._internal_x, previousPoint._internal_y, currentPoint._internal_x, currentPoint._internal_y));
|
|
previousPoint = currentPoint;
|
|
}
|
|
return minDistance;
|
|
}
|
|
function lineSegmentHorizontalBounds(firstItem, secondItem, lineType, items, toItemIndex) {
|
|
switch (lineType) {
|
|
case 2 /* LineType.Curved */: {
|
|
const [firstControlPoint, secondControlPoint] = getControlPoints(items, toItemIndex - 1, toItemIndex);
|
|
const minX = Math.min(firstItem._internal_x, secondItem._internal_x, firstControlPoint._internal_x, secondControlPoint._internal_x);
|
|
const maxX = Math.max(firstItem._internal_x, secondItem._internal_x, firstControlPoint._internal_x, secondControlPoint._internal_x);
|
|
return [minX, maxX];
|
|
}
|
|
case 1 /* LineType.WithSteps */:
|
|
case 0 /* LineType.Simple */:
|
|
default: {
|
|
const minX = Math.min(firstItem._internal_x, secondItem._internal_x);
|
|
const maxX = Math.max(firstItem._internal_x, secondItem._internal_x);
|
|
return [minX, maxX];
|
|
}
|
|
}
|
|
}
|
|
// eslint-disable-next-line max-params
|
|
function hitTestLineSegment(x, y, firstItem, secondItem, lineType, items, toItemIndex, radius) {
|
|
switch (lineType) {
|
|
case 1 /* LineType.WithSteps */:
|
|
{
|
|
const horizontalDistance = distanceToSegment(x, y, firstItem._internal_x, firstItem._internal_y, secondItem._internal_x, firstItem._internal_y);
|
|
const verticalDistance = distanceToSegment(x, y, secondItem._internal_x, firstItem._internal_y, secondItem._internal_x, secondItem._internal_y);
|
|
const minDistance = Math.min(horizontalDistance, verticalDistance);
|
|
return minDistance <= radius ? minDistance : null;
|
|
}
|
|
case 2 /* LineType.Curved */: {
|
|
const [firstControlPoint, secondControlPoint] = getControlPoints(items, toItemIndex - 1, toItemIndex);
|
|
const distance = distanceToBezierCurve(x, y, [firstItem, firstControlPoint, secondControlPoint, secondItem]);
|
|
return distance <= radius ? distance : null;
|
|
}
|
|
case 0 /* LineType.Simple */:
|
|
default:
|
|
{
|
|
const distance = distanceToSegment(x, y, firstItem._internal_x, firstItem._internal_y, secondItem._internal_x, secondItem._internal_y);
|
|
return distance <= radius ? distance : null;
|
|
}
|
|
}
|
|
}
|
|
// eslint-disable-next-line max-params, complexity
|
|
function hitTestLineSeries(items, visibleRange, x, y, lineType, lineWidth, pointMarkersRadius, barSpacing = 0, hitTestTolerance = 0) {
|
|
if (visibleRange === null || visibleRange.from >= visibleRange.to || items.length === 0) {
|
|
return null;
|
|
}
|
|
const radius = Math.max(lineWidth / 2, pointMarkersRadius ?? 0) + hitTestTolerance;
|
|
let pointMinDistance = Number.POSITIVE_INFINITY;
|
|
if (pointMarkersRadius !== undefined) {
|
|
const pointRadius = pointMarkersRadius + hitTestTolerance;
|
|
const pointCandidateFrom = lowerBoundByX(items, x - pointRadius, visibleRange.from, visibleRange.to);
|
|
const pointCandidateTo = upperBoundByX(items, x + pointRadius, pointCandidateFrom, visibleRange.to);
|
|
for (let itemIndex = pointCandidateFrom; itemIndex < pointCandidateTo; itemIndex++) {
|
|
const item = items[itemIndex];
|
|
if (!isWithinHorizontalSweep(x, item._internal_x, item._internal_x, pointMarkersRadius + hitTestTolerance)) {
|
|
continue;
|
|
}
|
|
const distance = Math.hypot(x - item._internal_x, y - item._internal_y);
|
|
if (distance <= pointMarkersRadius + hitTestTolerance) {
|
|
pointMinDistance = Math.min(pointMinDistance, distance);
|
|
}
|
|
}
|
|
}
|
|
if (visibleRange.to - visibleRange.from < 2) {
|
|
const item = items[visibleRange.from];
|
|
const singlePointHalfWidth = Math.max(barSpacing / 2, radius);
|
|
const distance = distanceToSegment(x, y, item._internal_x - singlePointHalfWidth, item._internal_y, item._internal_x + singlePointHalfWidth, item._internal_y);
|
|
if (distance <= radius) {
|
|
pointMinDistance = Math.min(pointMinDistance, distance);
|
|
}
|
|
return Number.isFinite(pointMinDistance) ? hoveredSeriesHitTestResult(pointMinDistance, 2 /* HitTestPriority.Point */, 'series-point') : null;
|
|
}
|
|
let lineMinDistance = Number.POSITIVE_INFINITY;
|
|
const lineCandidateFrom = lowerBoundByX(items, x - radius, visibleRange.from, visibleRange.to);
|
|
const lineCandidateTo = upperBoundByX(items, x + radius, lineCandidateFrom, visibleRange.to);
|
|
const segmentFrom = Math.max(visibleRange.from + 1, lineCandidateFrom);
|
|
const segmentTo = Math.min(visibleRange.to, lineCandidateTo + 1);
|
|
for (let itemIndex = segmentFrom; itemIndex < segmentTo; itemIndex++) {
|
|
const previousItem = items[itemIndex - 1];
|
|
const currentItem = items[itemIndex];
|
|
const [leftX, rightX] = lineSegmentHorizontalBounds(previousItem, currentItem, lineType, items, itemIndex);
|
|
if (!isWithinHorizontalSweep(x, leftX, rightX, radius)) {
|
|
continue;
|
|
}
|
|
const distance = hitTestLineSegment(x, y, previousItem, currentItem, lineType, items, itemIndex, radius);
|
|
if (distance !== null) {
|
|
lineMinDistance = Math.min(lineMinDistance, distance);
|
|
}
|
|
}
|
|
if (Number.isFinite(pointMinDistance)) {
|
|
return hoveredSeriesHitTestResult(pointMinDistance, 2 /* HitTestPriority.Point */, 'series-point');
|
|
}
|
|
return Number.isFinite(lineMinDistance) ? hoveredSeriesHitTestResult(lineMinDistance, 1 /* HitTestPriority.Line */, 'series-line') : null;
|
|
}
|
|
|
|
class LinePaneViewBase extends SeriesPaneViewBase {
|
|
constructor(series, model) {
|
|
super(series, model, true);
|
|
}
|
|
_internal__convertToCoordinates(priceScale, timeScale, firstValue) {
|
|
timeScale._internal_indexesToCoordinates(this._internal__items, undefinedIfNull(this._internal__itemsVisibleRange));
|
|
priceScale._internal_pointsArrayToCoordinates(this._internal__items, firstValue, undefinedIfNull(this._internal__itemsVisibleRange));
|
|
}
|
|
_internal__createRawItemBase(time, price) {
|
|
return {
|
|
_internal_time: time,
|
|
_internal_price: price,
|
|
_internal_x: NaN,
|
|
_internal_y: NaN,
|
|
};
|
|
}
|
|
_internal__fillRawPoints() {
|
|
const colorer = this._internal__series._internal_barColorer();
|
|
this._internal__items = this._internal__series._internal_conflatedBars()._internal_rows().map((row) => {
|
|
const isConflated = (row._internal_originalDataCount ?? 1) > 1;
|
|
let value;
|
|
if (isConflated) {
|
|
const high = row._internal_value[1 /* PlotRowValueIndex.High */];
|
|
const low = row._internal_value[2 /* PlotRowValueIndex.Low */];
|
|
const close = row._internal_value[3 /* PlotRowValueIndex.Close */];
|
|
const highMove = Math.abs(high - close);
|
|
const lowMove = Math.abs(low - close);
|
|
// in case of conflation we want to show more extreme price to represent the range
|
|
// and we choose the one which is further from the close price
|
|
value = (highMove > lowMove) ? high : low;
|
|
}
|
|
else {
|
|
value = row._internal_value[3 /* PlotRowValueIndex.Close */];
|
|
}
|
|
return this._internal__createRawItem(row._internal_index, value, colorer);
|
|
});
|
|
}
|
|
}
|
|
|
|
class LineHitTestPaneViewBase extends LinePaneViewBase {
|
|
_internal__hitTestImpl(x, y) {
|
|
const options = this._internal__series._internal_options();
|
|
return hitTestLineSeries(this._internal__items, this._internal__itemsVisibleRange, x, y, options.lineType, options.lineVisible ? options.lineWidth : 1, options.pointMarkersVisible ? (options.pointMarkersRadius || options.lineWidth / 2 + 2) : undefined, this._internal__model._internal_timeScale()._internal_barSpacing(), options.hitTestTolerance);
|
|
}
|
|
}
|
|
|
|
class SeriesLinePaneView extends LineHitTestPaneViewBase {
|
|
constructor() {
|
|
super(...arguments);
|
|
this._internal__renderer = new PaneRendererLine();
|
|
}
|
|
_internal__createRawItem(time, price, colorer) {
|
|
return {
|
|
...this._internal__createRawItemBase(time, price),
|
|
...colorer._internal_barStyle(time),
|
|
};
|
|
}
|
|
_internal__prepareRendererData() {
|
|
const options = this._internal__series._internal_options();
|
|
const data = {
|
|
_internal_items: this._internal__items,
|
|
_internal_lineStyle: options.lineStyle,
|
|
_internal_lineType: options.lineVisible ? options.lineType : undefined,
|
|
_internal_lineWidth: options.lineWidth,
|
|
_internal_pointMarkersRadius: options.pointMarkersVisible ? (options.pointMarkersRadius || options.lineWidth / 2 + 2) : undefined,
|
|
_internal_visibleRange: this._internal__itemsVisibleRange,
|
|
_internal_barWidth: this._internal__model._internal_timeScale()._internal_barSpacing(),
|
|
};
|
|
this._internal__renderer._internal_setData(data);
|
|
}
|
|
}
|
|
|
|
const lineStyleDefaults = {
|
|
color: '#2196f3',
|
|
lineStyle: 0 /* LineStyle.Solid */,
|
|
lineWidth: 3,
|
|
lineType: 0 /* LineType.Simple */,
|
|
lineVisible: true,
|
|
crosshairMarkerVisible: true,
|
|
crosshairMarkerRadius: 4,
|
|
crosshairMarkerBorderColor: '',
|
|
crosshairMarkerBorderWidth: 2,
|
|
crosshairMarkerBackgroundColor: '',
|
|
lastPriceAnimation: 0 /* LastPriceAnimationMode.Disabled */,
|
|
pointMarkersVisible: false,
|
|
};
|
|
const createPaneView$5 = (series, model) => new SeriesLinePaneView(series, model);
|
|
const createLineSeries = () => {
|
|
const definition = {
|
|
type: 'Line',
|
|
isBuiltIn: true,
|
|
defaultOptions: lineStyleDefaults,
|
|
/**
|
|
* @internal
|
|
*/
|
|
_internal_createPaneView: createPaneView$5,
|
|
};
|
|
return definition;
|
|
};
|
|
const lineSeries = createLineSeries();
|
|
|
|
function createDebouncedMicroTaskHandler(callback) {
|
|
let scheduled = false;
|
|
return function (...args) {
|
|
if (!scheduled) {
|
|
scheduled = true;
|
|
queueMicrotask(() => {
|
|
callback(...args);
|
|
scheduled = false;
|
|
});
|
|
}
|
|
};
|
|
}
|
|
function markWithGreaterWeight$1(a, b) {
|
|
return a.weight > b.weight ? a : b;
|
|
}
|
|
function toInternalHorzScaleItem(item) {
|
|
return item;
|
|
}
|
|
function fromInternalHorzScaleItem(item) {
|
|
return item;
|
|
}
|
|
class YieldCurveHorzScaleBehavior {
|
|
constructor() {
|
|
this._private__pointsChangedDelegate = new Delegate();
|
|
this._private__invalidateWhitespace = createDebouncedMicroTaskHandler(() => this._private__pointsChangedDelegate._internal_fire(this._private__largestIndex));
|
|
this._private__largestIndex = 0;
|
|
}
|
|
/** Data changes might require that the whitespace be generated again */
|
|
_internal_whitespaceInvalidated() {
|
|
return this._private__pointsChangedDelegate;
|
|
}
|
|
_internal_destroy() {
|
|
this._private__pointsChangedDelegate._internal_destroy();
|
|
}
|
|
options() {
|
|
return this._private__options;
|
|
}
|
|
setOptions(options) {
|
|
this._private__options = options;
|
|
}
|
|
preprocessData(data) {
|
|
// No preprocessing needed for yield curve data
|
|
}
|
|
updateFormatter(options) {
|
|
if (!this._private__options) {
|
|
return;
|
|
}
|
|
this._private__options.localization = options;
|
|
}
|
|
createConverterToInternalObj(data) {
|
|
this._private__invalidateWhitespace();
|
|
return (time) => {
|
|
if (time > this._private__largestIndex) {
|
|
this._private__largestIndex = time;
|
|
}
|
|
return toInternalHorzScaleItem(time);
|
|
};
|
|
}
|
|
key(internalItem) {
|
|
return internalItem;
|
|
}
|
|
cacheKey(internalItem) {
|
|
return fromInternalHorzScaleItem(internalItem);
|
|
}
|
|
convertHorzItemToInternal(item) {
|
|
return toInternalHorzScaleItem(item);
|
|
}
|
|
formatHorzItem(item) {
|
|
return this._private__formatTime(item);
|
|
}
|
|
formatTickmark(item) {
|
|
return this._private__formatTime(item.time);
|
|
}
|
|
maxTickMarkWeight(marks) {
|
|
return marks.reduce(markWithGreaterWeight$1, marks[0]).weight;
|
|
}
|
|
fillWeightsForPoints(sortedTimePoints, startIndex) {
|
|
const timeWeight = (time) => {
|
|
if (time % 120 === 0) {
|
|
return 10;
|
|
}
|
|
if (time % 60 === 0) {
|
|
return 9;
|
|
}
|
|
if (time % 36 === 0) {
|
|
return 8;
|
|
}
|
|
if (time % 12 === 0) {
|
|
return 7;
|
|
}
|
|
if (time % 6 === 0) {
|
|
return 6;
|
|
}
|
|
if (time % 3 === 0) {
|
|
return 5;
|
|
}
|
|
if (time % 1 === 0) {
|
|
return 4;
|
|
}
|
|
return 0;
|
|
};
|
|
for (let index = startIndex; index < sortedTimePoints.length; ++index) {
|
|
sortedTimePoints[index].timeWeight = timeWeight(fromInternalHorzScaleItem(sortedTimePoints[index].time));
|
|
}
|
|
this._private__largestIndex = fromInternalHorzScaleItem(sortedTimePoints[sortedTimePoints.length - 1].time);
|
|
this._private__invalidateWhitespace();
|
|
}
|
|
_private__formatTime(months) {
|
|
if (this._private__options.localization?.timeFormatter) {
|
|
return this._private__options.localization.timeFormatter(months);
|
|
}
|
|
if (months < 12) {
|
|
return `${months}M`;
|
|
}
|
|
const years = Math.floor(months / 12);
|
|
const remainingMonths = months % 12;
|
|
if (remainingMonths === 0) {
|
|
return `${years}Y`;
|
|
}
|
|
return `${years}Y${remainingMonths}M`;
|
|
}
|
|
}
|
|
|
|
const yieldChartOptionsDefaults = {
|
|
baseResolution: 1,
|
|
minimumTimeRange: 120,
|
|
startTimeRange: 0,
|
|
};
|
|
|
|
function generateWhitespaceData({ _internal_start: start, _internal_end: end, _internal_resolution: resolution, }) {
|
|
return Array.from({ length: Math.floor((end - start) / resolution) + 1 },
|
|
// eslint-disable-next-line quote-props
|
|
(item, i) => ({ 'time': start + i * resolution }));
|
|
}
|
|
function buildWhitespaceState(options, lastIndex) {
|
|
return {
|
|
_internal_start: Math.max(0, options.startTimeRange),
|
|
_internal_end: Math.max(0, options.minimumTimeRange, lastIndex || 0),
|
|
_internal_resolution: Math.max(1, options.baseResolution),
|
|
};
|
|
}
|
|
const generateWhitespaceHash = ({ _internal_start: start, _internal_end: end, _internal_resolution: resolution, }) => `${start}~${end}~${resolution}`;
|
|
const defaultOptions = {
|
|
yieldCurve: yieldChartOptionsDefaults,
|
|
// and add sensible default options for yield charts which
|
|
// are different from the usual defaults.
|
|
timeScale: {
|
|
ignoreWhitespaceIndices: true,
|
|
},
|
|
leftPriceScale: {
|
|
visible: true,
|
|
},
|
|
rightPriceScale: {
|
|
visible: false,
|
|
},
|
|
localization: {
|
|
priceFormatter: (value) => {
|
|
return value.toFixed(3) + '%';
|
|
},
|
|
},
|
|
};
|
|
const lineStyleDefaultOptionOverrides = {
|
|
lastValueVisible: false,
|
|
priceLineVisible: false,
|
|
};
|
|
class YieldChartApi extends ChartApi {
|
|
constructor(container, options) {
|
|
const fullOptions = merge(defaultOptions, options || {});
|
|
const horzBehaviour = new YieldCurveHorzScaleBehavior();
|
|
super(container, horzBehaviour, fullOptions);
|
|
horzBehaviour.setOptions(this.options());
|
|
this._initWhitespaceSeries();
|
|
}
|
|
addSeries(definition, options = {}, paneIndex = 0) {
|
|
if (definition.isBuiltIn && ['Area', 'Line'].includes(definition.type) === false) {
|
|
throw new Error('Yield curve only support Area and Line series');
|
|
}
|
|
const optionOverrides = {
|
|
...lineStyleDefaultOptionOverrides,
|
|
...options,
|
|
};
|
|
return super.addSeries(definition, optionOverrides, paneIndex);
|
|
}
|
|
_initWhitespaceSeries() {
|
|
const horzBehaviour = this.horzBehaviour();
|
|
const whiteSpaceSeries = this.addSeries(lineSeries);
|
|
let currentWhitespaceHash;
|
|
function updateWhitespace(lastIndex) {
|
|
const newWhitespaceState = buildWhitespaceState(horzBehaviour.options().yieldCurve, lastIndex);
|
|
const newWhitespaceHash = generateWhitespaceHash(newWhitespaceState);
|
|
if (newWhitespaceHash !== currentWhitespaceHash) {
|
|
currentWhitespaceHash = newWhitespaceHash;
|
|
whiteSpaceSeries.setData(generateWhitespaceData(newWhitespaceState));
|
|
}
|
|
}
|
|
updateWhitespace(0);
|
|
horzBehaviour._internal_whitespaceInvalidated()._internal_subscribe(updateWhitespace);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a yield curve chart with the specified options.
|
|
*
|
|
* A yield curve chart differs from the default chart type
|
|
* in the following ways:
|
|
* - Horizontal scale is linearly spaced, and defined in monthly
|
|
* time duration units
|
|
* - Whitespace is ignored for the crosshair and grid lines
|
|
*
|
|
* @param container - ID of HTML element or element itself
|
|
* @param options - The yield chart options.
|
|
* @returns An interface to the created chart
|
|
*/
|
|
function createYieldCurveChart(container, options) {
|
|
const htmlElement = fetchHtmlElement(container);
|
|
const chartApi = new YieldChartApi(htmlElement, options);
|
|
return chartApi;
|
|
}
|
|
|
|
function markWithGreaterWeight(a, b) {
|
|
return a.weight > b.weight ? a : b;
|
|
}
|
|
class HorzScaleBehaviorPrice {
|
|
options() {
|
|
return this._private__options;
|
|
}
|
|
setOptions(options) {
|
|
this._private__options = options;
|
|
}
|
|
preprocessData(data) { }
|
|
updateFormatter(options) {
|
|
if (!this._private__options) {
|
|
return;
|
|
}
|
|
this._private__options.localization = options;
|
|
}
|
|
createConverterToInternalObj(data) {
|
|
return (price) => price;
|
|
}
|
|
key(internalItem) {
|
|
return internalItem;
|
|
}
|
|
cacheKey(internalItem) {
|
|
return internalItem;
|
|
}
|
|
convertHorzItemToInternal(item) {
|
|
return item;
|
|
}
|
|
formatHorzItem(item) {
|
|
return item.toFixed(this._private__precision());
|
|
}
|
|
formatTickmark(item, localizationOptions) {
|
|
return item.time.toFixed(this._private__precision());
|
|
}
|
|
maxTickMarkWeight(marks) {
|
|
return marks.reduce(markWithGreaterWeight, marks[0]).weight;
|
|
}
|
|
fillWeightsForPoints(sortedTimePoints, startIndex) {
|
|
const priceWeight = (price) => {
|
|
if (price === Math.ceil(price / 100) * 100) {
|
|
return 8;
|
|
}
|
|
if (price === Math.ceil(price / 50) * 50) {
|
|
return 7;
|
|
}
|
|
if (price === Math.ceil(price / 25) * 25) {
|
|
return 6;
|
|
}
|
|
if (price === Math.ceil(price / 10) * 10) {
|
|
return 5;
|
|
}
|
|
if (price === Math.ceil(price / 5) * 5) {
|
|
return 4;
|
|
}
|
|
if (price === Math.ceil(price)) {
|
|
return 3;
|
|
}
|
|
if (price * 2 === Math.ceil(price * 2)) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
};
|
|
for (let index = startIndex; index < sortedTimePoints.length; ++index) {
|
|
sortedTimePoints[index].timeWeight = priceWeight(sortedTimePoints[index].time);
|
|
}
|
|
}
|
|
_private__precision() {
|
|
return this._private__options.localization
|
|
.precision;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates an 'options' chart with price values on the horizontal scale.
|
|
*
|
|
* This function is used to create a specialized chart type where the horizontal scale
|
|
* represents price values instead of time. It's particularly useful for visualizing
|
|
* option chains, price distributions, or any data where price is the primary x-axis metric.
|
|
*
|
|
* @param container - The DOM element or its id where the chart will be rendered.
|
|
* @param options - Optional configuration options for the price chart.
|
|
* @returns An instance of IChartApiBase configured for price-based horizontal scaling.
|
|
*/
|
|
function createOptionsChart(container, options) {
|
|
return createChartEx(container, new HorzScaleBehaviorPrice(), options);
|
|
}
|
|
|
|
function finishStyledArea(baseLevelCoordinate, scope, style, areaFirstItem, newAreaFirstItem) {
|
|
const { context, horizontalPixelRatio, verticalPixelRatio } = scope;
|
|
context.lineTo(newAreaFirstItem._internal_x * horizontalPixelRatio, baseLevelCoordinate * verticalPixelRatio);
|
|
context.lineTo(areaFirstItem._internal_x * horizontalPixelRatio, baseLevelCoordinate * verticalPixelRatio);
|
|
context.closePath();
|
|
context.fillStyle = style;
|
|
context.fill();
|
|
}
|
|
class PaneRendererAreaBase extends BitmapCoordinatesPaneRenderer {
|
|
constructor() {
|
|
super(...arguments);
|
|
this._internal__data = null;
|
|
}
|
|
_internal_setData(data) {
|
|
this._internal__data = data;
|
|
}
|
|
_internal__drawImpl(renderingScope) {
|
|
if (this._internal__data === null) {
|
|
return;
|
|
}
|
|
const { _internal_items: items, _internal_visibleRange: visibleRange, _internal_barWidth: barWidth, _internal_lineWidth: lineWidth, _internal_lineStyle: lineStyle, _internal_lineType: lineType } = this._internal__data;
|
|
const baseLevelCoordinate = this._internal__data._internal_baseLevelCoordinate ??
|
|
(this._internal__data._internal_invertFilledArea ? 0 : renderingScope.mediaSize.height);
|
|
if (visibleRange === null) {
|
|
return;
|
|
}
|
|
const ctx = renderingScope.context;
|
|
ctx.lineCap = 'butt';
|
|
ctx.lineJoin = 'round';
|
|
ctx.lineWidth = lineWidth;
|
|
setLineStyle(ctx, lineStyle);
|
|
// walk lines with width=1 to have more accurate gradient's filling
|
|
ctx.lineWidth = 1;
|
|
walkLine(renderingScope, items, lineType, visibleRange, barWidth, this._internal__fillStyle.bind(this), finishStyledArea.bind(null, baseLevelCoordinate));
|
|
}
|
|
}
|
|
|
|
class GradientStyleCache {
|
|
// eslint-disable-next-line complexity
|
|
_internal_get(scope, params) {
|
|
const cachedParams = this._private__params;
|
|
const { _internal_topColor1: topColor1, _internal_topColor2: topColor2, _internal_bottomColor1: bottomColor1, _internal_bottomColor2: bottomColor2, _internal_baseLevelCoordinate: baseLevelCoordinate, _internal_topCoordinate: topCoordinate, _internal_bottomCoordinate: bottomCoordinate, } = params;
|
|
if (this._private__cachedValue === undefined ||
|
|
cachedParams === undefined ||
|
|
cachedParams._internal_topColor1 !== topColor1 ||
|
|
cachedParams._internal_topColor2 !== topColor2 ||
|
|
cachedParams._internal_bottomColor1 !== bottomColor1 ||
|
|
cachedParams._internal_bottomColor2 !== bottomColor2 ||
|
|
cachedParams._internal_baseLevelCoordinate !== baseLevelCoordinate ||
|
|
cachedParams._internal_topCoordinate !== topCoordinate ||
|
|
cachedParams._internal_bottomCoordinate !== bottomCoordinate) {
|
|
const { verticalPixelRatio } = scope;
|
|
const multiplier = baseLevelCoordinate || topCoordinate > 0 ? verticalPixelRatio : 1;
|
|
const top = topCoordinate * multiplier;
|
|
const bottom = bottomCoordinate === scope.bitmapSize.height ? bottomCoordinate : bottomCoordinate * multiplier;
|
|
const baseline = (baseLevelCoordinate ?? 0) * multiplier;
|
|
const gradient = scope.context.createLinearGradient(0, top, 0, bottom);
|
|
gradient.addColorStop(0, topColor1);
|
|
if (baseLevelCoordinate !== null && baseLevelCoordinate !== undefined) {
|
|
const range = bottom - top;
|
|
const baselineRatio = clamp(((baseline - top) / range), 0, 1);
|
|
gradient.addColorStop(baselineRatio, topColor2);
|
|
gradient.addColorStop(baselineRatio, bottomColor1);
|
|
}
|
|
gradient.addColorStop(1, bottomColor2);
|
|
this._private__cachedValue = gradient;
|
|
this._private__params = params;
|
|
}
|
|
return this._private__cachedValue;
|
|
}
|
|
}
|
|
|
|
class PaneRendererBaselineArea extends PaneRendererAreaBase {
|
|
constructor() {
|
|
super(...arguments);
|
|
this._private__fillCache = new GradientStyleCache();
|
|
}
|
|
_internal__fillStyle(renderingScope, item) {
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
const data = this._internal__data;
|
|
return this._private__fillCache._internal_get(renderingScope, {
|
|
_internal_topColor1: item._internal_topFillColor1,
|
|
_internal_topColor2: item._internal_topFillColor2,
|
|
_internal_bottomColor1: item._internal_bottomFillColor1,
|
|
_internal_bottomColor2: item._internal_bottomFillColor2,
|
|
_internal_baseLevelCoordinate: data._internal_baseLevelCoordinate,
|
|
_internal_topCoordinate: data._internal_topCoordinate ?? 0,
|
|
_internal_bottomCoordinate: data._internal_bottomCoordinate ?? renderingScope.bitmapSize.height,
|
|
});
|
|
}
|
|
}
|
|
|
|
class PaneRendererBaselineLine extends PaneRendererLineBase {
|
|
constructor() {
|
|
super(...arguments);
|
|
this._private__strokeCache = new GradientStyleCache();
|
|
}
|
|
_internal__strokeStyle(renderingScope, item) {
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
const data = this._internal__data;
|
|
return this._private__strokeCache._internal_get(renderingScope, {
|
|
_internal_topColor1: item._internal_topLineColor,
|
|
_internal_topColor2: item._internal_topLineColor,
|
|
_internal_bottomColor1: item._internal_bottomLineColor,
|
|
_internal_bottomColor2: item._internal_bottomLineColor,
|
|
_internal_baseLevelCoordinate: data._internal_baseLevelCoordinate,
|
|
_internal_topCoordinate: data._internal_topCoordinate ?? 0,
|
|
_internal_bottomCoordinate: data._internal_bottomCoordinate ?? renderingScope.bitmapSize.height,
|
|
});
|
|
}
|
|
}
|
|
|
|
class SeriesBaselinePaneView extends LineHitTestPaneViewBase {
|
|
constructor(series, model) {
|
|
super(series, model);
|
|
this._internal__renderer = new CompositeRenderer();
|
|
this._private__baselineAreaRenderer = new PaneRendererBaselineArea();
|
|
this._private__baselineLineRenderer = new PaneRendererBaselineLine();
|
|
this._internal__renderer._internal_setRenderers([this._private__baselineAreaRenderer, this._private__baselineLineRenderer]);
|
|
}
|
|
_internal__createRawItem(time, price, colorer) {
|
|
return {
|
|
...this._internal__createRawItemBase(time, price),
|
|
...colorer._internal_barStyle(time),
|
|
};
|
|
}
|
|
_internal__prepareRendererData() {
|
|
const firstValue = this._internal__series._internal_firstValue();
|
|
if (firstValue === null) {
|
|
return;
|
|
}
|
|
const options = this._internal__series._internal_options();
|
|
const baseLevelCoordinate = this._internal__series._internal_priceScale()._internal_priceToCoordinate(options.baseValue.price, firstValue._internal_value);
|
|
const barWidth = this._internal__model._internal_timeScale()._internal_barSpacing();
|
|
if (this._internal__itemsVisibleRange === null || this._internal__items.length === 0) {
|
|
return;
|
|
}
|
|
let topCoordinate;
|
|
let bottomCoordinate;
|
|
if (options.relativeGradient) {
|
|
topCoordinate = this._internal__items[this._internal__itemsVisibleRange.from]._internal_y;
|
|
bottomCoordinate = this._internal__items[this._internal__itemsVisibleRange.from]._internal_y;
|
|
for (let i = this._internal__itemsVisibleRange.from; i < this._internal__itemsVisibleRange.to; i++) {
|
|
const item = this._internal__items[i];
|
|
if (item._internal_y < topCoordinate) {
|
|
topCoordinate = item._internal_y;
|
|
}
|
|
if (item._internal_y > bottomCoordinate) {
|
|
bottomCoordinate = item._internal_y;
|
|
}
|
|
}
|
|
}
|
|
this._private__baselineAreaRenderer._internal_setData({
|
|
_internal_items: this._internal__items,
|
|
_internal_lineWidth: options.lineWidth,
|
|
_internal_lineStyle: options.lineStyle,
|
|
_internal_lineType: options.lineType,
|
|
_internal_baseLevelCoordinate: baseLevelCoordinate,
|
|
_internal_topCoordinate: topCoordinate,
|
|
_internal_bottomCoordinate: bottomCoordinate,
|
|
_internal_invertFilledArea: false,
|
|
_internal_visibleRange: this._internal__itemsVisibleRange,
|
|
_internal_barWidth: barWidth,
|
|
});
|
|
this._private__baselineLineRenderer._internal_setData({
|
|
_internal_items: this._internal__items,
|
|
_internal_lineWidth: options.lineWidth,
|
|
_internal_lineStyle: options.lineStyle,
|
|
_internal_lineType: options.lineVisible ? options.lineType : undefined,
|
|
_internal_pointMarkersRadius: options.pointMarkersVisible ? (options.pointMarkersRadius || options.lineWidth / 2 + 2) : undefined,
|
|
_internal_baseLevelCoordinate: baseLevelCoordinate,
|
|
_internal_topCoordinate: topCoordinate,
|
|
_internal_bottomCoordinate: bottomCoordinate,
|
|
_internal_visibleRange: this._internal__itemsVisibleRange,
|
|
_internal_barWidth: barWidth,
|
|
});
|
|
}
|
|
}
|
|
|
|
const baselineStyleDefaults = {
|
|
baseValue: {
|
|
type: 'price',
|
|
price: 0,
|
|
},
|
|
relativeGradient: false,
|
|
topFillColor1: 'rgba(38, 166, 154, 0.28)',
|
|
topFillColor2: 'rgba(38, 166, 154, 0.05)',
|
|
topLineColor: 'rgba(38, 166, 154, 1)',
|
|
bottomFillColor1: 'rgba(239, 83, 80, 0.05)',
|
|
bottomFillColor2: 'rgba(239, 83, 80, 0.28)',
|
|
bottomLineColor: 'rgba(239, 83, 80, 1)',
|
|
lineWidth: 3,
|
|
lineStyle: 0 /* LineStyle.Solid */,
|
|
lineType: 0 /* LineType.Simple */,
|
|
lineVisible: true,
|
|
crosshairMarkerVisible: true,
|
|
crosshairMarkerRadius: 4,
|
|
crosshairMarkerBorderColor: '',
|
|
crosshairMarkerBorderWidth: 2,
|
|
crosshairMarkerBackgroundColor: '',
|
|
lastPriceAnimation: 0 /* LastPriceAnimationMode.Disabled */,
|
|
pointMarkersVisible: false,
|
|
};
|
|
const createPaneView$4 = (series, model) => new SeriesBaselinePaneView(series, model);
|
|
const createSeries$4 = () => {
|
|
const definition = {
|
|
type: 'Baseline',
|
|
isBuiltIn: true,
|
|
defaultOptions: baselineStyleDefaults,
|
|
/**
|
|
* @internal
|
|
*/
|
|
_internal_createPaneView: createPaneView$4,
|
|
};
|
|
return definition;
|
|
};
|
|
const baselineSeries = createSeries$4();
|
|
|
|
class PaneRendererArea extends PaneRendererAreaBase {
|
|
constructor() {
|
|
super(...arguments);
|
|
this._private__fillCache = new GradientStyleCache();
|
|
}
|
|
_internal__fillStyle(renderingScope, item) {
|
|
return this._private__fillCache._internal_get(renderingScope, {
|
|
_internal_topColor1: item._internal_topColor,
|
|
_internal_topColor2: '',
|
|
_internal_bottomColor1: '',
|
|
_internal_bottomColor2: item._internal_bottomColor,
|
|
_internal_topCoordinate: this._internal__data?._internal_topCoordinate ?? 0,
|
|
_internal_bottomCoordinate: renderingScope.bitmapSize.height,
|
|
});
|
|
}
|
|
}
|
|
|
|
class SeriesAreaPaneView extends LineHitTestPaneViewBase {
|
|
constructor(series, model) {
|
|
super(series, model);
|
|
this._internal__renderer = new CompositeRenderer();
|
|
this._private__areaRenderer = new PaneRendererArea();
|
|
this._private__lineRenderer = new PaneRendererLine();
|
|
this._internal__renderer._internal_setRenderers([this._private__areaRenderer, this._private__lineRenderer]);
|
|
}
|
|
_internal__createRawItem(time, price, colorer) {
|
|
return {
|
|
...this._internal__createRawItemBase(time, price),
|
|
...colorer._internal_barStyle(time),
|
|
};
|
|
}
|
|
_internal__prepareRendererData() {
|
|
const options = this._internal__series._internal_options();
|
|
if (this._internal__itemsVisibleRange === null || this._internal__items.length === 0) {
|
|
return;
|
|
}
|
|
let topCoordinate;
|
|
if (options.relativeGradient) {
|
|
topCoordinate = this._internal__items[this._internal__itemsVisibleRange.from]._internal_y;
|
|
for (let i = this._internal__itemsVisibleRange.from; i < this._internal__itemsVisibleRange.to; i++) {
|
|
const item = this._internal__items[i];
|
|
if (item._internal_y < topCoordinate) {
|
|
topCoordinate = item._internal_y;
|
|
}
|
|
}
|
|
}
|
|
this._private__areaRenderer._internal_setData({
|
|
_internal_lineType: options.lineType,
|
|
_internal_items: this._internal__items,
|
|
_internal_lineStyle: options.lineStyle,
|
|
_internal_lineWidth: options.lineWidth,
|
|
_internal_baseLevelCoordinate: null,
|
|
_internal_topCoordinate: topCoordinate,
|
|
_internal_invertFilledArea: options.invertFilledArea,
|
|
_internal_visibleRange: this._internal__itemsVisibleRange,
|
|
_internal_barWidth: this._internal__model._internal_timeScale()._internal_barSpacing(),
|
|
});
|
|
this._private__lineRenderer._internal_setData({
|
|
_internal_lineType: options.lineVisible ? options.lineType : undefined,
|
|
_internal_items: this._internal__items,
|
|
_internal_lineStyle: options.lineStyle,
|
|
_internal_lineWidth: options.lineWidth,
|
|
_internal_visibleRange: this._internal__itemsVisibleRange,
|
|
_internal_barWidth: this._internal__model._internal_timeScale()._internal_barSpacing(),
|
|
_internal_pointMarkersRadius: options.pointMarkersVisible ? (options.pointMarkersRadius || options.lineWidth / 2 + 2) : undefined,
|
|
});
|
|
}
|
|
}
|
|
|
|
const areaStyleDefaults = {
|
|
topColor: 'rgba( 46, 220, 135, 0.4)',
|
|
bottomColor: 'rgba( 40, 221, 100, 0)',
|
|
invertFilledArea: false,
|
|
relativeGradient: false,
|
|
lineColor: '#33D778',
|
|
lineStyle: 0 /* LineStyle.Solid */,
|
|
lineWidth: 3,
|
|
lineType: 0 /* LineType.Simple */,
|
|
lineVisible: true,
|
|
crosshairMarkerVisible: true,
|
|
crosshairMarkerRadius: 4,
|
|
crosshairMarkerBorderColor: '',
|
|
crosshairMarkerBorderWidth: 2,
|
|
crosshairMarkerBackgroundColor: '',
|
|
lastPriceAnimation: 0 /* LastPriceAnimationMode.Disabled */,
|
|
pointMarkersVisible: false,
|
|
};
|
|
const createPaneView$3 = (series, model) => new SeriesAreaPaneView(series, model);
|
|
const createSeries$3 = () => {
|
|
const definition = {
|
|
type: 'Area',
|
|
isBuiltIn: true,
|
|
defaultOptions: areaStyleDefaults,
|
|
/**
|
|
* @internal
|
|
*/
|
|
_internal_createPaneView: createPaneView$3,
|
|
};
|
|
return definition;
|
|
};
|
|
const areaSeries = createSeries$3();
|
|
|
|
function optimalBarWidth(barSpacing, pixelRatio) {
|
|
return Math.floor(barSpacing * 0.3 * pixelRatio);
|
|
}
|
|
function optimalCandlestickWidth(barSpacing, pixelRatio) {
|
|
const barSpacingSpecialCaseFrom = 2.5;
|
|
const barSpacingSpecialCaseTo = 4;
|
|
const barSpacingSpecialCaseCoeff = 3;
|
|
if (barSpacing >= barSpacingSpecialCaseFrom && barSpacing <= barSpacingSpecialCaseTo) {
|
|
return Math.floor(barSpacingSpecialCaseCoeff * pixelRatio);
|
|
}
|
|
// coeff should be 1 on small barspacing and go to 0.8 while groing bar spacing
|
|
const barSpacingReducingCoeff = 0.2;
|
|
const coeff = 1 - barSpacingReducingCoeff * Math.atan(Math.max(barSpacingSpecialCaseTo, barSpacing) - barSpacingSpecialCaseTo) / (Math.PI * 0.5);
|
|
const res = Math.floor(barSpacing * coeff * pixelRatio);
|
|
const scaledBarSpacing = Math.floor(barSpacing * pixelRatio);
|
|
const optimal = Math.min(res, scaledBarSpacing);
|
|
return Math.max(Math.floor(pixelRatio), optimal);
|
|
}
|
|
|
|
class PaneRendererBars extends BitmapCoordinatesPaneRenderer {
|
|
constructor() {
|
|
super(...arguments);
|
|
this._private__data = null;
|
|
this._private__barWidth = 0;
|
|
this._private__barLineWidth = 0;
|
|
}
|
|
_internal_setData(data) {
|
|
this._private__data = data;
|
|
}
|
|
// eslint-disable-next-line complexity
|
|
_internal__drawImpl({ context: ctx, horizontalPixelRatio, verticalPixelRatio }) {
|
|
if (this._private__data === null || this._private__data._internal_bars.length === 0 || this._private__data._internal_visibleRange === null) {
|
|
return;
|
|
}
|
|
this._private__barWidth = this._private__calcBarWidth(horizontalPixelRatio);
|
|
// grid and crosshair have line width = Math.floor(pixelRatio)
|
|
// if this value is odd, we have to make bars' width odd
|
|
// if this value is even, we have to make bars' width even
|
|
// in order of keeping crosshair-over-bar drawing symmetric
|
|
if (this._private__barWidth >= 2) {
|
|
const lineWidth = Math.max(1, Math.floor(horizontalPixelRatio));
|
|
if ((lineWidth % 2) !== (this._private__barWidth % 2)) {
|
|
this._private__barWidth--;
|
|
}
|
|
}
|
|
// if scale is compressed, bar could become less than 1 CSS pixel
|
|
this._private__barLineWidth = this._private__data._internal_thinBars ? Math.min(this._private__barWidth, Math.floor(horizontalPixelRatio)) : this._private__barWidth;
|
|
let prevColor = null;
|
|
const drawOpenClose = this._private__barLineWidth <= this._private__barWidth && this._private__data._internal_barSpacing >= Math.floor(1.5 * horizontalPixelRatio);
|
|
for (let i = this._private__data._internal_visibleRange.from; i < this._private__data._internal_visibleRange.to; ++i) {
|
|
const bar = this._private__data._internal_bars[i];
|
|
if (prevColor !== bar._internal_barColor) {
|
|
ctx.fillStyle = bar._internal_barColor;
|
|
prevColor = bar._internal_barColor;
|
|
}
|
|
const bodyWidthHalf = Math.floor(this._private__barLineWidth * 0.5);
|
|
const bodyCenter = Math.round(bar._internal_x * horizontalPixelRatio);
|
|
const bodyLeft = bodyCenter - bodyWidthHalf;
|
|
const bodyWidth = this._private__barLineWidth;
|
|
const bodyRight = bodyLeft + bodyWidth - 1;
|
|
const high = Math.min(bar._internal_highY, bar._internal_lowY);
|
|
const low = Math.max(bar._internal_highY, bar._internal_lowY);
|
|
const bodyTop = Math.round(high * verticalPixelRatio) - bodyWidthHalf;
|
|
const bodyBottom = Math.round(low * verticalPixelRatio) + bodyWidthHalf;
|
|
const bodyHeight = Math.max((bodyBottom - bodyTop), this._private__barLineWidth);
|
|
ctx.fillRect(bodyLeft, bodyTop, bodyWidth, bodyHeight);
|
|
const sideWidth = Math.ceil(this._private__barWidth * 1.5);
|
|
if (drawOpenClose) {
|
|
if (this._private__data._internal_openVisible) {
|
|
const openLeft = bodyCenter - sideWidth;
|
|
let openTop = Math.max(bodyTop, Math.round(bar._internal_openY * verticalPixelRatio) - bodyWidthHalf);
|
|
let openBottom = openTop + bodyWidth - 1;
|
|
if (openBottom > bodyTop + bodyHeight - 1) {
|
|
openBottom = bodyTop + bodyHeight - 1;
|
|
openTop = openBottom - bodyWidth + 1;
|
|
}
|
|
ctx.fillRect(openLeft, openTop, bodyLeft - openLeft, openBottom - openTop + 1);
|
|
}
|
|
const closeRight = bodyCenter + sideWidth;
|
|
let closeTop = Math.max(bodyTop, Math.round(bar._internal_closeY * verticalPixelRatio) - bodyWidthHalf);
|
|
let closeBottom = closeTop + bodyWidth - 1;
|
|
if (closeBottom > bodyTop + bodyHeight - 1) {
|
|
closeBottom = bodyTop + bodyHeight - 1;
|
|
closeTop = closeBottom - bodyWidth + 1;
|
|
}
|
|
ctx.fillRect(bodyRight + 1, closeTop, closeRight - bodyRight, closeBottom - closeTop + 1);
|
|
}
|
|
}
|
|
}
|
|
_private__calcBarWidth(pixelRatio) {
|
|
const limit = Math.floor(pixelRatio);
|
|
return Math.max(limit, Math.floor(optimalBarWidth(ensureNotNull(this._private__data)._internal_barSpacing, pixelRatio)));
|
|
}
|
|
}
|
|
|
|
class BarsPaneViewBase extends SeriesPaneViewBase {
|
|
constructor(series, model) {
|
|
super(series, model, false);
|
|
}
|
|
_internal__hitTestImpl(x, y) {
|
|
return hitTestSeriesRange(this._internal__items, this._internal__itemsVisibleRange, x, y, this._internal__model._internal_timeScale()._internal_barSpacing(), this._internal__series._internal_options().hitTestTolerance, (bar, out) => {
|
|
out[0] = bar._internal_highY;
|
|
out[1] = bar._internal_lowY;
|
|
});
|
|
}
|
|
_internal__convertToCoordinates(priceScale, timeScale, firstValue) {
|
|
timeScale._internal_indexesToCoordinates(this._internal__items, undefinedIfNull(this._internal__itemsVisibleRange));
|
|
priceScale._internal_barPricesToCoordinates(this._internal__items, firstValue, undefinedIfNull(this._internal__itemsVisibleRange));
|
|
}
|
|
_internal__createDefaultItem(time, bar, colorer) {
|
|
return {
|
|
_internal_time: time,
|
|
_internal_open: bar._internal_value[0 /* PlotRowValueIndex.Open */],
|
|
_internal_high: bar._internal_value[1 /* PlotRowValueIndex.High */],
|
|
_internal_low: bar._internal_value[2 /* PlotRowValueIndex.Low */],
|
|
_internal_close: bar._internal_value[3 /* PlotRowValueIndex.Close */],
|
|
_internal_x: NaN,
|
|
_internal_openY: NaN,
|
|
_internal_highY: NaN,
|
|
_internal_lowY: NaN,
|
|
_internal_closeY: NaN,
|
|
};
|
|
}
|
|
_internal__fillRawPoints() {
|
|
const colorer = this._internal__series._internal_barColorer();
|
|
this._internal__items = this._internal__series._internal_conflatedBars()._internal_rows().map((row) => this._internal__createRawItem(row._internal_index, row, colorer));
|
|
}
|
|
}
|
|
|
|
class SeriesBarsPaneView extends BarsPaneViewBase {
|
|
constructor() {
|
|
super(...arguments);
|
|
this._internal__renderer = new PaneRendererBars();
|
|
}
|
|
_internal__createRawItem(time, bar, colorer) {
|
|
return {
|
|
...this._internal__createDefaultItem(time, bar, colorer),
|
|
...colorer._internal_barStyle(time),
|
|
};
|
|
}
|
|
_internal__prepareRendererData() {
|
|
const barStyleProps = this._internal__series._internal_options();
|
|
this._internal__renderer._internal_setData({
|
|
_internal_bars: this._internal__items,
|
|
_internal_barSpacing: this._internal__model._internal_timeScale()._internal_barSpacing(),
|
|
_internal_openVisible: barStyleProps.openVisible,
|
|
_internal_thinBars: barStyleProps.thinBars,
|
|
_internal_visibleRange: this._internal__itemsVisibleRange,
|
|
});
|
|
}
|
|
}
|
|
|
|
const barStyleDefaults = {
|
|
upColor: '#26a69a',
|
|
downColor: '#ef5350',
|
|
openVisible: true,
|
|
thinBars: true,
|
|
};
|
|
const createPaneView$2 = (series, model) => new SeriesBarsPaneView(series, model);
|
|
const createSeries$2 = () => {
|
|
const definition = {
|
|
type: 'Bar',
|
|
isBuiltIn: true,
|
|
defaultOptions: barStyleDefaults,
|
|
/**
|
|
* @internal
|
|
*/
|
|
_internal_createPaneView: createPaneView$2,
|
|
};
|
|
return definition;
|
|
};
|
|
const barSeries = createSeries$2();
|
|
|
|
class PaneRendererCandlesticks extends BitmapCoordinatesPaneRenderer {
|
|
constructor() {
|
|
super(...arguments);
|
|
this._private__data = null;
|
|
// scaled with pixelRatio
|
|
this._private__barWidth = 0;
|
|
}
|
|
_internal_setData(data) {
|
|
this._private__data = data;
|
|
}
|
|
_internal__drawImpl(renderingScope) {
|
|
if (this._private__data === null || this._private__data._internal_bars.length === 0 || this._private__data._internal_visibleRange === null) {
|
|
return;
|
|
}
|
|
const { horizontalPixelRatio } = renderingScope;
|
|
// now we know pixelRatio and we could calculate barWidth effectively
|
|
this._private__barWidth = optimalCandlestickWidth(this._private__data._internal_barSpacing, horizontalPixelRatio);
|
|
// grid and crosshair have line width = Math.floor(pixelRatio)
|
|
// if this value is odd, we have to make candlesticks' width odd
|
|
// if this value is even, we have to make candlesticks' width even
|
|
// in order of keeping crosshair-over-candlesticks drawing symmetric
|
|
if (this._private__barWidth >= 2) {
|
|
const wickWidth = Math.floor(horizontalPixelRatio);
|
|
if ((wickWidth % 2) !== (this._private__barWidth % 2)) {
|
|
this._private__barWidth--;
|
|
}
|
|
}
|
|
const bars = this._private__data._internal_bars;
|
|
if (this._private__data._internal_wickVisible) {
|
|
this._private__drawWicks(renderingScope, bars, this._private__data._internal_visibleRange);
|
|
}
|
|
if (this._private__data._internal_borderVisible) {
|
|
this._private__drawBorder(renderingScope, bars, this._private__data._internal_visibleRange);
|
|
}
|
|
const borderWidth = this._private__calculateBorderWidth(horizontalPixelRatio);
|
|
if (!this._private__data._internal_borderVisible || this._private__barWidth > borderWidth * 2) {
|
|
this._private__drawCandles(renderingScope, bars, this._private__data._internal_visibleRange);
|
|
}
|
|
}
|
|
_private__drawWicks(renderingScope, bars, visibleRange) {
|
|
if (this._private__data === null) {
|
|
return;
|
|
}
|
|
const { context: ctx, horizontalPixelRatio, verticalPixelRatio } = renderingScope;
|
|
let prevWickColor = '';
|
|
let wickWidth = Math.min(Math.floor(horizontalPixelRatio), Math.floor(this._private__data._internal_barSpacing * horizontalPixelRatio));
|
|
wickWidth = Math.max(Math.floor(horizontalPixelRatio), Math.min(wickWidth, this._private__barWidth));
|
|
const wickOffset = Math.floor(wickWidth * 0.5);
|
|
let prevEdge = null;
|
|
for (let i = visibleRange.from; i < visibleRange.to; i++) {
|
|
const bar = bars[i];
|
|
if (bar._internal_barWickColor !== prevWickColor) {
|
|
ctx.fillStyle = bar._internal_barWickColor;
|
|
prevWickColor = bar._internal_barWickColor;
|
|
}
|
|
const top = Math.round(Math.min(bar._internal_openY, bar._internal_closeY) * verticalPixelRatio);
|
|
const bottom = Math.round(Math.max(bar._internal_openY, bar._internal_closeY) * verticalPixelRatio);
|
|
const high = Math.round(bar._internal_highY * verticalPixelRatio);
|
|
const low = Math.round(bar._internal_lowY * verticalPixelRatio);
|
|
const scaledX = Math.round(horizontalPixelRatio * bar._internal_x);
|
|
let left = scaledX - wickOffset;
|
|
const right = left + wickWidth - 1;
|
|
if (prevEdge !== null) {
|
|
left = Math.max(prevEdge + 1, left);
|
|
left = Math.min(left, right);
|
|
}
|
|
const width = right - left + 1;
|
|
ctx.fillRect(left, high, width, top - high);
|
|
ctx.fillRect(left, bottom + 1, width, low - bottom);
|
|
prevEdge = right;
|
|
}
|
|
}
|
|
_private__calculateBorderWidth(pixelRatio) {
|
|
let borderWidth = Math.floor(1 /* Constants.BarBorderWidth */ * pixelRatio);
|
|
if (this._private__barWidth <= 2 * borderWidth) {
|
|
borderWidth = Math.floor((this._private__barWidth - 1) * 0.5);
|
|
}
|
|
const res = Math.max(Math.floor(pixelRatio), borderWidth);
|
|
if (this._private__barWidth <= res * 2) {
|
|
// do not draw bodies, restore original value
|
|
return Math.max(Math.floor(pixelRatio), Math.floor(1 /* Constants.BarBorderWidth */ * pixelRatio));
|
|
}
|
|
return res;
|
|
}
|
|
_private__drawBorder(renderingScope, bars, visibleRange) {
|
|
if (this._private__data === null) {
|
|
return;
|
|
}
|
|
const { context: ctx, horizontalPixelRatio, verticalPixelRatio } = renderingScope;
|
|
let prevBorderColor = '';
|
|
const borderWidth = this._private__calculateBorderWidth(horizontalPixelRatio);
|
|
let prevEdge = null;
|
|
for (let i = visibleRange.from; i < visibleRange.to; i++) {
|
|
const bar = bars[i];
|
|
if (bar._internal_barBorderColor !== prevBorderColor) {
|
|
ctx.fillStyle = bar._internal_barBorderColor;
|
|
prevBorderColor = bar._internal_barBorderColor;
|
|
}
|
|
let left = Math.round(bar._internal_x * horizontalPixelRatio) - Math.floor(this._private__barWidth * 0.5);
|
|
// this is important to calculate right before patching left
|
|
const right = left + this._private__barWidth - 1;
|
|
const top = Math.round(Math.min(bar._internal_openY, bar._internal_closeY) * verticalPixelRatio);
|
|
const bottom = Math.round(Math.max(bar._internal_openY, bar._internal_closeY) * verticalPixelRatio);
|
|
if (prevEdge !== null) {
|
|
left = Math.max(prevEdge + 1, left);
|
|
left = Math.min(left, right);
|
|
}
|
|
if (this._private__data._internal_barSpacing * horizontalPixelRatio > 2 * borderWidth) {
|
|
fillRectInnerBorder(ctx, left, top, right - left + 1, bottom - top + 1, borderWidth);
|
|
}
|
|
else {
|
|
const width = right - left + 1;
|
|
ctx.fillRect(left, top, width, bottom - top + 1);
|
|
}
|
|
prevEdge = right;
|
|
}
|
|
}
|
|
_private__drawCandles(renderingScope, bars, visibleRange) {
|
|
if (this._private__data === null) {
|
|
return;
|
|
}
|
|
const { context: ctx, horizontalPixelRatio, verticalPixelRatio } = renderingScope;
|
|
let prevBarColor = '';
|
|
const borderWidth = this._private__calculateBorderWidth(horizontalPixelRatio);
|
|
for (let i = visibleRange.from; i < visibleRange.to; i++) {
|
|
const bar = bars[i];
|
|
let top = Math.round(Math.min(bar._internal_openY, bar._internal_closeY) * verticalPixelRatio);
|
|
let bottom = Math.round(Math.max(bar._internal_openY, bar._internal_closeY) * verticalPixelRatio);
|
|
let left = Math.round(bar._internal_x * horizontalPixelRatio) - Math.floor(this._private__barWidth * 0.5);
|
|
let right = left + this._private__barWidth - 1;
|
|
if (bar._internal_barColor !== prevBarColor) {
|
|
const barColor = bar._internal_barColor;
|
|
ctx.fillStyle = barColor;
|
|
prevBarColor = barColor;
|
|
}
|
|
if (this._private__data._internal_borderVisible) {
|
|
left += borderWidth;
|
|
top += borderWidth;
|
|
right -= borderWidth;
|
|
bottom -= borderWidth;
|
|
}
|
|
if (top > bottom) {
|
|
continue;
|
|
}
|
|
ctx.fillRect(left, top, right - left + 1, bottom - top + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
class SeriesCandlesticksPaneView extends BarsPaneViewBase {
|
|
constructor() {
|
|
super(...arguments);
|
|
this._internal__renderer = new PaneRendererCandlesticks();
|
|
}
|
|
_internal__createRawItem(time, bar, colorer) {
|
|
return {
|
|
...this._internal__createDefaultItem(time, bar, colorer),
|
|
...colorer._internal_barStyle(time),
|
|
};
|
|
}
|
|
_internal__prepareRendererData() {
|
|
const candlestickStyleProps = this._internal__series._internal_options();
|
|
this._internal__renderer._internal_setData({
|
|
_internal_bars: this._internal__items,
|
|
_internal_barSpacing: this._internal__model._internal_timeScale()._internal_barSpacing(),
|
|
_internal_wickVisible: candlestickStyleProps.wickVisible,
|
|
_internal_borderVisible: candlestickStyleProps.borderVisible,
|
|
_internal_visibleRange: this._internal__itemsVisibleRange,
|
|
});
|
|
}
|
|
}
|
|
|
|
const candlestickStyleDefaults = {
|
|
upColor: '#26a69a',
|
|
downColor: '#ef5350',
|
|
wickVisible: true,
|
|
borderVisible: true,
|
|
borderColor: '#378658',
|
|
borderUpColor: '#26a69a',
|
|
borderDownColor: '#ef5350',
|
|
wickColor: '#737375',
|
|
wickUpColor: '#26a69a',
|
|
wickDownColor: '#ef5350',
|
|
};
|
|
const createPaneView$1 = (series, model) => new SeriesCandlesticksPaneView(series, model);
|
|
const createSeries$1 = () => {
|
|
const definition = {
|
|
type: 'Candlestick',
|
|
isBuiltIn: true,
|
|
defaultOptions: candlestickStyleDefaults,
|
|
/**
|
|
* @internal
|
|
*/
|
|
_internal_createPaneView: createPaneView$1,
|
|
};
|
|
return definition;
|
|
};
|
|
const candlestickSeries = createSeries$1();
|
|
|
|
const showSpacingMinimalBarWidth = 1;
|
|
const alignToMinimalWidthLimit = 4;
|
|
class PaneRendererHistogram extends BitmapCoordinatesPaneRenderer {
|
|
constructor() {
|
|
super(...arguments);
|
|
this._private__data = null;
|
|
this._private__precalculatedCache = [];
|
|
}
|
|
_internal_setData(data) {
|
|
this._private__data = data;
|
|
this._private__precalculatedCache = [];
|
|
}
|
|
_internal__drawImpl({ context: ctx, horizontalPixelRatio, verticalPixelRatio }) {
|
|
if (this._private__data === null || this._private__data._internal_items.length === 0 || this._private__data._internal_visibleRange === null) {
|
|
return;
|
|
}
|
|
if (!this._private__precalculatedCache.length) {
|
|
this._private__fillPrecalculatedCache(horizontalPixelRatio);
|
|
}
|
|
const tickWidth = Math.max(1, Math.floor(verticalPixelRatio));
|
|
const histogramBase = Math.round((this._private__data._internal_histogramBase) * verticalPixelRatio);
|
|
const topHistogramBase = histogramBase - Math.floor(tickWidth / 2);
|
|
const bottomHistogramBase = topHistogramBase + tickWidth;
|
|
for (let i = this._private__data._internal_visibleRange.from; i < this._private__data._internal_visibleRange.to; i++) {
|
|
const item = this._private__data._internal_items[i];
|
|
const current = this._private__precalculatedCache[i - this._private__data._internal_visibleRange.from];
|
|
const y = Math.round(item._internal_y * verticalPixelRatio);
|
|
ctx.fillStyle = item._internal_barColor;
|
|
let top;
|
|
let bottom;
|
|
if (y <= topHistogramBase) {
|
|
top = y;
|
|
bottom = bottomHistogramBase;
|
|
}
|
|
else {
|
|
top = topHistogramBase;
|
|
bottom = y - Math.floor(tickWidth / 2) + tickWidth;
|
|
}
|
|
ctx.fillRect(current._internal_left, top, current._internal_right - current._internal_left + 1, bottom - top);
|
|
}
|
|
}
|
|
// eslint-disable-next-line complexity
|
|
_private__fillPrecalculatedCache(pixelRatio) {
|
|
if (this._private__data === null || this._private__data._internal_items.length === 0 || this._private__data._internal_visibleRange === null) {
|
|
this._private__precalculatedCache = [];
|
|
return;
|
|
}
|
|
const spacing = Math.ceil(this._private__data._internal_barSpacing * pixelRatio) <= showSpacingMinimalBarWidth ? 0 : Math.max(1, Math.floor(pixelRatio));
|
|
const columnWidth = Math.round(this._private__data._internal_barSpacing * pixelRatio) - spacing;
|
|
this._private__precalculatedCache = new Array(this._private__data._internal_visibleRange.to - this._private__data._internal_visibleRange.from);
|
|
for (let i = this._private__data._internal_visibleRange.from; i < this._private__data._internal_visibleRange.to; i++) {
|
|
const item = this._private__data._internal_items[i];
|
|
// force cast to avoid ensureDefined call
|
|
const x = Math.round(item._internal_x * pixelRatio);
|
|
let left;
|
|
let right;
|
|
if (columnWidth % 2) {
|
|
const halfWidth = (columnWidth - 1) / 2;
|
|
left = x - halfWidth;
|
|
right = x + halfWidth;
|
|
}
|
|
else {
|
|
// shift pixel to left
|
|
const halfWidth = columnWidth / 2;
|
|
left = x - halfWidth;
|
|
right = x + halfWidth - 1;
|
|
}
|
|
this._private__precalculatedCache[i - this._private__data._internal_visibleRange.from] = {
|
|
_internal_left: left,
|
|
_internal_right: right,
|
|
_internal_roundedCenter: x,
|
|
_internal_center: (item._internal_x * pixelRatio),
|
|
_internal_time: item._internal_time,
|
|
};
|
|
}
|
|
// correct positions
|
|
for (let i = this._private__data._internal_visibleRange.from + 1; i < this._private__data._internal_visibleRange.to; i++) {
|
|
const current = this._private__precalculatedCache[i - this._private__data._internal_visibleRange.from];
|
|
const prev = this._private__precalculatedCache[i - this._private__data._internal_visibleRange.from - 1];
|
|
if (current._internal_time !== prev._internal_time + 1) {
|
|
continue;
|
|
}
|
|
if (current._internal_left - prev._internal_right !== (spacing + 1)) {
|
|
// have to align
|
|
if (prev._internal_roundedCenter > prev._internal_center) {
|
|
// prev wasshifted to left, so add pixel to right
|
|
prev._internal_right = current._internal_left - spacing - 1;
|
|
}
|
|
else {
|
|
// extend current to left
|
|
current._internal_left = prev._internal_right + spacing + 1;
|
|
}
|
|
}
|
|
}
|
|
let minWidth = Math.ceil(this._private__data._internal_barSpacing * pixelRatio);
|
|
for (let i = this._private__data._internal_visibleRange.from; i < this._private__data._internal_visibleRange.to; i++) {
|
|
const current = this._private__precalculatedCache[i - this._private__data._internal_visibleRange.from];
|
|
// this could happen if barspacing < 1
|
|
if (current._internal_right < current._internal_left) {
|
|
current._internal_right = current._internal_left;
|
|
}
|
|
const width = current._internal_right - current._internal_left + 1;
|
|
minWidth = Math.min(width, minWidth);
|
|
}
|
|
if (spacing > 0 && minWidth < alignToMinimalWidthLimit) {
|
|
for (let i = this._private__data._internal_visibleRange.from; i < this._private__data._internal_visibleRange.to; i++) {
|
|
const current = this._private__precalculatedCache[i - this._private__data._internal_visibleRange.from];
|
|
const width = current._internal_right - current._internal_left + 1;
|
|
if (width > minWidth) {
|
|
if (current._internal_roundedCenter > current._internal_center) {
|
|
current._internal_right -= 1;
|
|
}
|
|
else {
|
|
current._internal_left += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class SeriesHistogramPaneView extends LinePaneViewBase {
|
|
constructor() {
|
|
super(...arguments);
|
|
this._internal__renderer = new PaneRendererHistogram();
|
|
}
|
|
_internal__hitTestImpl(x, y) {
|
|
const histogramBase = this._internal__series._internal_priceScale()._internal_priceToCoordinate(this._internal__series._internal_options().base, ensureNotNull(this._internal__series._internal_firstValue())._internal_value);
|
|
if (histogramBase === null) {
|
|
return null;
|
|
}
|
|
return hitTestSeriesRange(this._internal__items, this._internal__itemsVisibleRange, x, y, this._internal__model._internal_timeScale()._internal_barSpacing(), this._internal__series._internal_options().hitTestTolerance, (item, out) => {
|
|
out[0] = item._internal_y;
|
|
out[1] = histogramBase;
|
|
});
|
|
}
|
|
_internal__createRawItem(time, price, colorer) {
|
|
return {
|
|
...this._internal__createRawItemBase(time, price),
|
|
...colorer._internal_barStyle(time),
|
|
};
|
|
}
|
|
_internal__prepareRendererData() {
|
|
const data = {
|
|
_internal_items: this._internal__items,
|
|
_internal_barSpacing: this._internal__model._internal_timeScale()._internal_barSpacing(),
|
|
_internal_visibleRange: this._internal__itemsVisibleRange,
|
|
_internal_histogramBase: this._internal__series._internal_priceScale()._internal_priceToCoordinate(this._internal__series._internal_options().base, ensureNotNull(this._internal__series._internal_firstValue())._internal_value),
|
|
};
|
|
this._internal__renderer._internal_setData(data);
|
|
}
|
|
}
|
|
|
|
const histogramStyleDefaults = {
|
|
color: '#26a69a',
|
|
base: 0,
|
|
};
|
|
const createPaneView = (series, model) => new SeriesHistogramPaneView(series, model);
|
|
const createSeries = () => {
|
|
const definition = {
|
|
type: 'Histogram',
|
|
isBuiltIn: true,
|
|
defaultOptions: histogramStyleDefaults,
|
|
/**
|
|
* @internal
|
|
*/
|
|
_internal_createPaneView: createPaneView,
|
|
};
|
|
return definition;
|
|
};
|
|
const histogramSeries = createSeries();
|
|
|
|
class PanePrimitiveWrapper {
|
|
constructor(pane, primitive) {
|
|
this._private__pane = pane;
|
|
this._private__primitive = primitive;
|
|
this._private__attach();
|
|
}
|
|
detach() {
|
|
this._private__pane.detachPrimitive(this._private__primitive);
|
|
}
|
|
getPane() {
|
|
return this._private__pane;
|
|
}
|
|
applyOptions(options) {
|
|
this._private__primitive._internal_applyOptions?.(options);
|
|
}
|
|
_private__attach() {
|
|
this._private__pane.attachPrimitive(this._private__primitive);
|
|
}
|
|
}
|
|
|
|
const textWatermarkOptionsDefaults = {
|
|
visible: true,
|
|
horzAlign: 'center',
|
|
vertAlign: 'center',
|
|
lines: [],
|
|
};
|
|
const textWatermarkLineOptionsDefaults = {
|
|
color: 'rgba(0, 0, 0, 0.5)',
|
|
fontSize: 48,
|
|
fontFamily: defaultFontFamily,
|
|
fontStyle: '',
|
|
text: '',
|
|
};
|
|
|
|
class TextWatermarkRenderer {
|
|
constructor(options) {
|
|
this._private__metricsCache = new Map();
|
|
this._private__data = options;
|
|
}
|
|
draw(target) {
|
|
target.useMediaCoordinateSpace((scope) => {
|
|
if (!this._private__data.visible) {
|
|
return;
|
|
}
|
|
const { context: ctx, mediaSize } = scope;
|
|
let textHeight = 0;
|
|
for (const line of this._private__data.lines) {
|
|
if (line.text.length === 0) {
|
|
continue;
|
|
}
|
|
ctx.font = line._internal_font;
|
|
const textWidth = this._private__metrics(ctx, line.text);
|
|
if (textWidth > mediaSize.width) {
|
|
line._internal_zoom = mediaSize.width / textWidth;
|
|
}
|
|
else {
|
|
line._internal_zoom = 1;
|
|
}
|
|
textHeight += line.lineHeight * line._internal_zoom;
|
|
}
|
|
let vertOffset = 0;
|
|
switch (this._private__data.vertAlign) {
|
|
case 'top':
|
|
vertOffset = 0;
|
|
break;
|
|
case 'center':
|
|
vertOffset = Math.max((mediaSize.height - textHeight) / 2, 0);
|
|
break;
|
|
case 'bottom':
|
|
vertOffset = Math.max(mediaSize.height - textHeight, 0);
|
|
break;
|
|
}
|
|
for (const line of this._private__data.lines) {
|
|
ctx.save();
|
|
ctx.fillStyle = line.color;
|
|
let horzOffset = 0;
|
|
switch (this._private__data.horzAlign) {
|
|
case 'left':
|
|
ctx.textAlign = 'left';
|
|
horzOffset = line.lineHeight / 2;
|
|
break;
|
|
case 'center':
|
|
ctx.textAlign = 'center';
|
|
horzOffset = mediaSize.width / 2;
|
|
break;
|
|
case 'right':
|
|
ctx.textAlign = 'right';
|
|
horzOffset = mediaSize.width - 1 - line.lineHeight / 2;
|
|
break;
|
|
}
|
|
ctx.translate(horzOffset, vertOffset);
|
|
ctx.textBaseline = 'top';
|
|
ctx.font = line._internal_font;
|
|
ctx.scale(line._internal_zoom, line._internal_zoom);
|
|
ctx.fillText(line.text, 0, line._internal_vertOffset);
|
|
ctx.restore();
|
|
vertOffset += line.lineHeight * line._internal_zoom;
|
|
}
|
|
});
|
|
}
|
|
_private__metrics(ctx, text) {
|
|
const fontCache = this._private__fontCache(ctx.font);
|
|
let result = fontCache.get(text);
|
|
if (result === undefined) {
|
|
result = ctx.measureText(text).width;
|
|
fontCache.set(text, result);
|
|
}
|
|
return result;
|
|
}
|
|
_private__fontCache(font) {
|
|
let fontCache = this._private__metricsCache.get(font);
|
|
if (fontCache === undefined) {
|
|
fontCache = new Map();
|
|
this._private__metricsCache.set(font, fontCache);
|
|
}
|
|
return fontCache;
|
|
}
|
|
}
|
|
|
|
class TextWatermarkPaneView {
|
|
constructor(options) {
|
|
this._private__options = buildRendererOptions$1(options);
|
|
}
|
|
_internal_update(options) {
|
|
this._private__options = buildRendererOptions$1(options);
|
|
}
|
|
renderer() {
|
|
return new TextWatermarkRenderer(this._private__options);
|
|
}
|
|
}
|
|
function buildRendererLineOptions(lineOption) {
|
|
return {
|
|
...lineOption,
|
|
_internal_font: makeFont(lineOption.fontSize, lineOption.fontFamily, lineOption.fontStyle),
|
|
lineHeight: lineOption.lineHeight || lineOption.fontSize * 1.2,
|
|
_internal_vertOffset: 0,
|
|
_internal_zoom: 0,
|
|
};
|
|
}
|
|
function buildRendererOptions$1(options) {
|
|
return {
|
|
...options,
|
|
lines: options.lines.map(buildRendererLineOptions),
|
|
};
|
|
}
|
|
|
|
function mergeLineOptionsWithDefaults(options) {
|
|
return {
|
|
...textWatermarkLineOptionsDefaults,
|
|
...options,
|
|
};
|
|
}
|
|
function mergeOptionsWithDefaults$2(options) {
|
|
return {
|
|
...textWatermarkOptionsDefaults,
|
|
...options,
|
|
lines: options.lines?.map(mergeLineOptionsWithDefaults) ?? [],
|
|
};
|
|
}
|
|
class TextWatermark {
|
|
constructor(options) {
|
|
this._private__options = mergeOptionsWithDefaults$2(options);
|
|
this._private__paneViews = [new TextWatermarkPaneView(this._private__options)];
|
|
}
|
|
updateAllViews() {
|
|
this._private__paneViews.forEach((pw) => pw._internal_update(this._private__options));
|
|
}
|
|
paneViews() {
|
|
return this._private__paneViews;
|
|
}
|
|
attached({ requestUpdate }) {
|
|
this._internal_requestUpdate = requestUpdate;
|
|
}
|
|
detached() {
|
|
this._internal_requestUpdate = undefined;
|
|
}
|
|
_internal_applyOptions(options) {
|
|
this._private__options = mergeOptionsWithDefaults$2({ ...this._private__options, ...options });
|
|
if (this._internal_requestUpdate) {
|
|
this._internal_requestUpdate();
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Creates an image watermark.
|
|
*
|
|
* @param pane - Target pane.
|
|
* @param options - Watermark options.
|
|
*
|
|
* @returns Image watermark wrapper.
|
|
*
|
|
* @example
|
|
* ```js
|
|
* import { createTextWatermark } from 'lightweight-charts';
|
|
*
|
|
* const firstPane = chart.panes()[0];
|
|
* const textWatermark = createTextWatermark(firstPane, {
|
|
* horzAlign: 'center',
|
|
* vertAlign: 'center',
|
|
* lines: [
|
|
* {
|
|
* text: 'Hello',
|
|
* color: 'rgba(255,0,0,0.5)',
|
|
* fontSize: 100,
|
|
* fontStyle: 'bold',
|
|
* },
|
|
* {
|
|
* text: 'This is a text watermark',
|
|
* color: 'rgba(0,0,255,0.5)',
|
|
* fontSize: 50,
|
|
* fontStyle: 'italic',
|
|
* fontFamily: 'monospace',
|
|
* },
|
|
* ],
|
|
* });
|
|
* // to change options
|
|
* textWatermark.applyOptions({ horzAlign: 'left' });
|
|
* // to remove watermark from the pane
|
|
* textWatermark.detach();
|
|
* ```
|
|
*/
|
|
function createTextWatermark(pane, options) {
|
|
return new PanePrimitiveWrapper(pane, new TextWatermark(options));
|
|
}
|
|
|
|
const imageWatermarkOptionsDefaults = {
|
|
alpha: 1,
|
|
padding: 0,
|
|
};
|
|
|
|
class ImageWatermarkRenderer {
|
|
constructor(data) {
|
|
this._private__data = data;
|
|
}
|
|
draw(target) {
|
|
target.useMediaCoordinateSpace((scope) => {
|
|
const ctx = scope.context;
|
|
const pos = this._private__determinePlacement(this._private__data, scope.mediaSize);
|
|
if (!pos || !this._private__data._internal_imgElement) {
|
|
return;
|
|
}
|
|
ctx.globalAlpha = this._private__data.alpha ?? 1;
|
|
ctx.drawImage(this._private__data._internal_imgElement, pos._internal_x, pos._internal_y, pos._internal_width, pos._internal_height);
|
|
});
|
|
}
|
|
_private__determinePlacement(data, paneSize) {
|
|
const { maxHeight, maxWidth, _internal_imgHeight: imgHeight, _internal_imgWidth: imgWidth, padding } = data;
|
|
const plotCentreX = Math.round(paneSize.width / 2);
|
|
const plotCentreY = Math.round(paneSize.height / 2);
|
|
const paddingSize = padding ?? 0;
|
|
let availableWidth = paneSize.width - 2 * paddingSize;
|
|
let availableHeight = paneSize.height - 2 * paddingSize;
|
|
if (maxHeight) {
|
|
availableHeight = Math.min(availableHeight, maxHeight);
|
|
}
|
|
if (maxWidth) {
|
|
availableWidth = Math.min(availableWidth, maxWidth);
|
|
}
|
|
const scaleX = availableWidth / imgWidth;
|
|
const scaleY = availableHeight / imgHeight;
|
|
const scaleToUse = Math.min(scaleX, scaleY);
|
|
const drawWidth = imgWidth * scaleToUse;
|
|
const drawHeight = imgHeight * scaleToUse;
|
|
const x = plotCentreX - 0.5 * drawWidth;
|
|
const y = plotCentreY - 0.5 * drawHeight;
|
|
return {
|
|
_internal_x: x,
|
|
_internal_y: y,
|
|
_internal_height: drawHeight,
|
|
_internal_width: drawWidth,
|
|
};
|
|
}
|
|
}
|
|
|
|
class ImageWatermarkPaneView {
|
|
constructor(options) {
|
|
this._private__image = null;
|
|
this._private__imageWidth = 0; // don't draw until loaded
|
|
this._private__imageHeight = 0;
|
|
this._private__options = options;
|
|
this._private__rendererOptions = buildRendererOptions(this._private__options, this._private__image, this._private__imageWidth, this._private__imageHeight);
|
|
}
|
|
_internal_stateUpdate(state) {
|
|
if (state._internal_imageWidth !== undefined) {
|
|
this._private__imageWidth = state._internal_imageWidth;
|
|
}
|
|
if (state._internal_imageHeight !== undefined) {
|
|
this._private__imageHeight = state._internal_imageHeight;
|
|
}
|
|
if (state._internal_image !== undefined) {
|
|
this._private__image = state._internal_image;
|
|
}
|
|
this._internal_update();
|
|
}
|
|
_internal_optionsUpdate(options) {
|
|
this._private__options = options;
|
|
this._internal_update();
|
|
}
|
|
zOrder() {
|
|
return 'bottom';
|
|
}
|
|
_internal_update() {
|
|
this._private__rendererOptions = buildRendererOptions(this._private__options, this._private__image, this._private__imageWidth, this._private__imageHeight);
|
|
}
|
|
renderer() {
|
|
return new ImageWatermarkRenderer(this._private__rendererOptions);
|
|
}
|
|
}
|
|
function buildRendererOptions(options, imgElement, imgWidth, imgHeight) {
|
|
return {
|
|
...options,
|
|
_internal_imgElement: imgElement,
|
|
_internal_imgWidth: imgWidth,
|
|
_internal_imgHeight: imgHeight,
|
|
};
|
|
}
|
|
|
|
function mergeOptionsWithDefaults$1(options) {
|
|
return {
|
|
...imageWatermarkOptionsDefaults,
|
|
...options,
|
|
};
|
|
}
|
|
class ImageWatermark {
|
|
constructor(imageUrl, options) {
|
|
this._private__imgElement = null;
|
|
this._private__imageUrl = imageUrl;
|
|
this._private__options = mergeOptionsWithDefaults$1(options);
|
|
this._private__paneViews = [new ImageWatermarkPaneView(this._private__options)];
|
|
}
|
|
updateAllViews() {
|
|
this._private__paneViews.forEach((pw) => pw._internal_update());
|
|
}
|
|
paneViews() {
|
|
return this._private__paneViews;
|
|
}
|
|
attached(attachedParams) {
|
|
const { requestUpdate } = attachedParams;
|
|
this._private__requestUpdate = requestUpdate;
|
|
this._private__imgElement = new Image();
|
|
this._private__imgElement.onload = () => {
|
|
const imageHeight = this._private__imgElement?.naturalHeight ?? 1;
|
|
const imageWidth = this._private__imgElement?.naturalWidth ?? 1;
|
|
this._private__paneViews.forEach((pv) => pv._internal_stateUpdate({
|
|
_internal_imageHeight: imageHeight,
|
|
_internal_imageWidth: imageWidth,
|
|
_internal_image: this._private__imgElement,
|
|
}));
|
|
if (this._private__requestUpdate) {
|
|
this._private__requestUpdate();
|
|
}
|
|
};
|
|
this._private__imgElement.src = this._private__imageUrl;
|
|
}
|
|
detached() {
|
|
this._private__requestUpdate = undefined;
|
|
this._private__imgElement = null;
|
|
}
|
|
_internal_applyOptions(options) {
|
|
this._private__options = mergeOptionsWithDefaults$1({ ...this._private__options, ...options });
|
|
this._private__updateOptions();
|
|
if (this._internal_requestUpdate) {
|
|
this._internal_requestUpdate();
|
|
}
|
|
}
|
|
_internal_requestUpdate() {
|
|
if (this._private__requestUpdate) {
|
|
this._private__requestUpdate();
|
|
}
|
|
}
|
|
_private__updateOptions() {
|
|
this._private__paneViews.forEach((pw) => pw._internal_optionsUpdate(this._private__options));
|
|
}
|
|
}
|
|
/**
|
|
* Creates an image watermark.
|
|
*
|
|
* @param pane - Target pane.
|
|
* @param imageUrl - Image URL.
|
|
* @param options - Watermark options.
|
|
*
|
|
* @returns Image watermark wrapper.
|
|
*
|
|
* @example
|
|
* ```js
|
|
* import { createImageWatermark } from 'lightweight-charts';
|
|
*
|
|
* const firstPane = chart.panes()[0];
|
|
* const imageWatermark = createImageWatermark(firstPane, '/images/my-image.png', {
|
|
* alpha: 0.5,
|
|
* padding: 20,
|
|
* });
|
|
* // to change options
|
|
* imageWatermark.applyOptions({ padding: 10 });
|
|
* // to remove watermark from the pane
|
|
* imageWatermark.detach();
|
|
* ```
|
|
*/
|
|
function createImageWatermark(pane, imageUrl, options) {
|
|
return new PanePrimitiveWrapper(pane, new ImageWatermark(imageUrl, options));
|
|
}
|
|
|
|
class SeriesPrimitiveAdapter {
|
|
constructor(series, primitive) {
|
|
this._internal__series = series;
|
|
this._internal__primitive = primitive;
|
|
this._private__attach();
|
|
}
|
|
detach() {
|
|
this._internal__series.detachPrimitive(this._internal__primitive);
|
|
}
|
|
getSeries() {
|
|
return this._internal__series;
|
|
}
|
|
applyOptions(options) {
|
|
if (this._internal__primitive && this._internal__primitive._internal_applyOptions) {
|
|
this._internal__primitive._internal_applyOptions(options);
|
|
}
|
|
}
|
|
_private__attach() {
|
|
this._internal__series.attachPrimitive(this._internal__primitive);
|
|
}
|
|
}
|
|
|
|
const seriesMarkerOptionsDefaults = {
|
|
autoScale: true,
|
|
zOrder: 'normal',
|
|
};
|
|
|
|
function size(barSpacing, coeff) {
|
|
const result = Math.min(Math.max(barSpacing, 12 /* Constants.MinShapeSize */), 30 /* Constants.MaxShapeSize */) * coeff;
|
|
return ceiledOdd(result);
|
|
}
|
|
function shapeSize(shape, originalSize) {
|
|
switch (shape) {
|
|
case 'arrowDown':
|
|
case 'arrowUp':
|
|
return size(originalSize, 1);
|
|
case 'circle':
|
|
return size(originalSize, 0.8);
|
|
case 'square':
|
|
return size(originalSize, 0.7);
|
|
}
|
|
}
|
|
function calculateShapeHeight(barSpacing) {
|
|
return ceiledEven(size(barSpacing, 1));
|
|
}
|
|
function shapeMargin(barSpacing) {
|
|
return Math.max(size(barSpacing, 0.1), 3 /* Constants.MinShapeMargin */);
|
|
}
|
|
function calculateAdjustedMargin(margin, hasSide, hasInBar) {
|
|
if (hasSide) {
|
|
return margin;
|
|
}
|
|
else if (hasInBar) {
|
|
return Math.ceil(margin / 2);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function drawArrow(up, ctx, coords, size) {
|
|
const arrowSize = shapeSize('arrowUp', size);
|
|
const halfArrowSize = ((arrowSize - 1) / 2) * coords._internal_pixelRatio;
|
|
const baseSize = ceiledOdd(size / 2);
|
|
const halfBaseSize = ((baseSize - 1) / 2) * coords._internal_pixelRatio;
|
|
ctx.beginPath();
|
|
if (up) {
|
|
ctx.moveTo(coords._internal_x - halfArrowSize, coords._internal_y);
|
|
ctx.lineTo(coords._internal_x, coords._internal_y - halfArrowSize);
|
|
ctx.lineTo(coords._internal_x + halfArrowSize, coords._internal_y);
|
|
ctx.lineTo(coords._internal_x + halfBaseSize, coords._internal_y);
|
|
ctx.lineTo(coords._internal_x + halfBaseSize, coords._internal_y + halfArrowSize);
|
|
ctx.lineTo(coords._internal_x - halfBaseSize, coords._internal_y + halfArrowSize);
|
|
ctx.lineTo(coords._internal_x - halfBaseSize, coords._internal_y);
|
|
}
|
|
else {
|
|
ctx.moveTo(coords._internal_x - halfArrowSize, coords._internal_y);
|
|
ctx.lineTo(coords._internal_x, coords._internal_y + halfArrowSize);
|
|
ctx.lineTo(coords._internal_x + halfArrowSize, coords._internal_y);
|
|
ctx.lineTo(coords._internal_x + halfBaseSize, coords._internal_y);
|
|
ctx.lineTo(coords._internal_x + halfBaseSize, coords._internal_y - halfArrowSize);
|
|
ctx.lineTo(coords._internal_x - halfBaseSize, coords._internal_y - halfArrowSize);
|
|
ctx.lineTo(coords._internal_x - halfBaseSize, coords._internal_y);
|
|
}
|
|
ctx.fill();
|
|
}
|
|
function hitTestArrow(up, centerX, centerY, size, x, y) {
|
|
const arrowSize = shapeSize('arrowUp', size);
|
|
const halfArrowSize = (arrowSize - 1) / 2;
|
|
const baseSize = ceiledOdd(size / 2);
|
|
const halfBaseSize = (baseSize - 1) / 2;
|
|
const triangleTolerance = 3;
|
|
const rectTolerance = 2;
|
|
const baseLeft = centerX - halfBaseSize - rectTolerance;
|
|
const baseRight = centerX + halfBaseSize + rectTolerance;
|
|
const baseTop = up ? centerY : centerY - halfArrowSize;
|
|
const baseBottom = up ? centerY + halfArrowSize : centerY;
|
|
if (x >= baseLeft && x <= baseRight &&
|
|
y >= baseTop - rectTolerance && y <= baseBottom + rectTolerance) {
|
|
return true;
|
|
}
|
|
const isInTriangleBounds = () => {
|
|
const headLeft = centerX - halfArrowSize - triangleTolerance;
|
|
const headRight = centerX + halfArrowSize + triangleTolerance;
|
|
const headTop = up ? centerY - halfArrowSize - triangleTolerance : centerY;
|
|
const headBottom = up ? centerY : centerY + halfArrowSize + triangleTolerance;
|
|
if (x < headLeft || x > headRight ||
|
|
y < headTop || y > headBottom) {
|
|
return false;
|
|
}
|
|
const dx = Math.abs(x - centerX);
|
|
const dy = up
|
|
? Math.abs(y - centerY) // up arrow
|
|
: Math.abs(y - centerY); // down arrow
|
|
return dy + triangleTolerance >= dx / 2;
|
|
};
|
|
return isInTriangleBounds();
|
|
}
|
|
|
|
function drawCircle(ctx, coords, size) {
|
|
const circleSize = shapeSize('circle', size);
|
|
const halfSize = (circleSize - 1) / 2;
|
|
ctx.beginPath();
|
|
ctx.arc(coords._internal_x, coords._internal_y, halfSize * coords._internal_pixelRatio, 0, 2 * Math.PI, false);
|
|
ctx.fill();
|
|
}
|
|
function hitTestCircle(centerX, centerY, size, x, y) {
|
|
const circleSize = shapeSize('circle', size);
|
|
const tolerance = 2 + circleSize / 2;
|
|
const xOffset = centerX - x;
|
|
const yOffset = centerY - y;
|
|
const dist = Math.sqrt(xOffset * xOffset + yOffset * yOffset);
|
|
return dist <= tolerance;
|
|
}
|
|
|
|
function drawSquare(ctx, coords, size) {
|
|
const squareSize = shapeSize('square', size);
|
|
const halfSize = ((squareSize - 1) * coords._internal_pixelRatio) / 2;
|
|
const left = coords._internal_x - halfSize;
|
|
const top = coords._internal_y - halfSize;
|
|
ctx.fillRect(left, top, squareSize * coords._internal_pixelRatio, squareSize * coords._internal_pixelRatio);
|
|
}
|
|
function hitTestSquare(centerX, centerY, size, x, y) {
|
|
const squareSize = shapeSize('square', size);
|
|
const halfSize = (squareSize - 1) / 2;
|
|
const left = centerX - halfSize;
|
|
const top = centerY - halfSize;
|
|
return x >= left && x <= left + squareSize &&
|
|
y >= top && y <= top + squareSize;
|
|
}
|
|
|
|
function drawText(ctx, text, x, y, horizontalPixelRatio, verticalPixelRatio) {
|
|
ctx.save();
|
|
ctx.scale(horizontalPixelRatio, verticalPixelRatio);
|
|
ctx.fillText(text, x, y);
|
|
ctx.restore();
|
|
}
|
|
function hitTestText(textX, textY, textWidth, textHeight, x, y) {
|
|
const halfHeight = textHeight / 2;
|
|
return x >= textX && x <= textX + textWidth &&
|
|
y >= textY - halfHeight && y <= textY + halfHeight;
|
|
}
|
|
|
|
class SeriesMarkersRenderer {
|
|
constructor() {
|
|
this._private__data = null;
|
|
this._private__textWidthCache = new TextWidthCache();
|
|
this._private__fontSize = -1;
|
|
this._private__fontFamily = '';
|
|
this._private__font = '';
|
|
this._private__zOrder = 'normal';
|
|
}
|
|
_internal_setData(data) {
|
|
this._private__data = data;
|
|
}
|
|
_internal_setParams(fontSize, fontFamily, zOrder) {
|
|
if (this._private__fontSize !== fontSize || this._private__fontFamily !== fontFamily) {
|
|
this._private__fontSize = fontSize;
|
|
this._private__fontFamily = fontFamily;
|
|
this._private__font = makeFont(fontSize, fontFamily);
|
|
this._private__textWidthCache._internal_reset();
|
|
}
|
|
this._private__zOrder = zOrder;
|
|
}
|
|
_internal_hitTest(x, y) {
|
|
if (this._private__data === null || this._private__data._internal_visibleRange === null) {
|
|
return null;
|
|
}
|
|
for (let i = this._private__data._internal_visibleRange.from; i < this._private__data._internal_visibleRange.to; i++) {
|
|
const item = this._private__data._internal_items[i];
|
|
if (item && hitTestItem(item, x, y)) {
|
|
return {
|
|
zOrder: 'normal',
|
|
externalId: item._internal_externalId ?? '',
|
|
itemType: 'marker',
|
|
};
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
draw(target) {
|
|
if (this._private__zOrder === 'aboveSeries') {
|
|
return;
|
|
}
|
|
target.useBitmapCoordinateSpace((scope) => {
|
|
this._internal__drawImpl(scope);
|
|
});
|
|
}
|
|
drawBackground(target) {
|
|
if (this._private__zOrder !== 'aboveSeries') {
|
|
return;
|
|
}
|
|
target.useBitmapCoordinateSpace((scope) => {
|
|
this._internal__drawImpl(scope);
|
|
});
|
|
}
|
|
_internal__drawImpl({ context: ctx, horizontalPixelRatio, verticalPixelRatio }) {
|
|
if (this._private__data === null || this._private__data._internal_visibleRange === null) {
|
|
return;
|
|
}
|
|
ctx.textBaseline = 'middle';
|
|
ctx.font = this._private__font;
|
|
for (let index = this._private__data._internal_visibleRange.from; index < this._private__data._internal_visibleRange.to; index++) {
|
|
const item = this._private__data._internal_items[index];
|
|
if (item._internal_text !== undefined) {
|
|
item._internal_text._internal_width = this._private__textWidthCache._internal_measureText(ctx, item._internal_text._internal_content);
|
|
item._internal_text._internal_height = this._private__fontSize;
|
|
item._internal_text._internal_x = item._internal_x - item._internal_text._internal_width / 2;
|
|
}
|
|
drawItem(item, ctx, horizontalPixelRatio, verticalPixelRatio);
|
|
}
|
|
}
|
|
}
|
|
function bitmapShapeItemCoordinates(item, horizontalPixelRatio, verticalPixelRatio) {
|
|
const tickWidth = Math.max(1, Math.floor(horizontalPixelRatio));
|
|
const correction = (tickWidth % 2) / 2;
|
|
return {
|
|
_internal_x: Math.round(item._internal_x * horizontalPixelRatio) + correction,
|
|
_internal_y: item._internal_y * verticalPixelRatio,
|
|
_internal_pixelRatio: horizontalPixelRatio,
|
|
};
|
|
}
|
|
function drawItem(item, ctx, horizontalPixelRatio, verticalPixelRatio) {
|
|
ctx.fillStyle = item._internal_color;
|
|
if (item._internal_text !== undefined) {
|
|
drawText(ctx, item._internal_text._internal_content, item._internal_text._internal_x, item._internal_text._internal_y, horizontalPixelRatio, verticalPixelRatio);
|
|
}
|
|
drawShape(item, ctx, bitmapShapeItemCoordinates(item, horizontalPixelRatio, verticalPixelRatio));
|
|
}
|
|
function drawShape(item, ctx, coordinates) {
|
|
if (item._internal_size === 0) {
|
|
return;
|
|
}
|
|
switch (item._internal_shape) {
|
|
case 'arrowDown':
|
|
drawArrow(false, ctx, coordinates, item._internal_size);
|
|
return;
|
|
case 'arrowUp':
|
|
drawArrow(true, ctx, coordinates, item._internal_size);
|
|
return;
|
|
case 'circle':
|
|
drawCircle(ctx, coordinates, item._internal_size);
|
|
return;
|
|
case 'square':
|
|
drawSquare(ctx, coordinates, item._internal_size);
|
|
return;
|
|
}
|
|
ensureNever(item._internal_shape);
|
|
}
|
|
function hitTestItem(item, x, y) {
|
|
if (item._internal_text !== undefined && hitTestText(item._internal_text._internal_x, item._internal_text._internal_y, item._internal_text._internal_width, item._internal_text._internal_height, x, y)) {
|
|
return true;
|
|
}
|
|
return hitTestShape(item, x, y);
|
|
}
|
|
function hitTestShape(item, x, y) {
|
|
if (item._internal_size === 0) {
|
|
return false;
|
|
}
|
|
switch (item._internal_shape) {
|
|
case 'arrowDown':
|
|
return hitTestArrow(true, item._internal_x, item._internal_y, item._internal_size, x, y);
|
|
case 'arrowUp':
|
|
return hitTestArrow(false, item._internal_x, item._internal_y, item._internal_size, x, y);
|
|
case 'circle':
|
|
return hitTestCircle(item._internal_x, item._internal_y, item._internal_size, x, y);
|
|
case 'square':
|
|
return hitTestSquare(item._internal_x, item._internal_y, item._internal_size, x, y);
|
|
}
|
|
}
|
|
|
|
function isPriceMarker(position) {
|
|
return position === 'atPriceTop' || position === 'atPriceBottom' || position === 'atPriceMiddle';
|
|
}
|
|
function getPrice(seriesData, marker, isInverted) {
|
|
if (isPriceMarker(marker.position) && marker.price !== undefined) {
|
|
return marker.price;
|
|
}
|
|
if (isValueData(seriesData)) {
|
|
return seriesData.value;
|
|
}
|
|
if (isOhlcData(seriesData)) {
|
|
if (marker.position === 'inBar') {
|
|
return seriesData.close;
|
|
}
|
|
if (marker.position === 'aboveBar') {
|
|
if (!isInverted) {
|
|
return seriesData.high;
|
|
}
|
|
return seriesData.low;
|
|
}
|
|
if (marker.position === 'belowBar') {
|
|
if (!isInverted) {
|
|
return seriesData.low;
|
|
}
|
|
return seriesData.high;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
// eslint-disable-next-line max-params, complexity
|
|
function fillSizeAndY(rendererItem, marker, seriesData, offsets, textHeight, shapeMargin, series, chart) {
|
|
const price = getPrice(seriesData, marker, series.priceScale().options().invertScale);
|
|
if (price === undefined) {
|
|
return;
|
|
}
|
|
const ignoreOffset = isPriceMarker(marker.position);
|
|
const timeScale = chart.timeScale();
|
|
const sizeMultiplier = isNumber(marker.size) ? Math.max(marker.size, 0) : 1;
|
|
const shapeSize = calculateShapeHeight(timeScale.options().barSpacing) * sizeMultiplier;
|
|
const halfSize = shapeSize / 2;
|
|
rendererItem._internal_size = shapeSize;
|
|
const position = marker.position;
|
|
switch (position) {
|
|
case 'inBar':
|
|
case 'atPriceMiddle': {
|
|
rendererItem._internal_y = ensureNotNull(series.priceToCoordinate(price));
|
|
if (rendererItem._internal_text !== undefined) {
|
|
rendererItem._internal_text._internal_y = rendererItem._internal_y + halfSize + shapeMargin + textHeight * (0.5 + 0.1 /* Constants.TextMargin */);
|
|
}
|
|
return;
|
|
}
|
|
case 'aboveBar':
|
|
case 'atPriceTop': {
|
|
const offset = ignoreOffset ? 0 : offsets._internal_aboveBar;
|
|
rendererItem._internal_y = (ensureNotNull(series.priceToCoordinate(price)) - halfSize - offset);
|
|
if (rendererItem._internal_text !== undefined) {
|
|
rendererItem._internal_text._internal_y = rendererItem._internal_y - halfSize - textHeight * (0.5 + 0.1 /* Constants.TextMargin */);
|
|
offsets._internal_aboveBar += textHeight * (1 + 2 * 0.1 /* Constants.TextMargin */);
|
|
}
|
|
if (!ignoreOffset) {
|
|
offsets._internal_aboveBar += shapeSize + shapeMargin;
|
|
}
|
|
return;
|
|
}
|
|
case 'belowBar':
|
|
case 'atPriceBottom': {
|
|
const offset = ignoreOffset ? 0 : offsets._internal_belowBar;
|
|
rendererItem._internal_y = (ensureNotNull(series.priceToCoordinate(price)) + halfSize + offset);
|
|
if (rendererItem._internal_text !== undefined) {
|
|
rendererItem._internal_text._internal_y = (rendererItem._internal_y + halfSize + shapeMargin + textHeight * (0.5 + 0.1 /* Constants.TextMargin */));
|
|
offsets._internal_belowBar += textHeight * (1 + 2 * 0.1 /* Constants.TextMargin */);
|
|
}
|
|
if (!ignoreOffset) {
|
|
offsets._internal_belowBar += shapeSize + shapeMargin;
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
function isValueData(data) {
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
return 'value' in data && typeof data.value === 'number';
|
|
}
|
|
function isOhlcData(data) {
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
return 'open' in data && 'high' in data && 'low' in data && 'close' in data;
|
|
}
|
|
class SeriesMarkersPaneView {
|
|
constructor(series, chart, options) {
|
|
this._private__markers = [];
|
|
this._private__invalidated = true;
|
|
this._private__dataInvalidated = true;
|
|
this._private__renderer = new SeriesMarkersRenderer();
|
|
this._private__series = series;
|
|
this._private__chart = chart;
|
|
this._private__data = {
|
|
_internal_items: [],
|
|
_internal_visibleRange: null,
|
|
};
|
|
this._private__options = options;
|
|
}
|
|
renderer() {
|
|
if (!this._private__series.options().visible) {
|
|
return null;
|
|
}
|
|
if (this._private__invalidated) {
|
|
this._internal__makeValid();
|
|
}
|
|
const layout = this._private__chart.options()['layout'];
|
|
this._private__renderer._internal_setParams(layout.fontSize, layout.fontFamily, this._private__options.zOrder);
|
|
this._private__renderer._internal_setData(this._private__data);
|
|
return this._private__renderer;
|
|
}
|
|
_internal_setMarkers(markers) {
|
|
this._private__markers = markers;
|
|
this._internal_update('data');
|
|
}
|
|
_internal_update(updateType) {
|
|
this._private__invalidated = true;
|
|
if (updateType === 'data') {
|
|
this._private__dataInvalidated = true;
|
|
}
|
|
}
|
|
_internal_updateOptions(options) {
|
|
this._private__invalidated = true;
|
|
this._private__options = options;
|
|
}
|
|
zOrder() {
|
|
return this._private__options.zOrder === 'aboveSeries' ? 'top' : this._private__options.zOrder;
|
|
}
|
|
_internal__makeValid() {
|
|
const timeScale = this._private__chart.timeScale();
|
|
const seriesMarkers = this._private__markers;
|
|
if (this._private__dataInvalidated) {
|
|
this._private__data._internal_items = seriesMarkers.map((marker) => ({
|
|
_internal_time: marker.time,
|
|
_internal_x: 0,
|
|
_internal_y: 0,
|
|
_internal_size: 0,
|
|
_internal_shape: marker.shape,
|
|
_internal_color: marker.color,
|
|
_internal_externalId: marker.id,
|
|
_internal_internalId: marker._internal_internalId,
|
|
_internal_text: undefined,
|
|
}));
|
|
this._private__dataInvalidated = false;
|
|
}
|
|
const layoutOptions = this._private__chart.options()['layout'];
|
|
this._private__data._internal_visibleRange = null;
|
|
const visibleBars = timeScale.getVisibleLogicalRange();
|
|
if (visibleBars === null) {
|
|
return;
|
|
}
|
|
const visibleBarsRange = new RangeImpl(Math.floor(visibleBars.from), Math.ceil(visibleBars.to));
|
|
const firstValue = this._private__series.data()[0];
|
|
if (firstValue === null) {
|
|
return;
|
|
}
|
|
if (this._private__data._internal_items.length === 0) {
|
|
return;
|
|
}
|
|
let prevTimeIndex = NaN;
|
|
const shapeMargin$1 = shapeMargin(timeScale.options().barSpacing);
|
|
const offsets = {
|
|
_internal_aboveBar: shapeMargin$1,
|
|
_internal_belowBar: shapeMargin$1,
|
|
};
|
|
this._private__data._internal_visibleRange = visibleTimedValues(this._private__data._internal_items, visibleBarsRange, true);
|
|
for (let index = this._private__data._internal_visibleRange.from; index < this._private__data._internal_visibleRange.to; index++) {
|
|
const marker = seriesMarkers[index];
|
|
if (marker.time !== prevTimeIndex) {
|
|
// new bar, reset stack counter
|
|
offsets._internal_aboveBar = shapeMargin$1;
|
|
offsets._internal_belowBar = shapeMargin$1;
|
|
prevTimeIndex = marker.time;
|
|
}
|
|
const rendererItem = this._private__data._internal_items[index];
|
|
rendererItem._internal_x = ensureNotNull(timeScale.logicalToCoordinate(marker.time));
|
|
if (marker.text !== undefined && marker.text.length > 0) {
|
|
rendererItem._internal_text = {
|
|
_internal_content: marker.text,
|
|
_internal_x: 0,
|
|
_internal_y: 0,
|
|
_internal_width: 0,
|
|
_internal_height: 0,
|
|
};
|
|
}
|
|
const dataAt = this._private__series.dataByIndex(marker.time, 0 /* MismatchDirection.None */);
|
|
if (dataAt === null) {
|
|
continue;
|
|
}
|
|
fillSizeAndY(rendererItem, marker, dataAt, offsets, layoutOptions.fontSize, shapeMargin$1, this._private__series, this._private__chart);
|
|
}
|
|
this._private__invalidated = false;
|
|
}
|
|
}
|
|
|
|
function mergeOptionsWithDefaults(options) {
|
|
return {
|
|
...seriesMarkerOptionsDefaults,
|
|
...options,
|
|
};
|
|
}
|
|
class SeriesMarkersPrimitive {
|
|
constructor(options) {
|
|
this._private__paneView = null;
|
|
this._private__markers = [];
|
|
this._private__indexedMarkers = [];
|
|
this._private__dataChangedHandler = null;
|
|
this._private__series = null;
|
|
this._private__chart = null;
|
|
this._private__autoScaleMarginsInvalidated = true;
|
|
this._private__autoScaleMargins = null;
|
|
this._private__markersPositions = null;
|
|
this._private__cachedBarSpacing = null;
|
|
this._private__recalculationRequired = true;
|
|
this._private__options = mergeOptionsWithDefaults(options);
|
|
}
|
|
attached(param) {
|
|
this._private__recalculateMarkers();
|
|
this._private__chart = param.chart;
|
|
this._private__series = param.series;
|
|
this._private__paneView = new SeriesMarkersPaneView(this._private__series, ensureNotNull(this._private__chart), this._private__options);
|
|
this._private__requestUpdate = param.requestUpdate;
|
|
this._private__series.subscribeDataChanged((scope) => this._private__onDataChanged(scope));
|
|
this._private__recalculationRequired = true;
|
|
this._internal_requestUpdate();
|
|
}
|
|
_internal_requestUpdate() {
|
|
if (this._private__requestUpdate) {
|
|
this._private__requestUpdate();
|
|
}
|
|
}
|
|
detached() {
|
|
if (this._private__series && this._private__dataChangedHandler) {
|
|
this._private__series.unsubscribeDataChanged(this._private__dataChangedHandler);
|
|
}
|
|
this._private__chart = null;
|
|
this._private__series = null;
|
|
this._private__paneView = null;
|
|
this._private__dataChangedHandler = null;
|
|
}
|
|
_internal_setMarkers(markers) {
|
|
this._private__recalculationRequired = true;
|
|
this._private__markers = markers;
|
|
this._private__recalculateMarkers();
|
|
this._private__autoScaleMarginsInvalidated = true;
|
|
this._private__markersPositions = null;
|
|
this._internal_requestUpdate();
|
|
}
|
|
_internal_markers() {
|
|
return this._private__markers;
|
|
}
|
|
paneViews() {
|
|
return this._private__paneView ? [this._private__paneView] : [];
|
|
}
|
|
updateAllViews() {
|
|
this._private__updateAllViews();
|
|
}
|
|
hitTest(x, y) {
|
|
if (this._private__paneView) {
|
|
return this._private__paneView.renderer()?._internal_hitTest(x, y) ?? null;
|
|
}
|
|
return null;
|
|
}
|
|
autoscaleInfo(startTimePoint, endTimePoint) {
|
|
if (this._private__options.autoScale && this._private__paneView) {
|
|
const margins = this._private__getAutoScaleMargins();
|
|
if (margins) {
|
|
return {
|
|
priceRange: null,
|
|
margins: margins,
|
|
};
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
_internal_applyOptions(options) {
|
|
this._private__options = mergeOptionsWithDefaults({ ...this._private__options, ...options });
|
|
if (this._internal_requestUpdate) {
|
|
this._internal_requestUpdate();
|
|
}
|
|
}
|
|
_private__getAutoScaleMargins() {
|
|
const chart = ensureNotNull(this._private__chart);
|
|
const barSpacing = chart.timeScale().options().barSpacing;
|
|
if (this._private__autoScaleMarginsInvalidated || barSpacing !== this._private__cachedBarSpacing) {
|
|
this._private__cachedBarSpacing = barSpacing;
|
|
if (this._private__markers.length > 0) {
|
|
const shapeMargin$1 = shapeMargin(barSpacing);
|
|
const marginValue = calculateShapeHeight(barSpacing) * 1.5 + shapeMargin$1 * 2;
|
|
const positions = this._private__getMarkerPositions();
|
|
this._private__autoScaleMargins = {
|
|
above: calculateAdjustedMargin(marginValue, positions.aboveBar, positions.inBar),
|
|
below: calculateAdjustedMargin(marginValue, positions.belowBar, positions.inBar),
|
|
};
|
|
}
|
|
else {
|
|
this._private__autoScaleMargins = null;
|
|
}
|
|
this._private__autoScaleMarginsInvalidated = false;
|
|
}
|
|
return this._private__autoScaleMargins;
|
|
}
|
|
_private__getMarkerPositions() {
|
|
if (this._private__markersPositions === null) {
|
|
this._private__markersPositions = this._private__markers.reduce((acc, marker) => {
|
|
if (!acc[marker.position]) {
|
|
acc[marker.position] = true;
|
|
}
|
|
return acc;
|
|
}, {
|
|
inBar: false,
|
|
aboveBar: false,
|
|
belowBar: false,
|
|
atPriceTop: false,
|
|
atPriceBottom: false,
|
|
atPriceMiddle: false,
|
|
});
|
|
}
|
|
return this._private__markersPositions;
|
|
}
|
|
_private__recalculateMarkers() {
|
|
if (!this._private__recalculationRequired || !this._private__chart || !this._private__series) {
|
|
return;
|
|
}
|
|
const timeScale = this._private__chart.timeScale();
|
|
const seriesData = this._private__series?.data();
|
|
if (timeScale.getVisibleLogicalRange() == null || !this._private__series || seriesData.length === 0) {
|
|
this._private__indexedMarkers = [];
|
|
return;
|
|
}
|
|
const firstDataIndex = timeScale.timeToIndex(ensureNotNull(seriesData[0].time), true);
|
|
this._private__indexedMarkers = this._private__markers.map((marker, index) => {
|
|
const timePointIndex = timeScale.timeToIndex(marker.time, true);
|
|
const searchMode = timePointIndex < firstDataIndex ? 1 /* MismatchDirection.NearestRight */ : -1 /* MismatchDirection.NearestLeft */;
|
|
const seriesDataByIndex = ensureNotNull(this._private__series).dataByIndex(timePointIndex, searchMode);
|
|
const finalIndex = timeScale.timeToIndex(ensureNotNull(seriesDataByIndex).time, false);
|
|
// You must explicitly define the types so that the minification build processes the field names correctly
|
|
const baseMarker = {
|
|
time: finalIndex,
|
|
position: marker.position,
|
|
shape: marker.shape,
|
|
color: marker.color,
|
|
id: marker.id,
|
|
_internal_internalId: index,
|
|
text: marker.text,
|
|
size: marker.size,
|
|
price: marker.price,
|
|
_internal_originalTime: marker.time,
|
|
};
|
|
if (marker.position === 'atPriceTop' ||
|
|
marker.position === 'atPriceBottom' ||
|
|
marker.position === 'atPriceMiddle') {
|
|
if (marker.price === undefined) {
|
|
throw new Error(`Price is required for position ${marker.position}`);
|
|
}
|
|
return {
|
|
...baseMarker,
|
|
position: marker.position, // TypeScript knows this is SeriesMarkerPricePosition
|
|
price: marker.price,
|
|
};
|
|
}
|
|
else {
|
|
return {
|
|
...baseMarker,
|
|
position: marker.position, // TypeScript knows this is SeriesMarkerBarPosition
|
|
price: marker.price, // Optional for bar positions
|
|
};
|
|
}
|
|
});
|
|
this._private__recalculationRequired = false;
|
|
}
|
|
_private__updateAllViews(updateType) {
|
|
if (this._private__paneView) {
|
|
this._private__recalculateMarkers();
|
|
this._private__paneView._internal_setMarkers(this._private__indexedMarkers);
|
|
this._private__paneView._internal_updateOptions(this._private__options);
|
|
this._private__paneView._internal_update(updateType);
|
|
}
|
|
}
|
|
_private__onDataChanged(scope) {
|
|
this._private__recalculationRequired = true;
|
|
this._internal_requestUpdate();
|
|
}
|
|
}
|
|
|
|
class SeriesMarkersPrimitiveWrapper extends SeriesPrimitiveAdapter {
|
|
constructor(series, primitive, markers) {
|
|
super(series, primitive);
|
|
if (markers) {
|
|
this.setMarkers(markers);
|
|
}
|
|
}
|
|
setMarkers(markers) {
|
|
this._internal__primitive._internal_setMarkers(markers);
|
|
}
|
|
markers() {
|
|
return this._internal__primitive._internal_markers();
|
|
}
|
|
}
|
|
/**
|
|
* A function to create a series markers primitive.
|
|
*
|
|
* @param series - The series to which the primitive will be attached.
|
|
*
|
|
* @param markers - An array of markers to be displayed on the series.
|
|
*
|
|
* @param options - Options for the series markers plugin.
|
|
*
|
|
* @example
|
|
* ```js
|
|
* import { createSeriesMarkers } from 'lightweight-charts';
|
|
*
|
|
* const seriesMarkers = createSeriesMarkers(
|
|
* series,
|
|
* [
|
|
* {
|
|
* color: 'green',
|
|
* position: 'inBar',
|
|
* shape: 'arrowDown',
|
|
* time: 1556880900,
|
|
* },
|
|
* ]
|
|
* );
|
|
* // and then you can modify the markers
|
|
* // set it to empty array to remove all markers
|
|
* seriesMarkers.setMarkers([]);
|
|
*
|
|
* // `seriesMarkers.markers()` returns current markers
|
|
* ```
|
|
*/
|
|
function createSeriesMarkers(series, markers, options) {
|
|
const wrapper = new SeriesMarkersPrimitiveWrapper(series, new SeriesMarkersPrimitive(options ?? {}));
|
|
if (markers) {
|
|
wrapper.setMarkers(markers);
|
|
}
|
|
return wrapper;
|
|
}
|
|
|
|
class ExpiringMarkerManager {
|
|
constructor(updateCallback) {
|
|
this._private__markers = new Map();
|
|
this._private__updateCallback = updateCallback;
|
|
}
|
|
_internal_setMarker(marker, key, timeout) {
|
|
this._internal_clearMarker(key);
|
|
if (timeout !== undefined) {
|
|
const timeoutId = window.setTimeout(() => {
|
|
this._private__markers.delete(key);
|
|
this._private__triggerUpdate();
|
|
}, timeout);
|
|
const markerWithTimeout = {
|
|
...marker,
|
|
_internal_timeoutId: timeoutId,
|
|
_internal_expiresAt: Date.now() + timeout,
|
|
};
|
|
this._private__markers.set(key, markerWithTimeout);
|
|
}
|
|
else {
|
|
// For markers without timeout, we set timeoutId and expiresAt to undefined
|
|
this._private__markers.set(key, {
|
|
...marker,
|
|
_internal_timeoutId: undefined,
|
|
_internal_expiresAt: undefined,
|
|
});
|
|
}
|
|
this._private__triggerUpdate();
|
|
}
|
|
_internal_clearMarker(key) {
|
|
const marker = this._private__markers.get(key);
|
|
if (marker && marker._internal_timeoutId !== undefined) {
|
|
window.clearTimeout(marker._internal_timeoutId);
|
|
}
|
|
this._private__markers.delete(key);
|
|
this._private__triggerUpdate();
|
|
}
|
|
_internal_clearAllMarkers() {
|
|
for (const [point] of this._private__markers) {
|
|
this._internal_clearMarker(point);
|
|
}
|
|
}
|
|
_internal_getMarkers() {
|
|
const now = Date.now();
|
|
const activeMarkers = [];
|
|
for (const [time, marker] of this._private__markers) {
|
|
if (!marker._internal_expiresAt || marker._internal_expiresAt > now) {
|
|
activeMarkers.push({ time: marker.time, sign: marker.sign, value: marker.value });
|
|
}
|
|
else {
|
|
this._internal_clearMarker(time);
|
|
}
|
|
}
|
|
return activeMarkers;
|
|
}
|
|
_internal_setUpdateCallback(callback) {
|
|
this._private__updateCallback = callback;
|
|
}
|
|
_private__triggerUpdate() {
|
|
if (this._private__updateCallback) {
|
|
this._private__updateCallback();
|
|
}
|
|
}
|
|
}
|
|
|
|
const upDownMarkersPluginOptionDefaults = {
|
|
positiveColor: '#22AB94',
|
|
negativeColor: '#F7525F',
|
|
updateVisibilityDuration: 5000,
|
|
};
|
|
|
|
class MarkersPrimitiveRenderer {
|
|
constructor(data, neutralColor, negativeColor, positiveColor) {
|
|
this._private__data = data;
|
|
this._private__neutralColor = neutralColor;
|
|
this._private__negativeColor = negativeColor;
|
|
this._private__positiveColor = positiveColor;
|
|
}
|
|
draw(target) {
|
|
target.useBitmapCoordinateSpace((scope) => {
|
|
const ctx = scope.context;
|
|
const tickWidth = Math.max(1, Math.floor(scope.horizontalPixelRatio));
|
|
const correction = (tickWidth % 2) / 2;
|
|
const rad = 4 /* Constants.Radius */ * scope.verticalPixelRatio + correction;
|
|
this._private__data.forEach((item) => {
|
|
const centreX = Math.round(item._internal_x * scope.horizontalPixelRatio) + correction;
|
|
ctx.beginPath();
|
|
const color = this._private__getColor(item._internal_sign);
|
|
ctx.fillStyle = color;
|
|
ctx.arc(centreX, item._internal_y * scope.verticalPixelRatio, rad, 0, 2 * Math.PI, false);
|
|
ctx.fill();
|
|
if (item._internal_sign) {
|
|
ctx.strokeStyle = color;
|
|
ctx.lineWidth = Math.floor(2 /* Constants.ArrowLineWidth */ * scope.horizontalPixelRatio);
|
|
ctx.beginPath();
|
|
ctx.moveTo((item._internal_x - 4.7 /* Constants.ArrowSize */) * scope.horizontalPixelRatio + correction, (item._internal_y - 7 /* Constants.ArrowOffset */ * item._internal_sign) *
|
|
scope.verticalPixelRatio);
|
|
ctx.lineTo(item._internal_x * scope.horizontalPixelRatio + correction, (item._internal_y -
|
|
7 /* Constants.ArrowOffset */ * item._internal_sign -
|
|
7 /* Constants.ArrowOffset */ * item._internal_sign * 0.5 /* Constants.VerticalScale */) *
|
|
scope.verticalPixelRatio);
|
|
ctx.lineTo((item._internal_x + 4.7 /* Constants.ArrowSize */) * scope.horizontalPixelRatio + correction, (item._internal_y - 7 /* Constants.ArrowOffset */ * item._internal_sign) *
|
|
scope.verticalPixelRatio);
|
|
ctx.stroke();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
_private__getColor(sign) {
|
|
if (sign === 0) {
|
|
return this._private__neutralColor;
|
|
}
|
|
return sign > 0 ? this._private__positiveColor : this._private__negativeColor;
|
|
}
|
|
}
|
|
|
|
function isAreaStyleOptions(opts, seriesType) {
|
|
return seriesType === 'Area';
|
|
}
|
|
function getNeutralColor(opts, seriesType) {
|
|
if (isAreaStyleOptions(opts, seriesType)) {
|
|
return opts.lineColor;
|
|
}
|
|
return opts.color;
|
|
}
|
|
class MarkersPrimitivePaneView {
|
|
constructor(series, timeScale, options) {
|
|
this._private__data = [];
|
|
this._private__series = series;
|
|
this._private__timeScale = timeScale;
|
|
this._private__options = options;
|
|
}
|
|
_internal_update(markers) {
|
|
this._private__data = markers.map((marker) => {
|
|
const y = this._private__series.priceToCoordinate(marker.value);
|
|
if (y === null) {
|
|
return null;
|
|
}
|
|
const x = ensureNotNull(this._private__timeScale.timeToCoordinate(marker.time));
|
|
return {
|
|
_internal_x: x,
|
|
_internal_y: y,
|
|
_internal_sign: marker.sign,
|
|
};
|
|
})
|
|
.filter(notNull);
|
|
}
|
|
renderer() {
|
|
const options = this._private__series.options();
|
|
const seriesType = this._private__series.seriesType();
|
|
const neutralColor = getNeutralColor(options, seriesType);
|
|
return new MarkersPrimitiveRenderer(this._private__data, neutralColor, this._private__options.negativeColor, this._private__options.positiveColor);
|
|
}
|
|
}
|
|
|
|
function isLineData(item, type) {
|
|
return type === 'Line' || type === 'Area';
|
|
}
|
|
class UpDownMarkersPrimitive {
|
|
constructor(options) {
|
|
this._private__chart = undefined;
|
|
this._private__series = undefined;
|
|
this._private__paneViews = [];
|
|
this._private__horzScaleBehavior = null;
|
|
this._private__managedDataPoints = new Map();
|
|
this._private__markersManager = new ExpiringMarkerManager(() => this._internal_requestUpdate());
|
|
this._private__options = {
|
|
...upDownMarkersPluginOptionDefaults,
|
|
...options,
|
|
};
|
|
}
|
|
_internal_applyOptions(options) {
|
|
this._private__options = {
|
|
...this._private__options,
|
|
...options,
|
|
};
|
|
this._internal_requestUpdate();
|
|
}
|
|
_internal_setMarkers(markers) {
|
|
this._private__markersManager._internal_clearAllMarkers();
|
|
const horzBehaviour = this._private__horzScaleBehavior;
|
|
if (!horzBehaviour) {
|
|
return;
|
|
}
|
|
markers.forEach((marker) => {
|
|
this._private__markersManager._internal_setMarker(marker, horzBehaviour.key(marker.time));
|
|
});
|
|
}
|
|
_internal_markers() {
|
|
return this._private__markersManager._internal_getMarkers();
|
|
}
|
|
_internal_requestUpdate() {
|
|
this._private__requestUpdate?.();
|
|
}
|
|
attached(params) {
|
|
const { chart, series, requestUpdate, horzScaleBehavior, } = params;
|
|
this._private__chart = chart;
|
|
this._private__series = series;
|
|
this._private__horzScaleBehavior = horzScaleBehavior;
|
|
const seriesType = this._private__series.seriesType();
|
|
if (seriesType !== 'Area' && seriesType !== 'Line') {
|
|
throw new Error('UpDownMarkersPrimitive is only supported for Area and Line series types');
|
|
}
|
|
this._private__paneViews = [
|
|
new MarkersPrimitivePaneView(this._private__series, this._private__chart.timeScale(), this._private__options),
|
|
];
|
|
this._private__requestUpdate = requestUpdate;
|
|
this._internal_requestUpdate();
|
|
}
|
|
detached() {
|
|
this._private__chart = undefined;
|
|
this._private__series = undefined;
|
|
this._private__requestUpdate = undefined;
|
|
}
|
|
_internal_chart() {
|
|
return ensureDefined(this._private__chart);
|
|
}
|
|
_internal_series() {
|
|
return ensureDefined(this._private__series);
|
|
}
|
|
updateAllViews() {
|
|
this._private__paneViews.forEach((pw) => pw._internal_update(this._internal_markers()));
|
|
}
|
|
paneViews() {
|
|
return this._private__paneViews;
|
|
}
|
|
_internal_setData(data) {
|
|
if (!this._private__series) {
|
|
throw new Error('Primitive not attached to series');
|
|
}
|
|
const seriesType = this._private__series.seriesType();
|
|
this._private__managedDataPoints.clear();
|
|
const horzBehaviour = this._private__horzScaleBehavior;
|
|
if (horzBehaviour) {
|
|
data.forEach((d) => {
|
|
if (isFulfilledData(d) && isLineData(d, seriesType)) {
|
|
this._private__managedDataPoints.set(horzBehaviour.key(d.time), d.value);
|
|
}
|
|
});
|
|
}
|
|
ensureDefined(this._private__series).setData(data);
|
|
}
|
|
_internal_update(data, historicalUpdate) {
|
|
if (!this._private__series || !this._private__horzScaleBehavior) {
|
|
throw new Error('Primitive not attached to series');
|
|
}
|
|
const seriesType = this._private__series.seriesType();
|
|
const horzKey = this._private__horzScaleBehavior.key(data.time);
|
|
if (isWhitespaceData(data)) {
|
|
this._private__managedDataPoints.delete(horzKey);
|
|
}
|
|
if (isFulfilledData(data) && isLineData(data, seriesType)) {
|
|
const existingPrice = this._private__managedDataPoints.get(horzKey);
|
|
if (existingPrice) {
|
|
this._private__markersManager._internal_setMarker({
|
|
time: data.time,
|
|
value: data.value,
|
|
sign: getSign(data.value, existingPrice),
|
|
}, horzKey, this._private__options.updateVisibilityDuration);
|
|
}
|
|
}
|
|
ensureDefined(this._private__series).update(data, historicalUpdate);
|
|
}
|
|
_internal_clearMarkers() {
|
|
this._private__markersManager._internal_clearAllMarkers();
|
|
}
|
|
}
|
|
function getSign(newValue, oldValue) {
|
|
if (newValue === oldValue) {
|
|
return 0;
|
|
}
|
|
return newValue - oldValue > 0 ? 1 : -1;
|
|
}
|
|
|
|
class SeriesUpDownMarkerPrimitiveWrapper extends SeriesPrimitiveAdapter {
|
|
setData(data) {
|
|
return this._internal__primitive._internal_setData(data);
|
|
}
|
|
update(data, historicalUpdate) {
|
|
return this._internal__primitive._internal_update(data, historicalUpdate);
|
|
}
|
|
markers() {
|
|
return this._internal__primitive._internal_markers();
|
|
}
|
|
setMarkers(markers) {
|
|
return this._internal__primitive._internal_setMarkers(markers);
|
|
}
|
|
clearMarkers() {
|
|
return this._internal__primitive._internal_clearMarkers();
|
|
}
|
|
}
|
|
/**
|
|
* Creates and attaches the Series Up Down Markers Plugin.
|
|
*
|
|
* @param series - Series to which attach the Up Down Markers Plugin
|
|
* @param options - options for the Up Down Markers Plugin
|
|
*
|
|
* @returns Api for Series Up Down Marker Plugin. {@link ISeriesUpDownMarkerPluginApi}
|
|
*
|
|
* @example
|
|
* ```js
|
|
* import { createUpDownMarkers, createChart, LineSeries } from 'lightweight-charts';
|
|
*
|
|
* const chart = createChart('container');
|
|
* const lineSeries = chart.addSeries(LineSeries);
|
|
* const upDownMarkers = createUpDownMarkers(lineSeries, {
|
|
* positiveColor: '#22AB94',
|
|
* negativeColor: '#F7525F',
|
|
* updateVisibilityDuration: 5000,
|
|
* });
|
|
* // to add some data
|
|
* upDownMarkers.setData(
|
|
* [
|
|
* { time: '2020-02-02', value: 12.34 },
|
|
* //... more line series data
|
|
* ]
|
|
* );
|
|
* // ... Update some values
|
|
* upDownMarkers.update({ time: '2020-02-02', value: 13.54 }, true);
|
|
* // to remove plugin from the series
|
|
* upDownMarkers.detach();
|
|
* ```
|
|
*/
|
|
function createUpDownMarkers(series, options = {}) {
|
|
const wrapper = new SeriesUpDownMarkerPrimitiveWrapper(series, new UpDownMarkersPrimitive(options));
|
|
return wrapper;
|
|
}
|
|
|
|
/// <reference types="_build-time-constants" />
|
|
const customSeriesDefaultOptions = {
|
|
...seriesOptionsDefaults,
|
|
...customStyleDefaults$1,
|
|
};
|
|
/**
|
|
* Returns the current version as a string. For example `'3.3.0'`.
|
|
*/
|
|
function version() {
|
|
return "5.2.0";
|
|
}
|
|
|
|
var LightweightChartsModule = /*#__PURE__*/Object.freeze({
|
|
__proto__: null,
|
|
AreaSeries: areaSeries,
|
|
BarSeries: barSeries,
|
|
BaselineSeries: baselineSeries,
|
|
CandlestickSeries: candlestickSeries,
|
|
get ColorType () { return ColorType; },
|
|
get CrosshairMode () { return CrosshairMode; },
|
|
HistogramSeries: histogramSeries,
|
|
get LastPriceAnimationMode () { return LastPriceAnimationMode; },
|
|
LineSeries: lineSeries,
|
|
get LineStyle () { return LineStyle; },
|
|
get LineType () { return LineType; },
|
|
get MismatchDirection () { return MismatchDirection; },
|
|
get PriceLineSource () { return PriceLineSource; },
|
|
get PriceScaleMode () { return PriceScaleMode; },
|
|
get TickMarkType () { return TickMarkType; },
|
|
get TrackingModeExitMode () { return TrackingModeExitMode; },
|
|
createChart: createChart,
|
|
createChartEx: createChartEx,
|
|
createImageWatermark: createImageWatermark,
|
|
createOptionsChart: createOptionsChart,
|
|
createSeriesMarkers: createSeriesMarkers,
|
|
createTextWatermark: createTextWatermark,
|
|
createUpDownMarkers: createUpDownMarkers,
|
|
createYieldCurveChart: createYieldCurveChart,
|
|
customSeriesDefaultOptions: customSeriesDefaultOptions,
|
|
defaultHorzScaleBehavior: defaultHorzScaleBehavior,
|
|
isBusinessDay: isBusinessDay,
|
|
isUTCTimestamp: isUTCTimestamp,
|
|
version: version
|
|
});
|
|
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
// put all exports from package to window.LightweightCharts object
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-member-access
|
|
window.LightweightCharts = LightweightChartsModule;
|
|
|
|
})();
|