@santiment-network/chart
Version:
Santiment Charts
1,365 lines (1,344 loc) • 682 kB
JavaScript
/*!
* @license
* TradingView Lightweight Charts™ v1.0.22
* Copyright (c) 2026 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,
opacity: 1,
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._listeners = [];
}
subscribe(callback, linkedObject, singleshot) {
const listener = {
callback,
linkedObject,
singleshot: singleshot === true,
};
this._listeners.push(listener);
}
unsubscribe(callback) {
const index = this._listeners.findIndex((listener) => callback === listener.callback);
if (index > -1) {
this._listeners.splice(index, 1);
}
}
unsubscribeAll(linkedObject) {
this._listeners = this._listeners.filter((listener) => listener.linkedObject !== linkedObject);
}
fire(param1, param2, param3) {
const listenersSnapshot = [...this._listeners];
this._listeners = this._listeners.filter((listener) => !listener.singleshot);
listenersSnapshot.forEach((listener) => listener.callback(param1, param2, param3));
}
hasListeners() {
return this._listeners.length > 0;
}
destroy() {
this._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._rendererOptions = {
borderSize: 1 /* RendererConstants.BorderSize */,
tickLength: 5 /* RendererConstants.TickLength */,
fontSize: NaN,
font: '',
fontFamily: '',
color: '',
paneBackgroundColor: '',
paddingBottom: 0,
paddingInner: 0,
paddingOuter: 0,
paddingTop: 0,
baselineOffset: 0,
};
this._chartModel = chartModel;
}
options() {
const rendererOptions = this._rendererOptions;
const currentFontSize = this._fontSize();
const currentFontFamily = this._fontFamily();
if (rendererOptions.fontSize !== currentFontSize || rendererOptions.fontFamily !== currentFontFamily) {
rendererOptions.fontSize = currentFontSize;
rendererOptions.fontFamily = currentFontFamily;
rendererOptions.font = makeFont(currentFontSize, currentFontFamily);
rendererOptions.paddingTop = 2.5 / 12 * currentFontSize; // 2.5 px for 12px font
rendererOptions.paddingBottom = rendererOptions.paddingTop;
rendererOptions.paddingInner = currentFontSize / 12 * rendererOptions.tickLength;
rendererOptions.paddingOuter = currentFontSize / 12 * rendererOptions.tickLength;
rendererOptions.baselineOffset = 0;
}
rendererOptions.color = this._textColor();
rendererOptions.paneBackgroundColor = this._paneBackgroundColor();
return this._rendererOptions;
}
_textColor() {
return this._chartModel.options()['layout'].textColor;
}
_paneBackgroundColor() {
return this._chartModel.backgroundTopColor();
}
_fontSize() {
return this._chartModel.options()['layout'].fontSize;
}
_fontFamily() {
return this._chartModel.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._rgbaCache = new Map();
this._customParsers = customParsers;
if (initialCache) {
this._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.
*/
applyAlpha(color, alpha) {
// special case optimization
if (color === 'transparent') {
return color;
}
const originRgba = this._parseColor(color);
const originAlpha = originRgba[3];
return `rgba(${originRgba[0]}, ${originRgba[1]}, ${originRgba[2]}, ${alpha * originAlpha})`;
}
generateContrastColors(background) {
const rgba = this._parseColor(background);
return {
background: `rgb(${rgba[0]}, ${rgba[1]}, ${rgba[2]})`, // no alpha
foreground: rgbaToGrayscale(rgba) > 160 ? 'black' : 'white',
};
}
colorStringToGrayscale(background) {
return rgbaToGrayscale(this._parseColor(background));
}
gradientColorAtPercent(topColor, bottomColor, percent) {
const [topR, topG, topB, topA] = this._parseColor(topColor);
const [bottomR, bottomG, bottomB, bottomA] = this._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]})`;
}
_parseColor(color) {
const cached = this._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._customParsers.length) {
for (const parser of this._customParsers) {
const result = parser(color);
if (result) {
this._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._rgbaCache.set(color, rgba);
return rgba;
}
}
class CompositeRenderer {
constructor() {
this._renderers = [];
}
setRenderers(renderers) {
this._renderers = renderers;
}
draw(target, isHovered, hitTestData) {
this._renderers.forEach((r) => {
r.draw(target, isHovered, hitTestData);
});
}
}
class BitmapCoordinatesPaneRenderer {
draw(target, isHovered, hitTestData) {
target.useBitmapCoordinateSpace((scope) => this._drawImpl(scope, isHovered, hitTestData));
}
}
class PaneRendererMarks extends BitmapCoordinatesPaneRenderer {
constructor() {
super(...arguments);
this._data = null;
}
setData(data) {
this._data = data;
}
_drawImpl({ context: ctx, horizontalPixelRatio, verticalPixelRatio }) {
if (this._data === null || this._data.visibleRange === null) {
return;
}
const visibleRange = this._data.visibleRange;
const data = this._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.items[i];
const centerX = Math.round(point.x * horizontalPixelRatio) + correction; // correct x coordinate only
const centerY = point.y * verticalPixelRatio;
const radius = radiusMedia * verticalPixelRatio + correction;
ctx.moveTo(centerX, centerY);
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
}
ctx.fill();
};
if (data.lineWidth > 0) {
ctx.fillStyle = data.backColor;
draw(data.radius + data.lineWidth);
}
ctx.fillStyle = data.lineColor;
draw(data.radius);
}
}
function createEmptyMarkerData() {
return {
items: [{
x: 0,
y: 0,
time: 0,
price: 0,
}],
lineColor: '',
backColor: '',
radius: 0,
lineWidth: 0,
visibleRange: null,
};
}
const rangeForSinglePoint = { from: 0, to: 1 };
class CrosshairMarksPaneView {
constructor(chartModel, crosshair, pane) {
this._compositeRenderer = new CompositeRenderer();
this._markersRenderers = [];
this._markersData = [];
this._invalidated = true;
this._chartModel = chartModel;
this._crosshair = crosshair;
this._pane = pane;
this._compositeRenderer.setRenderers(this._markersRenderers);
}
update(updateType) {
this._createMarkerRenderersIfNeeded();
this._invalidated = true;
}
renderer() {
if (this._invalidated) {
this._updateImpl();
this._invalidated = false;
}
return this._compositeRenderer;
}
_createMarkerRenderersIfNeeded() {
const serieses = this._pane.orderedSources();
if (serieses.length !== this._markersRenderers.length) {
this._markersData = serieses.map(createEmptyMarkerData);
this._markersRenderers = this._markersData.map((data) => {
const res = new PaneRendererMarks();
res.setData(data);
return res;
});
this._compositeRenderer.setRenderers(this._markersRenderers);
}
}
_updateImpl() {
const forceHidden = this._crosshair.options().mode === 2 /* CrosshairMode.Hidden */ || !this._crosshair.visible();
const serieses = this._pane.orderedSeries();
const timePointIndex = this._crosshair.appliedIndex();
const timeScale = this._chartModel.timeScale();
this._createMarkerRenderersIfNeeded();
serieses.forEach((s, index) => {
const data = this._markersData[index];
const seriesData = s.markerDataAtIndex(timePointIndex);
const firstValue = s.firstValue();
if (forceHidden || seriesData === null || !s.visible() || firstValue === null) {
data.visibleRange = null;
return;
}
data.lineColor = seriesData.backgroundColor;
data.radius = seriesData.radius;
data.lineWidth = seriesData.borderWidth;
data.items[0].price = seriesData.price;
data.items[0].y = s.priceScale().priceToCoordinate(seriesData.price, firstValue.value);
data.backColor = seriesData.borderColor ?? this._chartModel.backgroundColorAtYPercentFromTop(data.items[0].y / s.priceScale().height());
data.items[0].time = timePointIndex;
data.items[0].x = timeScale.indexToCoordinate(timePointIndex);
data.visibleRange = rangeForSinglePoint;
});
}
}
class CrosshairRenderer extends BitmapCoordinatesPaneRenderer {
constructor(data) {
super();
this._data = data;
}
_drawImpl({ context: ctx, bitmapSize, horizontalPixelRatio, verticalPixelRatio }) {
if (this._data === null) {
return;
}
const vertLinesVisible = this._data.vertLine.visible;
const horzLinesVisible = this._data.horzLine.visible;
if (!vertLinesVisible && !horzLinesVisible) {
return;
}
const x = Math.round(this._data.x * horizontalPixelRatio);
const y = Math.round(this._data.y * verticalPixelRatio);
ctx.lineCap = 'butt';
if (vertLinesVisible && x >= 0) {
ctx.lineWidth = Math.floor(this._data.vertLine.lineWidth * horizontalPixelRatio);
ctx.strokeStyle = this._data.vertLine.color;
ctx.fillStyle = this._data.vertLine.color;
setLineStyle(ctx, this._data.vertLine.lineStyle);
drawVerticalLine(ctx, x, 0, bitmapSize.height);
}
if (horzLinesVisible && y >= 0) {
ctx.lineWidth = Math.floor(this._data.horzLine.lineWidth * verticalPixelRatio);
ctx.strokeStyle = this._data.horzLine.color;
ctx.fillStyle = this._data.horzLine.color;
setLineStyle(ctx, this._data.horzLine.lineStyle);
drawHorizontalLine(ctx, y, 0, bitmapSize.width);
}
}
}
class CrosshairPaneView {
constructor(source, pane) {
this._invalidated = true;
this._rendererData = {
vertLine: {
lineWidth: 1,
lineStyle: 0,
color: '',
visible: false,
},
horzLine: {
lineWidth: 1,
lineStyle: 0,
color: '',
visible: false,
},
x: 0,
y: 0,
};
this._renderer = new CrosshairRenderer(this._rendererData);
this._source = source;
this._pane = pane;
}
update() {
this._invalidated = true;
}
renderer(pane) {
if (this._invalidated) {
this._updateImpl();
this._invalidated = false;
}
return this._renderer;
}
_updateImpl() {
const visible = this._source.visible();
const crosshairOptions = this._pane.model().options().crosshair;
const data = this._rendererData;
if (crosshairOptions.mode === 2 /* CrosshairMode.Hidden */) {
data.horzLine.visible = false;
data.vertLine.visible = false;
return;
}
data.horzLine.visible = visible && this._source.horzLineVisible(this._pane);
data.vertLine.visible = visible && this._source.vertLineVisible();
data.horzLine.lineWidth = crosshairOptions.horzLine.width;
data.horzLine.lineStyle = crosshairOptions.horzLine.style;
data.horzLine.color = crosshairOptions.horzLine.color;
data.vertLine.lineWidth = crosshairOptions.vertLine.width;
data.vertLine.lineStyle = crosshairOptions.vertLine.style;
data.vertLine.color = crosshairOptions.vertLine.color;
data.x = this._source.appliedX();
data.y = this._source.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.setData(data, commonData);
}
setData(data, commonData) {
this._data = data;
this._commonData = commonData;
}
height(rendererOptions, useSecondLine) {
if (!this._data.visible) {
return 0;
}
return rendererOptions.fontSize + rendererOptions.paddingTop + rendererOptions.paddingBottom;
}
draw(target, rendererOptions, textWidthCache, align) {
if (!this._data.visible || this._data.text.length === 0) {
return;
}
const textColor = this._data.color;
const backgroundColor = this._commonData.background;
const geometry = target.useBitmapCoordinateSpace((scope) => {
const ctx = scope.context;
ctx.font = rendererOptions.font;
const geom = this._calculateGeometry(scope, rendererOptions, textWidthCache, align);
const gb = geom.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.alignRight) {
drawRoundRectWithBorder(ctx, gb.xOutside, gb.yTop, gb.totalWidth, gb.totalHeight, backgroundColor, gb.horzBorder, [gb.radius, 0, 0, gb.radius], backgroundColor);
}
else {
drawRoundRectWithBorder(ctx, gb.xInside, gb.yTop, gb.totalWidth, gb.totalHeight, backgroundColor, gb.horzBorder, [0, gb.radius, gb.radius, 0], backgroundColor);
}
// draw tick
if (this._data.tickVisible) {
ctx.fillStyle = textColor;
ctx.fillRect(gb.xInside, gb.yMid, gb.xTick - gb.xInside, gb.tickHeight);
}
// draw separator
if (this._data.borderVisible) {
ctx.fillStyle = rendererOptions.paneBackgroundColor;
ctx.fillRect(geom.alignRight ? gb.right - gb.horzBorder : 0, gb.yTop, gb.horzBorder, gb.yBottom - gb.yTop);
}
return geom;
});
target.useMediaCoordinateSpace(({ context: ctx }) => {
const gm = geometry.media;
ctx.font = rendererOptions.font;
ctx.textAlign = geometry.alignRight ? 'right' : 'left';
ctx.textBaseline = 'middle';
ctx.fillStyle = textColor;
ctx.fillText(this._data.text, gm.xText, (gm.yTop + gm.yBottom) / 2 + gm.textMidCorrection);
});
}
_calculateGeometry(scope, rendererOptions, textWidthCache, align) {
const { context: ctx, bitmapSize, mediaSize, horizontalPixelRatio, verticalPixelRatio } = scope;
const tickSize = (this._data.tickVisible || !this._data.moveTextToInvisibleTick) ? rendererOptions.tickLength : 0;
const horzBorder = this._data.separatorVisible ? rendererOptions.borderSize : 0;
const paddingTop = rendererOptions.paddingTop + this._commonData.additionalPaddingTop;
const paddingBottom = rendererOptions.paddingBottom + this._commonData.additionalPaddingBottom;
const paddingInner = rendererOptions.paddingInner;
const paddingOuter = rendererOptions.paddingOuter;
const text = this._data.text;
const actualTextHeight = rendererOptions.fontSize;
const textMidCorrection = textWidthCache.yMidCorrection(ctx, text);
const textWidth = Math.ceil(textWidthCache.measureText(ctx, text));
const totalHeight = actualTextHeight + paddingTop + paddingBottom;
const totalWidth = rendererOptions.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._commonData.fixedCoordinate ?? this._commonData.renderCoordinate ?? this._commonData.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 {
alignRight,
bitmap: {
yTop: yTopBitmap,
yMid: yMidBitmap,
yBottom: yBottomBitmap,
totalWidth: totalWidthBitmap,
totalHeight: totalHeightBitmap,
// TODO: it is better to have different horizontal and vertical radii
radius: 2 * horizontalPixelRatio,
horzBorder: horzBorderBitmap,
xOutside: xOutsideBitmap,
xInside: xInsideBitmap,
xTick: xTickBitmap,
tickHeight: tickHeightBitmap,
right: bitmapSize.width,
},
media: {
yTop: yTopBitmap / verticalPixelRatio,
yBottom: yBottomBitmap / verticalPixelRatio,
xText,
textMidCorrection,
},
};
}
}
class PriceAxisView {
constructor(ctor) {
this._commonRendererData = {
coordinate: 0,
background: '#000',
additionalPaddingBottom: 0,
additionalPaddingTop: 0,
};
this._axisRendererData = {
text: '',
visible: false,
tickVisible: true,
moveTextToInvisibleTick: false,
borderColor: '',
color: '#FFF',
borderVisible: false,
separatorVisible: false,
};
this._paneRendererData = {
text: '',
visible: false,
tickVisible: false,
moveTextToInvisibleTick: true,
borderColor: '',
color: '#FFF',
borderVisible: true,
separatorVisible: true,
};
this._invalidated = true;
this._axisRenderer = new (ctor || PriceAxisViewRenderer)(this._axisRendererData, this._commonRendererData);
this._paneRenderer = new (ctor || PriceAxisViewRenderer)(this._paneRendererData, this._commonRendererData);
}
text() {
this._updateRendererDataIfNeeded();
return this._axisRendererData.text;
}
coordinate() {
this._updateRendererDataIfNeeded();
return this._commonRendererData.coordinate;
}
update() {
this._invalidated = true;
}
height(rendererOptions, useSecondLine = false) {
return Math.max(this._axisRenderer.height(rendererOptions, useSecondLine), this._paneRenderer.height(rendererOptions, useSecondLine));
}
getFixedCoordinate() {
return this._commonRendererData.fixedCoordinate ?? null;
}
getRenderCoordinate() {
return this._commonRendererData.fixedCoordinate ?? this._commonRendererData.renderCoordinate ?? this.coordinate();
}
setRenderCoordinate(value) {
this._commonRendererData.renderCoordinate = value ?? undefined;
}
isVisible() {
this._updateRendererDataIfNeeded();
return this._axisRendererData.visible || this._paneRendererData.visible;
}
isAxisLabelVisible() {
this._updateRendererDataIfNeeded();
return this._axisRendererData.visible;
}
renderer(priceScale) {
this._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._axisRendererData.tickVisible = this._axisRendererData.tickVisible && priceScale.options().ticksVisible;
this._paneRendererData.tickVisible = this._paneRendererData.tickVisible && priceScale.options().ticksVisible;
this._axisRenderer.setData(this._axisRendererData, this._commonRendererData);
this._paneRenderer.setData(this._paneRendererData, this._commonRendererData);
return this._axisRenderer;
}
paneRenderer() {
this._updateRendererDataIfNeeded();
this._axisRenderer.setData(this._axisRendererData, this._commonRendererData);
this._paneRenderer.setData(this._paneRendererData, this._commonRendererData);
return this._paneRenderer;
}
_updateRendererDataIfNeeded() {
if (this._invalidated) {
this._axisRendererData.tickVisible = true;
this._paneRendererData.tickVisible = false;
this._updateRendererData(this._axisRendererData, this._paneRendererData, this._commonRendererData);
}
}
}
class CrosshairPriceAxisView extends PriceAxisView {
constructor(source, priceScale, valueProvider) {
super();
this._source = source;
this._priceScale = priceScale;
this._valueProvider = valueProvider;
}
_updateRendererData(axisRendererData, paneRendererData, commonRendererData) {
axisRendererData.visible = false;
if (this._source.options().mode === 2 /* CrosshairMode.Hidden */) {
return;
}
const options = this._source.options().horzLine;
if (!options.labelVisible) {
return;
}
const firstValue = this._priceScale.firstValue();
if (!this._source.visible() || this._priceScale.isEmpty() || (firstValue === null)) {
return;
}
const colors = this._priceScale.colorParser().generateContrastColors(options.labelBackgroundColor);
commonRendererData.background = colors.background;
axisRendererData.color = colors.foreground;
const additionalPadding = 2 / 12 * this._priceScale.fontSize();
commonRendererData.additionalPaddingTop = additionalPadding;
commonRendererData.additionalPaddingBottom = additionalPadding;
const value = this._valueProvider(this._priceScale);
commonRendererData.coordinate = value.coordinate;
axisRendererData.text = this._priceScale.formatPrice(value.price, firstValue);
axisRendererData.visible = true;
}
}
const optimizationReplacementRe = /[1-9]/g;
const radius$1 = 2;
class TimeAxisViewRenderer {
constructor() {
this._data = null;
}
setData(data) {
this._data = data;
}
draw(target, rendererOptions) {
if (this._data === null || this._data.visible === false || this._data.text.length === 0) {
return;
}
const textWidth = target.useMediaCoordinateSpace(({ context: ctx }) => {
ctx.font = rendererOptions.font;
return Math.round(rendererOptions.widthCache.measureText(ctx, ensureNotNull(this._data).text, optimizationReplacementRe));
});
if (textWidth <= 0) {
return;
}
const horzMargin = rendererOptions.paddingHorizontal;
const labelWidth = textWidth + 2 * horzMargin;
const labelWidthHalf = labelWidth / 2;
const timeScaleWidth = this._data.width;
let coordinate = this._data.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.borderSize +
rendererOptions.tickLength +
rendererOptions.paddingTop +
rendererOptions.fontSize +
rendererOptions.paddingBottom);
target.useBitmapCoordinateSpace(({ context: ctx, horizontalPixelRatio, verticalPixelRatio }) => {
const data = ensureNotNull(this._data);
ctx.fillStyle = data.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.tickVisible) {
const tickX = Math.round(data.coordinate * horizontalPixelRatio);
const tickTop = y1scaled;
const tickBottom = Math.round((tickTop + rendererOptions.tickLength) * verticalPixelRatio);
ctx.fillStyle = data.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._data);
const yText = y1 +
rendererOptions.borderSize +
rendererOptions.tickLength +
rendererOptions.paddingTop +
rendererOptions.fontSize / 2;
ctx.font = rendererOptions.font;
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.fillStyle = data.color;
const textYCorrection = rendererOptions.widthCache.yMidCorrection(ctx, 'Apr0');
ctx.translate(x1 + horzMargin, yText + textYCorrection);
ctx.fillText(data.text, 0, 0);
});
}
}
class CrosshairTimeAxisView {
constructor(crosshair, model, valueProvider) {
this._invalidated = true;
this._renderer = new TimeAxisViewRenderer();
this._rendererData = {
visible: false,
background: '#4c525e',
color: 'white',
text: '',
width: 0,
coordinate: NaN,
tickVisible: true,
};
this._crosshair = crosshair;
this._model = model;
this._valueProvider = valueProvider;
}
update() {
this._invalidated = true;
}
renderer() {
if (this._invalidated) {
this._updateImpl();
this._invalidated = false;
}
this._renderer.setData(this._rendererData);
return this._renderer;
}
_updateImpl() {
const data = this._rendererData;
data.visible = false;
if (this._crosshair.options().mode === 2 /* CrosshairMode.Hidden */) {
return;
}
const options = this._crosshair.options().vertLine;
if (!options.labelVisible) {
return;
}
const timeScale = this._model.timeScale();
if (timeScale.isEmpty()) {
return;
}
data.width = timeScale.width();
const value = this._valueProvider();
if (value === null) {
return;
}
data.coordinate = value.coordinate;
const currentTime = timeScale.indexToTimeScalePoint(this._crosshair.appliedIndex());
data.text = timeScale.formatDateTime(ensureNotNull(currentTime));
data.visible = true;
const colors = this._model.colorParser().generateContrastColors(options.labelBackgroundColor);
data.background = colors.background;
data.color = colors.foreground;
data.tickVisible = timeScale.options().ticksVisible;
}
}
class DataSource {
constructor() {
this._priceScale = null;
this._zorder = 0;
}
zorder() {
return this._zorder;
}
setZorder(zorder) {
this._zorder = zorder;
}
priceScale() {
return this._priceScale;
}
setPriceScale(priceScale) {
this._priceScale = priceScale;
}
labelPaneViews(pane) {
return [];
}
timeAxisViews() {
return [];
}
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._pane = null;
this._price = NaN;
this._index = 0;
this._visible = false; // initially the crosshair should not be visible, until the user interacts.
this._priceAxisViews = new Map();
this._subscribed = false;
this._crosshairPaneViewCache = new WeakMap();
this._markersPaneViewCache = new WeakMap();
this._x = NaN;
this._y = NaN;
this._originX = NaN;
this._originY = NaN;
this._model = model;
this._options = options;
const valuePriceProvider = (rawPriceProvider, rawCoordinateProvider) => {
return (priceScale) => {
const coordinate = rawCoordinateProvider();
const rawPrice = rawPriceProvider();
if (priceScale === ensureNotNull(this._pane).defaultPriceScale()) {
// price must be defined
return { price: rawPrice, coordinate: coordinate };
}
else {
// always convert from coordinate
const firstValue = ensureNotNull(priceScale.firstValue());
const price = priceScale.coordinateToPrice(coordinate, firstValue);
return { price: price, coordinate: coordinate };
}
};
};
const valueTimeProvider = (rawIndexProvider, rawCoordinateProvider) => {
return () => {
const time = this._model.timeScale().indexToTime(rawIndexProvider());
const coordinate = rawCoordinateProvider();
if (!time || !Number.isFinite(coordinate)) {
return null;
}
return {
time,
coordinate,
};
};
};
// for current position always return both price and coordinate
this._currentPosPriceProvider = valuePriceProvider(() => this._price, () => this._y);
const currentPosTimeProvider = valueTimeProvider(() => this._index, () => this.appliedX());
this._timeAxisView = new CrosshairTimeAxisView(this, model, currentPosTimeProvider);
}
options() {
return this._options;
}
saveOriginCoord(x, y) {
this._originX = x;
this._originY = y;
}
clearOriginCoord() {
this._originX = NaN;
this._originY = NaN;
}
originCoordX() {
return this._originX;
}
originCoordY() {
return this._originY;
}
setPosition(index, price, pane) {
if (!this._subscribed) {
this._subscribed = true;
}
this._visible = true;
this._tryToUpdateViews(index, price, pane);
}
appliedIndex() {
return this._index;
}
appliedX() {
return this._x;
}
appliedY() {
return this._y;
}
visible() {
return this._visible;
}
clearPosition() {
this._visible = false;
this._setIndexToLastSeriesBarIndex();
this._price = NaN;
this._x = NaN;
this._y = NaN;
this._pane = null;
this.clearOriginCoord();
this.updateAllViews();
}
snapToVisibleSeriesIfNeeded(index) {
if (!this._options.doNotSnapToHiddenSeriesIndices) {
return index;
}
const model = this._model;
const timeScale = model.timeScale();
let closestLeftIndex = null;
let closestRightIndex = null;
for (const series of model.visibleSerieses()) {
const leftResult = series.bars().search(index, -1 /* MismatchDirection.NearestLeft */);
if (leftResult) {
if (leftResult.index === index) {
return index; // already snapped
}
if