if (!window.ToolBox) { class ToolBox { constructor(chart) { this.onTrendSelect = this.onTrendSelect.bind(this) this.onHorzSelect = this.onHorzSelect.bind(this) this.onRaySelect = this.onRaySelect.bind(this) this.chart = chart this.drawings = [] this.chart.cursor = 'default' this.makingDrawing = false this.interval = 24 * 60 * 60 * 1000 this.activeBackgroundColor = 'rgba(0, 122, 255, 0.7)' this.activeIconColor = 'rgb(240, 240, 240)' this.iconColor = 'lightgrey' this.backgroundColor = 'transparent' this.hoverColor = 'rgba(60, 60, 60, 0.7)' this.elem = this.makeToolBox() this.subscribeHoverMove() } toJSON() { // Exclude the chart attribute from serialization const {chart, ...serialized} = this; return serialized; } makeToolBox() { let toolBoxElem = document.createElement('div') toolBoxElem.style.position = 'absolute' toolBoxElem.style.zIndex = '2000' toolBoxElem.style.display = 'flex' toolBoxElem.style.alignItems = 'center' toolBoxElem.style.top = '25%' toolBoxElem.style.borderRight = '2px solid #3C434C' toolBoxElem.style.borderTop = '2px solid #3C434C' toolBoxElem.style.borderBottom = '2px solid #3C434C' toolBoxElem.style.borderTopRightRadius = '4px' toolBoxElem.style.borderBottomRightRadius = '4px' toolBoxElem.style.backgroundColor = 'rgba(25, 27, 30, 0.5)' toolBoxElem.style.flexDirection = 'column' this.chart.activeIcon = null let trend = this.makeToolBoxElement(this.onTrendSelect, 'KeyT', ``) let horz = this.makeToolBoxElement(this.onHorzSelect, 'KeyH', ``) let ray = this.makeToolBoxElement(this.onRaySelect, 'KeyR', ``) //let testB = this.makeToolBoxElement(this.onTrendSelect, ``) toolBoxElem.appendChild(trend) toolBoxElem.appendChild(horz) toolBoxElem.appendChild(ray) //toolBoxElem.appendChild(testB) this.chart.div.append(toolBoxElem) let commandZHandler = (toDelete) => { if (!toDelete) return if ('price' in toDelete) { if (toDelete.id !== 'toolBox') return commandZHandler(this.drawings[this.drawings.indexOf(toDelete) - 1]) this.chart.series.removePriceLine(toDelete.line) } else { let logical if (toDelete.ray) logical = this.chart.chart.timeScale().getVisibleLogicalRange() this.chart.chart.removeSeries(toDelete.line); if (toDelete.ray) this.chart.chart.timeScale().setVisibleLogicalRange(logical) } this.drawings.splice(this.drawings.indexOf(toDelete)) this.saveDrawings() } this.chart.commandFunctions.push((event) => { if ((event.metaKey || event.ctrlKey) && event.code === 'KeyZ') { commandZHandler(this.drawings[this.drawings.length - 1]) return true } }); return toolBoxElem } makeToolBoxElement(action, keyCmd, paths) { let icon = { elem: document.createElement('div'), action: action, } icon.elem.style.margin = '3px' icon.elem.style.borderRadius = '4px' icon.elem.style.display = 'flex' let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("width", "29"); svg.setAttribute("height", "29"); let group = document.createElementNS("http://www.w3.org/2000/svg", "g"); group.innerHTML = paths group.setAttribute("fill", this.iconColor) svg.appendChild(group) icon.elem.appendChild(svg); icon.elem.addEventListener('mouseenter', () => { icon.elem.style.backgroundColor = icon === this.chart.activeIcon ? this.activeBackgroundColor : this.hoverColor document.body.style.cursor = 'pointer' }) icon.elem.addEventListener('mouseleave', () => { icon.elem.style.backgroundColor = icon === this.chart.activeIcon ? this.activeBackgroundColor : this.backgroundColor document.body.style.cursor = this.chart.cursor }) icon.elem.addEventListener('click', () => { if (this.chart.activeIcon) { this.chart.activeIcon.elem.style.backgroundColor = this.backgroundColor group.setAttribute("fill", this.iconColor) document.body.style.cursor = 'crosshair' this.chart.cursor = 'crosshair' this.chart.activeIcon.action(false) if (this.chart.activeIcon === icon) { return this.chart.activeIcon = null } } this.chart.activeIcon = icon group.setAttribute("fill", this.activeIconColor) icon.elem.style.backgroundColor = this.activeBackgroundColor document.body.style.cursor = 'crosshair' this.chart.cursor = 'crosshair' this.chart.activeIcon.action(true) }) this.chart.commandFunctions.push((event) => { if (event.altKey && event.code === keyCmd) { if (this.chart.activeIcon) { this.chart.activeIcon.elem.style.backgroundColor = this.backgroundColor group.setAttribute("fill", this.iconColor) document.body.style.cursor = 'crosshair' this.chart.cursor = 'crosshair' this.chart.activeIcon.action(false) } this.chart.activeIcon = icon group.setAttribute("fill", this.activeIconColor) icon.elem.style.backgroundColor = this.activeBackgroundColor document.body.style.cursor = 'crosshair' this.chart.cursor = 'crosshair' this.chart.activeIcon.action(true) return true } }) return icon.elem } onTrendSelect(toggle, ray = false) { let trendLine = { line: null, markers: null, data: null, from: null, to: null, ray: ray, } let firstTime = null let firstPrice = null let currentTime = null if (!toggle) { this.chart.chart.unsubscribeClick(this.clickHandler) return } let crosshairHandlerTrend = (param) => { this.chart.chart.unsubscribeCrosshairMove(crosshairHandlerTrend) if (!this.makingDrawing) return let logical let lastCandleTime = this.chart.candleData[this.chart.candleData.length - 1].time currentTime = this.chart.chart.timeScale().coordinateToTime(param.point.x) if (!currentTime) { let barsToMove = param.logical - this.chart.chart.timeScale().coordinateToLogical(this.chart.chart.timeScale().timeToCoordinate(lastCandleTime)) logical = barsToMove <= 0 ? null : this.chart.chart.timeScale().getVisibleLogicalRange() currentTime = dateToChartTime(new Date(chartTimeToDate(this.chart.candleData[this.chart.candleData.length - 1].time).getTime() + (barsToMove * this.interval)), this.interval) } else if (chartTimeToDate(lastCandleTime).getTime() <= chartTimeToDate(currentTime).getTime()) { logical = this.chart.chart.timeScale().getVisibleLogicalRange() } let currentPrice = this.chart.series.coordinateToPrice(param.point.y) if (!currentTime) return this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend) trendLine.data = calculateTrendLine(firstTime, firstPrice, currentTime, currentPrice, this.interval, this.chart, ray) trendLine.from = trendLine.data[0].time trendLine.to = trendLine.data[trendLine.data.length - 1].time if (ray) logical = this.chart.chart.timeScale().getVisibleLogicalRange() trendLine.line.setData(trendLine.data) if (logical) { this.chart.chart.applyOptions({handleScroll: true}) setTimeout(() => { this.chart.chart.timeScale().setVisibleLogicalRange(logical) }, 1) setTimeout(() => { this.chart.chart.applyOptions({handleScroll: false}) }, 50) } if (!ray) { trendLine.markers = [ {time: firstTime, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1}, {time: currentTime, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1} ] trendLine.line.setMarkers(trendLine.markers) } setTimeout(() => { this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend) }, 10); } this.clickHandler = (param) => { if (!this.makingDrawing) { this.makingDrawing = true trendLine.line = this.chart.chart.addLineSeries({ lineWidth: 2, lastValueVisible: false, priceLineVisible: false, crosshairMarkerVisible: false, autoscaleInfoProvider: () => ({ priceRange: { minValue: 1_000_000_000, maxValue: 0, }, }), }) firstPrice = this.chart.series.coordinateToPrice(param.point.y) firstTime = !ray ? this.chart.chart.timeScale().coordinateToTime(param.point.x) : this.chart.candleData[this.chart.candleData.length - 1].time this.chart.chart.applyOptions({ handleScroll: false }) this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend) } else { this.chart.chart.applyOptions({ handleScroll: true }) this.makingDrawing = false trendLine.line.setMarkers([]) this.drawings.push(trendLine) this.chart.chart.unsubscribeCrosshairMove(crosshairHandlerTrend) this.chart.chart.unsubscribeClick(this.clickHandler) document.body.style.cursor = 'default' this.chart.cursor = 'default' this.chart.activeIcon.elem.style.backgroundColor = this.backgroundColor this.chart.activeIcon = null this.saveDrawings() } } this.chart.chart.subscribeClick(this.clickHandler) } clickHandlerHorz = (param) => { let price = this.chart.series.coordinateToPrice(param.point.y) let lineStyle = LightweightCharts.LineStyle.Solid let line = new HorizontalLine(this.chart, 'toolBox', price, null, 2, lineStyle, true) this.drawings.push(line) this.chart.chart.unsubscribeClick(this.clickHandlerHorz) document.body.style.cursor = 'default' this.chart.cursor = 'default' this.chart.activeIcon.elem.style.backgroundColor = this.backgroundColor this.chart.activeIcon = null this.saveDrawings() } onHorzSelect(toggle) { !toggle ? this.chart.chart.unsubscribeClick(this.clickHandlerHorz) : this.chart.chart.subscribeClick(this.clickHandlerHorz) } onRaySelect(toggle) { this.onTrendSelect(toggle, true) } subscribeHoverMove() { let hoveringOver = null let x, y let hoverOver = (param) => { if (!param.point || this.makingDrawing) return this.chart.chart.unsubscribeCrosshairMove(hoverOver) x = param.point.x y = param.point.y this.drawings.forEach((drawing) => { let boundaryConditional let horizontal = false if ('price' in drawing) { horizontal = true let priceCoordinate = this.chart.series.priceToCoordinate(drawing.price) boundaryConditional = Math.abs(priceCoordinate - param.point.y) < 6 } else { let trendData = param.seriesData.get(drawing.line); if (!trendData) return let priceCoordinate = this.chart.series.priceToCoordinate(trendData.value) let timeCoordinate = this.chart.chart.timeScale().timeToCoordinate(trendData.time) boundaryConditional = Math.abs(priceCoordinate - param.point.y) < 6 && Math.abs(timeCoordinate - param.point.x) < 6 } if (boundaryConditional) { if (hoveringOver === drawing) return if (!horizontal && !drawing.ray) drawing.line.setMarkers(drawing.markers) document.body.style.cursor = 'pointer' document.addEventListener('mousedown', checkForClick) document.addEventListener('mouseup', checkForRelease) hoveringOver = drawing } else if (hoveringOver === drawing) { if (!horizontal && !drawing.ray) drawing.line.setMarkers([]) document.body.style.cursor = this.chart.cursor hoveringOver = null } }) this.chart.chart.subscribeCrosshairMove(hoverOver) } let originalIndex let originalTime let originalPrice let mouseDown = false let clickedEnd = false let checkForClick = (event) => { mouseDown = true document.body.style.cursor = 'grabbing' this.chart.chart.applyOptions({ handleScroll: false }) this.chart.chart.unsubscribeCrosshairMove(hoverOver) // let [x, y] = [event.clientX, event.clientY] // if ('topBar' in this.chart) y = y - this.chart.topBar.offsetHeight if ('price' in hoveringOver) { originalPrice = hoveringOver.price this.chart.chart.subscribeCrosshairMove(crosshairHandlerHorz) } else if (Math.abs(this.chart.chart.timeScale().timeToCoordinate(hoveringOver.data[0].time) - x) < 4 && !hoveringOver.ray) { clickedEnd = 'first' this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend) } else if (Math.abs(this.chart.chart.timeScale().timeToCoordinate(hoveringOver.data[hoveringOver.data.length - 1].time) - x) < 4 && !hoveringOver.ray) { clickedEnd = 'last' this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend) } else { originalPrice = this.chart.series.coordinateToPrice(y) originalTime = this.chart.chart.timeScale().coordinateToTime(x * this.chart.scale.width) this.chart.chart.subscribeCrosshairMove(checkForDrag) } originalIndex = this.chart.chart.timeScale().coordinateToLogical(x) document.removeEventListener('mousedown', checkForClick) } let checkForRelease = (event) => { mouseDown = false document.body.style.cursor = this.chart.cursor this.chart.chart.applyOptions({handleScroll: true}) if (hoveringOver && 'price' in hoveringOver && hoveringOver.id !== 'toolBox') { this.chart.callbackFunction(`on_horizontal_line_move__${this.chart.id}__${hoveringOver.id};;;${hoveringOver.price.toFixed(8)}`); } hoveringOver = null document.removeEventListener('mousedown', checkForClick) document.removeEventListener('mouseup', checkForRelease) this.chart.chart.subscribeCrosshairMove(hoverOver) this.saveDrawings() } let checkForDrag = (param) => { if (!param.point) return this.chart.chart.unsubscribeCrosshairMove(checkForDrag) if (!mouseDown) return let priceAtCursor = this.chart.series.coordinateToPrice(param.point.y) let priceDiff = priceAtCursor - originalPrice let barsToMove = param.logical - originalIndex let startBarIndex = this.chart.candleData.findIndex(item => chartTimeToDate(item.time).getTime() === chartTimeToDate(hoveringOver.data[0].time).getTime()) let endBarIndex = this.chart.candleData.findIndex(item => chartTimeToDate(item.time).getTime() === chartTimeToDate(hoveringOver.data[hoveringOver.data.length - 1].time).getTime()) let startBar let endBar if (hoveringOver.ray) { endBar = this.chart.candleData[startBarIndex + barsToMove] startBar = hoveringOver.data[hoveringOver.data.length - 1] } else { startBar = this.chart.candleData[startBarIndex + barsToMove] endBar = endBarIndex === -1 ? null : this.chart.candleData[endBarIndex + barsToMove] } let endDate = endBar ? endBar.time : dateToChartTime(new Date(chartTimeToDate(hoveringOver.data[hoveringOver.data.length - 1].time).getTime() + (barsToMove * this.interval)), this.interval) let startDate = startBar.time let startValue = hoveringOver.data[0].value + priceDiff let endValue = hoveringOver.data[hoveringOver.data.length - 1].value + priceDiff hoveringOver.data = calculateTrendLine(startDate, startValue, endDate, endValue, this.interval, this.chart, hoveringOver.ray) let logical if (chartTimeToDate(hoveringOver.data[hoveringOver.data.length - 1].time).getTime() >= chartTimeToDate(this.chart.candleData[this.chart.candleData.length - 1].time).getTime()) { logical = this.chart.chart.timeScale().getVisibleLogicalRange() } hoveringOver.from = hoveringOver.data[0].time hoveringOver.to = hoveringOver.data[hoveringOver.data.length - 1].time hoveringOver.line.setData(hoveringOver.data) if (logical) this.chart.chart.timeScale().setVisibleLogicalRange(logical) if (!hoveringOver.ray) { hoveringOver.markers = [ {time: startDate, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1}, {time: endDate, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1} ] hoveringOver.line.setMarkers(hoveringOver.markers) } originalIndex = param.logical originalPrice = priceAtCursor this.chart.chart.subscribeCrosshairMove(checkForDrag) } let crosshairHandlerTrend = (param) => { if (!param.point) return this.chart.chart.unsubscribeCrosshairMove(crosshairHandlerTrend) if (!mouseDown) return let currentPrice = this.chart.series.coordinateToPrice(param.point.y) let currentTime = this.chart.chart.timeScale().coordinateToTime(param.point.x) let [firstTime, firstPrice] = [null, null] if (clickedEnd === 'last') { firstTime = hoveringOver.data[0].time firstPrice = hoveringOver.data[0].value } else if (clickedEnd === 'first') { firstTime = hoveringOver.data[hoveringOver.data.length - 1].time firstPrice = hoveringOver.data[hoveringOver.data.length - 1].value } let logical let lastCandleTime = this.chart.candleData[this.chart.candleData.length - 1].time if (!currentTime) { let barsToMove = param.logical - this.chart.chart.timeScale().coordinateToLogical(this.chart.chart.timeScale().timeToCoordinate(lastCandleTime)) logical = barsToMove <= 0 ? null : this.chart.chart.timeScale().getVisibleLogicalRange() currentTime = dateToChartTime(new Date(chartTimeToDate(this.chart.candleData[this.chart.candleData.length - 1].time).getTime() + (barsToMove * this.interval)), this.interval) } else if (chartTimeToDate(lastCandleTime).getTime() <= chartTimeToDate(currentTime).getTime()) { logical = this.chart.chart.timeScale().getVisibleLogicalRange() } hoveringOver.data = calculateTrendLine(firstTime, firstPrice, currentTime, currentPrice, this.interval, this.chart) hoveringOver.line.setData(hoveringOver.data) hoveringOver.from = hoveringOver.data[0].time hoveringOver.to = hoveringOver.data[hoveringOver.data.length - 1].time if (logical) this.chart.chart.timeScale().setVisibleLogicalRange(logical) hoveringOver.markers = [ {time: firstTime, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1}, {time: currentTime, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1} ] hoveringOver.line.setMarkers(hoveringOver.markers) setTimeout(() => { this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend) }, 10); } let crosshairHandlerHorz = (param) => { if (!param.point) return this.chart.chart.unsubscribeCrosshairMove(crosshairHandlerHorz) if (!mouseDown) return hoveringOver.updatePrice(this.chart.series.coordinateToPrice(param.point.y)) setTimeout(() => { this.chart.chart.subscribeCrosshairMove(crosshairHandlerHorz) }, 10) } this.chart.chart.subscribeCrosshairMove(hoverOver) } renderDrawings() { //let logical = this.chart.chart.timeScale().getVisibleLogicalRange() this.drawings.forEach((item) => { if ('price' in item) return let startDate = dateToChartTime(new Date(Math.round(chartTimeToDate(item.from).getTime() / this.interval) * this.interval), this.interval) let endDate = dateToChartTime(new Date(Math.round(chartTimeToDate(item.to).getTime() / this.interval) * this.interval), this.interval) let data = calculateTrendLine(startDate, item.data[0].value, endDate, item.data[item.data.length - 1].value, this.interval, this.chart, item.ray) if (data.length !== 0) item.data = data item.line.setData(data) }) //this.chart.chart.timeScale().setVisibleLogicalRange(logical) } clearDrawings() { this.drawings.forEach((item) => { if ('price' in item) this.chart.series.removePriceLine(item.line) else this.chart.chart.removeSeries(item.line) }) this.drawings = [] } saveDrawings() { let drawingsString = JSON.stringify(this.drawings, (key, value) => { if (key === '' && Array.isArray(value)) { return value.filter(item => !(item && typeof item === 'object' && 'priceLine' in item && item.id !== 'toolBox')); } else if (key === 'line' || (value && typeof value === 'object' && 'priceLine' in value && value.id !== 'toolBox')) { return undefined; } return value; }); this.chart.callbackFunction(`save_drawings__${this.chart.id}__${drawingsString}`) } loadDrawings(drawings) { this.drawings = drawings this.chart.chart.applyOptions({ handleScroll: false }) let logical = this.chart.chart.timeScale().getVisibleLogicalRange() this.drawings.forEach((item) => { if ('price' in item) { this.drawings[this.drawings.indexOf(item)] = new HorizontalLine(this.chart, 'toolBox', item.priceLine.price, item.priceLine.color, 2, item.priceLine.lineStyle, item.priceLine.axisLabelVisible) } else { this.drawings[this.drawings.indexOf(item)].line = this.chart.chart.addLineSeries({ lineWidth: 2, lastValueVisible: false, priceLineVisible: false, crosshairMarkerVisible: false, autoscaleInfoProvider: () => ({ priceRange: { minValue: 1_000_000_000, maxValue: 0, }, }), }) let startDate = dateToChartTime(new Date(Math.round(chartTimeToDate(item.from).getTime() / this.interval) * this.interval), this.interval) let endDate = dateToChartTime(new Date(Math.round(chartTimeToDate(item.to).getTime() / this.interval) * this.interval), this.interval) let data = calculateTrendLine(startDate, item.data[0].value, endDate, item.data[item.data.length - 1].value, this.interval, this.chart, item.ray) if (data.length !== 0) item.data = data item.line.setData(data) } }) this.chart.chart.applyOptions({ handleScroll: true }) this.chart.chart.timeScale().setVisibleLogicalRange(logical) } } window.ToolBox = ToolBox }