Polygon:
- Added async methods to polygon. - The `requests` library is no longer required, with `urllib` being used instead. - Added the `get_bar_data` function, which returns a dataframe of aggregate data from polygon. - Opened up the `subscribe` and `unsubscribe` functions Enhancements: - Tables will now scroll when the rows exceed table height. Bugs: - Fixed a bug preventing async functions being used with horizontal line event. - Fixed a bug causing the legend to show duplicate lines if the line was created after the legend. - Fixed a bug causing the line hide icon to persist within the legend after deletion (#75) - Fixed a bug causing the search box to be unfocused when the chart is loaded.
This commit is contained in:
BIN
docs/source/_static/fonts/MaisonNeue-Demi.otf
Normal file
BIN
docs/source/_static/fonts/MaisonNeue-Demi.otf
Normal file
Binary file not shown.
BIN
docs/source/_static/fonts/MaisonNeue-Medium.otf
Normal file
BIN
docs/source/_static/fonts/MaisonNeue-Medium.otf
Normal file
Binary file not shown.
BIN
docs/source/_static/fonts/MaisonNeue-MediumItalic.otf
Normal file
BIN
docs/source/_static/fonts/MaisonNeue-MediumItalic.otf
Normal file
Binary file not shown.
478
docs/source/_static/funcs.js
Normal file
478
docs/source/_static/funcs.js
Normal file
@ -0,0 +1,478 @@
|
||||
if (!window.Chart) {
|
||||
class Chart {
|
||||
constructor(chartId, innerWidth, innerHeight, position, autoSize) {
|
||||
this.makeCandlestickSeries = this.makeCandlestickSeries.bind(this)
|
||||
this.reSize = this.reSize.bind(this)
|
||||
this.id = chartId
|
||||
this.lines = []
|
||||
this.wrapper = document.createElement('div')
|
||||
this.div = document.createElement('div')
|
||||
this.scale = {
|
||||
width: innerWidth,
|
||||
height: innerHeight,
|
||||
}
|
||||
this.container = document.getElementById('wrapper')
|
||||
this.commandFunctions = []
|
||||
this.chart = LightweightCharts.createChart(this.div, {
|
||||
width: this.container.clientWidth * innerWidth,
|
||||
height: this.container.clientHeight * innerHeight,
|
||||
layout: {
|
||||
textColor: '#d1d4dc',
|
||||
background: {
|
||||
color: '#000000',
|
||||
type: LightweightCharts.ColorType.Solid,
|
||||
},
|
||||
fontSize: 12
|
||||
},
|
||||
rightPriceScale: {
|
||||
scaleMargins: {top: 0.3, bottom: 0.25},
|
||||
},
|
||||
timeScale: {timeVisible: true, secondsVisible: false},
|
||||
crosshair: {
|
||||
mode: LightweightCharts.CrosshairMode.Normal,
|
||||
vertLine: {
|
||||
labelBackgroundColor: 'rgb(46, 46, 46)'
|
||||
},
|
||||
horzLine: {
|
||||
labelBackgroundColor: 'rgb(55, 55, 55)'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
vertLines: {color: 'rgba(29, 30, 38, 5)'},
|
||||
horzLines: {color: 'rgba(29, 30, 58, 5)'},
|
||||
},
|
||||
handleScroll: {vertTouchDrag: true},
|
||||
})
|
||||
this.wrapper.style.width = `${100 * innerWidth}%`
|
||||
this.wrapper.style.height = `${100 * innerHeight}%`
|
||||
this.wrapper.style.display = 'flex'
|
||||
this.wrapper.style.flexDirection = 'column'
|
||||
this.wrapper.style.position = 'relative'
|
||||
this.wrapper.style.display = 'flex'
|
||||
this.wrapper.style.float = position
|
||||
|
||||
this.div.style.position = 'relative'
|
||||
this.div.style.display = 'flex'
|
||||
this.wrapper.appendChild(this.div)
|
||||
document.getElementById('wrapper').append(this.wrapper)
|
||||
|
||||
document.addEventListener('keydown', (event) => {
|
||||
for (let i = 0; i < this.commandFunctions.length; i++) {
|
||||
if (this.commandFunctions[i](event)) break
|
||||
}
|
||||
})
|
||||
if (!autoSize) return
|
||||
window.addEventListener('resize', () => this.reSize())
|
||||
}
|
||||
reSize() {
|
||||
let topBarOffset = 'topBar' in this ? this.topBar.offsetHeight : 0
|
||||
this.chart.resize(this.container.clientWidth * this.scale.width, (this.container.clientHeight * this.scale.height) - topBarOffset)
|
||||
}
|
||||
makeCandlestickSeries() {
|
||||
this.markers = []
|
||||
this.horizontal_lines = []
|
||||
this.candleData = []
|
||||
this.precision = 2
|
||||
let up = 'rgba(39, 157, 130, 100)'
|
||||
let down = 'rgba(200, 97, 100, 100)'
|
||||
this.series = this.chart.addCandlestickSeries({
|
||||
color: 'rgb(0, 120, 255)', upColor: up, borderUpColor: up, wickUpColor: up,
|
||||
downColor: down, borderDownColor: down, wickDownColor: down, lineWidth: 2,
|
||||
})
|
||||
this.volumeSeries = this.chart.addHistogramSeries({
|
||||
color: '#26a69a',
|
||||
priceFormat: {type: 'volume'},
|
||||
priceScaleId: '',
|
||||
})
|
||||
this.series.priceScale().applyOptions({
|
||||
scaleMargins: {top: 0.2, bottom: 0.2},
|
||||
});
|
||||
this.volumeSeries.priceScale().applyOptions({
|
||||
scaleMargins: {top: 0.8, bottom: 0},
|
||||
});
|
||||
}
|
||||
toJSON() {
|
||||
// Exclude the chart attribute from serialization
|
||||
const {chart, ...serialized} = this;
|
||||
return serialized;
|
||||
}
|
||||
}
|
||||
window.Chart = Chart
|
||||
|
||||
class HorizontalLine {
|
||||
constructor(chart, lineId, price, color, width, style, axisLabelVisible, text) {
|
||||
this.updatePrice = this.updatePrice.bind(this)
|
||||
this.deleteLine = this.deleteLine.bind(this)
|
||||
this.chart = chart
|
||||
this.price = price
|
||||
this.color = color
|
||||
this.id = lineId
|
||||
this.priceLine = {
|
||||
price: this.price,
|
||||
color: this.color,
|
||||
lineWidth: width,
|
||||
lineStyle: style,
|
||||
axisLabelVisible: axisLabelVisible,
|
||||
title: text,
|
||||
}
|
||||
this.line = this.chart.series.createPriceLine(this.priceLine)
|
||||
this.chart.horizontal_lines.push(this)
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
// Exclude the chart attribute from serialization
|
||||
const {chart, line, ...serialized} = this;
|
||||
return serialized;
|
||||
}
|
||||
|
||||
updatePrice(price) {
|
||||
this.chart.series.removePriceLine(this.line)
|
||||
this.price = price
|
||||
this.priceLine.price = this.price
|
||||
this.line = this.chart.series.createPriceLine(this.priceLine)
|
||||
}
|
||||
|
||||
updateLabel(text) {
|
||||
this.chart.series.removePriceLine(this.line)
|
||||
this.priceLine.title = text
|
||||
this.line = this.chart.series.createPriceLine(this.priceLine)
|
||||
}
|
||||
|
||||
updateColor(color) {
|
||||
this.chart.series.removePriceLine(this.line)
|
||||
this.color = color
|
||||
this.priceLine.color = this.color
|
||||
this.line = this.chart.series.createPriceLine(this.priceLine)
|
||||
}
|
||||
|
||||
deleteLine() {
|
||||
this.chart.series.removePriceLine(this.line)
|
||||
this.chart.horizontal_lines.splice(this.chart.horizontal_lines.indexOf(this))
|
||||
delete this
|
||||
}
|
||||
}
|
||||
|
||||
window.HorizontalLine = HorizontalLine
|
||||
|
||||
class Legend {
|
||||
constructor(chart, ohlcEnabled, percentEnabled, linesEnabled,
|
||||
color = 'rgb(191, 195, 203)', fontSize = '11', fontFamily = 'Monaco') {
|
||||
this.div = document.createElement('div')
|
||||
this.div.style.position = 'absolute'
|
||||
this.div.style.zIndex = '3000'
|
||||
this.div.style.pointerEvents = 'none'
|
||||
this.div.style.top = '10px'
|
||||
this.div.style.left = '10px'
|
||||
this.div.style.display = 'flex'
|
||||
this.div.style.flexDirection = 'column'
|
||||
this.div.style.maxWidth = `${(chart.scale.width * 100) - 8}vw`
|
||||
this.div.style.color = color
|
||||
this.div.style.fontSize = fontSize + 'px'
|
||||
this.div.style.fontFamily = fontFamily
|
||||
this.candle = document.createElement('div')
|
||||
|
||||
this.div.appendChild(this.candle)
|
||||
chart.div.appendChild(this.div)
|
||||
|
||||
this.color = color
|
||||
|
||||
this.linesEnabled = linesEnabled
|
||||
this.makeLines(chart)
|
||||
|
||||
let legendItemFormat = (num, decimal) => num.toFixed(decimal).toString().padStart(8, ' ')
|
||||
|
||||
let shorthandFormat = (num) => {
|
||||
if (num >= 1000000) {
|
||||
return (num / 1000000).toFixed(1) + 'M';
|
||||
} else if (num >= 1000) {
|
||||
return (num / 1000).toFixed(1) + 'K';
|
||||
}
|
||||
return num.toString().padStart(8, ' ');
|
||||
}
|
||||
|
||||
chart.chart.subscribeCrosshairMove((param) => {
|
||||
if (param.time) {
|
||||
let data = param.seriesData.get(chart.series);
|
||||
let finalString = '<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'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 syncCrosshairs(childChart, parentChart) {
|
||||
function crosshairHandler (e, thisChart, otherChart, otherHandler) {
|
||||
thisChart.applyOptions({crosshair: { horzLine: {
|
||||
visible: true,
|
||||
labelVisible: true,
|
||||
}}})
|
||||
otherChart.applyOptions({crosshair: { horzLine: {
|
||||
visible: false,
|
||||
labelVisible: false,
|
||||
}}})
|
||||
|
||||
otherChart.unsubscribeCrosshairMove(otherHandler)
|
||||
if (e.time !== undefined) {
|
||||
let xx = otherChart.timeScale().timeToCoordinate(e.time);
|
||||
otherChart.setCrosshairXY(xx,300,true);
|
||||
} else if (e.point !== undefined){
|
||||
otherChart.setCrosshairXY(e.point.x,300,false);
|
||||
}
|
||||
otherChart.subscribeCrosshairMove(otherHandler)
|
||||
}
|
||||
let parent = 0
|
||||
let child = 0
|
||||
let parentCrosshairHandler = (e) => {
|
||||
parent ++
|
||||
if (parent < 10) return
|
||||
child = 0
|
||||
crosshairHandler(e, parentChart, childChart, childCrosshairHandler)
|
||||
}
|
||||
let childCrosshairHandler = (e) => {
|
||||
child ++
|
||||
if (child < 10) return
|
||||
parent = 0
|
||||
crosshairHandler(e, childChart, parentChart, parentCrosshairHandler)
|
||||
}
|
||||
parentChart.subscribeCrosshairMove(parentCrosshairHandler)
|
||||
childChart.subscribeCrosshairMove(childCrosshairHandler)
|
||||
}
|
||||
|
||||
function stampToDate(stampOrBusiness) {
|
||||
return new Date(stampOrBusiness*1000)
|
||||
}
|
||||
function dateToStamp(date) {
|
||||
return Math.floor(date.getTime()/1000)
|
||||
}
|
||||
|
||||
function lastBar(obj) {
|
||||
return obj[obj.length-1]
|
||||
}
|
||||
|
||||
function calculateTrendLine(startDate, startValue, endDate, endValue, interval, chart, ray=false) {
|
||||
let reversed = false
|
||||
if (stampToDate(endDate).getTime() < stampToDate(startDate).getTime()) {
|
||||
reversed = true;
|
||||
[startDate, endDate] = [endDate, startDate];
|
||||
}
|
||||
let startIndex
|
||||
if (stampToDate(startDate).getTime() < stampToDate(chart.candleData[0].time).getTime()) {
|
||||
startIndex = 0
|
||||
}
|
||||
else {
|
||||
startIndex = chart.candleData.findIndex(item => stampToDate(item.time).getTime() === stampToDate(startDate).getTime())
|
||||
}
|
||||
|
||||
if (startIndex === -1) {
|
||||
return []
|
||||
}
|
||||
let endIndex
|
||||
if (ray) {
|
||||
endIndex = chart.candleData.length+1000
|
||||
startValue = endValue
|
||||
}
|
||||
else {
|
||||
endIndex = chart.candleData.findIndex(item => stampToDate(item.time).getTime() === stampToDate(endDate).getTime())
|
||||
if (endIndex === -1) {
|
||||
let barsBetween = (stampToDate(endDate)-stampToDate(chart.candleData[chart.candleData.length-1].time))/interval
|
||||
endIndex = chart.candleData.length-1+barsBetween
|
||||
}
|
||||
}
|
||||
|
||||
let numBars = endIndex-startIndex
|
||||
const rate_of_change = (endValue - startValue) / numBars;
|
||||
const trendData = [];
|
||||
let currentDate = null
|
||||
let iPastData = 0
|
||||
for (let i = 0; i <= numBars; i++) {
|
||||
if (chart.candleData[startIndex+i]) {
|
||||
currentDate = chart.candleData[startIndex+i].time
|
||||
}
|
||||
else {
|
||||
iPastData ++
|
||||
currentDate = dateToStamp(new Date(stampToDate(chart.candleData[chart.candleData.length-1].time).getTime()+(iPastData*interval)))
|
||||
}
|
||||
|
||||
const currentValue = reversed ? startValue + rate_of_change * (numBars - i) : startValue + rate_of_change * i;
|
||||
trendData.push({ time: currentDate, value: currentValue });
|
||||
}
|
||||
return trendData;
|
||||
}
|
||||
|
||||
|
||||
if (!window.ContextMenu) {
|
||||
class ContextMenu {
|
||||
constructor() {
|
||||
this.menu = document.createElement('div')
|
||||
this.menu.style.position = 'absolute'
|
||||
this.menu.style.zIndex = '10000'
|
||||
this.menu.style.background = 'rgb(50, 50, 50)'
|
||||
this.menu.style.color = '#ececed'
|
||||
this.menu.style.display = 'none'
|
||||
this.menu.style.borderRadius = '5px'
|
||||
this.menu.style.padding = '3px 3px'
|
||||
this.menu.style.fontSize = '13px'
|
||||
this.menu.style.cursor = 'default'
|
||||
document.body.appendChild(this.menu)
|
||||
this.hoverItem = null
|
||||
|
||||
let closeMenu = (event) => {
|
||||
if (!this.menu.contains(event.target)) {
|
||||
this.menu.style.display = 'none';
|
||||
this.listen(false)
|
||||
}
|
||||
}
|
||||
|
||||
this.onRightClick = (event) => {
|
||||
event.preventDefault();
|
||||
this.menu.style.left = event.clientX + 'px';
|
||||
this.menu.style.top = event.clientY + 'px';
|
||||
this.menu.style.display = 'block';
|
||||
document.removeEventListener('click', closeMenu)
|
||||
document.addEventListener('click', closeMenu)
|
||||
}
|
||||
}
|
||||
listen(active) {
|
||||
active ? document.addEventListener('contextmenu', this.onRightClick) : document.removeEventListener('contextmenu', this.onRightClick)
|
||||
}
|
||||
menuItem(text, action, hover=false) {
|
||||
let item = document.createElement('span')
|
||||
item.style.display = 'flex'
|
||||
item.style.alignItems = 'center'
|
||||
item.style.justifyContent = 'space-between'
|
||||
item.style.padding = '2px 10px'
|
||||
item.style.margin = '1px 0px'
|
||||
item.style.borderRadius = '3px'
|
||||
this.menu.appendChild(item)
|
||||
|
||||
let elem = document.createElement('span')
|
||||
elem.innerText = text
|
||||
item.appendChild(elem)
|
||||
|
||||
if (hover) {
|
||||
let arrow = document.createElement('span')
|
||||
arrow.innerText = `►`
|
||||
arrow.style.fontSize = '8px'
|
||||
item.appendChild(arrow)
|
||||
}
|
||||
|
||||
elem.addEventListener('mouseover', (event) => {
|
||||
item.style.backgroundColor = 'rgba(0, 122, 255, 0.3)'
|
||||
if (this.hoverItem && this.hoverItem.closeAction) this.hoverItem.closeAction()
|
||||
this.hoverItem = {elem: elem, action: action, closeAction: hover}
|
||||
})
|
||||
elem.addEventListener('mouseout', (event) => item.style.backgroundColor = 'transparent')
|
||||
if (!hover) elem.addEventListener('click', (event) => {action(event); this.menu.style.display = 'none'})
|
||||
else {
|
||||
let timeout
|
||||
elem.addEventListener('mouseover', () => timeout = setTimeout(() => action(item.getBoundingClientRect()), 100))
|
||||
elem.addEventListener('mouseout', () => clearTimeout(timeout))
|
||||
}
|
||||
}
|
||||
separator() {
|
||||
let separator = document.createElement('div')
|
||||
separator.style.width = '90%'
|
||||
separator.style.height = '1px'
|
||||
separator.style.margin = '3px 0px'
|
||||
separator.style.backgroundColor = '#3C434C'
|
||||
this.menu.appendChild(separator)
|
||||
}
|
||||
|
||||
}
|
||||
window.ContextMenu = ContextMenu
|
||||
}
|
||||
|
||||
window.callbackFunction = () => undefined;
|
||||
38761
docs/source/_static/ohlcv.json
Normal file
38761
docs/source/_static/ohlcv.json
Normal file
File diff suppressed because it is too large
Load Diff
7
docs/source/_static/pkg.js
Normal file
7
docs/source/_static/pkg.js
Normal file
File diff suppressed because one or more lines are too long
140
docs/source/_static/splash.css
Normal file
140
docs/source/_static/splash.css
Normal file
@ -0,0 +1,140 @@
|
||||
@font-face {
|
||||
font-family: 'Maison Neue';
|
||||
src: url('fonts/MaisonNeue-Demi.otf') format('opentype');
|
||||
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Maison Neue Italic';
|
||||
src: url('fonts/MaisonNeue-MediumItalic.otf') format('opentype');
|
||||
}
|
||||
|
||||
|
||||
|
||||
.splash-head {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
align-self: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.splash-page-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.theme-button {
|
||||
position: absolute;
|
||||
top: 70px;
|
||||
right: -50px;
|
||||
}
|
||||
|
||||
.top-nav {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.top-nav ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
background-color: var(--color-background-hover);
|
||||
border-radius: 5px;
|
||||
margin: 2rem 0 0;
|
||||
}
|
||||
|
||||
.top-nav li {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.top-nav li a {
|
||||
text-decoration: none;
|
||||
border-radius: 3px;
|
||||
color: var(--color-foreground-primary);
|
||||
padding: 0.3rem 0.6rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
|
||||
}
|
||||
|
||||
.top-nav li a:hover {
|
||||
background-color: var(--color-background-item)
|
||||
}
|
||||
|
||||
|
||||
|
||||
#wrapper {
|
||||
width: 500px;
|
||||
|
||||
height: 350px;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
border: 2px solid var(--color-foreground-muted);
|
||||
box-sizing: border-box;
|
||||
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#main-content {
|
||||
margin: 30px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#curved-arrow {
|
||||
transition: transform 0.1s;
|
||||
transform: rotate(-42deg);
|
||||
padding: 0 3vw;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@media (max-width: 968px) {
|
||||
#curved-arrow {
|
||||
transform: rotate(-70deg) scalex(-1);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 550px) {
|
||||
.splash-head h1 {
|
||||
font-size: 30px;
|
||||
}
|
||||
#main-content {
|
||||
flex-direction: column;
|
||||
}
|
||||
#curved-arrow {
|
||||
padding: 4vw 0;
|
||||
width: 60px;
|
||||
height: 60px
|
||||
}
|
||||
}
|
||||
@media (max-width: 450px) {
|
||||
.splash-head h1 {
|
||||
font-size: 22px;
|
||||
}
|
||||
.splash-head i {
|
||||
font-size: 13px;
|
||||
}
|
||||
.top-nav a {
|
||||
font-size: 12px;
|
||||
}
|
||||
.theme-button {
|
||||
right: -20px;
|
||||
}
|
||||
#wrapper {
|
||||
width: 300px;
|
||||
height: 250px;
|
||||
}
|
||||
}
|
||||
688
docs/source/_static/toolbox.js
Normal file
688
docs/source/_static/toolbox.js
Normal file
@ -0,0 +1,688 @@
|
||||
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.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(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.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 && 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(this.drawings[this.drawings.length - 1])
|
||||
return true
|
||||
}
|
||||
});
|
||||
|
||||
return toolBoxElem
|
||||
}
|
||||
|
||||
makeToolBoxElement(action, keyCmd, paths) {
|
||||
let icon = {
|
||||
elem: document.createElement('div'),
|
||||
action: action,
|
||||
}
|
||||
icon.elem.style.margin = '3px'
|
||||
icon.elem.style.borderRadius = '4px'
|
||||
icon.elem.style.display = 'flex'
|
||||
|
||||
let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
svg.setAttribute("width", "29");
|
||||
svg.setAttribute("height", "29");
|
||||
|
||||
let group = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
||||
group.innerHTML = paths
|
||||
group.setAttribute("fill", this.iconColor)
|
||||
|
||||
svg.appendChild(group)
|
||||
icon.elem.appendChild(svg);
|
||||
|
||||
icon.elem.addEventListener('mouseenter', () => {
|
||||
icon.elem.style.backgroundColor = icon === this.chart.activeIcon ? this.activeBackgroundColor : this.hoverColor
|
||||
})
|
||||
icon.elem.addEventListener('mouseleave', () => {
|
||||
icon.elem.style.backgroundColor = icon === this.chart.activeIcon ? this.activeBackgroundColor : this.backgroundColor
|
||||
})
|
||||
icon.elem.addEventListener('mousedown', () => {
|
||||
icon.elem.style.backgroundColor = icon === this.chart.activeIcon ? this.activeBackgroundColor : this.clickBackgroundColor
|
||||
})
|
||||
icon.elem.addEventListener('mouseup', () => {
|
||||
icon.elem.style.backgroundColor = icon === this.chart.activeIcon ? this.activeBackgroundColor : 'transparent'
|
||||
})
|
||||
icon.elem.addEventListener('click', () => {
|
||||
if (this.chart.activeIcon) {
|
||||
this.chart.activeIcon.elem.style.backgroundColor = this.backgroundColor
|
||||
group.setAttribute("fill", this.iconColor)
|
||||
document.body.style.cursor = 'crosshair'
|
||||
this.chart.cursor = 'crosshair'
|
||||
this.chart.activeIcon.action(false)
|
||||
if (this.chart.activeIcon === icon) {
|
||||
return this.chart.activeIcon = null
|
||||
}
|
||||
}
|
||||
this.chart.activeIcon = icon
|
||||
group.setAttribute("fill", this.activeIconColor)
|
||||
icon.elem.style.backgroundColor = this.activeBackgroundColor
|
||||
document.body.style.cursor = 'crosshair'
|
||||
this.chart.cursor = 'crosshair'
|
||||
this.chart.activeIcon.action(true)
|
||||
})
|
||||
this.chart.commandFunctions.push((event) => {
|
||||
if (event.altKey && event.code === keyCmd) {
|
||||
event.preventDefault()
|
||||
if (this.chart.activeIcon) {
|
||||
this.chart.activeIcon.elem.style.backgroundColor = this.backgroundColor
|
||||
group.setAttribute("fill", this.iconColor)
|
||||
document.body.style.cursor = 'crosshair'
|
||||
this.chart.cursor = 'crosshair'
|
||||
this.chart.activeIcon.action(false)
|
||||
}
|
||||
this.chart.activeIcon = icon
|
||||
group.setAttribute("fill", this.activeIconColor)
|
||||
icon.elem.style.backgroundColor = this.activeBackgroundColor
|
||||
document.body.style.cursor = 'crosshair'
|
||||
this.chart.cursor = 'crosshair'
|
||||
this.chart.activeIcon.action(true)
|
||||
return true
|
||||
}
|
||||
})
|
||||
return icon.elem
|
||||
}
|
||||
|
||||
onTrendSelect(toggle, ray = false) {
|
||||
let trendLine = {
|
||||
line: null,
|
||||
color: 'rgb(15, 139, 237)',
|
||||
markers: null,
|
||||
data: null,
|
||||
from: null,
|
||||
to: null,
|
||||
ray: ray,
|
||||
}
|
||||
let firstTime = null
|
||||
let firstPrice = null
|
||||
let currentTime = null
|
||||
|
||||
|
||||
if (!toggle) {
|
||||
this.chart.chart.unsubscribeClick(this.clickHandler)
|
||||
return
|
||||
}
|
||||
let crosshairHandlerTrend = (param) => {
|
||||
this.chart.chart.unsubscribeCrosshairMove(crosshairHandlerTrend)
|
||||
|
||||
if (!this.makingDrawing) return
|
||||
|
||||
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: false})
|
||||
let logical = this.chart.chart.timeScale().getVisibleLogicalRange()
|
||||
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))
|
||||
currentTime = dateToStamp(new Date(stampToDate(this.chart.candleData[this.chart.candleData.length - 1].time).getTime() + (barsToMove * this.interval)))
|
||||
}
|
||||
let currentPrice = this.chart.series.coordinateToPrice(param.point.y)
|
||||
|
||||
|
||||
if (!currentTime) return this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
|
||||
let data = calculateTrendLine(firstTime, firstPrice, currentTime, currentPrice, this.interval, this.chart, ray)
|
||||
trendLine.from = [data[0].time, data[0].value]
|
||||
trendLine.to = [data[data.length - 1].time, data[data.length-1].value]
|
||||
|
||||
trendLine.line.setData(data)
|
||||
|
||||
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: true})
|
||||
this.chart.chart.timeScale().setVisibleLogicalRange(logical)
|
||||
|
||||
if (!ray) {
|
||||
trendLine.markers = [
|
||||
{time: firstTime, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1},
|
||||
{time: currentTime, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1}
|
||||
]
|
||||
trendLine.line.setMarkers(trendLine.markers)
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
|
||||
}, 10);
|
||||
}
|
||||
|
||||
this.clickHandler = (param) => {
|
||||
if (!this.makingDrawing) {
|
||||
this.makingDrawing = true
|
||||
trendLine.line = this.chart.chart.addLineSeries({
|
||||
color: 'rgb(15, 139, 237)',
|
||||
lineWidth: 2,
|
||||
lastValueVisible: false,
|
||||
priceLineVisible: false,
|
||||
crosshairMarkerVisible: false,
|
||||
autoscaleInfoProvider: () => ({
|
||||
priceRange: {
|
||||
minValue: 1_000_000_000,
|
||||
maxValue: 0,
|
||||
},
|
||||
}),
|
||||
})
|
||||
firstPrice = this.chart.series.coordinateToPrice(param.point.y)
|
||||
firstTime = !ray ? this.chart.chart.timeScale().coordinateToTime(param.point.x) : this.chart.candleData[this.chart.candleData.length - 1].time
|
||||
this.chart.chart.applyOptions({handleScroll: false})
|
||||
this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
|
||||
}
|
||||
else {
|
||||
this.chart.chart.applyOptions({handleScroll: true})
|
||||
this.makingDrawing = false
|
||||
trendLine.line.setMarkers([])
|
||||
this.drawings.push(trendLine)
|
||||
this.chart.chart.unsubscribeCrosshairMove(crosshairHandlerTrend)
|
||||
this.chart.chart.unsubscribeClick(this.clickHandler)
|
||||
document.body.style.cursor = 'default'
|
||||
this.chart.cursor = 'default'
|
||||
this.chart.activeIcon.elem.style.backgroundColor = this.backgroundColor
|
||||
this.chart.activeIcon = null
|
||||
this.saveDrawings()
|
||||
}
|
||||
}
|
||||
this.chart.chart.subscribeClick(this.clickHandler)
|
||||
}
|
||||
|
||||
clickHandlerHorz = (param) => {
|
||||
let price = this.chart.series.coordinateToPrice(param.point.y)
|
||||
let lineStyle = LightweightCharts.LineStyle.Solid
|
||||
let line = new HorizontalLine(this.chart, 'toolBox', price,'red', 2, lineStyle, true)
|
||||
this.drawings.push(line)
|
||||
this.chart.chart.unsubscribeClick(this.clickHandlerHorz)
|
||||
document.body.style.cursor = 'default'
|
||||
this.chart.cursor = 'default'
|
||||
this.chart.activeIcon.elem.style.backgroundColor = this.backgroundColor
|
||||
this.chart.activeIcon = null
|
||||
this.saveDrawings()
|
||||
}
|
||||
onHorzSelect(toggle) {
|
||||
!toggle ? this.chart.chart.unsubscribeClick(this.clickHandlerHorz) : this.chart.chart.subscribeClick(this.clickHandlerHorz)
|
||||
}
|
||||
|
||||
onRaySelect(toggle) {
|
||||
this.onTrendSelect(toggle, true)
|
||||
}
|
||||
|
||||
subscribeHoverMove() {
|
||||
let hoveringOver = null
|
||||
let x, y
|
||||
let colorPicker = new ColorPicker(this.saveDrawings)
|
||||
|
||||
let onClickDelete = () => this.deleteDrawing(contextMenu.drawing)
|
||||
let onClickColor = (rect) => colorPicker.openMenu(rect, contextMenu.drawing)
|
||||
let contextMenu = new ContextMenu()
|
||||
contextMenu.menuItem('Color Picker', onClickColor, () =>{
|
||||
document.removeEventListener('click', colorPicker.closeMenu)
|
||||
colorPicker.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 (!mouseDown) {
|
||||
document.removeEventListener('mousedown', checkForClick)
|
||||
document.removeEventListener('mouseup', checkForRelease)
|
||||
}
|
||||
}
|
||||
})
|
||||
this.chart.chart.subscribeCrosshairMove(hoverOver)
|
||||
}
|
||||
let originalIndex
|
||||
let originalTime
|
||||
let originalPrice
|
||||
let mouseDown = false
|
||||
let clickedEnd = false
|
||||
let labelColor
|
||||
let checkForClick = (event) => {
|
||||
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) => {
|
||||
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 (!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 => stampToDate(item.time).getTime() === stampToDate(hoveringOver.from[0]).getTime())
|
||||
let endBarIndex = this.chart.candleData.findIndex(item => stampToDate(item.time).getTime() === stampToDate(hoveringOver.to[0]).getTime())
|
||||
|
||||
let startDate
|
||||
let endBar
|
||||
if (hoveringOver.ray) {
|
||||
endBar = this.chart.candleData[startBarIndex + barsToMove]
|
||||
startDate = hoveringOver.to[0]
|
||||
} else {
|
||||
startDate = this.chart.candleData[startBarIndex + barsToMove].time
|
||||
endBar = endBarIndex === -1 ? null : this.chart.candleData[endBarIndex + barsToMove]
|
||||
}
|
||||
|
||||
let endDate = endBar ? endBar.time : dateToStamp(new Date(stampToDate(hoveringOver.to[0]).getTime() + (barsToMove * this.interval)))
|
||||
let startValue = hoveringOver.from[1] + priceDiff
|
||||
let endValue = hoveringOver.to[1] + priceDiff
|
||||
let data = calculateTrendLine(startDate, startValue, endDate, endValue, this.interval, this.chart, hoveringOver.ray)
|
||||
|
||||
|
||||
let logical = this.chart.chart.timeScale().getVisibleLogicalRange()
|
||||
|
||||
hoveringOver.from = [data[0].time, data[0].value]
|
||||
hoveringOver.to = [data[data.length - 1].time, data[data.length - 1].value]
|
||||
hoveringOver.line.setData(data)
|
||||
|
||||
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.from[0]
|
||||
firstPrice = hoveringOver.from[1]
|
||||
} else if (clickedEnd === 'first') {
|
||||
firstTime = hoveringOver.to[0]
|
||||
firstPrice = hoveringOver.to[1]
|
||||
}
|
||||
|
||||
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: false})
|
||||
let logical = this.chart.chart.timeScale().getVisibleLogicalRange()
|
||||
|
||||
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))
|
||||
currentTime = dateToStamp(new Date(stampToDate(this.chart.candleData[this.chart.candleData.length - 1].time).getTime() + (barsToMove * this.interval)))
|
||||
}
|
||||
let data = calculateTrendLine(firstTime, firstPrice, currentTime, currentPrice, this.interval, this.chart)
|
||||
hoveringOver.line.setData(data)
|
||||
|
||||
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: true})
|
||||
this.chart.chart.timeScale().setVisibleLogicalRange(logical)
|
||||
|
||||
hoveringOver.from = [data[0].time, data[0].value]
|
||||
hoveringOver.to = [data[data.length - 1].time, data[data.length - 1].value]
|
||||
|
||||
|
||||
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() {
|
||||
this.drawings.forEach((item) => {
|
||||
if ('price' in item) return
|
||||
let startDate = dateToStamp(new Date(Math.round(stampToDate(item.from[0]).getTime() / this.interval) * this.interval))
|
||||
let endDate = dateToStamp(new Date(Math.round(stampToDate(item.to[0]).getTime() / this.interval) * this.interval))
|
||||
let data = calculateTrendLine(startDate, item.from[1], endDate, item.to[1], this.interval, this.chart, item.ray)
|
||||
item.from = [data[0].time, data[0].value]
|
||||
item.to = [data[data.length - 1].time, data[data.length-1].value]
|
||||
item.line.setData(data)
|
||||
})
|
||||
}
|
||||
|
||||
deleteDrawing(drawing) {
|
||||
if ('price' in drawing) {
|
||||
this.chart.series.removePriceLine(drawing.line)
|
||||
}
|
||||
else {
|
||||
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: false})
|
||||
this.chart.chart.removeSeries(drawing.line);
|
||||
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: true})
|
||||
}
|
||||
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
|
||||
this.chart.chart.applyOptions({handleScroll: false})
|
||||
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: false})
|
||||
this.drawings.forEach((item) => {
|
||||
let idx = this.drawings.indexOf(item)
|
||||
if ('price' in item) {
|
||||
this.drawings[idx] = new HorizontalLine(this.chart, 'toolBox', item.priceLine.price, item.priceLine.color, 2, item.priceLine.lineStyle, item.priceLine.axisLabelVisible)
|
||||
}
|
||||
else {
|
||||
this.drawings[idx].line = this.chart.chart.addLineSeries({
|
||||
lineWidth: 2,
|
||||
color: this.drawings[idx].color,
|
||||
lastValueVisible: false,
|
||||
priceLineVisible: false,
|
||||
crosshairMarkerVisible: false,
|
||||
autoscaleInfoProvider: () => ({
|
||||
priceRange: {
|
||||
minValue: 1_000_000_000,
|
||||
maxValue: 0,
|
||||
},
|
||||
}),
|
||||
})
|
||||
let startDate = dateToStamp(new Date(Math.round(stampToDate(item.from[0]).getTime() / this.interval) * this.interval))
|
||||
let endDate = dateToStamp(new Date(Math.round(stampToDate(item.to[0]).getTime() / this.interval) * this.interval))
|
||||
let data = calculateTrendLine(startDate, item.from[1], endDate, item.to[1], this.interval, this.chart, item.ray)
|
||||
item.from = [data[0].time, data[0].value]
|
||||
item.to = [data[data.length - 1].time, data[data.length-1].value]
|
||||
item.line.setData(data)
|
||||
}
|
||||
})
|
||||
this.chart.chart.applyOptions({handleScroll: true})
|
||||
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: true})
|
||||
}
|
||||
}
|
||||
window.ToolBox = ToolBox
|
||||
}
|
||||
|
||||
if (!window.ColorPicker) {
|
||||
class ColorPicker {
|
||||
constructor(saveDrawings) {
|
||||
this.saveDrawings = saveDrawings
|
||||
|
||||
this.container = document.createElement('div')
|
||||
this.container.style.maxWidth = '170px'
|
||||
this.container.style.backgroundColor = '#191B1E'
|
||||
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 #3C434C'
|
||||
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 = '#3C434C'
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user