2.0 first commit

This commit is contained in:
louisnw
2024-03-30 15:38:34 +00:00
parent a91ea493d7
commit e9f21b3b0e
69 changed files with 4081 additions and 2107 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,277 +0,0 @@
if (!window.TopBar) {
class TopBar {
constructor(chart) {
this.makeSwitcher = this.makeSwitcher.bind(this)
this.topBar = document.createElement('div')
this.topBar.style.backgroundColor = pane.backgroundColor
this.topBar.style.borderBottom = '2px solid '+pane.borderColor
this.topBar.style.display = 'flex'
this.topBar.style.alignItems = 'center'
let createTopBarContainer = (justification) => {
let div = document.createElement('div')
div.style.display = 'flex'
div.style.alignItems = 'center'
div.style.justifyContent = justification
div.style.flexGrow = '1'
this.topBar.appendChild(div)
return div
}
this.left = createTopBarContainer('flex-start')
this.right = createTopBarContainer('flex-end')
chart.wrapper.prepend(this.topBar)
chart.topBar = this.topBar
this.reSize = () => chart.reSize()
this.reSize()
}
makeSwitcher(items, activeItem, callbackName, align='left') {
let switcherElement = document.createElement('div');
switcherElement.style.margin = '4px 12px'
let widget = {
elem: switcherElement,
callbackName: callbackName,
intervalElements: null,
onItemClicked: null,
}
widget.intervalElements = items.map((item)=> {
let itemEl = document.createElement('button');
itemEl.style.border = 'none'
itemEl.style.padding = '2px 5px'
itemEl.style.margin = '0px 2px'
itemEl.style.fontSize = '13px'
itemEl.style.borderRadius = '4px'
itemEl.style.backgroundColor = item === activeItem ? pane.activeBackgroundColor : 'transparent'
itemEl.style.color = item === activeItem ? pane.activeColor : pane.color
itemEl.innerText = item;
document.body.appendChild(itemEl)
itemEl.style.minWidth = itemEl.clientWidth + 1 + 'px'
document.body.removeChild(itemEl)
itemEl.addEventListener('mouseenter', () => itemEl.style.backgroundColor = item === activeItem ? pane.activeBackgroundColor : pane.hoverBackgroundColor)
itemEl.addEventListener('mouseleave', () => itemEl.style.backgroundColor = item === activeItem ? pane.activeBackgroundColor : 'transparent')
itemEl.addEventListener('mousedown', () => itemEl.style.backgroundColor = item === activeItem ? pane.activeBackgroundColor : pane.clickBackgroundColor)
itemEl.addEventListener('mouseup', () => itemEl.style.backgroundColor = item === activeItem ? pane.activeBackgroundColor : pane.hoverBackgroundColor)
itemEl.addEventListener('click', () => widget.onItemClicked(item))
switcherElement.appendChild(itemEl);
return itemEl;
});
widget.onItemClicked = (item)=> {
if (item === activeItem) return
widget.intervalElements.forEach((element, index) => {
element.style.backgroundColor = items[index] === item ? pane.activeBackgroundColor : 'transparent'
element.style.color = items[index] === item ? pane.activeColor : pane.color
element.style.fontWeight = items[index] === item ? '500' : 'normal'
})
activeItem = item;
window.callbackFunction(`${widget.callbackName}_~_${item}`);
}
this.appendWidget(switcherElement, align, true)
return widget
}
makeTextBoxWidget(text, align='left') {
let textBox = document.createElement('div')
textBox.style.margin = '0px 18px'
textBox.style.fontSize = '16px'
textBox.style.color = pane.color
textBox.innerText = text
this.appendWidget(textBox, align, true)
return textBox
}
makeMenu(items, activeItem, separator, callbackName, align='right') {
let menu = document.createElement('div')
menu.style.position = 'absolute'
menu.style.display = 'none'
menu.style.zIndex = '100000'
menu.style.backgroundColor = pane.backgroundColor
menu.style.borderRadius = '2px'
menu.style.border = '2px solid '+pane.borderColor
menu.style.borderTop = 'none'
menu.style.alignItems = 'flex-start'
menu.style.maxHeight = '80%'
menu.style.overflowY = 'auto'
menu.style.scrollbar
let menuOpen = false
items.forEach(text => {
let button = this.makeButton(text, null, false, false)
button.elem.addEventListener('click', () => {
widget.elem.innerText = button.elem.innerText+' ↓'
window.callbackFunction(`${callbackName}_~_${button.elem.innerText}`)
menu.style.display = 'none'
menuOpen = false
});
button.elem.style.margin = '4px 4px'
button.elem.style.padding = '2px 2px'
menu.appendChild(button.elem)
})
let widget =
this.makeButton(activeItem+' ↓', null, separator, true, align)
widget.elem.addEventListener('click', () => {
menuOpen = !menuOpen
if (!menuOpen) return menu.style.display = 'none'
let rect = widget.elem.getBoundingClientRect()
menu.style.display = 'flex'
menu.style.flexDirection = 'column'
let center = rect.x+(rect.width/2)
menu.style.left = center-(menu.clientWidth/2)+'px'
menu.style.top = rect.y+rect.height+'px'
})
document.body.appendChild(menu)
}
makeButton(defaultText, callbackName, separator, append=true, align='left') {
let button = document.createElement('button')
button.style.border = 'none'
button.style.padding = '2px 5px'
button.style.margin = '4px 10px'
button.style.fontSize = '13px'
button.style.backgroundColor = 'transparent'
button.style.color = pane.color
button.style.borderRadius = '4px'
button.innerText = defaultText;
document.body.appendChild(button)
button.style.minWidth = button.clientWidth+1+'px'
document.body.removeChild(button)
let widget = {
elem: button,
callbackName: callbackName
}
button.addEventListener('mouseenter', () => button.style.backgroundColor = pane.hoverBackgroundColor)
button.addEventListener('mouseleave', () => button.style.backgroundColor = 'transparent')
if (callbackName) {
button.addEventListener('click', () => window.callbackFunction(`${widget.callbackName}_~_${button.innerText}`));
}
button.addEventListener('mousedown', () => {
button.style.backgroundColor = pane.activeBackgroundColor
button.style.color = pane.activeColor
button.style.fontWeight = '500'
})
button.addEventListener('mouseup', () => {
button.style.backgroundColor = pane.hoverBackgroundColor
button.style.color = pane.color
button.style.fontWeight = 'normal'
})
if (append) this.appendWidget(button, align, separator)
return widget
}
makeSeparator(align='left') {
let seperator = document.createElement('div')
seperator.style.width = '1px'
seperator.style.height = '20px'
seperator.style.backgroundColor = pane.borderColor
let div = align === 'left' ? this.left : this.right
div.appendChild(seperator)
}
appendWidget(widget, align, separator) {
let div = align === 'left' ? this.left : this.right
if (separator) {
if (align === 'left') div.appendChild(widget)
this.makeSeparator(align)
if (align === 'right') div.appendChild(widget)
} else div.appendChild(widget)
this.reSize()
}
}
window.TopBar = TopBar
}
function makeSearchBox(chart) {
let searchWindow = document.createElement('div')
searchWindow.style.position = 'absolute'
searchWindow.style.top = '0'
searchWindow.style.bottom = '200px'
searchWindow.style.left = '0'
searchWindow.style.right = '0'
searchWindow.style.margin = 'auto'
searchWindow.style.width = '150px'
searchWindow.style.height = '30px'
searchWindow.style.padding = '5px'
searchWindow.style.zIndex = '1000'
searchWindow.style.alignItems = 'center'
searchWindow.style.backgroundColor = 'rgba(30, 30, 30, 0.9)'
searchWindow.style.border = '2px solid '+pane.borderColor
searchWindow.style.borderRadius = '5px'
searchWindow.style.display = 'none'
let magnifyingGlass = document.createElement('div');
magnifyingGlass.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24px" height="24px" viewBox="0 0 24 24" version="1.1"><path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:lightgray;stroke-opacity:1;stroke-miterlimit:4;" d="M 15 15 L 21 21 M 10 17 C 6.132812 17 3 13.867188 3 10 C 3 6.132812 6.132812 3 10 3 C 13.867188 3 17 6.132812 17 10 C 17 13.867188 13.867188 17 10 17 Z M 10 17 "/></svg>`
let sBox = document.createElement('input');
sBox.type = 'text';
sBox.style.textAlign = 'center'
sBox.style.width = '100px'
sBox.style.marginLeft = '10px'
sBox.style.backgroundColor = pane.mutedBackgroundColor
sBox.style.color = pane.activeColor
sBox.style.fontSize = '20px'
sBox.style.border = 'none'
sBox.style.outline = 'none'
sBox.style.borderRadius = '2px'
searchWindow.appendChild(magnifyingGlass)
searchWindow.appendChild(sBox)
chart.div.appendChild(searchWindow);
let yPrice = null
chart.chart.subscribeCrosshairMove((param) => {
if (param.point) yPrice = param.point.y;
})
chart.commandFunctions.push((event) => {
if (selectedChart !== chart) return false
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' || event.key === 'Escape') {
if (event.key === 'Enter') window.callbackFunction(`search${chart.id}_~_${sBox.value}`)
searchWindow.style.display = 'none'
sBox.value = ''
return true
}
else return false
})
sBox.addEventListener('input', () => sBox.value = sBox.value.toUpperCase())
return {
window: searchWindow,
box: sBox,
}
}
function makeSpinner(chart) {
chart.spinner = document.createElement('div')
chart.spinner.style.width = '30px'
chart.spinner.style.height = '30px'
chart.spinner.style.border = '4px solid rgba(255, 255, 255, 0.6)'
chart.spinner.style.borderTop = '4px solid '+pane.activeBackgroundColor
chart.spinner.style.borderRadius = '50%'
chart.spinner.style.position = 'absolute'
chart.spinner.style.top = '50%'
chart.spinner.style.left = '50%'
chart.spinner.style.zIndex = '1000'
chart.spinner.style.transform = 'translate(-50%, -50%)'
chart.spinner.style.display = 'none'
chart.wrapper.appendChild(chart.spinner)
let rotation = 0;
const speed = 10;
function animateSpinner() {
rotation += speed
chart.spinner.style.transform = `translate(-50%, -50%) rotate(${rotation}deg)`
requestAnimationFrame(animateSpinner)
}
animateSpinner();
}

View File

@ -1,595 +0,0 @@
if (!window.Chart) {
window.pane = {
backgroundColor: '#0c0d0f',
hoverBackgroundColor: '#3c434c',
clickBackgroundColor: '#50565E',
activeBackgroundColor: 'rgba(0, 122, 255, 0.7)',
mutedBackgroundColor: 'rgba(0, 122, 255, 0.3)',
borderColor: '#3C434C',
color: '#d8d9db',
activeColor: '#ececed',
}
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.interval = null
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: pane.color,
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.legend = new Legend(this)
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.reSize()
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
}
})
window.selectedChart = this
this.wrapper.addEventListener('mouseover', (e) => window.selectedChart = this)
if (!autoSize) return
window.addEventListener('resize', () => this.reSize())
}
reSize() {
let topBarOffset = 'topBar' in this && this.scale.height !== 0 ? this.topBar.offsetHeight : 0
this.chart.resize(window.innerWidth * this.scale.width, (window.innerHeight * this.scale.height) - topBarOffset)
this.wrapper.style.width = `${100 * this.scale.width}%`
this.wrapper.style.height = `${100 * this.scale.height}%`
if (this.scale.height === 0 || this.scale.width === 0) {
this.legend.div.style.display = 'none'
if (this.toolBox) {
this.toolBox.elem.style.display = 'none'
}
}
else {
this.legend.div.style.display = 'flex'
if (this.toolBox) {
this.toolBox.elem.style.display = 'flex'
}
}
}
makeCandlestickSeries() {
this.markers = []
this.horizontal_lines = []
this.data = []
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: 'volume_scale',
})
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)
}
updateStyle(style) {
this.chart.series.removePriceLine(this.line)
this.priceLine.lineStyle = style
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) {
this.legendHandler = this.legendHandler.bind(this)
this.chart = chart
this.ohlcEnabled = false
this.percentEnabled = false
this.linesEnabled = false
this.colorBasedOnCandle = false
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.text = document.createElement('span')
this.text.style.lineHeight = '1.8'
this.candle = document.createElement('div')
this.div.appendChild(this.text)
this.div.appendChild(this.candle)
chart.div.appendChild(this.div)
this.makeLines(chart)
chart.chart.subscribeCrosshairMove(this.legendHandler)
}
toJSON() {
// Exclude the chart attribute from serialization
const {lines, chart, ...serialized} = this;
return serialized;
}
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'
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
}
}
legendItemFormat(num, decimal) { return num.toFixed(decimal).toString().padStart(8, ' ') }
shorthandFormat(num) {
const absNum = Math.abs(num)
if (absNum >= 1000000) {
return (num / 1000000).toFixed(1) + 'M';
} else if (absNum >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toString().padStart(8, ' ');
}
legendHandler(param, usingPoint= false) {
let options = this.chart.series.options()
if (!param.time) {
this.candle.style.color = 'transparent'
this.candle.innerHTML = this.candle.innerHTML.replace(options['upColor'], '').replace(options['downColor'], '')
return
}
let data, logical
if (usingPoint) {
let coordinate = this.chart.chart.timeScale().timeToCoordinate(param.time)
logical = this.chart.chart.timeScale().coordinateToLogical(coordinate)
data = this.chart.series.dataByIndex(logical)
}
else {
data = param.seriesData.get(this.chart.series);
}
this.candle.style.color = ''
let str = '<span style="line-height: 1.8;">'
if (data) {
if (this.ohlcEnabled) {
str += `O ${this.legendItemFormat(data.open, this.chart.precision)} `
str += `| H ${this.legendItemFormat(data.high, this.chart.precision)} `
str += `| L ${this.legendItemFormat(data.low, this.chart.precision)} `
str += `| C ${this.legendItemFormat(data.close, this.chart.precision)} `
}
if (this.percentEnabled) {
let percentMove = ((data.close - data.open) / data.open) * 100
let color = percentMove > 0 ? options['upColor'] : options['downColor']
let percentStr = `${percentMove >= 0 ? '+' : ''}${percentMove.toFixed(2)} %`
if (this.colorBasedOnCandle) {
str += `| <span style="color: ${color};">${percentStr}</span>`
} else {
str += '| ' + percentStr
}
}
let volumeData;
if (usingPoint) {
volumeData = this.chart.volumeSeries.dataByIndex(logical)
}
else {
volumeData = param.seriesData.get(this.chart.volumeSeries)
}
if (volumeData) {
str += this.ohlcEnabled ? `<br>V ${this.shorthandFormat(volumeData.value)}` : ''
}
}
this.candle.innerHTML = str + '</span>'
this.lines.forEach((line) => {
if (!this.linesEnabled) {
line.row.style.display = 'none'
return
}
line.row.style.display = 'flex'
let price
if (usingPoint) {
price = line.line.series.dataByIndex(logical)
}
else {
price = param.seriesData.get(line.line.series)
}
if (!price) return
else price = price.value
if (line.line.type === 'histogram') {
price = this.shorthandFormat(price)
} else {
price = this.legendItemFormat(price, line.line.precision)
}
line.div.innerHTML = `<span style="color: ${line.solid};">▨</span> ${line.line.name} : ${price}`
})
}
}
window.Legend = Legend
}
function syncCharts(childChart, parentChart, crosshairOnly= false) {
function crosshairHandler(chart, point) {
if (!point) {
chart.chart.clearCrosshairPosition()
return
}
chart.chart.setCrosshairPosition(point.value || point.close, point.time, chart.series);
chart.legend.legendHandler(point, true)
}
function getPoint(series, param) {
if (!param.time) return null;
return param.seriesData.get(series) || null;
}
let setChildRange, setParentRange;
if (crosshairOnly) {
setChildRange = (timeRange) => { }
setParentRange = (timeRange) => { }
}
else {
setChildRange = (timeRange) => childChart.chart.timeScale().setVisibleLogicalRange(timeRange)
setParentRange = (timeRange) => parentChart.chart.timeScale().setVisibleLogicalRange(timeRange)
}
let setParentCrosshair = (param) => {
crosshairHandler(parentChart, getPoint(childChart.series, param))
}
let setChildCrosshair = (param) => {
crosshairHandler(childChart, getPoint(parentChart.series, param))
}
let selected = parentChart
function addMouseOverListener(thisChart, otherChart, thisCrosshair, otherCrosshair, thisRange, otherRange) {
thisChart.wrapper.addEventListener('mouseover', (event) => {
if (selected === thisChart) return
selected = thisChart
otherChart.chart.timeScale().unsubscribeVisibleLogicalRangeChange(thisRange)
otherChart.chart.unsubscribeCrosshairMove(thisCrosshair)
thisChart.chart.timeScale().subscribeVisibleLogicalRangeChange(otherRange)
thisChart.chart.subscribeCrosshairMove(otherCrosshair)
})
}
addMouseOverListener(parentChart, childChart, setParentCrosshair, setChildCrosshair, setParentRange, setChildRange)
addMouseOverListener(childChart, parentChart, setChildCrosshair, setParentCrosshair, setChildRange, setParentRange)
parentChart.chart.timeScale().subscribeVisibleLogicalRangeChange(setChildRange)
parentChart.chart.subscribeCrosshairMove(setChildCrosshair)
}
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, 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.data[0].time).getTime()) {
startIndex = 0
}
else {
startIndex = chart.data.findIndex(item => stampToDate(item.time).getTime() === stampToDate(startDate).getTime())
}
if (startIndex === -1) {
throw new Error(`Could not calculate start index from time ${stampToDate(startDate)}.`)
}
let endIndex
if (ray) {
endIndex = chart.data.length+1000
startValue = endValue
}
else {
endIndex = chart.data.findIndex(item => stampToDate(item.time).getTime() === stampToDate(endDate).getTime())
if (endIndex === -1) {
let barsBetween = (endDate-lastBar(chart.data).time)/chart.interval
endIndex = chart.data.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.data[startIndex+i]) {
currentDate = chart.data[startIndex+i].time
}
else {
iPastData ++
currentDate = lastBar(chart.data).time+(iPastData*chart.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 = pane.activeColor
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
elem.style.pointerEvents = 'none'
item.appendChild(elem)
if (hover) {
let arrow = document.createElement('span')
arrow.innerText = ``
arrow.style.fontSize = '8px'
arrow.style.pointerEvents = 'none'
item.appendChild(arrow)
}
item.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}
})
item.addEventListener('mouseout', (event) => item.style.backgroundColor = 'transparent')
if (!hover) item.addEventListener('click', (event) => {action(event); this.menu.style.display = 'none'})
else {
let timeout
item.addEventListener('mouseover', () => timeout = setTimeout(() => action(item.getBoundingClientRect()), 100))
item.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 = pane.borderColor
this.menu.appendChild(separator)
}
}
window.ContextMenu = ContextMenu
}
window.callbackFunction = () => undefined;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,228 @@
:root {
--bg-color:#0c0d0f;
--hover-bg-color: #3c434c;
--click-bg-color: #50565E;
--active-bg-color: rgba(0, 122, 255, 0.7);
--muted-bg-color: rgba(0, 122, 255, 0.3);
--border-color: #3C434C;
--color: #d8d9db;
--active-color: #ececed;
}
body {
background-color: rgb(0,0,0);
color: rgba(19, 23, 34, 1);
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
Cantarell, "Helvetica Neue", sans-serif;
}
.handler {
display: flex;
flex-direction: column;
position: relative;
}
.toolbox {
position: absolute;
z-index: 2000;
display: flex;
align-items: center;
top: 25%;
border: 2px solid var(--border-color);
border-left: none;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
background-color: rgba(25, 27, 30, 0.5);
flex-direction: column;
}
.toolbox-button {
margin: 3px;
border-radius: 4px;
display: flex;
background-color: transparent;
}
.toolbox-button:hover {
background-color: rgba(80, 86, 94, 0.7);
}
.toolbox-button:active {
background-color: rgba(90, 106, 104, 0.7);
}
.active-toolbox-button {
background-color: var(--active-bg-color) !important;
}
.active-toolbox-button g {
fill: var(--active-color);
}
.context-menu {
position: absolute;
z-index: 1000;
background: rgb(50, 50, 50);
color: var(--active-color);
display: none;
border-radius: 5px;
padding: 3px 3px;
font-size: 13px;
cursor: default;
}
.context-menu-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 2px 10px;
margin: 1px 0px;
border-radius: 3px;
}
.context-menu-item:hover {
background-color: var(--muted-bg-color);
}
.color-picker {
max-width: 170px;
background-color: var(--bg-color);
position: absolute;
z-index: 10000;
display: none;
flex-direction: column;
align-items: center;
border: 2px solid var(--border-color);
border-radius: 8px;
cursor: default;
}
/* topbar-related */
.topbar {
background-color: var(--bg-color);
border-bottom: 2px solid var(--border-color);
display: flex;
align-items: center;
}
.topbar-container {
display: flex;
align-items: center;
flex-grow: 1;
}
.topbar-button {
border: none;
padding: 2px 5px;
margin: 4px 10px;
font-size: 13px;
border-radius: 4px;
color: var(--color);
background-color: transparent;
}
.topbar-button:hover {
background-color: var(--hover-bg-color)
}
.topbar-button:active {
background-color: var(--click-bg-color);
color: var(--active-color);
font-weight: 500;
}
.switcher-button:active {
background-color: var(--click-bg-color);
color: var(--color);
font-weight: normal;
}
.active-switcher-button {
background-color: var(--active-bg-color) !important;
color: var(--active-color) !important;
font-weight: 500;
}
.topbar-textbox {
margin: 0px 18px;
font-size: 16px;
color: var(--color);
}
.topbar-menu {
position: absolute;
display: none;
z-index: 10000;
background-color: var(--bg-color);
border-radius: 2px;
border: 2px solid var(--border-color);
border-top: none;
align-items: flex-start;
max-height: 80%;
overflow-y: auto;
}
.topbar-separator {
width: 1px;
height: 20px;
background-color: var(--border-color);
}
.searchbox {
position: absolute;
top: 0;
bottom: 200px;
left: 0;
right: 0;
margin: auto;
width: 150px;
height: 30px;
padding: 5px;
z-index: 1000;
align-items: center;
background-color: rgba(30 ,30, 30, 0.9);
border: 2px solid var(--border-color);
border-radius: 5px;
display: flex;
}
.searchbox input {
text-align: center;
width: 100px;
margin-left: 10px;
background-color: var(--muted-bg-color);
color: var(--active-color);
font-size: 20px;
border: none;
outline: none;
border-radius: 2px;
}
.spinner {
width: 30px;
height: 30px;
border: 4px solid rgba(255, 255, 255, 0.6);
border-top: 4px solid var(--active-bg-color);
border-radius: 50%;
position: absolute;
top: 50%;
left: 50%;
z-index: 1000;
transform: translate(-50%, -50%);
display: none;
}
.legend {
position: absolute;
z-index: 3000;
pointer-events: none;
top: 10px;
left: 10px;
display: flex;
flex-direction: column;
}
.legend-toggle-switch {
border-radius: 4px;
margin-left: 10px;
pointer-events: auto;
}
.legend-toggle-switch:hover {
cursor: pointer;
background-color: rgba(50, 50, 50, 0.5);
}

View File

@ -1,166 +0,0 @@
if (!window.Table) {
class Table {
constructor(width, height, headings, widths, alignments, position, draggable = false,
tableBackgroundColor, borderColor, borderWidth, textColors, backgroundColors) {
this.container = document.createElement('div')
this.callbackName = null
this.borderColor = borderColor
this.borderWidth = borderWidth
if (draggable) {
this.container.style.position = 'absolute'
this.container.style.cursor = 'move'
} else {
this.container.style.position = 'relative'
this.container.style.float = position
}
this.container.style.zIndex = '2000'
this.reSize(width, height)
this.container.style.display = 'flex'
this.container.style.flexDirection = 'column'
// this.container.style.justifyContent = 'space-between'
this.container.style.borderRadius = '5px'
this.container.style.color = 'white'
this.container.style.fontSize = '12px'
this.container.style.fontVariantNumeric = 'tabular-nums'
this.table = document.createElement('table')
this.table.style.width = '100%'
this.table.style.borderCollapse = 'collapse'
this.container.style.overflow = 'hidden'
this.rows = {}
this.headings = headings
this.widths = widths.map((width) => `${width * 100}%`)
this.alignments = alignments
let head = this.table.createTHead()
let row = head.insertRow()
for (let i = 0; i < this.headings.length; i++) {
let th = document.createElement('th')
th.textContent = this.headings[i]
th.style.width = this.widths[i]
th.style.letterSpacing = '0.03rem'
th.style.padding = '0.2rem 0px'
th.style.fontWeight = '500'
th.style.textAlign = 'center'
if (i !== 0) th.style.borderLeft = borderWidth+'px solid '+borderColor
th.style.position = 'sticky'
th.style.top = '0'
th.style.backgroundColor = backgroundColors.length > 0 ? backgroundColors[i] : tableBackgroundColor
th.style.color = textColors[i]
row.appendChild(th)
}
let overflowWrapper = document.createElement('div')
overflowWrapper.style.overflowY = 'auto'
overflowWrapper.style.overflowX = 'hidden'
overflowWrapper.style.backgroundColor = tableBackgroundColor
overflowWrapper.appendChild(this.table)
this.container.appendChild(overflowWrapper)
document.getElementById('wrapper').appendChild(this.container)
if (!draggable) return
let offsetX, offsetY;
this.onMouseDown = (event) => {
offsetX = event.clientX - this.container.offsetLeft;
offsetY = event.clientY - this.container.offsetTop;
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
}
let onMouseMove = (event) => {
this.container.style.left = (event.clientX - offsetX) + 'px';
this.container.style.top = (event.clientY - offsetY) + 'px';
}
let onMouseUp = () => {
// Remove the event listeners for dragging
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
}
this.container.addEventListener('mousedown', this.onMouseDown);
}
divToButton(div, callbackString) {
div.addEventListener('mouseover', () => div.style.backgroundColor = 'rgba(60, 60, 60, 0.6)')
div.addEventListener('mouseout', () => div.style.backgroundColor = 'transparent')
div.addEventListener('mousedown', () => div.style.backgroundColor = 'rgba(60, 60, 60)')
div.addEventListener('click', () => window.callbackFunction(callbackString))
div.addEventListener('mouseup', () => div.style.backgroundColor = 'rgba(60, 60, 60, 0.6)')
}
newRow(id, returnClickedCell=false) {
let row = this.table.insertRow()
row.style.cursor = 'default'
for (let i = 0; i < this.headings.length; i++) {
let cell = row.insertCell()
cell.style.width = this.widths[i];
cell.style.textAlign = this.alignments[i];
cell.style.border = this.borderWidth+'px solid '+this.borderColor
if (returnClickedCell) {
this.divToButton(cell, `${this.callbackName}_~_${id};;;${this.headings[i]}`)
}
row[this.headings[i]] = cell
}
if (!returnClickedCell) {
this.divToButton(row, `${this.callbackName}_~_${id}`)
}
this.rows[id] = row
}
deleteRow(id) {
this.table.deleteRow(this.rows[id].rowIndex)
delete this.rows[id]
}
clearRows() {
let numRows = Object.keys(this.rows).length
for (let i = 0; i < numRows; i++)
this.table.deleteRow(-1)
this.rows = {}
}
updateCell(rowId, column, val) {
this.rows[rowId][column].textContent = val
}
makeSection(id, type, numBoxes, func=false) {
let section = document.createElement('div')
section.style.display = 'flex'
section.style.width = '100%'
section.style.padding = '3px 0px'
section.style.backgroundColor = 'rgb(30, 30, 30)'
type === 'footer' ? this.container.appendChild(section) : this.container.prepend(section)
this[type] = []
for (let i = 0; i < numBoxes; i++) {
let textBox = document.createElement('div')
section.appendChild(textBox)
textBox.style.flex = '1'
textBox.style.textAlign = 'center'
if (func) {
this.divToButton(textBox, `${id}_~_${i}`)
textBox.style.borderRadius = '2px'
}
this[type].push(textBox)
}
}
reSize(width, height) {
this.container.style.width = width <= 1 ? width * 100 + '%' : width + 'px'
this.container.style.height = height <= 1 ? height * 100 + '%' : height + 'px'
}
}
window.Table = Table
}

View File

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="">
<head>
<title>lightweight-charts-python</title>
<link rel="stylesheet" href="styles.css">
<script type="importmap">
{
"imports": { "lightweight-charts": "./lightweight-charts.js" }
}
</script>
<meta name="viewport" content ="width=device-width, initial-scale=1">
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
Cantarell, "Helvetica Neue", sans-serif;
}
</style>
</head>
<body>
<div id="container"></div>
<script type="module" src="./bundle.js"></script>
</body>
</html>

View File

@ -1,736 +0,0 @@
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.saveDrawings = this.saveDrawings.bind(this)
this.chart = chart
this.drawings = []
this.chart.cursor = 'default'
this.makingDrawing = false
this.mouseDown = false
this.hoverBackgroundColor = 'rgba(80, 86, 94, 0.7)'
this.clickBackgroundColor = 'rgba(90, 106, 104, 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.border = '2px solid '+pane.borderColor
toolBoxElem.style.borderLeft = 'none'
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, 'KeyB', `<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 && toDelete.id !== 'toolBox') return commandZHandler(this.drawings[this.drawings.indexOf(toDelete) - 1])
this.deleteDrawing(toDelete)
}
this.chart.commandFunctions.push((event) => {
if ((event.metaKey || event.ctrlKey) && event.code === 'KeyZ') {
commandZHandler(lastBar(this.drawings))
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", pane.color)
svg.appendChild(group)
elem.appendChild(svg);
let icon = {elem: elem, action: action}
elem.addEventListener('mouseenter', () => {
elem.style.backgroundColor = icon === this.chart.activeIcon ? pane.activeBackgroundColor : this.hoverBackgroundColor
})
elem.addEventListener('mouseleave', () => {
elem.style.backgroundColor = icon === this.chart.activeIcon ? pane.activeBackgroundColor : 'transparent'
})
elem.addEventListener('mousedown', () => {
elem.style.backgroundColor = icon === this.chart.activeIcon ? pane.activeBackgroundColor : this.clickBackgroundColor
})
elem.addEventListener('mouseup', () => {
elem.style.backgroundColor = icon === this.chart.activeIcon ? pane.activeBackgroundColor : 'transparent'
})
elem.addEventListener('click', () => {
if (this.chart.activeIcon) {
this.chart.activeIcon.elem.style.backgroundColor = 'transparent'
group.setAttribute("fill", pane.color)
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", pane.activeColor)
elem.style.backgroundColor = pane.activeBackgroundColor
document.body.style.cursor = 'crosshair'
this.chart.cursor = 'crosshair'
this.chart.activeIcon.action(true)
})
this.chart.commandFunctions.push((event) => {
if (this.chart !== window.selectedChart) {
return
}
if (event.altKey && event.code === keyCmd) {
event.preventDefault()
if (this.chart.activeIcon) {
this.chart.activeIcon.elem.style.backgroundColor = 'transparent'
group.setAttribute("fill", pane.color)
document.body.style.cursor = 'crosshair'
this.chart.cursor = 'crosshair'
this.chart.activeIcon.action(false)
}
this.chart.activeIcon = icon
group.setAttribute("fill", pane.activeColor)
elem.style.backgroundColor = pane.activeBackgroundColor
document.body.style.cursor = 'crosshair'
this.chart.cursor = 'crosshair'
this.chart.activeIcon.action(true)
return true
}
})
return elem
}
removeActiveAndSave() {
document.body.style.cursor = 'default'
this.chart.cursor = 'default'
this.chart.activeIcon.elem.style.backgroundColor = 'transparent'
this.chart.activeIcon = null
this.saveDrawings()
}
onTrendSelect(toggle, ray = false) {
let trendLine = null
let firstTime = null
let firstPrice = null
let currentTime = null
if (!toggle) {
return this.chart.chart.unsubscribeClick(this.clickHandler)
}
let crosshairHandlerTrend = (param) => {
this.chart.chart.unsubscribeCrosshairMove(crosshairHandlerTrend)
if (!this.makingDrawing) return
currentTime = this.chart.chart.timeScale().coordinateToTime(param.point.x)
if (!currentTime) {
let barsToMove = param.logical - this.chart.data.length-1
currentTime = lastBar(this.chart.data).time+(barsToMove*this.chart.interval)
}
let currentPrice = this.chart.series.coordinateToPrice(param.point.y)
if (!currentTime) return this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
trendLine.calculateAndSet(firstTime, firstPrice, currentTime, currentPrice)
setTimeout(() => {
this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
}, 10);
}
this.clickHandler = (param) => {
if (!this.makingDrawing) {
this.makingDrawing = true
trendLine = new TrendLine(this.chart, 'rgb(15, 139, 237)', ray)
firstPrice = this.chart.series.coordinateToPrice(param.point.y)
firstTime = !ray ? this.chart.chart.timeScale().coordinateToTime(param.point.x) : lastBar(this.chart.data).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)
this.removeActiveAndSave()
}
}
this.chart.chart.subscribeClick(this.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,'red', 2, lineStyle, true)
this.drawings.push(line)
this.chart.chart.unsubscribeClick(clickHandlerHorz)
this.removeActiveAndSave()
}
if (toggle) this.chart.chart.subscribeClick(clickHandlerHorz)
else this.chart.chart.unsubscribeClick(clickHandlerHorz)
}
onRaySelect(toggle) {
this.onTrendSelect(toggle, true)
}
subscribeHoverMove() {
let hoveringOver = null
let x, y
let colorPicker = new ColorPicker(this.saveDrawings)
let stylePicker = new StylePicker(this.saveDrawings)
let onClickDelete = () => this.deleteDrawing(contextMenu.drawing)
let onClickColor = (rect) => colorPicker.openMenu(rect, contextMenu.drawing)
let onClickStyle = (rect) => stylePicker.openMenu(rect, contextMenu.drawing)
let contextMenu = new ContextMenu()
contextMenu.menuItem('Color Picker', onClickColor, () =>{
document.removeEventListener('click', colorPicker.closeMenu)
colorPicker.container.style.display = 'none'
})
contextMenu.menuItem('Style', onClickStyle, () => {
document.removeEventListener('click', stylePicker.closeMenu)
stylePicker.container.style.display = 'none'
})
contextMenu.separator()
contextMenu.menuItem('Delete Drawing', onClickDelete)
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
contextMenu.listen(true)
contextMenu.drawing = drawing
} else if (hoveringOver === drawing) {
if (!horizontal && !drawing.ray) drawing.line.setMarkers([])
document.body.style.cursor = this.chart.cursor
hoveringOver = null
contextMenu.listen(false)
if (!this.mouseDown) {
document.removeEventListener('mousedown', checkForClick)
document.removeEventListener('mouseup', checkForRelease)
}
}
})
this.chart.chart.subscribeCrosshairMove(hoverOver)
}
let originalIndex
let originalTime
let originalPrice
this.mouseDown = false
let clickedEnd = false
let labelColor
let checkForClick = (event) => {
this.mouseDown = true
document.body.style.cursor = 'grabbing'
this.chart.chart.applyOptions({handleScroll: false})
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: false})
this.chart.chart.unsubscribeCrosshairMove(hoverOver)
labelColor = this.chart.chart.options().crosshair.horzLine.labelBackgroundColor
this.chart.chart.applyOptions({crosshair: {horzLine: {labelBackgroundColor: hoveringOver.color}}})
if ('price' in hoveringOver) {
originalPrice = hoveringOver.price
this.chart.chart.subscribeCrosshairMove(crosshairHandlerHorz)
} else if (Math.abs(this.chart.chart.timeScale().timeToCoordinate(hoveringOver.from[0]) - x) < 4 && !hoveringOver.ray) {
clickedEnd = 'first'
this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
} else if (Math.abs(this.chart.chart.timeScale().timeToCoordinate(hoveringOver.to[0]) - 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) => {
this.mouseDown = false
document.body.style.cursor = this.chart.cursor
this.chart.chart.applyOptions({handleScroll: true})
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: true})
this.chart.chart.applyOptions({crosshair: {horzLine: {labelBackgroundColor: labelColor}}})
if (hoveringOver && 'price' in hoveringOver && hoveringOver.id !== 'toolBox') {
window.callbackFunction(`${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 (!this.mouseDown) return
let priceAtCursor = this.chart.series.coordinateToPrice(param.point.y)
let priceDiff = priceAtCursor - originalPrice
let barsToMove = param.logical - originalIndex
let startBarIndex = this.chart.data.findIndex(item => item.time === hoveringOver.from[0])
let endBarIndex = this.chart.data.findIndex(item => item.time === hoveringOver.to[0])
let startDate
let endBar
if (hoveringOver.ray) {
endBar = this.chart.data[startBarIndex + barsToMove]
startDate = hoveringOver.to[0]
} else {
startDate = this.chart.data[startBarIndex + barsToMove].time
endBar = endBarIndex === -1 ? null : this.chart.data[endBarIndex + barsToMove]
}
let endDate = endBar ? endBar.time : hoveringOver.to[0] + (barsToMove * this.chart.interval)
let startValue = hoveringOver.from[1] + priceDiff
let endValue = hoveringOver.to[1] + priceDiff
hoveringOver.calculateAndSet(startDate, startValue, endDate, endValue)
originalIndex = param.logical
originalPrice = priceAtCursor
this.chart.chart.subscribeCrosshairMove(checkForDrag)
}
let crosshairHandlerTrend = (param) => {
if (!param.point) return
this.chart.chart.unsubscribeCrosshairMove(crosshairHandlerTrend)
if (!this.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.from[0]
firstPrice = hoveringOver.from[1]
} else if (clickedEnd === 'first') {
firstTime = hoveringOver.to[0]
firstPrice = hoveringOver.to[1]
}
if (!currentTime) {
let barsToMove = param.logical - this.chart.data.length-1
currentTime = lastBar(this.chart.data).time + (barsToMove*this.chart.interval)
}
hoveringOver.calculateAndSet(firstTime, firstPrice, currentTime, currentPrice)
setTimeout(() => {
this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
}, 10);
}
let crosshairHandlerHorz = (param) => {
if (!param.point) return
this.chart.chart.unsubscribeCrosshairMove(crosshairHandlerHorz)
if (!this.mouseDown) return
hoveringOver.updatePrice(this.chart.series.coordinateToPrice(param.point.y))
setTimeout(() => {
this.chart.chart.subscribeCrosshairMove(crosshairHandlerHorz)
}, 10)
}
this.chart.chart.subscribeCrosshairMove(hoverOver)
}
renderDrawings() {
if (this.mouseDown) return
this.drawings.forEach((item) => {
if ('price' in item) return
let startDate = Math.round(item.from[0]/this.chart.interval)*this.chart.interval
let endDate = Math.round(item.to[0]/this.chart.interval)*this.chart.interval
item.calculateAndSet(startDate, item.from[1], endDate, item.to[1])
})
}
deleteDrawing(drawing) {
if ('price' in drawing) {
this.chart.series.removePriceLine(drawing.line)
}
else {
let range = this.chart.chart.timeScale().getVisibleLogicalRange()
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: false})
this.chart.chart.removeSeries(drawing.line);
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: true})
this.chart.chart.timeScale().setVisibleLogicalRange(range)
}
this.drawings.splice(this.drawings.indexOf(drawing), 1)
this.saveDrawings()
}
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;
});
window.callbackFunction(`save_drawings${this.chart.id}_~_${drawingsString}`)
}
loadDrawings(drawings) {
this.drawings = []
drawings.forEach((item) => {
let drawing = null
if ('price' in item) {
drawing = new HorizontalLine(this.chart, 'toolBox',
item.priceLine.price, item.priceLine.color, 2,
item.priceLine.lineStyle, item.priceLine.axisLabelVisible)
}
else {
let startDate = Math.round((item.from[0]/this.chart.interval)*this.chart.interval)
let endDate = Math.round((item.to[0]/this.chart.interval)*this.chart.interval)
drawing = new TrendLine(this.chart, item.color, item.ray)
drawing.calculateAndSet(startDate, item.from[1], endDate, item.to[1])
}
this.drawings.push(drawing)
})
}
}
window.ToolBox = ToolBox
class TrendLine {
constructor(chart, color, ray) {
this.calculateAndSet = this.calculateAndSet.bind(this)
this.line = chart.chart.addLineSeries({
color: color,
lineWidth: 2,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
autoscaleInfoProvider: () => ({
priceRange: {
minValue: 1_000_000_000,
maxValue: 0,
},
}),
})
this.color = color
this.markers = null
this.data = null
this.from = null
this.to = null
this.ray = ray
this.chart = chart
}
toJSON() {
// Exclude the chart attribute from serialization
const {chart, ...serialized} = this;
return serialized;
}
calculateAndSet(firstTime, firstPrice, currentTime, currentPrice) {
let data = calculateTrendLine(firstTime, firstPrice, currentTime, currentPrice, this.chart, this.ray)
this.from = [data[0].time, data[0].value]
this.to = [data[data.length - 1].time, data[data.length-1].value]
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: false})
let logical = this.chart.chart.timeScale().getVisibleLogicalRange()
this.line.setData(data)
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: true})
this.chart.chart.timeScale().setVisibleLogicalRange(logical)
if (!this.ray) {
this.markers = [
{time: this.from[0], position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1},
{time: this.to[0], position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1}
]
this.line.setMarkers(this.markers)
}
}
}
window.TrendLine = TrendLine
class ColorPicker {
constructor(saveDrawings) {
this.saveDrawings = saveDrawings
this.container = document.createElement('div')
this.container.style.maxWidth = '170px'
this.container.style.backgroundColor = pane.backgroundColor
this.container.style.position = 'absolute'
this.container.style.zIndex = '10000'
this.container.style.display = 'none'
this.container.style.flexDirection = 'column'
this.container.style.alignItems = 'center'
this.container.style.border = '2px solid '+pane.borderColor
this.container.style.borderRadius = '8px'
this.container.style.cursor = 'default'
let colorPicker = document.createElement('div')
colorPicker.style.margin = '10px'
colorPicker.style.display = 'flex'
colorPicker.style.flexWrap = 'wrap'
let colors = [
'#EBB0B0','#E9CEA1','#E5DF80','#ADEB97','#A3C3EA','#D8BDED',
'#E15F5D','#E1B45F','#E2D947','#4BE940','#639AE1','#D7A0E8',
'#E42C2A','#E49D30','#E7D827','#3CFF0A','#3275E4','#B06CE3',
'#F3000D','#EE9A14','#F1DA13','#2DFC0F','#1562EE','#BB00EF',
'#B50911','#E3860E','#D2BD11','#48DE0E','#1455B4','#6E009F',
'#7C1713','#B76B12','#8D7A13','#479C12','#165579','#51007E',
]
colors.forEach((color) => colorPicker.appendChild(this.makeColorBox(color)))
let separator = document.createElement('div')
separator.style.backgroundColor = pane.borderColor
separator.style.height = '1px'
separator.style.width = '130px'
let opacity = document.createElement('div')
opacity.style.margin = '10px'
let opacityText = document.createElement('div')
opacityText.style.color = 'lightgray'
opacityText.style.fontSize = '12px'
opacityText.innerText = 'Opacity'
let opacityValue = document.createElement('div')
opacityValue.style.color = 'lightgray'
opacityValue.style.fontSize = '12px'
let opacitySlider = document.createElement('input')
opacitySlider.type = 'range'
opacitySlider.value = this.opacity*100
opacityValue.innerText = opacitySlider.value+'%'
opacitySlider.oninput = () => {
opacityValue.innerText = opacitySlider.value+'%'
this.opacity = opacitySlider.value/100
this.updateColor()
}
opacity.appendChild(opacityText)
opacity.appendChild(opacitySlider)
opacity.appendChild(opacityValue)
this.container.appendChild(colorPicker)
this.container.appendChild(separator)
this.container.appendChild(opacity)
document.getElementById('wrapper').appendChild(this.container)
}
makeColorBox(color) {
let box = document.createElement('div')
box.style.width = '18px'
box.style.height = '18px'
box.style.borderRadius = '3px'
box.style.margin = '3px'
box.style.boxSizing = 'border-box'
box.style.backgroundColor = color
box.addEventListener('mouseover', (event) => box.style.border = '2px solid lightgray')
box.addEventListener('mouseout', (event) => box.style.border = 'none')
let rgbValues = this.extractRGB(color)
box.addEventListener('click', (event) => {
this.rgbValues = rgbValues
this.updateColor()
})
return box
}
extractRGB = (anyColor) => {
let dummyElem = document.createElement('div');
dummyElem.style.color = anyColor;
document.body.appendChild(dummyElem);
let computedColor = getComputedStyle(dummyElem).color;
document.body.removeChild(dummyElem);
let colorValues = computedColor.match(/\d+/g).map(Number);
let isRgba = computedColor.includes('rgba');
let opacity = isRgba ? parseFloat(computedColor.split(',')[3]) : 1
return [colorValues[0], colorValues[1], colorValues[2], opacity]
}
updateColor() {
let oColor = `rgba(${this.rgbValues[0]}, ${this.rgbValues[1]}, ${this.rgbValues[2]}, ${this.opacity})`
if ('price' in this.drawing) this.drawing.updateColor(oColor)
else {
this.drawing.color = oColor
this.drawing.line.applyOptions({color: oColor})
}
this.saveDrawings()
}
openMenu(rect, drawing) {
this.drawing = drawing
this.rgbValues = this.extractRGB(drawing.color)
this.opacity = parseFloat(this.rgbValues[3])
this.container.style.top = (rect.top-30)+'px'
this.container.style.left = rect.right+'px'
this.container.style.display = 'flex'
setTimeout(() => document.addEventListener('mousedown', (event) => {
if (!this.container.contains(event.target)) {
this.closeMenu()
}
}), 10)
}
closeMenu(event) {
document.removeEventListener('click', this.closeMenu)
this.container.style.display = 'none'
}
}
window.ColorPicker = ColorPicker
class StylePicker {
constructor(saveDrawings) {
this.saveDrawings = saveDrawings
this.container = document.createElement('div')
this.container.style.position = 'absolute'
this.container.style.zIndex = '10000'
this.container.style.background = 'rgb(50, 50, 50)'
this.container.style.color = pane.activeColor
this.container.style.display = 'none'
this.container.style.borderRadius = '5px'
this.container.style.padding = '3px 3px'
this.container.style.fontSize = '13px'
this.container.style.cursor = 'default'
let styles = [
{name: 'Solid', var: LightweightCharts.LineStyle.Solid},
{name: 'Dotted', var: LightweightCharts.LineStyle.Dotted},
{name: 'Dashed', var: LightweightCharts.LineStyle.Dashed},
{name: 'Large Dashed', var: LightweightCharts.LineStyle.LargeDashed},
{name: 'Sparse Dotted', var: LightweightCharts.LineStyle.SparseDotted},
]
styles.forEach((style) => {
this.container.appendChild(this.makeTextBox(style.name, style.var))
})
document.getElementById('wrapper').appendChild(this.container)
}
makeTextBox(text, style) {
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'
item.innerText = text
item.addEventListener('mouseover', (event) => item.style.backgroundColor = pane.mutedBackgroundColor)
item.addEventListener('mouseout', (event) => item.style.backgroundColor = 'transparent')
item.addEventListener('click', (event) => {
this.style = style
this.updateStyle()
})
return item
}
updateStyle() {
if ('price' in this.drawing) this.drawing.updateStyle(this.style)
else {
this.drawing.line.applyOptions({lineStyle: this.style})
}
this.saveDrawings()
}
openMenu(rect, drawing) {
this.drawing = drawing
this.container.style.top = (rect.top-30)+'px'
this.container.style.left = rect.right+'px'
this.container.style.display = 'block'
setTimeout(() => document.addEventListener('mousedown', (event) => {
if (!this.container.contains(event.target)) {
this.closeMenu()
}
}), 10)
}
closeMenu(event) {
document.removeEventListener('click', this.closeMenu)
this.container.style.display = 'none'
}
}
window.StylePicker = StylePicker
}