UNPKG

lightweight-charts

Version:

Performant financial charts built with HTML5 canvas

1,183 lines (1,162 loc) 656 kB
/*! * @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 */ import { size as size$1, bindCanvasElementBitmapSizeTo, equalSizes, tryCreateCanvasRenderingTarget2D } from 'fancy-canvas'; 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;