NEW FEATURE: Trendlines, Rays and the Toolbox

- Added `trend_line` and `ray_line` to the Common Methods.

- Added the `toolbox` parameter to chart declaration. This allows horizontal lines, trend lines and rays to be drawn on the chart using hotkeys and buttons.
    - cmd-Z will delete the last drawing.
    - Drawings can be moved by clicking and dragging.

- Added the `render_drawings` parameter to `set`, which will keep and re-render the drawings displayed on the chart (useful for multiple timeframes!)

Horizontal Lines
- The `horizontal_line` method now returns a HorizontalLine object, containing the methods `update` and `delete`.
- Added the `interactive` parameter to `horizontal_line`, allowing for callbacks to be emitted to the `on_horizontal_line_move` callback method when the line is dragged to a new price (stop losses, limit orders, etc.).

Enhancements:
- added the `precision` method to the Common Methods, allowing for the number of decimal places shown on the price scale to be declared.
- Lines displayed on legends now have toggle switches, allowing for their visibility to be controlled directly within the chart window.
- when using `set`, the column names can now be capitalised, and the `date` column can be the index.

Changes:
- Merged the `title` method into the `price_line` method.
This commit is contained in:
louisnw
2023-07-16 20:54:32 +01:00
parent 7850821c6a
commit e4459208d2
14 changed files with 1092 additions and 262 deletions

View File

@ -1,4 +1,4 @@
function makeSearchBox(chart, callbackFunction) {
function makeSearchBox(chart) {
let searchWindow = document.createElement('div')
searchWindow.style.position = 'absolute'
searchWindow.style.top = '0'
@ -49,39 +49,28 @@ function makeSearchBox(chart, callbackFunction) {
chart.wrapper.addEventListener('mouseout', (event) => {
selectedChart = false
})
document.addEventListener('keydown', function(event) {
if (!selectedChart) {return}
if (event.altKey && event.code === 'KeyH') {
let price = chart.series.coordinateToPrice(yPrice)
let colorList = [
'rgba(228, 0, 16, 0.7)',
'rgba(255, 133, 34, 0.7)',
'rgba(164, 59, 176, 0.7)',
'rgba(129, 59, 102, 0.7)',
'rgba(91, 20, 248, 0.7)',
'rgba(32, 86, 249, 0.7)',
]
let color = colorList[Math.floor(Math.random()*colorList.length)]
makeHorizontalLine(chart, 0, price, color, 2, LightweightCharts.LineStyle.Solid, true, '')
}
chart.commandFunctions.push((event) => {
if (searchWindow.style.display === 'none') {
if (/^[a-zA-Z0-9]$/.test(event.key)) {
searchWindow.style.display = 'flex';
sBox.focus();
return true
}
else return false
}
else if (event.key === 'Enter') {
callbackFunction(`on_search__${chart.id}__${sBox.value}`)
chart.callbackFunction(`on_search__${chart.id}__${sBox.value}`)
searchWindow.style.display = 'none'
sBox.value = ''
return true
}
else if (event.key === 'Escape') {
searchWindow.style.display = 'none'
sBox.value = ''
return true
}
});
else return false
})
sBox.addEventListener('input', function() {
sBox.value = sBox.value.toUpperCase();
});
@ -115,7 +104,7 @@ function makeSpinner(chart) {
animateSpinner();
}
function makeSwitcher(chart, items, activeItem, callbackFunction, callbackName, activeBackgroundColor, activeColor, inactiveColor, hoverColor) {
function makeSwitcher(chart, items, activeItem, callbackName, activeBackgroundColor, activeColor, inactiveColor, hoverColor) {
let switcherElement = document.createElement('div');
switcherElement.style.margin = '4px 14px'
switcherElement.style.zIndex = '1000'
@ -155,7 +144,7 @@ function makeSwitcher(chart, items, activeItem, callbackFunction, callbackName,
element.style.color = items[index] === item ? 'activeColor' : inactiveColor
});
activeItem = item;
callbackFunction(`${callbackName}__${chart.id}__${item}`);
chart.callbackFunction(`${callbackName}__${chart.id}__${item}`);
}
chart.topBar.appendChild(switcherElement)
makeSeperator(chart.topBar)

View File

@ -1,14 +1,17 @@
function makeChart(innerWidth, innerHeight, autoSize=true) {
function makeChart(callbackFunction, innerWidth, innerHeight, autoSize=true) {
let chart = {
markers: [],
horizontal_lines: [],
lines: [],
wrapper: document.createElement('div'),
div: document.createElement('div'),
legend: document.createElement('div'),
scale: {
width: innerWidth,
height: innerHeight
height: innerHeight,
},
callbackFunction: callbackFunction,
candleData: [],
commandFunctions: []
}
chart.chart = LightweightCharts.createChart(chart.div, {
width: window.innerWidth*innerWidth,
@ -57,15 +60,6 @@ function makeChart(innerWidth, innerHeight, autoSize=true) {
chart.volumeSeries.priceScale().applyOptions({
scaleMargins: {top: 0.8, bottom: 0},
});
chart.legend.style.position = 'absolute'
chart.legend.style.zIndex = '1000'
chart.legend.style.width = `${(chart.scale.width*100)-8}vw`
chart.legend.style.top = '10px'
chart.legend.style.left = '10px'
chart.legend.style.fontFamily = 'Monaco'
chart.legend.style.fontSize = '11px'
chart.legend.style.color = 'rgb(191, 195, 203)'
chart.wrapper.style.width = `${100*innerWidth}%`
chart.wrapper.style.height = `${100*innerHeight}%`
chart.wrapper.style.display = 'flex'
@ -73,35 +67,190 @@ function makeChart(innerWidth, innerHeight, autoSize=true) {
chart.div.style.position = 'relative'
chart.div.style.display = 'flex'
chart.div.appendChild(chart.legend)
chart.wrapper.appendChild(chart.div)
document.getElementById('wrapper').append(chart.wrapper)
if (!autoSize) return chart
document.addEventListener('keydown', (event) => {
for (let i=0; i<chart.commandFunctions.length; i++) {
if (chart.commandFunctions[i](event)) break
}
})
let topBarOffset = 0
window.addEventListener('resize', function() {
if ('topBar' in chart) topBarOffset = chart.topBar.offsetHeight
chart.chart.resize(window.innerWidth*innerWidth, (window.innerHeight*innerHeight)-topBarOffset)
});
if (!autoSize) return chart
window.addEventListener('resize', () => reSize(chart))
return chart
}
function makeHorizontalLine(chart, lineId, price, color, width, style, axisLabelVisible, text) {
let priceLine = {
price: price,
color: color,
lineWidth: width,
lineStyle: style,
axisLabelVisible: axisLabelVisible,
title: text,
};
let line = {
line: chart.series.createPriceLine(priceLine),
price: price,
id: lineId,
};
chart.horizontal_lines.push(line)
function reSize(chart) {
let topBarOffset = 'topBar' in chart ? chart.topBar.offsetHeight : 0
chart.chart.resize(window.innerWidth*chart.scale.width, (window.innerHeight*chart.scale.height)-topBarOffset)
}
if (!window.HorizontalLine) {
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.id = lineId
this.priceLine = {
price: this.price,
color: color,
lineWidth: width,
lineStyle: style,
axisLabelVisible: axisLabelVisible,
title: text,
}
this.line = this.chart.series.createPriceLine(this.priceLine)
this.chart.horizontal_lines.push(this)
}
updatePrice(price) {
this.chart.series.removePriceLine(this.line)
this.price = price
this.priceLine.price = this.price
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 = true, percentEnabled = true, linesEnabled = true,
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.top = '10px'
this.div.style.left = '10px'
this.div.style.display = 'flex'
this.div.style.flexDirection = 'column'
this.div.style.width = `${(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) => num.toFixed(2).toString().padStart(8, ' ')
chart.chart.subscribeCrosshairMove((param) => {
if (param.time) {
let data = param.seriesData.get(chart.series);
let finalString = '<span style="line-height: 1.8;">'
if (data) {
this.candle.style.color = ''
let ohlc = `O ${legendItemFormat(data.open)}
| H ${legendItemFormat(data.high)}
| L ${legendItemFormat(data.low)}
| C ${legendItemFormat(data.close)} `
let percentMove = ((data.close - data.open) / data.open) * 100
let percent = `| ${percentMove >= 0 ? '+' : ''}${percentMove.toFixed(2)} %`
finalString += ohlcEnabled ? ohlc : ''
finalString += percentEnabled ? percent : ''
}
this.candle.innerHTML = finalString + '</span>'
this.lines.forEach((line) => {
if (!param.seriesData.get(line.line.series)) return
let price = legendItemFormat(param.seriesData.get(line.line.series).value)
line.div.innerHTML = `<span style="color: ${line.line.color};">▨</span> ${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 = `
<path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:${this.color};stroke-opacity:1;stroke-miterlimit:4;" d="M 21.998437 12 C 21.998437 12 18.998437 18 12 18 C 5.001562 18 2.001562 12 2.001562 12 C 2.001562 12 5.001562 6 12 6 C 18.998437 6 21.998437 12 21.998437 12 Z M 21.998437 12 " transform="matrix(0.833333,0,0,0.833333,0,0)"/>
<path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:${this.color};stroke-opacity:1;stroke-miterlimit:4;" d="M 15 12 C 15 13.654687 13.654687 15 12 15 C 10.345312 15 9 13.654687 9 12 C 9 10.345312 10.345312 9 12 9 C 13.654687 9 15 10.345312 15 12 Z M 15 12 " transform="matrix(0.833333,0,0,0.833333,0,0)"/>\`
`
let closedEye = `
<path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:${this.color};stroke-opacity:1;stroke-miterlimit:4;" d="M 20.001562 9 C 20.001562 9 19.678125 9.665625 18.998437 10.514062 M 12 14.001562 C 10.392187 14.001562 9.046875 13.589062 7.95 12.998437 M 12 14.001562 C 13.607812 14.001562 14.953125 13.589062 16.05 12.998437 M 12 14.001562 L 12 17.498437 M 3.998437 9 C 3.998437 9 4.354687 9.735937 5.104687 10.645312 M 7.95 12.998437 L 5.001562 15.998437 M 7.95 12.998437 C 6.689062 12.328125 5.751562 11.423437 5.104687 10.645312 M 16.05 12.998437 L 18.501562 15.998437 M 16.05 12.998437 C 17.38125 12.290625 18.351562 11.320312 18.998437 10.514062 M 5.104687 10.645312 L 2.001562 12 M 18.998437 10.514062 L 21.998437 12 " transform="matrix(0.833333,0,0,0.833333,0,0)"/>
`
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'
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,
}
}
}
window.Legend = Legend
}
function syncCrosshairs(childChart, parentChart) {
function crosshairHandler (e, thisChart, otherChart, otherHandler) {
@ -139,4 +288,109 @@ function syncCrosshairs(childChart, parentChart) {
}
parentChart.subscribeCrosshairMove(parentCrosshairHandler)
childChart.subscribeCrosshairMove(childCrosshairHandler)
}
}
function chartTimeToDate(stampOrBusiness) {
if (typeof stampOrBusiness === 'number') {
stampOrBusiness = new Date(stampOrBusiness*1000)
}
else if (typeof stampOrBusiness === 'string') {
let [year, month, day] = stampOrBusiness.split('-').map(Number)
stampOrBusiness = new Date(Date.UTC(year, month-1, day))
}
else {
stampOrBusiness = new Date(Date.UTC(stampOrBusiness.year, stampOrBusiness.month - 1, stampOrBusiness.day))
}
return stampOrBusiness
}
function dateToChartTime(date, interval) {
if (interval >= 24*60*60*1000) {
return {day: date.getUTCDate(), month: date.getUTCMonth()+1, year: date.getUTCFullYear()}
}
return Math.floor(date.getTime()/1000)
}
function calculateTrendLine(startDate, startValue, endDate, endValue, interval, chart, ray=false) {
let reversed = false
if (chartTimeToDate(endDate).getTime() < chartTimeToDate(startDate).getTime()) {
reversed = true;
[startDate, endDate] = [endDate, startDate];
}
let startIndex
if (chartTimeToDate(startDate).getTime() < chartTimeToDate(chart.candleData[0].time).getTime()) {
startIndex = 0
}
else {
startIndex = chart.candleData.findIndex(item => chartTimeToDate(item.time).getTime() === chartTimeToDate(startDate).getTime())
}
if (startIndex === -1) {
return []
}
let endIndex
if (ray) {
endIndex = chart.candleData.length+1000
startValue = endValue
}
else {
endIndex = chart.candleData.findIndex(item => chartTimeToDate(item.time).getTime() === chartTimeToDate(endDate).getTime())
if (endIndex === -1) {
let barsBetween = (chartTimeToDate(endDate)-chartTimeToDate(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 = dateToChartTime(new Date(chartTimeToDate(chart.candleData[chart.candleData.length-1].time).getTime()+(iPastData*interval)), interval)
}
const currentValue = reversed ? startValue + rate_of_change * (numBars - i) : startValue + rate_of_change * i;
trendData.push({ time: currentDate, value: currentValue });
}
return trendData;
}
/*
let customMenu = document.createElement('div')
customMenu.style.position = 'absolute'
customMenu.style.zIndex = '10000'
customMenu.style.background = 'rgba(25, 25, 25, 0.7)'
customMenu.style.color = 'lightgrey'
customMenu.style.display = 'none'
customMenu.style.borderRadius = '5px'
customMenu.style.padding = '5px 10px'
document.body.appendChild(customMenu)
function menuItem(text) {
let elem = document.createElement('div')
elem.innerText = text
customMenu.appendChild(elem)
}
menuItem('Delete drawings')
menuItem('Hide all indicators')
menuItem('Save Chart State')
let closeMenu = (event) => {if (!customMenu.contains(event.target)) customMenu.style.display = 'none';}
document.addEventListener('contextmenu', function (event) {
event.preventDefault(); // Prevent default right-click menu
customMenu.style.left = event.clientX + 'px';
customMenu.style.top = event.clientY + 'px';
customMenu.style.display = 'block';
document.removeEventListener('click', closeMenu)
document.addEventListener('click', closeMenu)
});
*/

View File

@ -0,0 +1,484 @@
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()
}
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', `<rect x="3.84" y="13.67" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -5.9847 14.4482)" width="21.21" height="1.56"/><path d="M23,3.17L20.17,6L23,8.83L25.83,6L23,3.17z M23,7.41L21.59,6L23,4.59L24.41,6L23,7.41z"/><path d="M6,20.17L3.17,23L6,25.83L8.83,23L6,20.17z M6,24.41L4.59,23L6,21.59L7.41,23L6,24.41z"/>`)
let horz = this.makeToolBoxElement(this.onHorzSelect, 'KeyH', `<rect x="4" y="14" width="9" height="1"/><rect x="16" y="14" width="9" height="1"/><path d="M11.67,14.5l2.83,2.83l2.83-2.83l-2.83-2.83L11.67,14.5z M15.91,14.5l-1.41,1.41l-1.41-1.41l1.41-1.41L15.91,14.5z"/>`)
let ray = this.makeToolBoxElement(this.onRaySelect, 'KeyR', `<rect x="8" y="14" width="17" height="1"/><path d="M3.67,14.5l2.83,2.83l2.83-2.83L6.5,11.67L3.67,14.5z M7.91,14.5L6.5,15.91L5.09,14.5l1.41-1.41L7.91,14.5z"/>`)
//let testB = this.makeToolBoxElement(this.onTrendSelect, `<rect x="8" y="6" width="12" height="1"/><rect x="9" y="22" width="11" height="1"/><path d="M3.67,6.5L6.5,9.33L9.33,6.5L6.5,3.67L3.67,6.5z M7.91,6.5L6.5,7.91L5.09,6.5L6.5,5.09L7.91,6.5z"/><path d="M19.67,6.5l2.83,2.83l2.83-2.83L22.5,3.67L19.67,6.5z M23.91,6.5L22.5,7.91L21.09,6.5l1.41-1.41L23.91,6.5z"/><path d="M19.67,22.5l2.83,2.83l2.83-2.83l-2.83-2.83L19.67,22.5z M23.91,22.5l-1.41,1.41l-1.41-1.41l1.41-1.41L23.91,22.5z"/><path d="M3.67,22.5l2.83,2.83l2.83-2.83L6.5,19.67L3.67,22.5z M7.91,22.5L6.5,23.91L5.09,22.5l1.41-1.41L7.91,22.5z"/><rect x="22" y="9" width="1" height="11"/><rect x="6" y="9" width="1" height="11"/>`)
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.length - 1)
}
this.chart.commandFunctions.push((event) => {
if (event.metaKey && event.code === 'KeyZ') {
commandZHandler(this.drawings[this.drawings.length - 1])
return true
}
});
return toolBoxElem
}
makeToolBoxElement(action, keyCmd, paths) {
let elem = document.createElement('div')
elem.style.margin = '3px'
elem.style.borderRadius = '4px'
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)
elem.appendChild(svg);
elem.addEventListener('mouseenter', () => {
elem.style.backgroundColor = elem === this.chart.activeIcon ? this.activeBackgroundColor : this.hoverColor
document.body.style.cursor = 'pointer'
})
elem.addEventListener('mouseleave', () => {
elem.style.backgroundColor = elem === this.chart.activeIcon ? this.activeBackgroundColor : this.backgroundColor
document.body.style.cursor = this.chart.cursor
})
elem.addEventListener('click', () => {
if (this.chart.activeIcon) {
this.chart.activeIcon.style.backgroundColor = this.backgroundColor
group.setAttribute("fill", this.iconColor)
document.body.style.cursor = 'crosshair'
this.chart.cursor = 'crosshair'
action(false)
}
if (this.chart.activeIcon === elem) {
this.chart.activeIcon = null
return
}
this.chart.activeIcon = elem
group.setAttribute("fill", this.activeIconColor)
elem.style.backgroundColor = this.activeBackgroundColor
document.body.style.cursor = 'crosshair'
this.chart.cursor = 'crosshair'
action(true)
})
this.chart.commandFunctions.push((event) => {
if (event.altKey && event.code === keyCmd) {
if (this.chart.activeIcon) {
this.chart.activeIcon.style.backgroundColor = this.backgroundColor
group.setAttribute("fill", this.iconColor)
document.body.style.cursor = 'crosshair'
this.chart.cursor = 'crosshair'
action(false)
}
this.chart.activeIcon = elem
group.setAttribute("fill", this.activeIconColor)
elem.style.backgroundColor = this.activeBackgroundColor
document.body.style.cursor = 'crosshair'
this.chart.cursor = 'crosshair'
action(true)
return true
}
})
return 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.chart.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.chart.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.chart.clickHandler)
document.body.style.cursor = 'default'
this.chart.cursor = 'default'
this.chart.activeIcon.style.backgroundColor = this.backgroundColor
this.chart.activeIcon = null
}
}
this.chart.chart.subscribeClick(this.chart.clickHandler)
}
onHorzSelect(toggle) {
let 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(clickHandlerHorz)
document.body.style.cursor = 'default'
this.chart.cursor = 'default'
this.chart.activeIcon.style.backgroundColor = this.backgroundColor
this.chart.activeIcon = null
}
this.chart.chart.subscribeClick(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) => {
//if (!hoveringOver) return
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)
this.chart.chart.unsubscribeClick(checkForClick)
}
let checkForRelease = (event) => {
mouseDown = false
document.body.style.cursor = 'pointer'
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)
}
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 = []
}
}
window.ToolBox = ToolBox
}