UNPKG

@santiment-network/chart

Version:
1,365 lines (1,344 loc) 682 kB
/*! * @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