lightweight-charts
Version:
Performant financial charts built with HTML5 canvas
1,183 lines (1,164 loc) • 673 kB
JavaScript
/*!
* @license
* TradingView Lightweight Charts™ v5.0.6
* Copyright (c) 2025 TradingView, Inc.
* Licensed under Apache License 2.0 https://www.apache.org/licenses/LICENSE-2.0
*/
const customStyleDefaults$1 = {
color: '#2196f3',
};
const seriesOptionsDefaults = {
title: '',
visible: true,
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 setLineStyle(ctx, style) {
const dashPatterns = {
[0 /* LineStyle.Solid */]: [],
[1 /* LineStyle.Dotted */]: [ctx.lineWidth, ctx.lineWidth],
[2 /* LineStyle.Dashed */]: [2 * ctx.lineWidth, 2 * ctx.lineWidth],
[3 /* LineStyle.LargeDashed */]: [6 * ctx.lineWidth, 6 * ctx.lineWidth],
[4 /* LineStyle.SparseDotted */]: [ctx.lineWidth, 4 * ctx.lineWidth],
};
const dashPattern = dashPatterns[style];
ctx.setLineDash(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_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 || 0;
}
_internal_setFixedCoordinate(value) {
this._private__commonRendererData._internal_fixedCoordinate = value;
}
_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_indexToTime