519 lines
21 KiB
JavaScript
519 lines
21 KiB
JavaScript
if (!window.Chart) {
|
|
class Chart {
|
|
constructor(chartId, innerWidth, innerHeight, position, autoSize) {
|
|
this.makeCandlestickSeries = this.makeCandlestickSeries.bind(this)
|
|
this.reSize = this.reSize.bind(this)
|
|
this.id = chartId
|
|
this.lines = []
|
|
this.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: '#d1d4dc',
|
|
background: {
|
|
color: '#000000',
|
|
type: LightweightCharts.ColorType.Solid,
|
|
},
|
|
fontSize: 12
|
|
},
|
|
rightPriceScale: {
|
|
scaleMargins: {top: 0.3, bottom: 0.25},
|
|
},
|
|
timeScale: {timeVisible: true, secondsVisible: false},
|
|
crosshair: {
|
|
mode: LightweightCharts.CrosshairMode.Normal,
|
|
vertLine: {
|
|
labelBackgroundColor: 'rgb(46, 46, 46)'
|
|
},
|
|
horzLine: {
|
|
labelBackgroundColor: 'rgb(55, 55, 55)'
|
|
}
|
|
},
|
|
grid: {
|
|
vertLines: {color: 'rgba(29, 30, 38, 5)'},
|
|
horzLines: {color: 'rgba(29, 30, 58, 5)'},
|
|
},
|
|
handleScroll: {vertTouchDrag: true},
|
|
})
|
|
this.wrapper.style.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
|
|
}
|
|
})
|
|
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.legend) {
|
|
if (this.scale.height === 0 || this.scale.width === 0) {
|
|
this.legend.div.style.display = 'none'
|
|
}
|
|
else {
|
|
this.legend.div.style.display = 'flex'
|
|
}
|
|
}
|
|
|
|
}
|
|
makeCandlestickSeries() {
|
|
this.markers = []
|
|
this.horizontal_lines = []
|
|
this.candleData = []
|
|
this.precision = 2
|
|
let up = 'rgba(39, 157, 130, 100)'
|
|
let down = 'rgba(200, 97, 100, 100)'
|
|
this.series = this.chart.addCandlestickSeries({
|
|
color: 'rgb(0, 120, 255)', upColor: up, borderUpColor: up, wickUpColor: up,
|
|
downColor: down, borderDownColor: down, wickDownColor: down, lineWidth: 2,
|
|
})
|
|
this.volumeSeries = this.chart.addHistogramSeries({
|
|
color: '#26a69a',
|
|
priceFormat: {type: 'volume'},
|
|
priceScaleId: '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, ohlcEnabled, percentEnabled, linesEnabled,
|
|
color = 'rgb(191, 195, 203)', fontSize = '11', fontFamily = 'Monaco') {
|
|
this.div = document.createElement('div')
|
|
this.div.style.position = 'absolute'
|
|
this.div.style.zIndex = '3000'
|
|
this.div.style.pointerEvents = 'none'
|
|
this.div.style.top = '10px'
|
|
this.div.style.left = '10px'
|
|
this.div.style.display = 'flex'
|
|
this.div.style.flexDirection = 'column'
|
|
this.div.style.maxWidth = `${(chart.scale.width * 100) - 8}vw`
|
|
this.div.style.color = color
|
|
this.div.style.fontSize = fontSize + 'px'
|
|
this.div.style.fontFamily = fontFamily
|
|
this.candle = document.createElement('div')
|
|
|
|
this.div.appendChild(this.candle)
|
|
chart.div.appendChild(this.div)
|
|
|
|
this.color = color
|
|
|
|
this.linesEnabled = linesEnabled
|
|
this.makeLines(chart)
|
|
|
|
let legendItemFormat = (num, decimal) => num.toFixed(decimal).toString().padStart(8, ' ')
|
|
|
|
let shorthandFormat = (num) => {
|
|
if (num >= 1000000) {
|
|
return (num / 1000000).toFixed(1) + 'M';
|
|
} else if (num >= 1000) {
|
|
return (num / 1000).toFixed(1) + 'K';
|
|
}
|
|
return num.toString().padStart(8, ' ');
|
|
}
|
|
|
|
chart.chart.subscribeCrosshairMove((param) => {
|
|
if (param.time) {
|
|
let data = param.seriesData.get(chart.series);
|
|
let finalString = '<span style="line-height: 1.8;">'
|
|
if (data) {
|
|
this.candle.style.color = ''
|
|
let ohlc = `O ${legendItemFormat(data.open, chart.precision)}
|
|
| H ${legendItemFormat(data.high, chart.precision)}
|
|
| L ${legendItemFormat(data.low, chart.precision)}
|
|
| C ${legendItemFormat(data.close, chart.precision)} `
|
|
let percentMove = ((data.close - data.open) / data.open) * 100
|
|
let percent = `| ${percentMove >= 0 ? '+' : ''}${percentMove.toFixed(2)} %`
|
|
finalString += ohlcEnabled ? ohlc : ''
|
|
finalString += percentEnabled ? percent : ''
|
|
|
|
let volumeData = param.seriesData.get(chart.volumeSeries)
|
|
if (volumeData) finalString += ohlcEnabled ? `<br>V ${shorthandFormat(volumeData.value)}` : ''
|
|
}
|
|
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.line.precision)
|
|
line.div.innerHTML = `<span style="color: ${line.solid};">▨</span> ${line.line.name} : ${price}`
|
|
})
|
|
|
|
} else {
|
|
this.candle.style.color = 'transparent'
|
|
}
|
|
});
|
|
}
|
|
|
|
toJSON() {
|
|
// Exclude the chart attribute from serialization
|
|
const {lines, ...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
|
|
}
|
|
}
|
|
}
|
|
|
|
window.Legend = Legend
|
|
}
|
|
|
|
function syncCharts(childChart, parentChart) {
|
|
syncCrosshairs(childChart.chart, parentChart.chart)
|
|
syncRanges(childChart, parentChart)
|
|
}
|
|
function syncCrosshairs(childChart, parentChart) {
|
|
function crosshairHandler (e, thisChart, otherChart, otherHandler) {
|
|
thisChart.applyOptions({crosshair: { horzLine: {
|
|
visible: true,
|
|
labelVisible: true,
|
|
}}})
|
|
otherChart.applyOptions({crosshair: { horzLine: {
|
|
visible: false,
|
|
labelVisible: false,
|
|
}}})
|
|
|
|
otherChart.unsubscribeCrosshairMove(otherHandler)
|
|
if (e.time !== undefined) {
|
|
let xx = otherChart.timeScale().timeToCoordinate(e.time);
|
|
otherChart.setCrosshairXY(xx,300,true);
|
|
} else if (e.point !== undefined){
|
|
otherChart.setCrosshairXY(e.point.x,300,false);
|
|
}
|
|
otherChart.subscribeCrosshairMove(otherHandler)
|
|
}
|
|
let parent = 0
|
|
let child = 0
|
|
let parentCrosshairHandler = (e) => {
|
|
parent ++
|
|
if (parent < 10) return
|
|
child = 0
|
|
crosshairHandler(e, parentChart, childChart, childCrosshairHandler)
|
|
}
|
|
let childCrosshairHandler = (e) => {
|
|
child ++
|
|
if (child < 10) return
|
|
parent = 0
|
|
crosshairHandler(e, childChart, parentChart, parentCrosshairHandler)
|
|
}
|
|
parentChart.subscribeCrosshairMove(parentCrosshairHandler)
|
|
childChart.subscribeCrosshairMove(childCrosshairHandler)
|
|
}
|
|
function syncRanges(childChart, parentChart) {
|
|
let setChildRange = (timeRange) => childChart.chart.timeScale().setVisibleLogicalRange(timeRange)
|
|
let setParentRange = (timeRange) => parentChart.chart.timeScale().setVisibleLogicalRange(timeRange)
|
|
|
|
parentChart.wrapper.addEventListener('mouseover', (event) => {
|
|
childChart.chart.timeScale().unsubscribeVisibleLogicalRangeChange(setParentRange)
|
|
parentChart.chart.timeScale().subscribeVisibleLogicalRangeChange(setChildRange)
|
|
})
|
|
childChart.wrapper.addEventListener('mouseover', (event) => {
|
|
parentChart.chart.timeScale().unsubscribeVisibleLogicalRangeChange(setChildRange)
|
|
childChart.chart.timeScale().subscribeVisibleLogicalRangeChange(setParentRange)
|
|
})
|
|
parentChart.chart.timeScale().subscribeVisibleLogicalRangeChange(setChildRange)
|
|
}
|
|
|
|
function stampToDate(stampOrBusiness) {
|
|
return new Date(stampOrBusiness*1000)
|
|
}
|
|
function dateToStamp(date) {
|
|
return Math.floor(date.getTime()/1000)
|
|
}
|
|
|
|
function lastBar(obj) {
|
|
return obj[obj.length-1]
|
|
}
|
|
|
|
function calculateTrendLine(startDate, startValue, endDate, endValue, interval, chart, ray=false) {
|
|
let reversed = false
|
|
if (stampToDate(endDate).getTime() < stampToDate(startDate).getTime()) {
|
|
reversed = true;
|
|
[startDate, endDate] = [endDate, startDate];
|
|
}
|
|
let startIndex
|
|
if (stampToDate(startDate).getTime() < stampToDate(chart.candleData[0].time).getTime()) {
|
|
startIndex = 0
|
|
}
|
|
else {
|
|
startIndex = chart.candleData.findIndex(item => stampToDate(item.time).getTime() === stampToDate(startDate).getTime())
|
|
}
|
|
|
|
if (startIndex === -1) {
|
|
return []
|
|
}
|
|
let endIndex
|
|
if (ray) {
|
|
endIndex = chart.candleData.length+1000
|
|
startValue = endValue
|
|
}
|
|
else {
|
|
endIndex = chart.candleData.findIndex(item => stampToDate(item.time).getTime() === stampToDate(endDate).getTime())
|
|
if (endIndex === -1) {
|
|
let barsBetween = (stampToDate(endDate)-stampToDate(chart.candleData[chart.candleData.length-1].time))/interval
|
|
endIndex = chart.candleData.length-1+barsBetween
|
|
}
|
|
}
|
|
|
|
let numBars = endIndex-startIndex
|
|
const rate_of_change = (endValue - startValue) / numBars;
|
|
const trendData = [];
|
|
let currentDate = null
|
|
let iPastData = 0
|
|
for (let i = 0; i <= numBars; i++) {
|
|
if (chart.candleData[startIndex+i]) {
|
|
currentDate = chart.candleData[startIndex+i].time
|
|
}
|
|
else {
|
|
iPastData ++
|
|
currentDate = dateToStamp(new Date(stampToDate(chart.candleData[chart.candleData.length-1].time).getTime()+(iPastData*interval)))
|
|
}
|
|
|
|
const currentValue = reversed ? startValue + rate_of_change * (numBars - i) : startValue + rate_of_change * i;
|
|
trendData.push({ time: currentDate, value: currentValue });
|
|
}
|
|
return trendData;
|
|
}
|
|
|
|
|
|
if (!window.ContextMenu) {
|
|
class ContextMenu {
|
|
constructor() {
|
|
this.menu = document.createElement('div')
|
|
this.menu.style.position = 'absolute'
|
|
this.menu.style.zIndex = '10000'
|
|
this.menu.style.background = 'rgb(50, 50, 50)'
|
|
this.menu.style.color = '#ececed'
|
|
this.menu.style.display = 'none'
|
|
this.menu.style.borderRadius = '5px'
|
|
this.menu.style.padding = '3px 3px'
|
|
this.menu.style.fontSize = '13px'
|
|
this.menu.style.cursor = 'default'
|
|
document.body.appendChild(this.menu)
|
|
this.hoverItem = null
|
|
|
|
let closeMenu = (event) => {
|
|
if (!this.menu.contains(event.target)) {
|
|
this.menu.style.display = 'none';
|
|
this.listen(false)
|
|
}
|
|
}
|
|
|
|
this.onRightClick = (event) => {
|
|
event.preventDefault();
|
|
this.menu.style.left = event.clientX + 'px';
|
|
this.menu.style.top = event.clientY + 'px';
|
|
this.menu.style.display = 'block';
|
|
document.removeEventListener('click', closeMenu)
|
|
document.addEventListener('click', closeMenu)
|
|
}
|
|
}
|
|
listen(active) {
|
|
active ? document.addEventListener('contextmenu', this.onRightClick) : document.removeEventListener('contextmenu', this.onRightClick)
|
|
}
|
|
menuItem(text, action, hover=false) {
|
|
let item = document.createElement('span')
|
|
item.style.display = 'flex'
|
|
item.style.alignItems = 'center'
|
|
item.style.justifyContent = 'space-between'
|
|
item.style.padding = '2px 10px'
|
|
item.style.margin = '1px 0px'
|
|
item.style.borderRadius = '3px'
|
|
this.menu.appendChild(item)
|
|
|
|
let elem = document.createElement('span')
|
|
elem.innerText = text
|
|
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 = '#3C434C'
|
|
this.menu.appendChild(separator)
|
|
}
|
|
|
|
}
|
|
window.ContextMenu = ContextMenu
|
|
}
|
|
|
|
window.callbackFunction = () => undefined; |