if (!window.Chart) { class Chart { constructor(chartId, innerWidth, innerHeight, position, autoSize) { this.makeCandlestickSeries = this.makeCandlestickSeries.bind(this) this.reSize = this.reSize.bind(this) this.id = chartId this.lines = [] this.wrapper = document.createElement('div') this.div = document.createElement('div') this.scale = { width: innerWidth, height: innerHeight, } this.commandFunctions = [] this.chart = LightweightCharts.createChart(this.div, { width: window.innerWidth * innerWidth, height: window.innerHeight * innerHeight, layout: { textColor: '#d1d4dc', background: { color: '#000000', type: LightweightCharts.ColorType.Solid, }, fontSize: 12 }, rightPriceScale: { scaleMargins: {top: 0.3, bottom: 0.25}, }, timeScale: {timeVisible: true, secondsVisible: false}, crosshair: { mode: LightweightCharts.CrosshairMode.Normal, vertLine: { labelBackgroundColor: 'rgb(46, 46, 46)' }, horzLine: { labelBackgroundColor: 'rgb(55, 55, 55)' } }, grid: { vertLines: {color: 'rgba(29, 30, 38, 5)'}, horzLines: {color: 'rgba(29, 30, 58, 5)'}, }, handleScroll: {vertTouchDrag: true}, }) this.wrapper.style.width = `${100 * innerWidth}%` this.wrapper.style.height = `${100 * innerHeight}%` this.wrapper.style.display = 'flex' this.wrapper.style.flexDirection = 'column' this.wrapper.style.position = 'relative' this.wrapper.style.float = position this.div.style.position = 'relative' this.div.style.display = 'flex' this.wrapper.appendChild(this.div) document.getElementById('wrapper').append(this.wrapper) document.addEventListener('keydown', (event) => { for (let i = 0; i < this.commandFunctions.length; i++) { if (this.commandFunctions[i](event)) break } }) if (!autoSize) return window.addEventListener('resize', () => this.reSize()) } reSize() { let topBarOffset = 'topBar' in this ? this.topBar.offsetHeight : 0 this.chart.resize(window.innerWidth * this.scale.width, (window.innerHeight * this.scale.height) - topBarOffset) } makeCandlestickSeries() { this.markers = [] this.horizontal_lines = [] this.candleData = [] this.precision = 2 let up = 'rgba(39, 157, 130, 100)' let down = 'rgba(200, 97, 100, 100)' this.series = this.chart.addCandlestickSeries({ color: 'rgb(0, 120, 255)', upColor: up, borderUpColor: up, wickUpColor: up, downColor: down, borderDownColor: down, wickDownColor: down, lineWidth: 2, }) this.volumeSeries = this.chart.addHistogramSeries({ color: '#26a69a', priceFormat: {type: 'volume'}, priceScaleId: '', }) this.series.priceScale().applyOptions({ scaleMargins: {top: 0.2, bottom: 0.2}, }); this.volumeSeries.priceScale().applyOptions({ scaleMargins: {top: 0.8, bottom: 0}, }); } toJSON() { // Exclude the chart attribute from serialization const {chart, ...serialized} = this; return serialized; } } window.Chart = Chart class HorizontalLine { constructor(chart, lineId, price, color, width, style, axisLabelVisible, text) { this.updatePrice = this.updatePrice.bind(this) this.deleteLine = this.deleteLine.bind(this) this.chart = chart this.price = price this.color = color this.id = lineId this.priceLine = { price: this.price, color: this.color, lineWidth: width, lineStyle: style, axisLabelVisible: axisLabelVisible, title: text, } this.line = this.chart.series.createPriceLine(this.priceLine) this.chart.horizontal_lines.push(this) } toJSON() { // Exclude the chart attribute from serialization const {chart, line, ...serialized} = this; return serialized; } updatePrice(price) { this.chart.series.removePriceLine(this.line) this.price = price this.priceLine.price = this.price this.line = this.chart.series.createPriceLine(this.priceLine) } updateLabel(text) { this.chart.series.removePriceLine(this.line) this.priceLine.title = text this.line = this.chart.series.createPriceLine(this.priceLine) } updateColor(color) { this.chart.series.removePriceLine(this.line) this.color = color this.priceLine.color = this.color this.line = this.chart.series.createPriceLine(this.priceLine) } deleteLine() { this.chart.series.removePriceLine(this.line) this.chart.horizontal_lines.splice(this.chart.horizontal_lines.indexOf(this)) delete this } } window.HorizontalLine = HorizontalLine class Legend { constructor(chart, ohlcEnabled, percentEnabled, linesEnabled, color = 'rgb(191, 195, 203)', fontSize = '11', fontFamily = 'Monaco') { this.div = document.createElement('div') this.div.style.position = 'absolute' this.div.style.zIndex = '3000' this.div.style.pointerEvents = 'none' this.div.style.top = '10px' this.div.style.left = '10px' this.div.style.display = 'flex' this.div.style.flexDirection = 'column' this.div.style.maxWidth = `${(chart.scale.width * 100) - 8}vw` this.div.style.color = color this.div.style.fontSize = fontSize + 'px' this.div.style.fontFamily = fontFamily this.candle = document.createElement('div') this.div.appendChild(this.candle) chart.div.appendChild(this.div) this.color = color this.linesEnabled = linesEnabled this.makeLines(chart) let legendItemFormat = (num, decimal) => num.toFixed(decimal).toString().padStart(8, ' ') let shorthandFormat = (num) => { if (num >= 1000000) { return (num / 1000000).toFixed(1) + 'M'; } else if (num >= 1000) { return (num / 1000).toFixed(1) + 'K'; } return num.toString().padStart(8, ' '); } chart.chart.subscribeCrosshairMove((param) => { if (param.time) { let data = param.seriesData.get(chart.series); let finalString = '' if (data) { this.candle.style.color = '' let ohlc = `O ${legendItemFormat(data.open, chart.precision)} | H ${legendItemFormat(data.high, chart.precision)} | L ${legendItemFormat(data.low, chart.precision)} | C ${legendItemFormat(data.close, chart.precision)} ` let percentMove = ((data.close - data.open) / data.open) * 100 let percent = `| ${percentMove >= 0 ? '+' : ''}${percentMove.toFixed(2)} %` finalString += ohlcEnabled ? ohlc : '' finalString += percentEnabled ? percent : '' let volumeData = param.seriesData.get(chart.volumeSeries) if (volumeData) finalString += ohlcEnabled ? `
V ${shorthandFormat(volumeData.value)}` : '' } this.candle.innerHTML = finalString + '
' this.lines.forEach((line) => { if (!param.seriesData.get(line.line.series)) return let price = legendItemFormat(param.seriesData.get(line.line.series).value, line.line.precision) line.div.innerHTML = ` ${line.line.name} : ${price}` }) } else { this.candle.style.color = 'transparent' } }); } makeLines(chart) { this.lines = [] if (this.linesEnabled) chart.lines.forEach(line => this.lines.push(this.makeLineRow(line))) } makeLineRow(line) { let openEye = ` \` ` let closedEye = ` ` let row = document.createElement('div') row.style.display = 'flex' row.style.alignItems = 'center' let div = document.createElement('div') let toggle = document.createElement('div') toggle.style.borderRadius = '4px' toggle.style.marginLeft = '10px' toggle.style.pointerEvents = 'auto' let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("width", "22"); svg.setAttribute("height", "16"); let group = document.createElementNS("http://www.w3.org/2000/svg", "g"); group.innerHTML = openEye let on = true toggle.addEventListener('click', (event) => { if (on) { on = false group.innerHTML = closedEye line.series.applyOptions({ visible: false }) } else { on = true line.series.applyOptions({ visible: true }) group.innerHTML = openEye } }) toggle.addEventListener('mouseover', (event) => { document.body.style.cursor = 'pointer' toggle.style.backgroundColor = 'rgba(50, 50, 50, 0.5)' }) toggle.addEventListener('mouseleave', (event) => { document.body.style.cursor = 'default' toggle.style.backgroundColor = 'transparent' }) svg.appendChild(group) toggle.appendChild(svg); row.appendChild(div) row.appendChild(toggle) this.div.appendChild(row) return { div: div, row: row, toggle: toggle, line: line, solid: line.color.startsWith('rgba') ? line.color.replace(/[^,]+(?=\))/, '1') : line.color } } } window.Legend = Legend } function syncCrosshairs(childChart, parentChart) { function crosshairHandler (e, thisChart, otherChart, otherHandler) { thisChart.applyOptions({crosshair: { horzLine: { visible: true, labelVisible: true, }}}) otherChart.applyOptions({crosshair: { horzLine: { visible: false, labelVisible: false, }}}) otherChart.unsubscribeCrosshairMove(otherHandler) if (e.time !== undefined) { let xx = otherChart.timeScale().timeToCoordinate(e.time); otherChart.setCrosshairXY(xx,300,true); } else if (e.point !== undefined){ otherChart.setCrosshairXY(e.point.x,300,false); } otherChart.subscribeCrosshairMove(otherHandler) } let parent = 0 let child = 0 let parentCrosshairHandler = (e) => { parent ++ if (parent < 10) return child = 0 crosshairHandler(e, parentChart, childChart, childCrosshairHandler) } let childCrosshairHandler = (e) => { child ++ if (child < 10) return parent = 0 crosshairHandler(e, childChart, parentChart, parentCrosshairHandler) } parentChart.subscribeCrosshairMove(parentCrosshairHandler) childChart.subscribeCrosshairMove(childCrosshairHandler) } function stampToDate(stampOrBusiness) { return new Date(stampOrBusiness*1000) } function dateToStamp(date) { return Math.floor(date.getTime()/1000) } function lastBar(obj) { return obj[obj.length-1] } function calculateTrendLine(startDate, startValue, endDate, endValue, interval, chart, ray=false) { let reversed = false if (stampToDate(endDate).getTime() < stampToDate(startDate).getTime()) { reversed = true; [startDate, endDate] = [endDate, startDate]; } let startIndex if (stampToDate(startDate).getTime() < stampToDate(chart.candleData[0].time).getTime()) { startIndex = 0 } else { startIndex = chart.candleData.findIndex(item => stampToDate(item.time).getTime() === stampToDate(startDate).getTime()) } if (startIndex === -1) { return [] } let endIndex if (ray) { endIndex = chart.candleData.length+1000 startValue = endValue } else { endIndex = chart.candleData.findIndex(item => stampToDate(item.time).getTime() === stampToDate(endDate).getTime()) if (endIndex === -1) { let barsBetween = (stampToDate(endDate)-stampToDate(chart.candleData[chart.candleData.length-1].time))/interval endIndex = chart.candleData.length-1+barsBetween } } let numBars = endIndex-startIndex const rate_of_change = (endValue - startValue) / numBars; const trendData = []; let currentDate = null let iPastData = 0 for (let i = 0; i <= numBars; i++) { if (chart.candleData[startIndex+i]) { currentDate = chart.candleData[startIndex+i].time } else { iPastData ++ currentDate = dateToStamp(new Date(stampToDate(chart.candleData[chart.candleData.length-1].time).getTime()+(iPastData*interval))) } const currentValue = reversed ? startValue + rate_of_change * (numBars - i) : startValue + rate_of_change * i; trendData.push({ time: currentDate, value: currentValue }); } return trendData; } if (!window.ContextMenu) { class ContextMenu { constructor() { this.menu = document.createElement('div') this.menu.style.position = 'absolute' this.menu.style.zIndex = '10000' this.menu.style.background = 'rgb(50, 50, 50)' this.menu.style.color = '#ececed' this.menu.style.display = 'none' this.menu.style.borderRadius = '5px' this.menu.style.padding = '3px 3px' this.menu.style.fontSize = '13px' this.menu.style.cursor = 'default' document.body.appendChild(this.menu) this.hoverItem = null let closeMenu = (event) => { if (!this.menu.contains(event.target)) { this.menu.style.display = 'none'; this.listen(false) } } this.onRightClick = (event) => { event.preventDefault(); this.menu.style.left = event.clientX + 'px'; this.menu.style.top = event.clientY + 'px'; this.menu.style.display = 'block'; document.removeEventListener('click', closeMenu) document.addEventListener('click', closeMenu) } } listen(active) { active ? document.addEventListener('contextmenu', this.onRightClick) : document.removeEventListener('contextmenu', this.onRightClick) } menuItem(text, action, hover=false) { let item = document.createElement('span') item.style.display = 'flex' item.style.alignItems = 'center' item.style.justifyContent = 'space-between' item.style.padding = '2px 10px' item.style.margin = '1px 0px' item.style.borderRadius = '3px' this.menu.appendChild(item) let elem = document.createElement('span') elem.innerText = text item.appendChild(elem) if (hover) { let arrow = document.createElement('span') arrow.innerText = `►` arrow.style.fontSize = '8px' item.appendChild(arrow) } elem.addEventListener('mouseover', (event) => { item.style.backgroundColor = 'rgba(0, 122, 255, 0.3)' if (this.hoverItem && this.hoverItem.closeAction) this.hoverItem.closeAction() this.hoverItem = {elem: elem, action: action, closeAction: hover} }) elem.addEventListener('mouseout', (event) => item.style.backgroundColor = 'transparent') if (!hover) elem.addEventListener('click', (event) => {action(event); this.menu.style.display = 'none'}) else { let timeout elem.addEventListener('mouseover', () => timeout = setTimeout(() => action(item.getBoundingClientRect()), 100)) elem.addEventListener('mouseout', () => clearTimeout(timeout)) } } separator() { let separator = document.createElement('div') separator.style.width = '90%' separator.style.height = '1px' separator.style.margin = '3px 0px' separator.style.backgroundColor = '#3C434C' this.menu.appendChild(separator) } } window.ContextMenu = ContextMenu } window.callbackFunction = () => undefined;