@ -22,12 +22,12 @@ ___
|
|||||||
## Features
|
## Features
|
||||||
1. Streamlined for live data, with methods for updating directly from tick data.
|
1. Streamlined for live data, with methods for updating directly from tick data.
|
||||||
2. Multi-pane charts using [Subcharts](https://lightweight-charts-python.readthedocs.io/en/latest/reference/abstract_chart.html#AbstractChart.create_subchart).
|
2. Multi-pane charts using [Subcharts](https://lightweight-charts-python.readthedocs.io/en/latest/reference/abstract_chart.html#AbstractChart.create_subchart).
|
||||||
3. The [Toolbox](https://lightweight-charts-python.readthedocs.io/en/latest/reference/toolbox.html), allowing for trendlines, rays and horizontal lines to be drawn directly onto charts.
|
3. The [Toolbox](https://lightweight-charts-python.readthedocs.io/en/latest/reference/toolbox.html), allowing for trendlines, rectangles, rays and horizontal lines to be drawn directly onto charts.
|
||||||
4. [Events](https://lightweight-charts-python.readthedocs.io/en/latest/tutorials/events.html) allowing for timeframe selectors (1min, 5min, 30min etc.), searching, hotkeys, and more.
|
4. [Events](https://lightweight-charts-python.readthedocs.io/en/latest/tutorials/events.html) allowing for timeframe selectors (1min, 5min, 30min etc.), searching, hotkeys, and more.
|
||||||
5. [Tables](https://lightweight-charts-python.readthedocs.io/en/latest/reference/tables.html) for watchlists, order entry, and trade management.
|
5. [Tables](https://lightweight-charts-python.readthedocs.io/en/latest/reference/tables.html) for watchlists, order entry, and trade management.
|
||||||
6. Direct integration of market data through [Polygon.io's](https://polygon.io/?utm_source=affiliate&utm_campaign=pythonlwcharts) market data API.
|
6. Direct integration of market data through [Polygon.io's](https://polygon.io/?utm_source=affiliate&utm_campaign=pythonlwcharts) market data API.
|
||||||
|
|
||||||
__Supports:__ Jupyter Notebooks, PyQt5, PySide6, wxPython, Streamlit, and asyncio.
|
__Supports:__ Jupyter Notebooks, PyQt6, PyQt5, PySide6, wxPython, Streamlit, and asyncio.
|
||||||
|
|
||||||
PartTimeLarry: [Interactive Brokers API and TradingView Charts in Python](https://www.youtube.com/watch?v=TlhDI3PforA)
|
PartTimeLarry: [Interactive Brokers API and TradingView Charts in Python](https://www.youtube.com/watch?v=TlhDI3PforA)
|
||||||
___
|
___
|
||||||
|
|||||||
34
build.sh
Executable file
34
build.sh
Executable file
@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
YELLOW='\033[0;33m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
ERROR="${RED}[ERROR]${NC} "
|
||||||
|
INFO="${CYAN}[INFO]${NC} "
|
||||||
|
WARNING="${WARNING}[WARNING]${NC} "
|
||||||
|
|
||||||
|
rm -rf dist/bundle.js dist/typings/
|
||||||
|
|
||||||
|
if [[ $? -eq 0 ]]; then
|
||||||
|
echo -e "${INFO}deleted bundle.js and typings.."
|
||||||
|
else
|
||||||
|
echo -e "${WARNING}could not delete old dist files, continuing.."
|
||||||
|
fi
|
||||||
|
|
||||||
|
npx rollup -c rollup.config.js
|
||||||
|
if [[ $? -ne 0 ]]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cp dist/bundle.js src/general/styles.css lightweight_charts/js
|
||||||
|
if [[ $? -eq 0 ]]; then
|
||||||
|
echo -e "${INFO}copied bundle.js, style.css into python package"
|
||||||
|
else
|
||||||
|
echo -e "${ERROR}could not copy dist into python package ?"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo -e "\n${GREEN}[BUILD SUCCESS]${NC}"
|
||||||
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
# Alternative GUI's
|
# Alternative GUI's
|
||||||
|
|
||||||
|
|
||||||
## PyQt5 / PySide6
|
## PyQt6 / PyQt5 / PySide6
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|||||||
@ -36,7 +36,7 @@ def on_timeframe_selection(chart):
|
|||||||
if new_data.empty:
|
if new_data.empty:
|
||||||
return
|
return
|
||||||
# The symbol has not changed, so we want to re-render the drawings.
|
# The symbol has not changed, so we want to re-render the drawings.
|
||||||
chart.set(new_data, render_drawings=True)
|
chart.set(new_data, keep_drawings=True)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@ -22,7 +22,7 @@ The `websockets` library is required when using live data.
|
|||||||
|
|
||||||
```{important}
|
```{important}
|
||||||
When using live data and the standard `show` method, the `block` parameter __must__ be set to `True` in order for the data to congregate on the chart (`chart.show(block=True)`).
|
When using live data and the standard `show` method, the `block` parameter __must__ be set to `True` in order for the data to congregate on the chart (`chart.show(block=True)`).
|
||||||
If `show_async` is used with live data, `block` can be either value.
|
`show_async` can also be used with live data.
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ ___
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
```{py:method} set(data: pd.DataFrame, render_drawings: bool = False)
|
```{py:method} set(data: pd.DataFrame, keep_drawings: bool = False)
|
||||||
Sets the initial data for the chart.
|
Sets the initial data for the chart.
|
||||||
|
|
||||||
|
|
||||||
@ -17,9 +17,11 @@ Columns should be named:
|
|||||||
|
|
||||||
Time can be given in the index rather than a column, and volume can be omitted if volume is not used. Column names are not case sensitive.
|
Time can be given in the index rather than a column, and volume can be omitted if volume is not used. Column names are not case sensitive.
|
||||||
|
|
||||||
If `render_drawings` is `True`, any drawings made using the `toolbox` will be redrawn with the new data. This is designed to be used when switching to a different timeframe of the same symbol.
|
If `keep_drawings` is `True`, any drawings made using the `toolbox` will be redrawn with the new data. This is designed to be used when switching to a different timeframe of the same symbol.
|
||||||
|
|
||||||
`None` can also be given, which will erase all candle and volume data displayed on the chart.
|
`None` can also be given, which will erase all candle and volume data displayed on the chart.
|
||||||
|
|
||||||
|
You can also add columns to color the candles (https://tradingview.github.io/lightweight-charts/tutorials/customization/data-points)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
@ -27,7 +29,7 @@ ___
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
```{py:method} update(series: pd.Series, render_drawings: bool = False)
|
```{py:method} update(series: pd.Series, keep_drawings: bool = False)
|
||||||
Updates the chart data from a bar.
|
Updates the chart data from a bar.
|
||||||
|
|
||||||
Series labels should be akin to [`set`](#AbstractChart.set).
|
Series labels should be akin to [`set`](#AbstractChart.set).
|
||||||
|
|||||||
@ -45,7 +45,7 @@ ___
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
```{py:method} show_async(block: bool)
|
```{py:method} show_async()
|
||||||
:async:
|
:async:
|
||||||
|
|
||||||
Show the chart asynchronously.
|
Show the chart asynchronously.
|
||||||
@ -85,7 +85,7 @@ ___
|
|||||||
|
|
||||||
The `QtChart` object allows the use of charts within a `QMainWindow` object, and has similar functionality to the `Chart` object for manipulating data, configuring and styling.
|
The `QtChart` object allows the use of charts within a `QMainWindow` object, and has similar functionality to the `Chart` object for manipulating data, configuring and styling.
|
||||||
|
|
||||||
Either the `PyQt5` or `PySide6` libraries will work with this chart.
|
Either the `PyQt5`, `PyQt6` or `PySide6` libraries will work with this chart.
|
||||||
|
|
||||||
Callbacks can be received through the Qt event loop.
|
Callbacks can be received through the Qt event loop.
|
||||||
___
|
___
|
||||||
|
|||||||
@ -22,6 +22,11 @@ Fires when the range (visibleLogicalRange) changes.
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```{py:method} click -> (chart: Chart, time: NUM, price: NUM)
|
||||||
|
Fires when the mouse is clicked, returning the time and price of the clicked location.
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
````
|
````
|
||||||
|
|
||||||
Tutorial: [Topbar & Events](../tutorials/events.md)
|
Tutorial: [Topbar & Events](../tutorials/events.md)
|
||||||
|
|||||||
@ -101,7 +101,7 @@ async def update_clock(chart):
|
|||||||
async def main():
|
async def main():
|
||||||
chart = Chart()
|
chart = Chart()
|
||||||
chart.topbar.textbox('clock')
|
chart.topbar.textbox('clock')
|
||||||
await asyncio.gather(chart.show_async(block=True), update_clock(chart))
|
await asyncio.gather(chart.show_async(), update_clock(chart))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
@ -130,7 +130,6 @@ async def data_loop(chart):
|
|||||||
return
|
return
|
||||||
chart.update_from_tick(ticks.iloc[i])
|
chart.update_from_tick(ticks.iloc[i])
|
||||||
await asyncio.sleep(0.03)
|
await asyncio.sleep(0.03)
|
||||||
i += 1
|
|
||||||
|
|
||||||
|
|
||||||
def on_new_bar(chart):
|
def on_new_bar(chart):
|
||||||
@ -150,7 +149,7 @@ async def main():
|
|||||||
df = pd.read_csv('ohlc.csv')
|
df = pd.read_csv('ohlc.csv')
|
||||||
|
|
||||||
chart.set(df)
|
chart.set(df)
|
||||||
await asyncio.gather(chart.show_async(block=True), data_loop(chart))
|
await asyncio.gather(chart.show_async(), data_loop(chart))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
7
index.html
Normal file
7
index.html
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<!-- redirect to example page -->
|
||||||
|
<meta http-equiv="refresh" content="0; URL=src/example/" />
|
||||||
|
</head>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,43 +1,43 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import json
|
||||||
import multiprocessing as mp
|
import multiprocessing as mp
|
||||||
|
import typing
|
||||||
import webview
|
import webview
|
||||||
|
from webview.errors import JavascriptException
|
||||||
# temporary until we fix to pywebview v5
|
|
||||||
try:
|
|
||||||
from webview.errors import JavascriptException
|
|
||||||
except ModuleNotFoundError:
|
|
||||||
JavascriptException = Exception
|
|
||||||
|
|
||||||
from lightweight_charts import abstract
|
from lightweight_charts import abstract
|
||||||
from .util import parse_event_message, FLOAT
|
from .util import parse_event_message, FLOAT
|
||||||
|
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
|
||||||
|
|
||||||
class CallbackAPI:
|
class CallbackAPI:
|
||||||
def __init__(self, emit_queue):
|
def __init__(self, emit_queue):
|
||||||
self.emit_q = emit_queue
|
self.emit_queue = emit_queue
|
||||||
|
|
||||||
def callback(self, message: str):
|
def callback(self, message: str):
|
||||||
self.emit_q.put(message)
|
self.emit_queue.put(message)
|
||||||
|
|
||||||
|
|
||||||
class PyWV:
|
class PyWV:
|
||||||
def __init__(self, q, start_ev, exit_ev, loaded, emit_queue, return_queue, html, debug,
|
def __init__(self, q, emit_q, return_q, loaded_event):
|
||||||
width, height, x, y, screen, on_top, maximize, title):
|
|
||||||
self.queue = q
|
self.queue = q
|
||||||
self.return_queue = return_queue
|
self.return_queue = return_q
|
||||||
self.exit = exit_ev
|
self.emit_queue = emit_q
|
||||||
self.callback_api = CallbackAPI(emit_queue)
|
self.loaded_event = loaded_event
|
||||||
self.loaded: list = loaded
|
|
||||||
self.html = html
|
|
||||||
|
|
||||||
self.windows = []
|
self.is_alive = True
|
||||||
self.create_window(width, height, x, y, screen, on_top, maximize, title)
|
|
||||||
|
|
||||||
start_ev.wait()
|
self.callback_api = CallbackAPI(emit_q)
|
||||||
webview.start(debug=debug)
|
self.windows: typing.List[webview.Window] = []
|
||||||
self.exit.set()
|
self.loop()
|
||||||
|
|
||||||
def create_window(self, width, height, x, y, screen=None, on_top=False, maximize=False, title=''):
|
|
||||||
|
def create_window(
|
||||||
|
self, width, height, x, y, screen=None, on_top=False,
|
||||||
|
maximize=False, title=''
|
||||||
|
):
|
||||||
screen = webview.screens[screen] if screen is not None else None
|
screen = webview.screens[screen] if screen is not None else None
|
||||||
if maximize:
|
if maximize:
|
||||||
if screen is None:
|
if screen is None:
|
||||||
@ -45,68 +45,148 @@ class PyWV:
|
|||||||
width, height = active_screen.width, active_screen.height
|
width, height = active_screen.width, active_screen.height
|
||||||
else:
|
else:
|
||||||
width, height = screen.width, screen.height
|
width, height = screen.width, screen.height
|
||||||
self.windows.append(webview.create_window(
|
|
||||||
title, html=self.html, js_api=self.callback_api,
|
|
||||||
width=width, height=height, x=x, y=y, screen=screen,
|
|
||||||
on_top=on_top, background_color='#000000'))
|
|
||||||
self.windows[-1].events.loaded += lambda: self.loop(self.loaded[len(self.windows)-1])
|
|
||||||
|
|
||||||
def loop(self, loaded):
|
self.windows.append(webview.create_window(
|
||||||
loaded.set()
|
title,
|
||||||
while 1:
|
url=abstract.INDEX,
|
||||||
|
js_api=self.callback_api,
|
||||||
|
width=width,
|
||||||
|
height=height,
|
||||||
|
x=x,
|
||||||
|
y=y,
|
||||||
|
screen=screen,
|
||||||
|
on_top=on_top,
|
||||||
|
background_color='#000000')
|
||||||
|
)
|
||||||
|
|
||||||
|
self.windows[-1].events.loaded += lambda: self.loaded_event.set()
|
||||||
|
|
||||||
|
|
||||||
|
def loop(self):
|
||||||
|
# self.loaded_event.set()
|
||||||
|
while self.is_alive:
|
||||||
i, arg = self.queue.get()
|
i, arg = self.queue.get()
|
||||||
|
|
||||||
|
if i == 'start':
|
||||||
|
webview.start(debug=arg, func=self.loop)
|
||||||
|
self.is_alive = False
|
||||||
|
self.emit_queue.put('exit')
|
||||||
|
return
|
||||||
if i == 'create_window':
|
if i == 'create_window':
|
||||||
self.create_window(*arg)
|
self.create_window(*arg)
|
||||||
elif arg in ('show', 'hide'):
|
continue
|
||||||
getattr(self.windows[i], arg)()
|
|
||||||
elif arg == 'exit':
|
window = self.windows[i]
|
||||||
self.exit.set()
|
if arg == 'show':
|
||||||
|
window.show()
|
||||||
|
elif arg == 'hide':
|
||||||
|
window.hide()
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
if '_~_~RETURN~_~_' in arg:
|
if '_~_~RETURN~_~_' in arg:
|
||||||
self.return_queue.put(self.windows[i].evaluate_js(arg[14:]))
|
self.return_queue.put(window.evaluate_js(arg[14:]))
|
||||||
else:
|
else:
|
||||||
self.windows[i].evaluate_js(arg)
|
window.evaluate_js(arg)
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
return
|
return
|
||||||
except JavascriptException as e:
|
except JavascriptException as e:
|
||||||
pass
|
msg = eval(str(e))
|
||||||
# msg = eval(str(e))
|
raise JavascriptException(f"\n\nscript -> '{arg}',\nerror -> {msg['name']}[{msg['line']}:{msg['column']}]\n{msg['message']}")
|
||||||
# raise JavascriptException(f"\n\nscript -> '{arg}',\nerror -> {msg['name']}[{msg['line']}:{msg['column']}]\n{msg['message']}")
|
|
||||||
|
|
||||||
|
class WebviewHandler():
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._reset()
|
||||||
|
self.debug = False
|
||||||
|
|
||||||
|
def _reset(self):
|
||||||
|
self.loaded_event = mp.Event()
|
||||||
|
self.return_queue = mp.Queue()
|
||||||
|
self.function_call_queue = mp.Queue()
|
||||||
|
self.emit_queue = mp.Queue()
|
||||||
|
self.wv_process = mp.Process(
|
||||||
|
target=PyWV, args=(
|
||||||
|
self.function_call_queue, self.emit_queue,
|
||||||
|
self.return_queue, self.loaded_event
|
||||||
|
),
|
||||||
|
daemon=True
|
||||||
|
)
|
||||||
|
self.max_window_num = -1
|
||||||
|
|
||||||
|
def create_window(
|
||||||
|
self, width, height, x, y, screen=None, on_top=False,
|
||||||
|
maximize=False, title=''
|
||||||
|
):
|
||||||
|
self.function_call_queue.put((
|
||||||
|
'create_window',
|
||||||
|
(width, height, x, y, screen, on_top, maximize, title)
|
||||||
|
))
|
||||||
|
self.max_window_num += 1
|
||||||
|
return self.max_window_num
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self.loaded_event.clear()
|
||||||
|
self.wv_process.start()
|
||||||
|
self.function_call_queue.put(('start', self.debug))
|
||||||
|
self.loaded_event.wait()
|
||||||
|
|
||||||
|
def show(self, window_num):
|
||||||
|
self.function_call_queue.put((window_num, 'show'))
|
||||||
|
|
||||||
|
def hide(self, window_num):
|
||||||
|
self.function_call_queue.put((window_num, 'hide'))
|
||||||
|
|
||||||
|
def evaluate_js(self, window_num, script):
|
||||||
|
self.function_call_queue.put((window_num, script))
|
||||||
|
|
||||||
|
def exit(self):
|
||||||
|
if self.wv_process.is_alive():
|
||||||
|
self.wv_process.terminate()
|
||||||
|
self.wv_process.join()
|
||||||
|
self._reset()
|
||||||
|
|
||||||
|
|
||||||
class Chart(abstract.AbstractChart):
|
class Chart(abstract.AbstractChart):
|
||||||
MAX_WINDOWS = 10
|
|
||||||
_window_num = 0
|
|
||||||
_main_window_handlers = None
|
_main_window_handlers = None
|
||||||
_exit, _start = (mp.Event() for _ in range(2))
|
WV: WebviewHandler = WebviewHandler()
|
||||||
_q, _emit_q, _return_q = (mp.Queue() for _ in range(3))
|
|
||||||
_loaded_list = [mp.Event() for _ in range(MAX_WINDOWS)]
|
def __init__(
|
||||||
|
self,
|
||||||
|
width: int = 800,
|
||||||
|
height: int = 600,
|
||||||
|
x: int = None,
|
||||||
|
y: int = None,
|
||||||
|
title: str = '',
|
||||||
|
screen: int = None,
|
||||||
|
on_top: bool = False,
|
||||||
|
maximize: bool = False,
|
||||||
|
debug: bool = False,
|
||||||
|
toolbox: bool = False,
|
||||||
|
inner_width: float = 1.0,
|
||||||
|
inner_height: float = 1.0,
|
||||||
|
scale_candles_only: bool = False,
|
||||||
|
position: FLOAT = 'left'
|
||||||
|
):
|
||||||
|
Chart.WV.debug = debug
|
||||||
|
self._i = Chart.WV.create_window(
|
||||||
|
width, height, x, y, screen, on_top, maximize, title
|
||||||
|
)
|
||||||
|
|
||||||
|
window = abstract.Window(
|
||||||
|
script_func=lambda s: Chart.WV.evaluate_js(self._i, s),
|
||||||
|
js_api_code='pywebview.api.callback'
|
||||||
|
)
|
||||||
|
|
||||||
|
abstract.Window._return_q = Chart.WV.return_queue
|
||||||
|
|
||||||
def __init__(self, width: int = 800, height: int = 600, x: int = None, y: int = None, title: str = '',
|
|
||||||
screen: int = None, on_top: bool = False, maximize: bool = False, debug: bool = False,
|
|
||||||
toolbox: bool = False, inner_width: float = 1.0, inner_height: float = 1.0,
|
|
||||||
scale_candles_only: bool = False, position: FLOAT = 'left'):
|
|
||||||
self._i = Chart._window_num
|
|
||||||
self._loaded = Chart._loaded_list[self._i]
|
|
||||||
abstract.Window._return_q = Chart._return_q
|
|
||||||
Chart._window_num += 1
|
|
||||||
self.is_alive = True
|
self.is_alive = True
|
||||||
|
|
||||||
window = abstract.Window(lambda s: self._q.put((self._i, s)), 'pywebview.api.callback')
|
if Chart._main_window_handlers is None:
|
||||||
if self._i == 0:
|
|
||||||
super().__init__(window, inner_width, inner_height, scale_candles_only, toolbox, position=position)
|
super().__init__(window, inner_width, inner_height, scale_candles_only, toolbox, position=position)
|
||||||
Chart._main_window_handlers = self.win.handlers
|
Chart._main_window_handlers = self.win.handlers
|
||||||
self._process = mp.Process(target=PyWV, args=(
|
|
||||||
self._q, self._start, self._exit, Chart._loaded_list,
|
|
||||||
self._emit_q, self._return_q, abstract.TEMPLATE, debug,
|
|
||||||
width, height, x, y, screen, on_top, maximize, title
|
|
||||||
), daemon=True)
|
|
||||||
self._process.start()
|
|
||||||
else:
|
else:
|
||||||
window.handlers = Chart._main_window_handlers
|
window.handlers = Chart._main_window_handlers
|
||||||
super().__init__(window, inner_width, inner_height, scale_candles_only, toolbox, position=position)
|
super().__init__(window, inner_width, inner_height, scale_candles_only, toolbox, position=position)
|
||||||
self._q.put(('create_window', (width, height, x, y, screen, on_top, maximize, title)))
|
|
||||||
|
|
||||||
def show(self, block: bool = False):
|
def show(self, block: bool = False):
|
||||||
"""
|
"""
|
||||||
@ -114,34 +194,31 @@ class Chart(abstract.AbstractChart):
|
|||||||
:param block: blocks execution until the chart is closed.
|
:param block: blocks execution until the chart is closed.
|
||||||
"""
|
"""
|
||||||
if not self.win.loaded:
|
if not self.win.loaded:
|
||||||
self._start.set()
|
Chart.WV.start()
|
||||||
self._loaded.wait()
|
|
||||||
self.win.on_js_load()
|
self.win.on_js_load()
|
||||||
else:
|
else:
|
||||||
self._q.put((self._i, 'show'))
|
Chart.WV.show(self._i)
|
||||||
if block:
|
if block:
|
||||||
asyncio.run(self.show_async(block=True))
|
asyncio.run(self.show_async())
|
||||||
|
|
||||||
async def show_async(self, block=False):
|
async def show_async(self):
|
||||||
self.show(block=False)
|
self.show(block=False)
|
||||||
if not block:
|
|
||||||
asyncio.create_task(self.show_async(block=True))
|
|
||||||
return
|
|
||||||
try:
|
try:
|
||||||
from lightweight_charts import polygon
|
from lightweight_charts import polygon
|
||||||
[asyncio.create_task(self.polygon.async_set(*args)) for args in polygon._set_on_load]
|
[asyncio.create_task(self.polygon.async_set(*args)) for args in polygon._set_on_load]
|
||||||
while 1:
|
while 1:
|
||||||
while self._emit_q.empty() and not self._exit.is_set():
|
while Chart.WV.emit_queue.empty() and self.is_alive:
|
||||||
await asyncio.sleep(0.05)
|
await asyncio.sleep(0.05)
|
||||||
if self._exit.is_set():
|
if not self.is_alive:
|
||||||
self._exit.clear()
|
|
||||||
self.is_alive = False
|
|
||||||
self.exit()
|
|
||||||
return
|
return
|
||||||
elif not self._emit_q.empty():
|
response = Chart.WV.emit_queue.get()
|
||||||
func, args = parse_event_message(self.win, self._emit_q.get())
|
if response == 'exit':
|
||||||
|
Chart.WV.exit()
|
||||||
|
self.is_alive = False
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
func, args = parse_event_message(self.win, response)
|
||||||
await func(*args) if asyncio.iscoroutinefunction(func) else func(*args)
|
await func(*args) if asyncio.iscoroutinefunction(func) else func(*args)
|
||||||
continue
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -155,12 +232,5 @@ class Chart(abstract.AbstractChart):
|
|||||||
"""
|
"""
|
||||||
Exits and destroys the chart window.\n
|
Exits and destroys the chart window.\n
|
||||||
"""
|
"""
|
||||||
self._q.put((self._i, 'exit'))
|
Chart.WV.exit()
|
||||||
self._exit.wait() if self.win.loaded else None
|
|
||||||
self._process.terminate()
|
|
||||||
|
|
||||||
Chart._main_window_handlers = None
|
|
||||||
Chart._window_num = 0
|
|
||||||
Chart._q = mp.Queue()
|
|
||||||
Chart._exit.clear(), Chart._start.clear()
|
|
||||||
self.is_alive = False
|
self.is_alive = False
|
||||||
|
|||||||
276
lightweight_charts/drawings.py
Normal file
276
lightweight_charts/drawings.py
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from typing import Union, Optional
|
||||||
|
|
||||||
|
from lightweight_charts.util import js_json
|
||||||
|
|
||||||
|
from .util import NUM, Pane, as_enum, LINE_STYLE, TIME, snake_to_camel
|
||||||
|
|
||||||
|
|
||||||
|
class Drawing(Pane):
|
||||||
|
def __init__(self, chart, func=None):
|
||||||
|
super().__init__(chart.win)
|
||||||
|
self.chart = chart
|
||||||
|
|
||||||
|
def update(self, *points):
|
||||||
|
js_json_string = f'JSON.parse({json.dumps(points)})'
|
||||||
|
self.run_script(f'{self.id}.updatePoints(...{js_json_string})')
|
||||||
|
|
||||||
|
def delete(self):
|
||||||
|
"""
|
||||||
|
Irreversibly deletes the drawing.
|
||||||
|
"""
|
||||||
|
self.run_script(f'{self.id}.detach()')
|
||||||
|
|
||||||
|
def options(self, color='#1E80F0', style='solid', width=4):
|
||||||
|
self.run_script(f'''{self.id}.applyOptions({{
|
||||||
|
lineColor: '{color}',
|
||||||
|
lineStyle: {as_enum(style, LINE_STYLE)},
|
||||||
|
width: {width},
|
||||||
|
}})''')
|
||||||
|
|
||||||
|
class TwoPointDrawing(Drawing):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
drawing_type,
|
||||||
|
chart,
|
||||||
|
start_time: TIME,
|
||||||
|
start_value: NUM,
|
||||||
|
end_time: TIME,
|
||||||
|
end_value: NUM,
|
||||||
|
round: bool,
|
||||||
|
options: dict,
|
||||||
|
func=None
|
||||||
|
):
|
||||||
|
super().__init__(chart, func)
|
||||||
|
|
||||||
|
def make_js_point(time, price):
|
||||||
|
formatted_time = self.chart._single_datetime_format(time)
|
||||||
|
return f'''{{
|
||||||
|
"time": {formatted_time},
|
||||||
|
"logical": {self.chart.id}.chart.timeScale()
|
||||||
|
.coordinateToLogical(
|
||||||
|
{self.chart.id}.chart.timeScale()
|
||||||
|
.timeToCoordinate({formatted_time})
|
||||||
|
),
|
||||||
|
"price": {price}
|
||||||
|
}}'''
|
||||||
|
|
||||||
|
options_string = '\n'.join(f'{key}: {val},' for key, val in options.items())
|
||||||
|
|
||||||
|
self.run_script(f'''
|
||||||
|
{self.id} = new Lib.{drawing_type}(
|
||||||
|
{make_js_point(start_time, start_value)},
|
||||||
|
{make_js_point(end_time, end_value)},
|
||||||
|
{{
|
||||||
|
{options_string}
|
||||||
|
}}
|
||||||
|
)
|
||||||
|
{chart.id}.series.attachPrimitive({self.id})
|
||||||
|
''')
|
||||||
|
|
||||||
|
|
||||||
|
class HorizontalLine(Drawing):
|
||||||
|
def __init__(self, chart, price, color, width, style, text, axis_label_visible, func):
|
||||||
|
super().__init__(chart, func)
|
||||||
|
self.price = price
|
||||||
|
self.run_script(f'''
|
||||||
|
|
||||||
|
{self.id} = new Lib.HorizontalLine(
|
||||||
|
{{price: {price}}},
|
||||||
|
{{
|
||||||
|
lineColor: '{color}',
|
||||||
|
lineStyle: {as_enum(style, LINE_STYLE)},
|
||||||
|
width: {width},
|
||||||
|
text: `{text}`,
|
||||||
|
}},
|
||||||
|
callbackName={f"'{self.id}'" if func else 'null'}
|
||||||
|
)
|
||||||
|
{chart.id}.series.attachPrimitive({self.id})
|
||||||
|
''')
|
||||||
|
if not func:
|
||||||
|
return
|
||||||
|
|
||||||
|
def wrapper(p):
|
||||||
|
self.price = float(p)
|
||||||
|
func(chart, self)
|
||||||
|
|
||||||
|
async def wrapper_async(p):
|
||||||
|
self.price = float(p)
|
||||||
|
await func(chart, self)
|
||||||
|
|
||||||
|
self.win.handlers[self.id] = wrapper_async if asyncio.iscoroutinefunction(func) else wrapper
|
||||||
|
self.run_script(f'{chart.id}.toolBox?.addNewDrawing({self.id})')
|
||||||
|
|
||||||
|
def update(self, price: float):
|
||||||
|
"""
|
||||||
|
Moves the horizontal line to the given price.
|
||||||
|
"""
|
||||||
|
self.run_script(f'{self.id}.updatePoints({{price: {price}}})')
|
||||||
|
# self.run_script(f'{self.id}.updatePrice({price})')
|
||||||
|
self.price = price
|
||||||
|
|
||||||
|
def options(self, color='#1E80F0', style='solid', width=4, text=''):
|
||||||
|
super().options(color, style, width)
|
||||||
|
self.run_script(f'{self.id}.applyOptions({{text: `{text}`}})')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class VerticalLine(Drawing):
|
||||||
|
def __init__(self, chart, time, color, width, style, text, func=None):
|
||||||
|
super().__init__(chart, func)
|
||||||
|
self.time = time
|
||||||
|
self.run_script(f'''
|
||||||
|
|
||||||
|
{self.id} = new Lib.VerticalLine(
|
||||||
|
{{time: {self.chart._single_datetime_format(time)}}},
|
||||||
|
{{
|
||||||
|
lineColor: '{color}',
|
||||||
|
lineStyle: {as_enum(style, LINE_STYLE)},
|
||||||
|
width: {width},
|
||||||
|
text: `{text}`,
|
||||||
|
}},
|
||||||
|
callbackName={f"'{self.id}'" if func else 'null'}
|
||||||
|
)
|
||||||
|
{chart.id}.series.attachPrimitive({self.id})
|
||||||
|
''')
|
||||||
|
|
||||||
|
def update(self, time: TIME):
|
||||||
|
self.run_script(f'{self.id}.updatePoints({{time: {time}}})')
|
||||||
|
# self.run_script(f'{self.id}.updatePrice({price})')
|
||||||
|
self.price = price
|
||||||
|
|
||||||
|
def options(self, color='#1E80F0', style='solid', width=4, text=''):
|
||||||
|
super().options(color, style, width)
|
||||||
|
self.run_script(f'{self.id}.applyOptions({{text: `{text}`}})')
|
||||||
|
|
||||||
|
|
||||||
|
class RayLine(Drawing):
|
||||||
|
def __init__(self,
|
||||||
|
chart,
|
||||||
|
start_time: TIME,
|
||||||
|
value: NUM,
|
||||||
|
round: bool = False,
|
||||||
|
color: str = '#1E80F0',
|
||||||
|
width: int = 2,
|
||||||
|
style: LINE_STYLE = 'solid',
|
||||||
|
text: str = '',
|
||||||
|
func = None,
|
||||||
|
):
|
||||||
|
super().__init__(chart, func)
|
||||||
|
self.run_script(f'''
|
||||||
|
{self.id} = new Lib.RayLine(
|
||||||
|
{{time: {self.chart._single_datetime_format(start_time)}, price: {value}}},
|
||||||
|
{{
|
||||||
|
lineColor: '{color}',
|
||||||
|
lineStyle: {as_enum(style, LINE_STYLE)},
|
||||||
|
width: {width},
|
||||||
|
text: `{text}`,
|
||||||
|
}},
|
||||||
|
callbackName={f"'{self.id}'" if func else 'null'}
|
||||||
|
)
|
||||||
|
{chart.id}.series.attachPrimitive({self.id})
|
||||||
|
''')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Box(TwoPointDrawing):
|
||||||
|
def __init__(self,
|
||||||
|
chart,
|
||||||
|
start_time: TIME,
|
||||||
|
start_value: NUM,
|
||||||
|
end_time: TIME,
|
||||||
|
end_value: NUM,
|
||||||
|
round: bool,
|
||||||
|
line_color: str,
|
||||||
|
fill_color: str,
|
||||||
|
width: int,
|
||||||
|
style: LINE_STYLE,
|
||||||
|
func=None):
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
"Box",
|
||||||
|
chart,
|
||||||
|
start_time,
|
||||||
|
start_value,
|
||||||
|
end_time,
|
||||||
|
end_value,
|
||||||
|
round,
|
||||||
|
{
|
||||||
|
"lineColor": f'"{line_color}"',
|
||||||
|
"fillColor": f'"{fill_color}"',
|
||||||
|
"width": width,
|
||||||
|
"lineStyle": as_enum(style, LINE_STYLE)
|
||||||
|
},
|
||||||
|
func
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TrendLine(TwoPointDrawing):
|
||||||
|
def __init__(self,
|
||||||
|
chart,
|
||||||
|
start_time: TIME,
|
||||||
|
start_value: NUM,
|
||||||
|
end_time: TIME,
|
||||||
|
end_value: NUM,
|
||||||
|
round: bool,
|
||||||
|
line_color: str,
|
||||||
|
width: int,
|
||||||
|
style: LINE_STYLE,
|
||||||
|
func=None):
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
"TrendLine",
|
||||||
|
chart,
|
||||||
|
start_time,
|
||||||
|
start_value,
|
||||||
|
end_time,
|
||||||
|
end_value,
|
||||||
|
round,
|
||||||
|
{
|
||||||
|
"lineColor": f'"{line_color}"',
|
||||||
|
"width": width,
|
||||||
|
"lineStyle": as_enum(style, LINE_STYLE)
|
||||||
|
},
|
||||||
|
func
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO reimplement/fix
|
||||||
|
class VerticalSpan(Pane):
|
||||||
|
def __init__(self, series: 'SeriesCommon', start_time: Union[TIME, tuple, list], end_time: Optional[TIME] = None,
|
||||||
|
color: str = 'rgba(252, 219, 3, 0.2)'):
|
||||||
|
self._chart = series._chart
|
||||||
|
super().__init__(self._chart.win)
|
||||||
|
start_time, end_time = pd.to_datetime(start_time), pd.to_datetime(end_time)
|
||||||
|
self.run_script(f'''
|
||||||
|
{self.id} = {self._chart.id}.chart.addHistogramSeries({{
|
||||||
|
color: '{color}',
|
||||||
|
priceFormat: {{type: 'volume'}},
|
||||||
|
priceScaleId: 'vertical_line',
|
||||||
|
lastValueVisible: false,
|
||||||
|
priceLineVisible: false,
|
||||||
|
}})
|
||||||
|
{self.id}.priceScale('').applyOptions({{
|
||||||
|
scaleMargins: {{top: 0, bottom: 0}}
|
||||||
|
}})
|
||||||
|
''')
|
||||||
|
if end_time is None:
|
||||||
|
if isinstance(start_time, pd.DatetimeIndex):
|
||||||
|
data = [{'time': time.timestamp(), 'value': 1} for time in start_time]
|
||||||
|
else:
|
||||||
|
data = [{'time': start_time.timestamp(), 'value': 1}]
|
||||||
|
self.run_script(f'{self.id}.setData({data})')
|
||||||
|
else:
|
||||||
|
self.run_script(f'''
|
||||||
|
{self.id}.setData(calculateTrendLine(
|
||||||
|
{start_time.timestamp()}, 1, {end_time.timestamp()}, 1, {series.id}))
|
||||||
|
''')
|
||||||
|
|
||||||
|
def delete(self):
|
||||||
|
"""
|
||||||
|
Irreversibly deletes the vertical span.
|
||||||
|
"""
|
||||||
|
self.run_script(f'{self._chart.id}.chart.removeSeries({self.id})')
|
||||||
1
lightweight_charts/js/bundle.js
Normal file
1
lightweight_charts/js/bundle.js
Normal file
File diff suppressed because one or more lines are too long
@ -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();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@ -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;
|
|
||||||
7
lightweight_charts/js/lightweight-charts.js
Normal file
7
lightweight_charts/js/lightweight-charts.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
234
lightweight_charts/js/styles.css
Normal file
234
lightweight_charts/js/styles.css
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
: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-textbox-input {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--color);
|
||||||
|
border: 1px solid 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: none;
|
||||||
|
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);
|
||||||
|
}
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
23
lightweight_charts/js/test.html
Normal file
23
lightweight_charts/js/test.html
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="">
|
||||||
|
<head>
|
||||||
|
<title>lightweight-charts-python</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
<script src="./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 src="./bundle.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -247,7 +247,7 @@ class PolygonAPI:
|
|||||||
|
|
||||||
df = await async_get_bar_data(ticker, timeframe, start_date, end_date, limit)
|
df = await async_get_bar_data(ticker, timeframe, start_date, end_date, limit)
|
||||||
|
|
||||||
self._chart.set(df, render_drawings=_tickers.get(self._chart) == ticker)
|
self._chart.set(df, keep_drawings=_tickers.get(self._chart) == ticker)
|
||||||
_tickers[self._chart] = ticker
|
_tickers[self._chart] = ticker
|
||||||
|
|
||||||
if not live:
|
if not live:
|
||||||
@ -396,7 +396,7 @@ class PolygonChart(Chart):
|
|||||||
Tickers, security types and timeframes are to be defined within the chart window.
|
Tickers, security types and timeframes are to be defined within the chart window.
|
||||||
|
|
||||||
If using the standard `show` method, the `block` parameter must be set to True.
|
If using the standard `show` method, the `block` parameter must be set to True.
|
||||||
When using `show_async`, either is acceptable.
|
`show_async` can also be used.
|
||||||
"""
|
"""
|
||||||
def __init__(
|
def __init__(
|
||||||
self, api_key: str, live: bool = False, num_bars: int = 200, end_date: str = 'now', limit: int = 5_000,
|
self, api_key: str, live: bool = False, num_bars: int = 200, end_date: str = 'now', limit: int = 5_000,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import random
|
import random
|
||||||
from typing import Union
|
from typing import Union, Optional, Callable
|
||||||
|
|
||||||
from .util import jbool, Pane, NUM
|
from .util import jbool, Pane, NUM
|
||||||
|
|
||||||
@ -11,8 +11,8 @@ class Section(Pane):
|
|||||||
self._table = table
|
self._table = table
|
||||||
self.type = section_type
|
self.type = section_type
|
||||||
|
|
||||||
def __call__(self, number_of_text_boxes: int, func: callable = None):
|
def __call__(self, number_of_text_boxes: int, func: Optional[Callable] = None):
|
||||||
if func:
|
if func is not None:
|
||||||
self.win.handlers[self.id] = lambda boxId: func(self._table, int(boxId))
|
self.win.handlers[self.id] = lambda boxId: func(self._table, int(boxId))
|
||||||
self.run_script(f'''
|
self.run_script(f'''
|
||||||
{self._table.id}.makeSection("{self.id}", "{self.type}", {number_of_text_boxes}, {"true" if func else ""})
|
{self._table.id}.makeSection("{self.id}", "{self.type}", {number_of_text_boxes}, {"true" if func else ""})
|
||||||
@ -35,7 +35,8 @@ class Row(dict):
|
|||||||
|
|
||||||
def __setitem__(self, column, value):
|
def __setitem__(self, column, value):
|
||||||
if isinstance(column, tuple):
|
if isinstance(column, tuple):
|
||||||
return [self.__setitem__(col, val) for col, val in zip(column, value)]
|
[self.__setitem__(col, val) for col, val in zip(column, value)]
|
||||||
|
return
|
||||||
original_value = value
|
original_value = value
|
||||||
if column in self._table._formatters:
|
if column in self._table._formatters:
|
||||||
value = self._table._formatters[column].replace(self._table.VALUE, str(value))
|
value = self._table._formatters[column].replace(self._table.VALUE, str(value))
|
||||||
@ -47,23 +48,33 @@ class Row(dict):
|
|||||||
def text_color(self, column, color): self._style('textColor', column, color)
|
def text_color(self, column, color): self._style('textColor', column, color)
|
||||||
|
|
||||||
def _style(self, style, column, arg):
|
def _style(self, style, column, arg):
|
||||||
self.run_script(f"{self._table.id}.rows[{self.id}]['{column}'].style.{style} = '{arg}'")
|
self.run_script(f"{self._table.id}.styleCell({self.id}, '{column}', '{style}', '{arg}')")
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
self.run_script(f"{self._table.id}.deleteRow('{self.id}')")
|
self.run_script(f"{self._table.id}.deleteRow('{self.id}')")
|
||||||
self._table.pop(self.id)
|
self._table.pop(self.id)
|
||||||
|
|
||||||
|
|
||||||
class Table(Pane, dict):
|
class Table(Pane, dict):
|
||||||
VALUE = 'CELL__~__VALUE__~__PLACEHOLDER'
|
VALUE = 'CELL__~__VALUE__~__PLACEHOLDER'
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, window, width: NUM, height: NUM, headings: tuple, widths: tuple = None,
|
self,
|
||||||
alignments: tuple = None, position='left', draggable: bool = False,
|
window,
|
||||||
background_color: str = '#121417', border_color: str = 'rgb(70, 70, 70)',
|
width: NUM,
|
||||||
border_width: int = 1, heading_text_colors: tuple = None,
|
height: NUM,
|
||||||
heading_background_colors: tuple = None, return_clicked_cells: bool = False,
|
headings: tuple,
|
||||||
func: callable = None
|
widths: Optional[tuple] = None,
|
||||||
|
alignments: Optional[tuple] = None,
|
||||||
|
position='left',
|
||||||
|
draggable: bool = False,
|
||||||
|
background_color: str = '#121417',
|
||||||
|
border_color: str = 'rgb(70, 70, 70)',
|
||||||
|
border_width: int = 1,
|
||||||
|
heading_text_colors: Optional[tuple] = None,
|
||||||
|
heading_background_colors: Optional[tuple] = None,
|
||||||
|
return_clicked_cells: bool = False,
|
||||||
|
func: Optional[Callable] = None
|
||||||
):
|
):
|
||||||
dict.__init__(self)
|
dict.__init__(self)
|
||||||
Pane.__init__(self, window)
|
Pane.__init__(self, window)
|
||||||
@ -85,17 +96,20 @@ class Table(Pane, dict):
|
|||||||
self.win.handlers[self.id] = async_wrapper if asyncio.iscoroutinefunction(func) else wrapper
|
self.win.handlers[self.id] = async_wrapper if asyncio.iscoroutinefunction(func) else wrapper
|
||||||
self.return_clicked_cells = return_clicked_cells
|
self.return_clicked_cells = return_clicked_cells
|
||||||
|
|
||||||
headings = list(headings)
|
|
||||||
widths = list(widths) if widths else []
|
|
||||||
alignments = list(alignments) if alignments else []
|
|
||||||
heading_text_colors = list(heading_text_colors) if heading_text_colors else []
|
|
||||||
heading_background_colors = list(heading_background_colors) if heading_background_colors else []
|
|
||||||
|
|
||||||
self.run_script(f'''
|
self.run_script(f'''
|
||||||
{self.id} = new Table(
|
{self.id} = new Lib.Table(
|
||||||
{width}, {height}, {headings}, {widths}, {alignments}, '{position}', {jbool(draggable)},
|
{width},
|
||||||
'{background_color}', '{border_color}', {border_width}, {heading_text_colors},
|
{height},
|
||||||
{heading_background_colors}
|
{list(headings)},
|
||||||
|
{list(widths) if widths else []},
|
||||||
|
{list(alignments) if alignments else []},
|
||||||
|
'{position}',
|
||||||
|
{jbool(draggable)},
|
||||||
|
'{background_color}',
|
||||||
|
'{border_color}',
|
||||||
|
{border_width},
|
||||||
|
{list(heading_text_colors) if heading_text_colors else []},
|
||||||
|
{list(heading_background_colors) if heading_background_colors else []}
|
||||||
)''')
|
)''')
|
||||||
self.run_script(f'{self.id}.callbackName = "{self.id}"') if func else None
|
self.run_script(f'{self.id}.callbackName = "{self.id}"') if func else None
|
||||||
self.footer = Section(self, 'footer')
|
self.footer = Section(self, 'footer')
|
||||||
|
|||||||
@ -3,14 +3,12 @@ import json
|
|||||||
|
|
||||||
class ToolBox:
|
class ToolBox:
|
||||||
def __init__(self, chart):
|
def __init__(self, chart):
|
||||||
from lightweight_charts.abstract import JS
|
|
||||||
self.run_script = chart.run_script
|
self.run_script = chart.run_script
|
||||||
self.id = chart.id
|
self.id = chart.id
|
||||||
self._save_under = None
|
self._save_under = None
|
||||||
self.drawings = {}
|
self.drawings = {}
|
||||||
chart.win.handlers[f'save_drawings{self.id}'] = self._save_drawings
|
chart.win.handlers[f'save_drawings{self.id}'] = self._save_drawings
|
||||||
self.run_script(JS['toolbox'])
|
self.run_script(f'{self.id}.createToolBox()')
|
||||||
self.run_script(f'{self.id}.toolBox = new ToolBox({self.id})')
|
|
||||||
|
|
||||||
def save_drawings_under(self, widget: 'Widget'):
|
def save_drawings_under(self, widget: 'Widget'):
|
||||||
"""
|
"""
|
||||||
@ -24,7 +22,7 @@ class ToolBox:
|
|||||||
"""
|
"""
|
||||||
if not self.drawings.get(tag):
|
if not self.drawings.get(tag):
|
||||||
return
|
return
|
||||||
self.run_script(f'if ("toolBox" in {self.id}) {self.id}.toolBox.loadDrawings({json.dumps(self.drawings[tag])})')
|
self.run_script(f'if ({self.id}.toolBox) {self.id}.toolBox.loadDrawings({json.dumps(self.drawings[tag])})')
|
||||||
|
|
||||||
def import_drawings(self, file_path):
|
def import_drawings(self, file_path):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -8,12 +8,15 @@ ALIGN = Literal['left', 'right']
|
|||||||
|
|
||||||
|
|
||||||
class Widget(Pane):
|
class Widget(Pane):
|
||||||
def __init__(self, topbar, value, func=None):
|
def __init__(self, topbar, value, func: callable = None, convert_boolean=False):
|
||||||
super().__init__(topbar.win)
|
super().__init__(topbar.win)
|
||||||
self.value = value
|
self.value = value
|
||||||
|
|
||||||
def wrapper(v):
|
def wrapper(v):
|
||||||
self.value = v
|
if convert_boolean:
|
||||||
|
self.value = False if v == 'false' else True
|
||||||
|
else:
|
||||||
|
self.value = v
|
||||||
func(topbar._chart)
|
func(topbar._chart)
|
||||||
|
|
||||||
async def async_wrapper(v):
|
async def async_wrapper(v):
|
||||||
@ -24,9 +27,12 @@ class Widget(Pane):
|
|||||||
|
|
||||||
|
|
||||||
class TextWidget(Widget):
|
class TextWidget(Widget):
|
||||||
def __init__(self, topbar, initial_text, align):
|
def __init__(self, topbar, initial_text, align, func):
|
||||||
super().__init__(topbar, value=initial_text)
|
super().__init__(topbar, value=initial_text, func=func)
|
||||||
self.run_script(f'{self.id} = {topbar.id}.makeTextBoxWidget("{initial_text}", "{align}")')
|
|
||||||
|
callback_name = f'"{self.id}"' if func else ''
|
||||||
|
|
||||||
|
self.run_script(f'{self.id} = {topbar.id}.makeTextBoxWidget("{initial_text}", "{align}", {callback_name})')
|
||||||
|
|
||||||
def set(self, string):
|
def set(self, string):
|
||||||
self.value = string
|
self.value = string
|
||||||
@ -54,6 +60,7 @@ class MenuWidget(Widget):
|
|||||||
{self.id} = {topbar.id}.makeMenu({list(options)}, "{default}", {jbool(separator)}, "{self.id}", "{align}")
|
{self.id} = {topbar.id}.makeMenu({list(options)}, "{default}", {jbool(separator)}, "{self.id}", "{align}")
|
||||||
''')
|
''')
|
||||||
|
|
||||||
|
# TODO this will probably need to be fixed
|
||||||
def set(self, option):
|
def set(self, option):
|
||||||
if option not in self.options:
|
if option not in self.options:
|
||||||
raise ValueError(f"Option {option} not in menu options ({self.options})")
|
raise ValueError(f"Option {option} not in menu options ({self.options})")
|
||||||
@ -63,15 +70,19 @@ class MenuWidget(Widget):
|
|||||||
''')
|
''')
|
||||||
self.win.handlers[self.id](option)
|
self.win.handlers[self.id](option)
|
||||||
|
|
||||||
|
def update_items(self, *items: str):
|
||||||
|
self.options = list(items)
|
||||||
|
self.run_script(f'{self.id}.updateMenuItems({self.options})')
|
||||||
|
|
||||||
|
|
||||||
class ButtonWidget(Widget):
|
class ButtonWidget(Widget):
|
||||||
def __init__(self, topbar, button, separator, align, func):
|
def __init__(self, topbar, button, separator, align, toggle, func):
|
||||||
super().__init__(topbar, value=button, func=func)
|
super().__init__(topbar, value=False, func=func, convert_boolean=toggle)
|
||||||
self.run_script(
|
self.run_script(
|
||||||
f'{self.id} = {topbar.id}.makeButton("{button}", "{self.id}", {jbool(separator)}, true, "{align}")')
|
f'{self.id} = {topbar.id}.makeButton("{button}", "{self.id}", {jbool(separator)}, true, "{align}", {jbool(toggle)})')
|
||||||
|
|
||||||
def set(self, string):
|
def set(self, string):
|
||||||
self.value = string
|
# self.value = string
|
||||||
self.run_script(f'{self.id}.elem.innerText = "{string}"')
|
self.run_script(f'{self.id}.elem.innerText = "{string}"')
|
||||||
|
|
||||||
|
|
||||||
@ -85,10 +96,8 @@ class TopBar(Pane):
|
|||||||
def _create(self):
|
def _create(self):
|
||||||
if self._created:
|
if self._created:
|
||||||
return
|
return
|
||||||
from lightweight_charts.abstract import JS
|
|
||||||
self._created = True
|
self._created = True
|
||||||
self.run_script(JS['callback'])
|
self.run_script(f'{self.id} = {self._chart.id}.createTopBar()')
|
||||||
self.run_script(f'{self.id} = new TopBar({self._chart.id})')
|
|
||||||
|
|
||||||
def __getitem__(self, item):
|
def __getitem__(self, item):
|
||||||
if widget := self._widgets.get(item):
|
if widget := self._widgets.get(item):
|
||||||
@ -109,11 +118,11 @@ class TopBar(Pane):
|
|||||||
self._widgets[name] = MenuWidget(self, options, default if default else options[0], separator, align, func)
|
self._widgets[name] = MenuWidget(self, options, default if default else options[0], separator, align, func)
|
||||||
|
|
||||||
def textbox(self, name: str, initial_text: str = '',
|
def textbox(self, name: str, initial_text: str = '',
|
||||||
align: ALIGN = 'left'):
|
align: ALIGN = 'left', func: callable = None):
|
||||||
self._create()
|
self._create()
|
||||||
self._widgets[name] = TextWidget(self, initial_text, align)
|
self._widgets[name] = TextWidget(self, initial_text, align, func)
|
||||||
|
|
||||||
def button(self, name, button_text: str, separator: bool = True,
|
def button(self, name, button_text: str, separator: bool = True,
|
||||||
align: ALIGN = 'left', func: callable = None):
|
align: ALIGN = 'left', toggle: bool = False, func: callable = None):
|
||||||
self._create()
|
self._create()
|
||||||
self._widgets[name] = ButtonWidget(self, button_text, separator, align, func)
|
self._widgets[name] = ButtonWidget(self, button_text, separator, align, toggle, func)
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import json
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from random import choices
|
from random import choices
|
||||||
from typing import Literal, Union
|
from typing import Literal, Union
|
||||||
|
from numpy import isin
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
|
|
||||||
@ -11,6 +12,7 @@ class Pane:
|
|||||||
from lightweight_charts import Window
|
from lightweight_charts import Window
|
||||||
self.win: Window = window
|
self.win: Window = window
|
||||||
self.run_script = window.run_script
|
self.run_script = window.run_script
|
||||||
|
self.bulk_run = window.bulk_run
|
||||||
if hasattr(self, 'id'):
|
if hasattr(self, 'id'):
|
||||||
return
|
return
|
||||||
self.id = Window._id_gen.generate()
|
self.id = Window._id_gen.generate()
|
||||||
@ -19,7 +21,7 @@ class Pane:
|
|||||||
class IDGen(list):
|
class IDGen(list):
|
||||||
ascii = 'abcdefghijklmnopqrstuvwxyz'
|
ascii = 'abcdefghijklmnopqrstuvwxyz'
|
||||||
|
|
||||||
def generate(self):
|
def generate(self) -> str:
|
||||||
var = ''.join(choices(self.ascii, k=8))
|
var = ''.join(choices(self.ascii, k=8))
|
||||||
if var not in self:
|
if var not in self:
|
||||||
self.append(var)
|
self.append(var)
|
||||||
@ -37,13 +39,28 @@ def parse_event_message(window, string):
|
|||||||
def js_data(data: Union[pd.DataFrame, pd.Series]):
|
def js_data(data: Union[pd.DataFrame, pd.Series]):
|
||||||
if isinstance(data, pd.DataFrame):
|
if isinstance(data, pd.DataFrame):
|
||||||
d = data.to_dict(orient='records')
|
d = data.to_dict(orient='records')
|
||||||
filtered_records = [{k: v for k, v in record.items() if v is not None} for record in d]
|
filtered_records = [{k: v for k, v in record.items() if v is not None and not pd.isna(v)} for record in d]
|
||||||
else:
|
else:
|
||||||
d = data.to_dict()
|
d = data.to_dict()
|
||||||
filtered_records = {k: v for k, v in d.items()}
|
filtered_records = {k: v for k, v in d.items()}
|
||||||
return json.dumps(filtered_records, indent=2)
|
return json.dumps(filtered_records, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
def snake_to_camel(s: str):
|
||||||
|
components = s.split('_')
|
||||||
|
return components[0] + ''.join(x.title() for x in components[1:])
|
||||||
|
|
||||||
|
def js_json(d: dict):
|
||||||
|
filtered_dict = {}
|
||||||
|
for key, val in d.items():
|
||||||
|
if key in ('self') or val in (None,):
|
||||||
|
continue
|
||||||
|
if '_' in key:
|
||||||
|
key = snake_to_camel(key)
|
||||||
|
filtered_dict[key] = val
|
||||||
|
return f"JSON.parse('{json.dumps(filtered_dict)}')"
|
||||||
|
|
||||||
|
|
||||||
def jbool(b: bool): return 'true' if b is True else 'false' if b is False else None
|
def jbool(b: bool): return 'true' if b is True else 'false' if b is False else None
|
||||||
|
|
||||||
|
|
||||||
@ -53,32 +70,27 @@ MARKER_POSITION = Literal['above', 'below', 'inside']
|
|||||||
|
|
||||||
MARKER_SHAPE = Literal['arrow_up', 'arrow_down', 'circle', 'square']
|
MARKER_SHAPE = Literal['arrow_up', 'arrow_down', 'circle', 'square']
|
||||||
|
|
||||||
CROSSHAIR_MODE = Literal['normal', 'magnet']
|
CROSSHAIR_MODE = Literal['normal', 'magnet', 'hidden']
|
||||||
|
|
||||||
PRICE_SCALE_MODE = Literal['normal', 'logarithmic', 'percentage', 'index100']
|
PRICE_SCALE_MODE = Literal['normal', 'logarithmic', 'percentage', 'index100']
|
||||||
|
|
||||||
TIME = Union[datetime, pd.Timestamp, str]
|
TIME = Union[datetime, pd.Timestamp, str, float]
|
||||||
|
|
||||||
NUM = Union[float, int]
|
NUM = Union[float, int]
|
||||||
|
|
||||||
FLOAT = Literal['left', 'right', 'top', 'bottom']
|
FLOAT = Literal['left', 'right', 'top', 'bottom']
|
||||||
|
|
||||||
|
|
||||||
def line_style(line: LINE_STYLE):
|
def as_enum(value, string_types):
|
||||||
js = 'LightweightCharts.LineStyle.'
|
types = string_types.__args__
|
||||||
return js+line[:line.index('_')].title() + line[line.index('_') + 1:].title() if '_' in line else js+line.title()
|
return -1 if value not in types else types.index(value)
|
||||||
|
|
||||||
|
|
||||||
def crosshair_mode(mode: CROSSHAIR_MODE):
|
|
||||||
return f'LightweightCharts.CrosshairMode.{mode.title()}' if mode else None
|
|
||||||
|
|
||||||
|
|
||||||
def price_scale_mode(mode: PRICE_SCALE_MODE):
|
|
||||||
return f"LightweightCharts.PriceScaleMode.{'IndexedTo100' if mode == 'index100' else mode.title() if mode else None}"
|
|
||||||
|
|
||||||
|
|
||||||
def marker_shape(shape: MARKER_SHAPE):
|
def marker_shape(shape: MARKER_SHAPE):
|
||||||
return shape[:shape.index('_')]+shape[shape.index('_')+1:].title() if '_' in shape else shape
|
return {
|
||||||
|
'arrow_up': 'arrowUp',
|
||||||
|
'arrow_down': 'arrowDown',
|
||||||
|
}.get(shape) or shape
|
||||||
|
|
||||||
|
|
||||||
def marker_position(p: MARKER_POSITION):
|
def marker_position(p: MARKER_POSITION):
|
||||||
@ -86,8 +98,7 @@ def marker_position(p: MARKER_POSITION):
|
|||||||
'above': 'aboveBar',
|
'above': 'aboveBar',
|
||||||
'below': 'belowBar',
|
'below': 'belowBar',
|
||||||
'inside': 'inBar',
|
'inside': 'inBar',
|
||||||
None: None,
|
}.get(p)
|
||||||
}[p]
|
|
||||||
|
|
||||||
|
|
||||||
class Emitter:
|
class Emitter:
|
||||||
@ -127,27 +138,54 @@ class JSEmitter:
|
|||||||
class Events:
|
class Events:
|
||||||
def __init__(self, chart):
|
def __init__(self, chart):
|
||||||
self.new_bar = Emitter()
|
self.new_bar = Emitter()
|
||||||
from lightweight_charts.abstract import JS
|
|
||||||
self.search = JSEmitter(chart, f'search{chart.id}',
|
self.search = JSEmitter(chart, f'search{chart.id}',
|
||||||
lambda o: chart.run_script(f'''
|
lambda o: chart.run_script(f'''
|
||||||
{JS['callback']}
|
Lib.Handler.makeSpinner({chart.id})
|
||||||
makeSpinner({chart.id})
|
{chart.id}.search = Lib.Handler.makeSearchBox({chart.id})
|
||||||
{chart.id}.search = makeSearchBox({chart.id})
|
|
||||||
''')
|
''')
|
||||||
)
|
)
|
||||||
self.range_change = JSEmitter(chart, f'range_change{chart.id}',
|
salt = chart.id[chart.id.index('.')+1:]
|
||||||
|
self.range_change = JSEmitter(chart, f'range_change{salt}',
|
||||||
lambda o: chart.run_script(f'''
|
lambda o: chart.run_script(f'''
|
||||||
let checkLogicalRange = (logical) => {{
|
let checkLogicalRange{salt} = (logical) => {{
|
||||||
{chart.id}.chart.timeScale().unsubscribeVisibleLogicalRangeChange(checkLogicalRange)
|
{chart.id}.chart.timeScale().unsubscribeVisibleLogicalRangeChange(checkLogicalRange{salt})
|
||||||
|
|
||||||
let barsInfo = {chart.id}.series.barsInLogicalRange(logical)
|
let barsInfo = {chart.id}.series.barsInLogicalRange(logical)
|
||||||
if (barsInfo) window.callbackFunction(`range_change{chart.id}_~_${{barsInfo.barsBefore}};;;${{barsInfo.barsAfter}}`)
|
if (barsInfo) window.callbackFunction(`range_change{salt}_~_${{barsInfo.barsBefore}};;;${{barsInfo.barsAfter}}`)
|
||||||
|
|
||||||
setTimeout(() => {chart.id}.chart.timeScale().subscribeVisibleLogicalRangeChange(checkLogicalRange), 50)
|
setTimeout(() => {chart.id}.chart.timeScale().subscribeVisibleLogicalRangeChange(checkLogicalRange{salt}), 50)
|
||||||
}}
|
}}
|
||||||
{chart.id}.chart.timeScale().subscribeVisibleLogicalRangeChange(checkLogicalRange)
|
{chart.id}.chart.timeScale().subscribeVisibleLogicalRangeChange(checkLogicalRange{salt})
|
||||||
'''),
|
'''),
|
||||||
wrapper=lambda o, c, *arg: o(c, *[float(a) for a in arg])
|
wrapper=lambda o, c, *arg: o(c, *[float(a) for a in arg])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.click = JSEmitter(chart, f'subscribe_click{salt}',
|
||||||
|
lambda o: chart.run_script(f'''
|
||||||
|
let clickHandler{salt} = (param) => {{
|
||||||
|
if (!param.point) return;
|
||||||
|
const time = {chart.id}.chart.timeScale().coordinateToTime(param.point.x)
|
||||||
|
const price = {chart.id}.series.coordinateToPrice(param.point.y);
|
||||||
|
window.callbackFunction(`subscribe_click{salt}_~_${{time}};;;${{price}}`)
|
||||||
|
}}
|
||||||
|
{chart.id}.chart.subscribeClick(clickHandler{salt})
|
||||||
|
'''),
|
||||||
|
wrapper=lambda func, c, *args: func(c, *[float(a) for a in args])
|
||||||
|
)
|
||||||
|
|
||||||
|
class BulkRunScript:
|
||||||
|
def __init__(self, script_func):
|
||||||
|
self.enabled = False
|
||||||
|
self.scripts = []
|
||||||
|
self.script_func = script_func
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.enabled = True
|
||||||
|
|
||||||
|
def __exit__(self, *args):
|
||||||
|
self.enabled = False
|
||||||
|
self.script_func('\n'.join(self.scripts))
|
||||||
|
self.scripts = []
|
||||||
|
|
||||||
|
def add_script(self, script):
|
||||||
|
self.scripts.append(script)
|
||||||
|
|||||||
@ -10,18 +10,25 @@ except ImportError:
|
|||||||
wx = None
|
wx = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
using_py6 = False
|
using_pyside6 = False
|
||||||
from PyQt5.QtWebEngineWidgets import QWebEngineView
|
from PyQt5.QtWebEngineWidgets import QWebEngineView
|
||||||
from PyQt5.QtWebChannel import QWebChannel
|
from PyQt5.QtWebChannel import QWebChannel
|
||||||
from PyQt5.QtCore import QObject, pyqtSlot as Slot
|
from PyQt5.QtCore import QObject, pyqtSlot as Slot, QUrl, QTimer
|
||||||
except ImportError:
|
except ImportError:
|
||||||
using_py6 = True
|
using_pyside6 = True
|
||||||
try:
|
try:
|
||||||
from PySide6.QtWebEngineWidgets import QWebEngineView
|
from PySide6.QtWebEngineWidgets import QWebEngineView
|
||||||
from PySide6.QtWebChannel import QWebChannel
|
from PySide6.QtWebChannel import QWebChannel
|
||||||
from PySide6.QtCore import Qt, QObject, Slot
|
from PySide6.QtCore import Qt, QObject, Slot, QUrl, QTimer
|
||||||
except ImportError:
|
except ImportError:
|
||||||
QWebEngineView = None
|
try:
|
||||||
|
using_pyside6 = False
|
||||||
|
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||||
|
from PyQt6.QtWebChannel import QWebChannel
|
||||||
|
from PyQt6.QtCore import QObject, pyqtSlot as Slot, QUrl, QTimer
|
||||||
|
except ImportError:
|
||||||
|
QWebEngineView = None
|
||||||
|
|
||||||
|
|
||||||
if QWebEngineView:
|
if QWebEngineView:
|
||||||
class Bridge(QObject):
|
class Bridge(QObject):
|
||||||
@ -61,12 +68,13 @@ class WxChart(abstract.AbstractChart):
|
|||||||
inner_width, inner_height, scale_candles_only, toolbox)
|
inner_width, inner_height, scale_candles_only, toolbox)
|
||||||
|
|
||||||
self.webview.Bind(wx.html2.EVT_WEBVIEW_LOADED, lambda e: wx.CallLater(500, self.win.on_js_load))
|
self.webview.Bind(wx.html2.EVT_WEBVIEW_LOADED, lambda e: wx.CallLater(500, self.win.on_js_load))
|
||||||
self.webview.Bind(wx.html2.EVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, lambda e: emit_callback(self, e.GetString()))
|
self.webview.Bind(wx.html2.EVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, lambda e: emit_callback(self.win, e.GetString()))
|
||||||
self.webview.AddScriptMessageHandler('wx_msg')
|
self.webview.AddScriptMessageHandler('wx_msg')
|
||||||
self.webview.SetPage(abstract.TEMPLATE, '')
|
|
||||||
self.webview.AddUserScript(abstract.JS['toolbox']) if toolbox else None
|
|
||||||
|
|
||||||
def get_webview(self): return self.webview
|
self.webview.LoadURL("file://"+abstract.INDEX)
|
||||||
|
|
||||||
|
def get_webview(self):
|
||||||
|
return self.webview
|
||||||
|
|
||||||
|
|
||||||
class QtChart(abstract.AbstractChart):
|
class QtChart(abstract.AbstractChart):
|
||||||
@ -82,21 +90,25 @@ class QtChart(abstract.AbstractChart):
|
|||||||
self.bridge = Bridge(self)
|
self.bridge = Bridge(self)
|
||||||
self.web_channel.registerObject('bridge', self.bridge)
|
self.web_channel.registerObject('bridge', self.bridge)
|
||||||
self.webview.page().setWebChannel(self.web_channel)
|
self.webview.page().setWebChannel(self.web_channel)
|
||||||
self.webview.loadFinished.connect(self.win.on_js_load)
|
self.webview.loadFinished.connect(lambda: self.webview.page().runJavaScript('''
|
||||||
if using_py6:
|
let scriptElement = document.createElement("script")
|
||||||
|
scriptElement.src = 'qrc:///qtwebchannel/qwebchannel.js'
|
||||||
|
|
||||||
|
scriptElement.onload = function() {
|
||||||
|
var bridge = new QWebChannel(qt.webChannelTransport, function(channel) {
|
||||||
|
var pythonObject = channel.objects.bridge
|
||||||
|
window.pythonObject = pythonObject
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
document.head.appendChild(scriptElement)
|
||||||
|
|
||||||
|
'''))
|
||||||
|
self.webview.loadFinished.connect(lambda: QTimer.singleShot(200, self.win.on_js_load))
|
||||||
|
if using_pyside6:
|
||||||
self.webview.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
|
self.webview.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
|
||||||
self._html = f'''
|
self.webview.load(QUrl.fromLocalFile(abstract.INDEX))
|
||||||
{abstract.TEMPLATE[:85]}
|
|
||||||
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
|
|
||||||
<script>
|
|
||||||
var bridge = new QWebChannel(qt.webChannelTransport, function(channel) {{
|
|
||||||
var pythonObject = channel.objects.bridge;
|
|
||||||
window.pythonObject = pythonObject
|
|
||||||
}});
|
|
||||||
</script>
|
|
||||||
{abstract.TEMPLATE[85:]}
|
|
||||||
'''
|
|
||||||
self.webview.page().setHtml(self._html)
|
|
||||||
|
|
||||||
def get_webview(self): return self.webview
|
def get_webview(self): return self.webview
|
||||||
|
|
||||||
@ -104,7 +116,21 @@ class QtChart(abstract.AbstractChart):
|
|||||||
class StaticLWC(abstract.AbstractChart):
|
class StaticLWC(abstract.AbstractChart):
|
||||||
def __init__(self, width=None, height=None, inner_width=1, inner_height=1,
|
def __init__(self, width=None, height=None, inner_width=1, inner_height=1,
|
||||||
scale_candles_only: bool = False, toolbox=False, autosize=True):
|
scale_candles_only: bool = False, toolbox=False, autosize=True):
|
||||||
self._html = abstract.TEMPLATE.replace('</script>\n</body>\n</html>', '')
|
|
||||||
|
with open(abstract.INDEX.replace("test.html", 'styles.css'), 'r') as f:
|
||||||
|
css = f.read()
|
||||||
|
with open(abstract.INDEX.replace("test.html", 'bundle.js'), 'r') as f:
|
||||||
|
js = f.read()
|
||||||
|
with open(abstract.INDEX.replace("test.html", 'lightweight-charts.js'), 'r') as f:
|
||||||
|
lwc = f.read()
|
||||||
|
|
||||||
|
with open(abstract.INDEX, 'r') as f:
|
||||||
|
self._html = f.read() \
|
||||||
|
.replace('<link rel="stylesheet" href="styles.css">', f"<style>{css}</style>") \
|
||||||
|
.replace(' src="./lightweight-charts.js">', f'>{lwc}') \
|
||||||
|
.replace(' src="./bundle.js">', f'>{js}') \
|
||||||
|
.replace('</body>\n</html>', '<script>')
|
||||||
|
|
||||||
super().__init__(abstract.Window(run_script=self.run_script), inner_width, inner_height,
|
super().__init__(abstract.Window(run_script=self.run_script), inner_width, inner_height,
|
||||||
scale_candles_only, toolbox, autosize)
|
scale_candles_only, toolbox, autosize)
|
||||||
self.width = width
|
self.width = width
|
||||||
@ -146,10 +172,10 @@ class JupyterChart(StaticLWC):
|
|||||||
var element = document.getElementsByClassName("tv-lightweight-charts")[i];
|
var element = document.getElementsByClassName("tv-lightweight-charts")[i];
|
||||||
element.style.overflow = "visible"
|
element.style.overflow = "visible"
|
||||||
}}
|
}}
|
||||||
document.getElementById('wrapper').style.overflow = 'hidden'
|
document.getElementById('container').style.overflow = 'hidden'
|
||||||
document.getElementById('wrapper').style.borderRadius = '10px'
|
document.getElementById('container').style.borderRadius = '10px'
|
||||||
document.getElementById('wrapper').style.width = '{self.width}px'
|
document.getElementById('container').style.width = '{self.width}px'
|
||||||
document.getElementById('wrapper').style.height = '100%'
|
document.getElementById('container').style.height = '100%'
|
||||||
''')
|
''')
|
||||||
self.run_script(f'{self.id}.chart.resize({width}, {height})')
|
self.run_script(f'{self.id}.chart.resize({width}, {height})')
|
||||||
|
|
||||||
|
|||||||
737
package-lock.json
generated
Normal file
737
package-lock.json
generated
Normal file
@ -0,0 +1,737 @@
|
|||||||
|
{
|
||||||
|
"name": "lwc-plugin-trend-line",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "lwc-plugin-trend-line",
|
||||||
|
"dependencies": {
|
||||||
|
"dts-bundle-generator": "^8.0.1",
|
||||||
|
"fancy-canvas": "^2.1.0",
|
||||||
|
"lightweight-charts": "^4.1.0-rc2",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@rollup/plugin-terser": "^0.4.4",
|
||||||
|
"@rollup/plugin-typescript": "^11.1.6",
|
||||||
|
"rollup": "^4.13.0",
|
||||||
|
"typescript": "^5.4.3",
|
||||||
|
"vite": "^4.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
|
"version": "0.3.5",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/set-array": "^1.2.1",
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.4.10",
|
||||||
|
"@jridgewell/trace-mapping": "^0.3.24"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/resolve-uri": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/set-array": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/source-map": {
|
||||||
|
"version": "0.3.6",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/gen-mapping": "^0.3.5",
|
||||||
|
"@jridgewell/trace-mapping": "^0.3.25"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
|
"version": "1.4.15",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
|
"version": "0.3.25",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/resolve-uri": "^3.1.0",
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/plugin-terser": {
|
||||||
|
"version": "0.4.4",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"serialize-javascript": "^6.0.1",
|
||||||
|
"smob": "^1.0.0",
|
||||||
|
"terser": "^5.17.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"rollup": "^2.0.0||^3.0.0||^4.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"rollup": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/plugin-typescript": {
|
||||||
|
"version": "11.1.6",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@rollup/pluginutils": "^5.1.0",
|
||||||
|
"resolve": "^1.22.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"rollup": "^2.14.0||^3.0.0||^4.0.0",
|
||||||
|
"tslib": "*",
|
||||||
|
"typescript": ">=3.7.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"rollup": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"tslib": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/pluginutils": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "^1.0.0",
|
||||||
|
"estree-walker": "^2.0.2",
|
||||||
|
"picomatch": "^2.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"rollup": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-darwin-x64": {
|
||||||
|
"version": "4.13.0",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@types/estree": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/acorn": {
|
||||||
|
"version": "8.11.3",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"acorn": "bin/acorn"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ansi-regex": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ansi-styles": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/buffer-from": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/cliui": {
|
||||||
|
"version": "8.0.1",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"strip-ansi": "^6.0.1",
|
||||||
|
"wrap-ansi": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/color-convert": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "~1.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/color-name": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/commander": {
|
||||||
|
"version": "2.20.3",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/dts-bundle-generator": {
|
||||||
|
"version": "8.1.2",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"typescript": ">=5.0.2",
|
||||||
|
"yargs": "^17.6.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"dts-bundle-generator": "dist/bin/dts-bundle-generator.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/emoji-regex": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/esbuild": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"esbuild": "bin/esbuild"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@esbuild/android-arm": "0.18.20",
|
||||||
|
"@esbuild/android-arm64": "0.18.20",
|
||||||
|
"@esbuild/android-x64": "0.18.20",
|
||||||
|
"@esbuild/darwin-arm64": "0.18.20",
|
||||||
|
"@esbuild/darwin-x64": "0.18.20",
|
||||||
|
"@esbuild/freebsd-arm64": "0.18.20",
|
||||||
|
"@esbuild/freebsd-x64": "0.18.20",
|
||||||
|
"@esbuild/linux-arm": "0.18.20",
|
||||||
|
"@esbuild/linux-arm64": "0.18.20",
|
||||||
|
"@esbuild/linux-ia32": "0.18.20",
|
||||||
|
"@esbuild/linux-loong64": "0.18.20",
|
||||||
|
"@esbuild/linux-mips64el": "0.18.20",
|
||||||
|
"@esbuild/linux-ppc64": "0.18.20",
|
||||||
|
"@esbuild/linux-riscv64": "0.18.20",
|
||||||
|
"@esbuild/linux-s390x": "0.18.20",
|
||||||
|
"@esbuild/linux-x64": "0.18.20",
|
||||||
|
"@esbuild/netbsd-x64": "0.18.20",
|
||||||
|
"@esbuild/openbsd-x64": "0.18.20",
|
||||||
|
"@esbuild/sunos-x64": "0.18.20",
|
||||||
|
"@esbuild/win32-arm64": "0.18.20",
|
||||||
|
"@esbuild/win32-ia32": "0.18.20",
|
||||||
|
"@esbuild/win32-x64": "0.18.20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/escalade": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/estree-walker": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/fancy-canvas": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/function-bind": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-caller-file": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": "6.* || 8.* || >= 10.*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hasown": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-core-module": {
|
||||||
|
"version": "2.13.1",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"hasown": "^2.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-fullwidth-code-point": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightweight-charts": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"fancy-canvas": "2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nanoid": {
|
||||||
|
"version": "3.3.7",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"nanoid": "bin/nanoid.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/path-parse": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/picocolors": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/picomatch": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/postcss": {
|
||||||
|
"version": "8.4.37",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/postcss/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tidelift",
|
||||||
|
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"nanoid": "^3.3.7",
|
||||||
|
"picocolors": "^1.0.0",
|
||||||
|
"source-map-js": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || >=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/randombytes": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "^5.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/require-directory": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/resolve": {
|
||||||
|
"version": "1.22.8",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-core-module": "^2.13.0",
|
||||||
|
"path-parse": "^1.0.7",
|
||||||
|
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"resolve": "bin/resolve"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rollup": {
|
||||||
|
"version": "4.13.0",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "1.0.5"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"rollup": "dist/bin/rollup"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0",
|
||||||
|
"npm": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@rollup/rollup-android-arm-eabi": "4.13.0",
|
||||||
|
"@rollup/rollup-android-arm64": "4.13.0",
|
||||||
|
"@rollup/rollup-darwin-arm64": "4.13.0",
|
||||||
|
"@rollup/rollup-darwin-x64": "4.13.0",
|
||||||
|
"@rollup/rollup-linux-arm-gnueabihf": "4.13.0",
|
||||||
|
"@rollup/rollup-linux-arm64-gnu": "4.13.0",
|
||||||
|
"@rollup/rollup-linux-arm64-musl": "4.13.0",
|
||||||
|
"@rollup/rollup-linux-riscv64-gnu": "4.13.0",
|
||||||
|
"@rollup/rollup-linux-x64-gnu": "4.13.0",
|
||||||
|
"@rollup/rollup-linux-x64-musl": "4.13.0",
|
||||||
|
"@rollup/rollup-win32-arm64-msvc": "4.13.0",
|
||||||
|
"@rollup/rollup-win32-ia32-msvc": "4.13.0",
|
||||||
|
"@rollup/rollup-win32-x64-msvc": "4.13.0",
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/serialize-javascript": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"randombytes": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/smob": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/source-map": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/source-map-js": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/source-map-support": {
|
||||||
|
"version": "0.5.21",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-from": "^1.0.0",
|
||||||
|
"source-map": "^0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/string-width": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^8.0.0",
|
||||||
|
"is-fullwidth-code-point": "^3.0.0",
|
||||||
|
"strip-ansi": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/strip-ansi": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/supports-preserve-symlinks-flag": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/terser": {
|
||||||
|
"version": "5.29.2",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/source-map": "^0.3.3",
|
||||||
|
"acorn": "^8.8.2",
|
||||||
|
"commander": "^2.20.0",
|
||||||
|
"source-map-support": "~0.5.20"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"terser": "bin/terser"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.4.3",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vite": {
|
||||||
|
"version": "4.5.2",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"esbuild": "^0.18.10",
|
||||||
|
"postcss": "^8.4.27",
|
||||||
|
"rollup": "^3.27.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"vite": "bin/vite.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.18.0 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/node": ">= 14",
|
||||||
|
"less": "*",
|
||||||
|
"lightningcss": "^1.21.0",
|
||||||
|
"sass": "*",
|
||||||
|
"stylus": "*",
|
||||||
|
"sugarss": "*",
|
||||||
|
"terser": "^5.4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"less": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"lightningcss": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sass": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"stylus": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sugarss": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"terser": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vite/node_modules/rollup": {
|
||||||
|
"version": "3.29.4",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"rollup": "dist/bin/rollup"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.18.0",
|
||||||
|
"npm": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.0.0",
|
||||||
|
"string-width": "^4.1.0",
|
||||||
|
"strip-ansi": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/y18n": {
|
||||||
|
"version": "5.0.8",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs": {
|
||||||
|
"version": "17.7.2",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cliui": "^8.0.1",
|
||||||
|
"escalade": "^3.1.1",
|
||||||
|
"get-caller-file": "^2.0.5",
|
||||||
|
"require-directory": "^2.1.1",
|
||||||
|
"string-width": "^4.2.3",
|
||||||
|
"y18n": "^5.0.5",
|
||||||
|
"yargs-parser": "^21.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs-parser": {
|
||||||
|
"version": "21.1.1",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
package.json
Normal file
21
package.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "lwc-plugin-trend-line",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --config src/vite.config.js",
|
||||||
|
"build": "./build.sh"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@rollup/plugin-terser": "^0.4.4",
|
||||||
|
"@rollup/plugin-typescript": "^11.1.6",
|
||||||
|
"rollup": "^4.13.0",
|
||||||
|
"typescript": "^5.4.3",
|
||||||
|
"vite": "^4.3.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"dts-bundle-generator": "^8.0.1",
|
||||||
|
"fancy-canvas": "^2.1.0",
|
||||||
|
"lightweight-charts": "^4.1.0-rc2",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
21
rollup.config.js
Normal file
21
rollup.config.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import typescript from '@rollup/plugin-typescript';
|
||||||
|
import terser from '@rollup/plugin-terser';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
input: 'src/index.ts',
|
||||||
|
output: {
|
||||||
|
file: 'dist/bundle.js',
|
||||||
|
format: 'iife',
|
||||||
|
name: 'Lib',
|
||||||
|
globals: {
|
||||||
|
'lightweight-charts': 'LightweightCharts'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
external: ['lightweight-charts'],
|
||||||
|
plugins: [
|
||||||
|
typescript(),
|
||||||
|
terser(),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
6
setup.py
6
setup.py
@ -5,15 +5,15 @@ with open('README.md', 'r', encoding='utf-8') as f:
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='lightweight_charts',
|
name='lightweight_charts',
|
||||||
version='1.0.21',
|
version='2.0',
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
python_requires='>=3.8',
|
python_requires='>=3.8',
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'pandas',
|
'pandas',
|
||||||
'pywebview==4.4.1',
|
'pywebview>=5.0.5',
|
||||||
],
|
],
|
||||||
package_data={
|
package_data={
|
||||||
'lightweight_charts': ['js/*.js'],
|
'lightweight_charts': ['js/*'],
|
||||||
},
|
},
|
||||||
author='louisnw',
|
author='louisnw',
|
||||||
license='MIT',
|
license='MIT',
|
||||||
|
|||||||
153
src/box/box.ts
Normal file
153
src/box/box.ts
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
import {
|
||||||
|
MouseEventParams,
|
||||||
|
} from 'lightweight-charts';
|
||||||
|
|
||||||
|
import { Point } from '../drawing/data-source';
|
||||||
|
import { InteractionState } from '../drawing/drawing';
|
||||||
|
import { DrawingOptions, defaultOptions } from '../drawing/options';
|
||||||
|
import { BoxPaneView } from './pane-view';
|
||||||
|
import { TwoPointDrawing } from '../drawing/two-point-drawing';
|
||||||
|
|
||||||
|
|
||||||
|
export interface BoxOptions extends DrawingOptions {
|
||||||
|
fillEnabled: boolean;
|
||||||
|
fillColor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultBoxOptions = {
|
||||||
|
fillEnabled: true,
|
||||||
|
fillColor: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
...defaultOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class Box extends TwoPointDrawing {
|
||||||
|
_type = "Box";
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
p1: Point,
|
||||||
|
p2: Point,
|
||||||
|
options?: Partial<BoxOptions>
|
||||||
|
) {
|
||||||
|
super(p1, p2, options);
|
||||||
|
this._options = {
|
||||||
|
...defaultBoxOptions,
|
||||||
|
...options,
|
||||||
|
}
|
||||||
|
this._paneViews = [new BoxPaneView(this)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// autoscaleInfo(startTimePoint: Logical, endTimePoint: Logical): AutoscaleInfo | null {
|
||||||
|
// const p1Index = this._pointIndex(this._p1);
|
||||||
|
// const p2Index = this._pointIndex(this._p2);
|
||||||
|
// if (p1Index === null || p2Index === null) return null;
|
||||||
|
// if (endTimePoint < p1Index || startTimePoint > p2Index) return null;
|
||||||
|
// return {
|
||||||
|
// priceRange: {
|
||||||
|
// minValue: this._minPrice,
|
||||||
|
// maxValue: this._maxPrice,
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
_moveToState(state: InteractionState) {
|
||||||
|
switch(state) {
|
||||||
|
case InteractionState.NONE:
|
||||||
|
document.body.style.cursor = "default";
|
||||||
|
this._hovered = false;
|
||||||
|
this._unsubscribe("mousedown", this._handleMouseDownInteraction);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case InteractionState.HOVERING:
|
||||||
|
document.body.style.cursor = "pointer";
|
||||||
|
this._hovered = true;
|
||||||
|
this._unsubscribe("mouseup", this._handleMouseUpInteraction);
|
||||||
|
this._subscribe("mousedown", this._handleMouseDownInteraction)
|
||||||
|
this.chart.applyOptions({handleScroll: true});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case InteractionState.DRAGGINGP1:
|
||||||
|
case InteractionState.DRAGGINGP2:
|
||||||
|
case InteractionState.DRAGGINGP3:
|
||||||
|
case InteractionState.DRAGGINGP4:
|
||||||
|
case InteractionState.DRAGGING:
|
||||||
|
document.body.style.cursor = "grabbing";
|
||||||
|
document.body.addEventListener("mouseup", this._handleMouseUpInteraction);
|
||||||
|
this._subscribe("mouseup", this._handleMouseUpInteraction);
|
||||||
|
this.chart.applyOptions({handleScroll: false});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this._state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDrag(diff: any) {
|
||||||
|
if (this._state == InteractionState.DRAGGING || this._state == InteractionState.DRAGGINGP1) {
|
||||||
|
this._addDiffToPoint(this.p1, diff.logical, diff.price);
|
||||||
|
}
|
||||||
|
if (this._state == InteractionState.DRAGGING || this._state == InteractionState.DRAGGINGP2) {
|
||||||
|
this._addDiffToPoint(this.p2, diff.logical, diff.price);
|
||||||
|
}
|
||||||
|
if (this._state != InteractionState.DRAGGING) {
|
||||||
|
if (this._state == InteractionState.DRAGGINGP3) {
|
||||||
|
this._addDiffToPoint(this.p1, diff.logical, 0);
|
||||||
|
this._addDiffToPoint(this.p2, 0, diff.price);
|
||||||
|
}
|
||||||
|
if (this._state == InteractionState.DRAGGINGP4) {
|
||||||
|
this._addDiffToPoint(this.p1, 0, diff.price);
|
||||||
|
this._addDiffToPoint(this.p2, diff.logical, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _onMouseDown() {
|
||||||
|
this._startDragPoint = null;
|
||||||
|
const hoverPoint = this._latestHoverPoint;
|
||||||
|
const p1 = this._paneViews[0]._p1;
|
||||||
|
const p2 = this._paneViews[0]._p2;
|
||||||
|
|
||||||
|
if (!p1.x || !p2.x || !p1.y || !p2.y) return this._moveToState(InteractionState.DRAGGING);
|
||||||
|
|
||||||
|
const tolerance = 10;
|
||||||
|
if (Math.abs(hoverPoint.x-p1.x) < tolerance && Math.abs(hoverPoint.y-p1.y) < tolerance) {
|
||||||
|
this._moveToState(InteractionState.DRAGGINGP1)
|
||||||
|
}
|
||||||
|
else if (Math.abs(hoverPoint.x-p2.x) < tolerance && Math.abs(hoverPoint.y-p2.y) < tolerance) {
|
||||||
|
this._moveToState(InteractionState.DRAGGINGP2)
|
||||||
|
}
|
||||||
|
else if (Math.abs(hoverPoint.x-p1.x) < tolerance && Math.abs(hoverPoint.y-p2.y) < tolerance) {
|
||||||
|
this._moveToState(InteractionState.DRAGGINGP3)
|
||||||
|
}
|
||||||
|
else if (Math.abs(hoverPoint.x-p2.x) < tolerance && Math.abs(hoverPoint.y-p1.y) < tolerance) {
|
||||||
|
this._moveToState(InteractionState.DRAGGINGP4)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this._moveToState(InteractionState.DRAGGING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _mouseIsOverDrawing(param: MouseEventParams, tolerance = 4) {
|
||||||
|
if (!param.point) return false;
|
||||||
|
|
||||||
|
const x1 = this._paneViews[0]._p1.x;
|
||||||
|
const y1 = this._paneViews[0]._p1.y;
|
||||||
|
const x2 = this._paneViews[0]._p2.x;
|
||||||
|
const y2 = this._paneViews[0]._p2.y;
|
||||||
|
if (!x1 || !x2 || !y1 || !y2 ) return false;
|
||||||
|
|
||||||
|
const mouseX = param.point.x;
|
||||||
|
const mouseY = param.point.y;
|
||||||
|
|
||||||
|
const mainX = Math.min(x1, x2);
|
||||||
|
const mainY = Math.min(y1, y2);
|
||||||
|
|
||||||
|
const width = Math.abs(x1-x2);
|
||||||
|
const height = Math.abs(y1-y2);
|
||||||
|
|
||||||
|
const halfTolerance = tolerance/2;
|
||||||
|
|
||||||
|
return mouseX > mainX-halfTolerance && mouseX < mainX+width+halfTolerance &&
|
||||||
|
mouseY > mainY-halfTolerance && mouseY < mainY+height+halfTolerance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
44
src/box/pane-renderer.ts
Normal file
44
src/box/pane-renderer.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { ViewPoint } from "../drawing/pane-view";
|
||||||
|
import { CanvasRenderingTarget2D } from "fancy-canvas";
|
||||||
|
import { TwoPointDrawingPaneRenderer } from "../drawing/pane-renderer";
|
||||||
|
import { BoxOptions } from "./box";
|
||||||
|
import { setLineStyle } from "../helpers/canvas-rendering";
|
||||||
|
|
||||||
|
export class BoxPaneRenderer extends TwoPointDrawingPaneRenderer {
|
||||||
|
declare _options: BoxOptions;
|
||||||
|
|
||||||
|
constructor(p1: ViewPoint, p2: ViewPoint, options: BoxOptions, showCircles: boolean) {
|
||||||
|
super(p1, p2, options, showCircles)
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(target: CanvasRenderingTarget2D) {
|
||||||
|
target.useBitmapCoordinateSpace(scope => {
|
||||||
|
|
||||||
|
const ctx = scope.context;
|
||||||
|
|
||||||
|
const scaled = this._getScaledCoordinates(scope);
|
||||||
|
|
||||||
|
if (!scaled) return;
|
||||||
|
|
||||||
|
ctx.lineWidth = this._options.width;
|
||||||
|
ctx.strokeStyle = this._options.lineColor;
|
||||||
|
setLineStyle(ctx, this._options.lineStyle)
|
||||||
|
ctx.fillStyle = this._options.fillColor;
|
||||||
|
|
||||||
|
const mainX = Math.min(scaled.x1, scaled.x2);
|
||||||
|
const mainY = Math.min(scaled.y1, scaled.y2);
|
||||||
|
const width = Math.abs(scaled.x1-scaled.x2);
|
||||||
|
const height = Math.abs(scaled.y1-scaled.y2);
|
||||||
|
|
||||||
|
ctx.strokeRect(mainX, mainY, width, height);
|
||||||
|
ctx.fillRect(mainX, mainY, width, height);
|
||||||
|
|
||||||
|
if (!this._hovered) return;
|
||||||
|
this._drawEndCircle(scope, mainX, mainY);
|
||||||
|
this._drawEndCircle(scope, mainX+width, mainY);
|
||||||
|
this._drawEndCircle(scope, mainX+width, mainY+height);
|
||||||
|
this._drawEndCircle(scope, mainX, mainY+height);
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/box/pane-view.ts
Normal file
18
src/box/pane-view.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Box, BoxOptions } from './box';
|
||||||
|
import { BoxPaneRenderer } from './pane-renderer';
|
||||||
|
import { TwoPointDrawingPaneView } from '../drawing/pane-view';
|
||||||
|
|
||||||
|
export class BoxPaneView extends TwoPointDrawingPaneView {
|
||||||
|
constructor(source: Box) {
|
||||||
|
super(source)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer() {
|
||||||
|
return new BoxPaneRenderer(
|
||||||
|
this._p1,
|
||||||
|
this._p2,
|
||||||
|
this._source._options as BoxOptions,
|
||||||
|
this._source.hovered,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
144
src/context-menu/color-picker.ts
Normal file
144
src/context-menu/color-picker.ts
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
import { Drawing } from "../drawing/drawing";
|
||||||
|
import { DrawingOptions } from "../drawing/options";
|
||||||
|
import { GlobalParams } from "../general/global-params";
|
||||||
|
|
||||||
|
declare const window: GlobalParams;
|
||||||
|
|
||||||
|
|
||||||
|
export class ColorPicker {
|
||||||
|
private static readonly 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',
|
||||||
|
]
|
||||||
|
|
||||||
|
public _div: HTMLDivElement;
|
||||||
|
private saveDrawings: Function;
|
||||||
|
|
||||||
|
private opacity: number = 0;
|
||||||
|
private _opacitySlider: HTMLInputElement;
|
||||||
|
private _opacityLabel: HTMLDivElement;
|
||||||
|
private rgba: number[] | undefined;
|
||||||
|
|
||||||
|
constructor(saveDrawings: Function,
|
||||||
|
private colorOption: keyof DrawingOptions,
|
||||||
|
) {
|
||||||
|
this.saveDrawings = saveDrawings
|
||||||
|
|
||||||
|
this._div = document.createElement('div');
|
||||||
|
this._div.classList.add('color-picker');
|
||||||
|
|
||||||
|
let colorPicker = document.createElement('div')
|
||||||
|
colorPicker.style.margin = '10px'
|
||||||
|
colorPicker.style.display = 'flex'
|
||||||
|
colorPicker.style.flexWrap = 'wrap'
|
||||||
|
|
||||||
|
ColorPicker.colors.forEach((color) => colorPicker.appendChild(this.makeColorBox(color)))
|
||||||
|
|
||||||
|
let separator = document.createElement('div')
|
||||||
|
separator.style.backgroundColor = window.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'
|
||||||
|
|
||||||
|
this._opacityLabel = document.createElement('div')
|
||||||
|
this._opacityLabel.style.color = 'lightgray'
|
||||||
|
this._opacityLabel.style.fontSize = '12px'
|
||||||
|
|
||||||
|
this._opacitySlider = document.createElement('input')
|
||||||
|
this._opacitySlider.type = 'range'
|
||||||
|
this._opacitySlider.value = (this.opacity*100).toString();
|
||||||
|
this._opacityLabel.innerText = this._opacitySlider.value+'%'
|
||||||
|
this._opacitySlider.oninput = () => {
|
||||||
|
this._opacityLabel.innerText = this._opacitySlider.value+'%'
|
||||||
|
this.opacity = parseInt(this._opacitySlider.value)/100
|
||||||
|
this.updateColor()
|
||||||
|
}
|
||||||
|
|
||||||
|
opacity.appendChild(opacityText)
|
||||||
|
opacity.appendChild(this._opacitySlider)
|
||||||
|
opacity.appendChild(this._opacityLabel)
|
||||||
|
|
||||||
|
this._div.appendChild(colorPicker)
|
||||||
|
this._div.appendChild(separator)
|
||||||
|
this._div.appendChild(opacity)
|
||||||
|
window.containerDiv.appendChild(this._div)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private _updateOpacitySlider() {
|
||||||
|
this._opacitySlider.value = (this.opacity*100).toString();
|
||||||
|
this._opacityLabel.innerText = this._opacitySlider.value+'%';
|
||||||
|
}
|
||||||
|
|
||||||
|
makeColorBox(color: string) {
|
||||||
|
const 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', () => box.style.border = '2px solid lightgray')
|
||||||
|
box.addEventListener('mouseout', () => box.style.border = 'none')
|
||||||
|
|
||||||
|
const rgba = ColorPicker.extractRGBA(color)
|
||||||
|
box.addEventListener('click', () => {
|
||||||
|
this.rgba = rgba;
|
||||||
|
this.updateColor();
|
||||||
|
})
|
||||||
|
return box
|
||||||
|
}
|
||||||
|
|
||||||
|
private static extractRGBA(anyColor: string) {
|
||||||
|
const dummyElem = document.createElement('div');
|
||||||
|
dummyElem.style.color = anyColor;
|
||||||
|
document.body.appendChild(dummyElem);
|
||||||
|
const computedColor = getComputedStyle(dummyElem).color;
|
||||||
|
document.body.removeChild(dummyElem);
|
||||||
|
const rgb = computedColor.match(/\d+/g)?.map(Number);
|
||||||
|
if (!rgb) return [];
|
||||||
|
let isRgba = computedColor.includes('rgba');
|
||||||
|
let opacity = isRgba ? parseFloat(computedColor.split(',')[3]) : 1
|
||||||
|
return [rgb[0], rgb[1], rgb[2], opacity]
|
||||||
|
}
|
||||||
|
|
||||||
|
updateColor() {
|
||||||
|
if (!Drawing.lastHoveredObject || !this.rgba) return;
|
||||||
|
const oColor = `rgba(${this.rgba[0]}, ${this.rgba[1]}, ${this.rgba[2]}, ${this.opacity})`
|
||||||
|
Drawing.lastHoveredObject.applyOptions({[this.colorOption]: oColor})
|
||||||
|
this.saveDrawings()
|
||||||
|
}
|
||||||
|
openMenu(rect: DOMRect) {
|
||||||
|
if (!Drawing.lastHoveredObject) return;
|
||||||
|
this.rgba = ColorPicker.extractRGBA(
|
||||||
|
Drawing.lastHoveredObject._options[this.colorOption] as string
|
||||||
|
)
|
||||||
|
this.opacity = this.rgba[3];
|
||||||
|
this._updateOpacitySlider();
|
||||||
|
this._div.style.top = (rect.top-30)+'px'
|
||||||
|
this._div.style.left = rect.right+'px'
|
||||||
|
this._div.style.display = 'flex'
|
||||||
|
|
||||||
|
setTimeout(() => document.addEventListener('mousedown', (event: MouseEvent) => {
|
||||||
|
if (!this._div.contains(event.target as Node)) {
|
||||||
|
this.closeMenu()
|
||||||
|
}
|
||||||
|
}), 10)
|
||||||
|
}
|
||||||
|
closeMenu() {
|
||||||
|
document.body.removeEventListener('click', this.closeMenu)
|
||||||
|
this._div.style.display = 'none'
|
||||||
|
}
|
||||||
|
}
|
||||||
153
src/context-menu/context-menu.ts
Normal file
153
src/context-menu/context-menu.ts
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
import { Drawing } from "../drawing/drawing";
|
||||||
|
import { DrawingTool } from "../drawing/drawing-tool";
|
||||||
|
import { DrawingOptions } from "../drawing/options";
|
||||||
|
import { GlobalParams } from "../general/global-params";
|
||||||
|
import { ColorPicker } from "./color-picker";
|
||||||
|
import { StylePicker } from "./style-picker";
|
||||||
|
|
||||||
|
|
||||||
|
export function camelToTitle(inputString: string) {
|
||||||
|
const result = [];
|
||||||
|
for (const c of inputString) {
|
||||||
|
if (result.length == 0) {
|
||||||
|
result.push(c.toUpperCase());
|
||||||
|
} else if (c == c.toUpperCase()) {
|
||||||
|
result.push(' '+c);
|
||||||
|
} else result.push(c);
|
||||||
|
}
|
||||||
|
return result.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Item {
|
||||||
|
elem: HTMLSpanElement;
|
||||||
|
action: Function;
|
||||||
|
closeAction: Function | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare const window: GlobalParams;
|
||||||
|
|
||||||
|
|
||||||
|
export class ContextMenu {
|
||||||
|
private div: HTMLDivElement
|
||||||
|
private hoverItem: Item | null;
|
||||||
|
private items: HTMLElement[] = []
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private saveDrawings: Function,
|
||||||
|
private drawingTool: DrawingTool,
|
||||||
|
) {
|
||||||
|
this._onRightClick = this._onRightClick.bind(this);
|
||||||
|
this.div = document.createElement('div');
|
||||||
|
this.div.classList.add('context-menu');
|
||||||
|
document.body.appendChild(this.div);
|
||||||
|
this.hoverItem = null;
|
||||||
|
document.body.addEventListener('contextmenu', this._onRightClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleClick = (ev: MouseEvent) => this._onClick(ev);
|
||||||
|
|
||||||
|
private _onClick(ev: MouseEvent) {
|
||||||
|
if (!ev.target) return;
|
||||||
|
if (!this.div.contains(ev.target as Node)) {
|
||||||
|
this.div.style.display = 'none';
|
||||||
|
document.body.removeEventListener('click', this._handleClick);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onRightClick(ev: MouseEvent) {
|
||||||
|
if (!Drawing.hoveredObject) return;
|
||||||
|
|
||||||
|
for (const item of this.items) {
|
||||||
|
this.div.removeChild(item);
|
||||||
|
}
|
||||||
|
this.items = [];
|
||||||
|
|
||||||
|
for (const optionName of Object.keys(Drawing.hoveredObject._options)) {
|
||||||
|
let subMenu;
|
||||||
|
if (optionName.toLowerCase().includes('color')) {
|
||||||
|
subMenu = new ColorPicker(this.saveDrawings, optionName as keyof DrawingOptions);
|
||||||
|
} else if (optionName === 'lineStyle') {
|
||||||
|
subMenu = new StylePicker(this.saveDrawings)
|
||||||
|
} else continue;
|
||||||
|
|
||||||
|
let onClick = (rect: DOMRect) => subMenu.openMenu(rect)
|
||||||
|
this.menuItem(camelToTitle(optionName), onClick, () => {
|
||||||
|
document.removeEventListener('click', subMenu.closeMenu)
|
||||||
|
subMenu._div.style.display = 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let onClickDelete = () => this.drawingTool.delete(Drawing.lastHoveredObject);
|
||||||
|
this.separator()
|
||||||
|
this.menuItem('Delete Drawing', onClickDelete)
|
||||||
|
|
||||||
|
// const colorPicker = new ColorPicker(this.saveDrawings)
|
||||||
|
// const stylePicker = new StylePicker(this.saveDrawings)
|
||||||
|
|
||||||
|
// let onClickDelete = () => this._drawingTool.delete(Drawing.lastHoveredObject);
|
||||||
|
// let onClickColor = (rect: DOMRect) => colorPicker.openMenu(rect)
|
||||||
|
// let onClickStyle = (rect: DOMRect) => stylePicker.openMenu(rect)
|
||||||
|
|
||||||
|
// contextMenu.menuItem('Color Picker', onClickColor, () => {
|
||||||
|
// document.removeEventListener('click', colorPicker.closeMenu)
|
||||||
|
// colorPicker._div.style.display = 'none'
|
||||||
|
// })
|
||||||
|
// contextMenu.menuItem('Style', onClickStyle, () => {
|
||||||
|
// document.removeEventListener('click', stylePicker.closeMenu)
|
||||||
|
// stylePicker._div.style.display = 'none'
|
||||||
|
// })
|
||||||
|
// contextMenu.separator()
|
||||||
|
// contextMenu.menuItem('Delete Drawing', onClickDelete)
|
||||||
|
|
||||||
|
|
||||||
|
ev.preventDefault();
|
||||||
|
this.div.style.left = ev.clientX + 'px';
|
||||||
|
this.div.style.top = ev.clientY + 'px';
|
||||||
|
this.div.style.display = 'block';
|
||||||
|
document.body.addEventListener('click', this._handleClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
public menuItem(text: string, action: Function, hover: Function | null = null) {
|
||||||
|
const item = document.createElement('span');
|
||||||
|
item.classList.add('context-menu-item');
|
||||||
|
this.div.appendChild(item);
|
||||||
|
|
||||||
|
const 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', () => {
|
||||||
|
if (this.hoverItem && this.hoverItem.closeAction) this.hoverItem.closeAction()
|
||||||
|
this.hoverItem = {elem: elem, action: action, closeAction: hover}
|
||||||
|
})
|
||||||
|
if (!hover) item.addEventListener('click', (event) => {action(event); this.div.style.display = 'none'})
|
||||||
|
else {
|
||||||
|
let timeout: number;
|
||||||
|
item.addEventListener('mouseover', () => timeout = setTimeout(() => action(item.getBoundingClientRect()), 100))
|
||||||
|
item.addEventListener('mouseout', () => clearTimeout(timeout))
|
||||||
|
}
|
||||||
|
|
||||||
|
this.items.push(item);
|
||||||
|
|
||||||
|
}
|
||||||
|
public separator() {
|
||||||
|
const separator = document.createElement('div')
|
||||||
|
separator.style.width = '90%'
|
||||||
|
separator.style.height = '1px'
|
||||||
|
separator.style.margin = '3px 0px'
|
||||||
|
separator.style.backgroundColor = window.pane.borderColor
|
||||||
|
this.div.appendChild(separator)
|
||||||
|
|
||||||
|
this.items.push(separator);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
57
src/context-menu/style-picker.ts
Normal file
57
src/context-menu/style-picker.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { LineStyle } from "lightweight-charts";
|
||||||
|
import { GlobalParams } from "../general/global-params";
|
||||||
|
import { Drawing } from "../drawing/drawing";
|
||||||
|
|
||||||
|
declare const window: GlobalParams;
|
||||||
|
|
||||||
|
|
||||||
|
export class StylePicker {
|
||||||
|
private static readonly _styles = [
|
||||||
|
{name: 'Solid', var: LineStyle.Solid},
|
||||||
|
{name: 'Dotted', var: LineStyle.Dotted},
|
||||||
|
{name: 'Dashed', var: LineStyle.Dashed},
|
||||||
|
{name: 'Large Dashed', var: LineStyle.LargeDashed},
|
||||||
|
{name: 'Sparse Dotted', var: LineStyle.SparseDotted},
|
||||||
|
]
|
||||||
|
|
||||||
|
public _div: HTMLDivElement;
|
||||||
|
private _saveDrawings: Function;
|
||||||
|
|
||||||
|
constructor(saveDrawings: Function) {
|
||||||
|
this._saveDrawings = saveDrawings
|
||||||
|
|
||||||
|
this._div = document.createElement('div');
|
||||||
|
this._div.classList.add('context-menu');
|
||||||
|
StylePicker._styles.forEach((style) => {
|
||||||
|
this._div.appendChild(this._makeTextBox(style.name, style.var))
|
||||||
|
})
|
||||||
|
window.containerDiv.appendChild(this._div);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _makeTextBox(text: string, style: LineStyle) {
|
||||||
|
const item = document.createElement('span');
|
||||||
|
item.classList.add('context-menu-item');
|
||||||
|
item.innerText = text
|
||||||
|
item.addEventListener('click', () => {
|
||||||
|
Drawing.lastHoveredObject?.applyOptions({lineStyle: style});
|
||||||
|
this._saveDrawings();
|
||||||
|
})
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
openMenu(rect: DOMRect) {
|
||||||
|
this._div.style.top = (rect.top-30)+'px'
|
||||||
|
this._div.style.left = rect.right+'px'
|
||||||
|
this._div.style.display = 'block'
|
||||||
|
|
||||||
|
setTimeout(() => document.addEventListener('mousedown', (event: MouseEvent) => {
|
||||||
|
if (!this._div.contains(event.target as Node)) {
|
||||||
|
this.closeMenu()
|
||||||
|
}
|
||||||
|
}), 10)
|
||||||
|
}
|
||||||
|
closeMenu() {
|
||||||
|
document.removeEventListener('click', this.closeMenu)
|
||||||
|
this._div.style.display = 'none'
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/drawing/data-source.ts
Normal file
15
src/drawing/data-source.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import {
|
||||||
|
Logical,
|
||||||
|
Time,
|
||||||
|
} from 'lightweight-charts';
|
||||||
|
|
||||||
|
export interface Point {
|
||||||
|
time: Time | null;
|
||||||
|
logical: Logical;
|
||||||
|
price: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiffPoint {
|
||||||
|
logical: number;
|
||||||
|
price: number;
|
||||||
|
}
|
||||||
122
src/drawing/drawing-tool.ts
Normal file
122
src/drawing/drawing-tool.ts
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import {
|
||||||
|
IChartApi,
|
||||||
|
ISeriesApi,
|
||||||
|
Logical,
|
||||||
|
MouseEventParams,
|
||||||
|
SeriesType,
|
||||||
|
} from 'lightweight-charts';
|
||||||
|
import { Drawing } from './drawing';
|
||||||
|
import { HorizontalLine } from '../horizontal-line/horizontal-line';
|
||||||
|
|
||||||
|
|
||||||
|
export class DrawingTool {
|
||||||
|
private _chart: IChartApi;
|
||||||
|
private _series: ISeriesApi<SeriesType>;
|
||||||
|
private _finishDrawingCallback: Function | null = null;
|
||||||
|
|
||||||
|
private _drawings: Drawing[] = [];
|
||||||
|
private _activeDrawing: Drawing | null = null;
|
||||||
|
private _isDrawing: boolean = false;
|
||||||
|
private _drawingType: (new (...args: any[]) => Drawing) | null = null;
|
||||||
|
|
||||||
|
constructor(chart: IChartApi, series: ISeriesApi<SeriesType>, finishDrawingCallback: Function | null = null) {
|
||||||
|
this._chart = chart;
|
||||||
|
this._series = series;
|
||||||
|
this._finishDrawingCallback = finishDrawingCallback;
|
||||||
|
|
||||||
|
this._chart.subscribeClick(this._clickHandler);
|
||||||
|
this._chart.subscribeCrosshairMove(this._moveHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _clickHandler = (param: MouseEventParams) => this._onClick(param);
|
||||||
|
private _moveHandler = (param: MouseEventParams) => this._onMouseMove(param);
|
||||||
|
|
||||||
|
beginDrawing(DrawingType: new (...args: any[]) => Drawing) {
|
||||||
|
this._drawingType = DrawingType;
|
||||||
|
this._isDrawing = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
stopDrawing() {
|
||||||
|
this._isDrawing = false;
|
||||||
|
this._activeDrawing = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get drawings() {
|
||||||
|
return this._drawings;
|
||||||
|
}
|
||||||
|
|
||||||
|
addNewDrawing(drawing: Drawing) {
|
||||||
|
this._series.attachPrimitive(drawing);
|
||||||
|
this._drawings.push(drawing);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(d: Drawing | null) {
|
||||||
|
if (d == null) return;
|
||||||
|
const idx = this._drawings.indexOf(d);
|
||||||
|
if (idx == -1) return;
|
||||||
|
this._drawings.splice(idx, 1)
|
||||||
|
d.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
clearDrawings() {
|
||||||
|
for (const d of this._drawings) d.detach();
|
||||||
|
this._drawings = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
repositionOnTime() {
|
||||||
|
for (const drawing of this.drawings) {
|
||||||
|
const newPoints = []
|
||||||
|
for (const point of drawing.points) {
|
||||||
|
if (!point) {
|
||||||
|
newPoints.push(point);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const logical = point.time ? this._chart.timeScale()
|
||||||
|
.coordinateToLogical(
|
||||||
|
this._chart.timeScale().timeToCoordinate(point.time) || 0
|
||||||
|
) : point.logical;
|
||||||
|
newPoints.push({
|
||||||
|
time: point.time,
|
||||||
|
logical: logical as Logical,
|
||||||
|
price: point.price,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
drawing.updatePoints(...newPoints);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onClick(param: MouseEventParams) {
|
||||||
|
if (!this._isDrawing) return;
|
||||||
|
|
||||||
|
const point = Drawing._eventToPoint(param, this._series);
|
||||||
|
if (!point) return;
|
||||||
|
|
||||||
|
if (this._activeDrawing == null) {
|
||||||
|
if (this._drawingType == null) return;
|
||||||
|
|
||||||
|
this._activeDrawing = new this._drawingType(point, point);
|
||||||
|
this._series.attachPrimitive(this._activeDrawing);
|
||||||
|
if (this._drawingType == HorizontalLine) this._onClick(param);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this._drawings.push(this._activeDrawing);
|
||||||
|
this.stopDrawing();
|
||||||
|
|
||||||
|
if (!this._finishDrawingCallback) return;
|
||||||
|
this._finishDrawingCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onMouseMove(param: MouseEventParams) {
|
||||||
|
if (!param) return;
|
||||||
|
|
||||||
|
for (const t of this._drawings) t._handleHoverInteraction(param);
|
||||||
|
|
||||||
|
if (!this._isDrawing || !this._activeDrawing) return;
|
||||||
|
|
||||||
|
const point = Drawing._eventToPoint(param, this._series);
|
||||||
|
if (!point) return;
|
||||||
|
this._activeDrawing.updatePoints(null, point);
|
||||||
|
// this._activeDrawing.setSecondPoint(point);
|
||||||
|
}
|
||||||
|
}
|
||||||
180
src/drawing/drawing.ts
Normal file
180
src/drawing/drawing.ts
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
import {
|
||||||
|
ISeriesApi,
|
||||||
|
Logical,
|
||||||
|
MouseEventParams,
|
||||||
|
SeriesType
|
||||||
|
} from 'lightweight-charts';
|
||||||
|
|
||||||
|
import { PluginBase } from '../plugin-base';
|
||||||
|
import { DiffPoint, Point } from './data-source';
|
||||||
|
import { DrawingOptions, defaultOptions } from './options';
|
||||||
|
import { DrawingPaneView } from './pane-view';
|
||||||
|
|
||||||
|
export enum InteractionState {
|
||||||
|
NONE,
|
||||||
|
HOVERING,
|
||||||
|
DRAGGING,
|
||||||
|
DRAGGINGP1,
|
||||||
|
DRAGGINGP2,
|
||||||
|
DRAGGINGP3,
|
||||||
|
DRAGGINGP4,
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class Drawing extends PluginBase {
|
||||||
|
_paneViews: DrawingPaneView[] = [];
|
||||||
|
_options: DrawingOptions;
|
||||||
|
|
||||||
|
abstract _type: string;
|
||||||
|
protected _points: (Point|null)[] = [];
|
||||||
|
|
||||||
|
protected _state: InteractionState = InteractionState.NONE;
|
||||||
|
|
||||||
|
protected _startDragPoint: Point | null = null;
|
||||||
|
protected _latestHoverPoint: any | null = null;
|
||||||
|
|
||||||
|
protected static _mouseIsDown: boolean = false;
|
||||||
|
|
||||||
|
public static hoveredObject: Drawing | null = null;
|
||||||
|
public static lastHoveredObject: Drawing | null = null;
|
||||||
|
|
||||||
|
protected _listeners: any[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
options?: Partial<DrawingOptions>
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
this._options = {
|
||||||
|
...defaultOptions,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAllViews() {
|
||||||
|
this._paneViews.forEach(pw => pw.update());
|
||||||
|
}
|
||||||
|
|
||||||
|
paneViews() {
|
||||||
|
return this._paneViews;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyOptions(options: Partial<DrawingOptions>) {
|
||||||
|
this._options = {
|
||||||
|
...this._options,
|
||||||
|
...options,
|
||||||
|
}
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public updatePoints(...points: (Point | null)[]) {
|
||||||
|
for (let i=0; i<this.points.length; i++) {
|
||||||
|
if (points[i] == null) continue;
|
||||||
|
this.points[i] = points[i] as Point;
|
||||||
|
}
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
detach() {
|
||||||
|
this._options.lineColor = 'transparent';
|
||||||
|
this.requestUpdate();
|
||||||
|
this.series.detachPrimitive(this);
|
||||||
|
for (const s of this._listeners) {
|
||||||
|
document.body.removeEventListener(s.name, s.listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
get points() {
|
||||||
|
return this._points;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _subscribe(name: keyof DocumentEventMap, listener: any) {
|
||||||
|
document.body.addEventListener(name, listener);
|
||||||
|
this._listeners.push({name: name, listener: listener});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _unsubscribe(name: keyof DocumentEventMap, callback: any) {
|
||||||
|
document.body.removeEventListener(name, callback);
|
||||||
|
|
||||||
|
const toRemove = this._listeners.find((x) => x.name === name && x.listener === callback)
|
||||||
|
this._listeners.splice(this._listeners.indexOf(toRemove), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleHoverInteraction(param: MouseEventParams) {
|
||||||
|
this._latestHoverPoint = param.point;
|
||||||
|
if (Drawing._mouseIsDown) {
|
||||||
|
this._handleDragInteraction(param);
|
||||||
|
} else {
|
||||||
|
if (this._mouseIsOverDrawing(param)) {
|
||||||
|
if (this._state != InteractionState.NONE) return;
|
||||||
|
this._moveToState(InteractionState.HOVERING);
|
||||||
|
Drawing.hoveredObject = Drawing.lastHoveredObject = this;
|
||||||
|
} else {
|
||||||
|
if (this._state == InteractionState.NONE) return;
|
||||||
|
this._moveToState(InteractionState.NONE);
|
||||||
|
if (Drawing.hoveredObject === this) Drawing.hoveredObject = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static _eventToPoint(param: MouseEventParams, series: ISeriesApi<SeriesType>) {
|
||||||
|
if (!series || !param.point || !param.logical) return null;
|
||||||
|
const barPrice = series.coordinateToPrice(param.point.y);
|
||||||
|
if (barPrice == null) return null;
|
||||||
|
return {
|
||||||
|
time: param.time || null,
|
||||||
|
logical: param.logical,
|
||||||
|
price: barPrice.valueOf(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static _getDiff(p1: Point, p2: Point): DiffPoint {
|
||||||
|
const diff: DiffPoint = {
|
||||||
|
logical: p1.logical-p2.logical,
|
||||||
|
price: p1.price-p2.price,
|
||||||
|
}
|
||||||
|
return diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _addDiffToPoint(point: Point | null, logicalDiff: number, priceDiff: number) {
|
||||||
|
if (!point) return;
|
||||||
|
point.logical = point.logical + logicalDiff as Logical;
|
||||||
|
point.price = point.price+priceDiff;
|
||||||
|
point.time = this.series.dataByIndex(point.logical)?.time || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _handleMouseDownInteraction = () => {
|
||||||
|
// if (Drawing._mouseIsDown) return;
|
||||||
|
Drawing._mouseIsDown = true;
|
||||||
|
this._onMouseDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _handleMouseUpInteraction = () => {
|
||||||
|
// if (!Drawing._mouseIsDown) return;
|
||||||
|
Drawing._mouseIsDown = false;
|
||||||
|
this._moveToState(InteractionState.HOVERING);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleDragInteraction(param: MouseEventParams): void {
|
||||||
|
if (this._state != InteractionState.DRAGGING &&
|
||||||
|
this._state != InteractionState.DRAGGINGP1 &&
|
||||||
|
this._state != InteractionState.DRAGGINGP2 &&
|
||||||
|
this._state != InteractionState.DRAGGINGP3 &&
|
||||||
|
this._state != InteractionState.DRAGGINGP4) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const mousePoint = Drawing._eventToPoint(param, this.series);
|
||||||
|
if (!mousePoint) return;
|
||||||
|
this._startDragPoint = this._startDragPoint || mousePoint;
|
||||||
|
|
||||||
|
const diff = Drawing._getDiff(mousePoint, this._startDragPoint);
|
||||||
|
this._onDrag(diff);
|
||||||
|
this.requestUpdate();
|
||||||
|
|
||||||
|
this._startDragPoint = mousePoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract _onMouseDown(): void;
|
||||||
|
protected abstract _onDrag(diff: DiffPoint): void;
|
||||||
|
protected abstract _moveToState(state: InteractionState): void;
|
||||||
|
protected abstract _mouseIsOverDrawing(param: MouseEventParams): boolean;
|
||||||
|
}
|
||||||
14
src/drawing/options.ts
Normal file
14
src/drawing/options.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { LineStyle } from "lightweight-charts";
|
||||||
|
|
||||||
|
|
||||||
|
export interface DrawingOptions {
|
||||||
|
lineColor: string;
|
||||||
|
lineStyle: LineStyle
|
||||||
|
width: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultOptions: DrawingOptions = {
|
||||||
|
lineColor: '#1E80F0',
|
||||||
|
lineStyle: LineStyle.Solid,
|
||||||
|
width: 4,
|
||||||
|
};
|
||||||
65
src/drawing/pane-renderer.ts
Normal file
65
src/drawing/pane-renderer.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { ISeriesPrimitivePaneRenderer } from "lightweight-charts";
|
||||||
|
import { ViewPoint } from "./pane-view";
|
||||||
|
import { DrawingOptions } from "./options";
|
||||||
|
import { BitmapCoordinatesRenderingScope, CanvasRenderingTarget2D } from "fancy-canvas";
|
||||||
|
|
||||||
|
export abstract class DrawingPaneRenderer implements ISeriesPrimitivePaneRenderer {
|
||||||
|
_options: DrawingOptions;
|
||||||
|
|
||||||
|
constructor(options: DrawingOptions) {
|
||||||
|
this._options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract draw(target: CanvasRenderingTarget2D): void;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class TwoPointDrawingPaneRenderer extends DrawingPaneRenderer {
|
||||||
|
_p1: ViewPoint;
|
||||||
|
_p2: ViewPoint;
|
||||||
|
protected _hovered: boolean;
|
||||||
|
|
||||||
|
constructor(p1: ViewPoint, p2: ViewPoint, options: DrawingOptions, hovered: boolean) {
|
||||||
|
super(options);
|
||||||
|
this._p1 = p1;
|
||||||
|
this._p2 = p2;
|
||||||
|
this._hovered = hovered;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract draw(target: CanvasRenderingTarget2D): void;
|
||||||
|
|
||||||
|
_getScaledCoordinates(scope: BitmapCoordinatesRenderingScope) {
|
||||||
|
if (this._p1.x === null || this._p1.y === null ||
|
||||||
|
this._p2.x === null || this._p2.y === null) return null;
|
||||||
|
return {
|
||||||
|
x1: Math.round(this._p1.x * scope.horizontalPixelRatio),
|
||||||
|
y1: Math.round(this._p1.y * scope.verticalPixelRatio),
|
||||||
|
x2: Math.round(this._p2.x * scope.horizontalPixelRatio),
|
||||||
|
y2: Math.round(this._p2.y * scope.verticalPixelRatio),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// _drawTextLabel(scope: BitmapCoordinatesRenderingScope, text: string, x: number, y: number, left: boolean) {
|
||||||
|
// scope.context.font = '24px Arial';
|
||||||
|
// scope.context.beginPath();
|
||||||
|
// const offset = 5 * scope.horizontalPixelRatio;
|
||||||
|
// const textWidth = scope.context.measureText(text);
|
||||||
|
// const leftAdjustment = left ? textWidth.width + offset * 4 : 0;
|
||||||
|
// scope.context.fillStyle = this._options.labelBackgroundColor;
|
||||||
|
// scope.context.roundRect(x + offset - leftAdjustment, y - 24, textWidth.width + offset * 2, 24 + offset, 5);
|
||||||
|
// scope.context.fill();
|
||||||
|
// scope.context.beginPath();
|
||||||
|
// scope.context.fillStyle = this._options.labelTextColor;
|
||||||
|
// scope.context.fillText(text, x + offset * 2 - leftAdjustment, y);
|
||||||
|
// }
|
||||||
|
|
||||||
|
_drawEndCircle(scope: BitmapCoordinatesRenderingScope, x: number, y: number) {
|
||||||
|
const radius = 9
|
||||||
|
scope.context.fillStyle = '#000';
|
||||||
|
scope.context.beginPath();
|
||||||
|
scope.context.arc(x, y, radius, 0, 2 * Math.PI);
|
||||||
|
scope.context.stroke();
|
||||||
|
scope.context.fill();
|
||||||
|
// scope.context.strokeStyle = this._options.lineColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/drawing/pane-view.ts
Normal file
53
src/drawing/pane-view.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { Coordinate, ISeriesPrimitivePaneView } from 'lightweight-charts';
|
||||||
|
import { Drawing } from './drawing';
|
||||||
|
import { Point } from './data-source';
|
||||||
|
import { DrawingPaneRenderer } from './pane-renderer';
|
||||||
|
import { TwoPointDrawing } from './two-point-drawing';
|
||||||
|
|
||||||
|
|
||||||
|
export abstract class DrawingPaneView implements ISeriesPrimitivePaneView {
|
||||||
|
_source: Drawing;
|
||||||
|
|
||||||
|
constructor(source: Drawing) {
|
||||||
|
this._source = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract update(): void;
|
||||||
|
abstract renderer(): DrawingPaneRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ViewPoint {
|
||||||
|
x: Coordinate | null;
|
||||||
|
y: Coordinate | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class TwoPointDrawingPaneView extends DrawingPaneView {
|
||||||
|
_p1: ViewPoint = { x: null, y: null };
|
||||||
|
_p2: ViewPoint = { x: null, y: null };
|
||||||
|
|
||||||
|
_source: TwoPointDrawing;
|
||||||
|
|
||||||
|
constructor(source: TwoPointDrawing) {
|
||||||
|
super(source);
|
||||||
|
this._source = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
if (!this._source.p1 || !this._source.p2) return;
|
||||||
|
const series = this._source.series;
|
||||||
|
const y1 = series.priceToCoordinate(this._source.p1.price);
|
||||||
|
const y2 = series.priceToCoordinate(this._source.p2.price);
|
||||||
|
const x1 = this._getX(this._source.p1);
|
||||||
|
const x2 = this._getX(this._source.p2);
|
||||||
|
this._p1 = { x: x1, y: y1 };
|
||||||
|
this._p2 = { x: x2, y: y2 };
|
||||||
|
if (!x1 || !x2 || !y1 || !y2) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract renderer(): DrawingPaneRenderer;
|
||||||
|
|
||||||
|
_getX(p: Point) {
|
||||||
|
const timeScale = this._source.chart.timeScale();
|
||||||
|
return timeScale.logicalToCoordinate(p.logical);
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/drawing/two-point-drawing.ts
Normal file
38
src/drawing/two-point-drawing.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { Point } from './data-source';
|
||||||
|
import { DrawingOptions, defaultOptions } from './options';
|
||||||
|
import { Drawing } from './drawing';
|
||||||
|
import { TwoPointDrawingPaneView } from './pane-view';
|
||||||
|
|
||||||
|
|
||||||
|
export abstract class TwoPointDrawing extends Drawing {
|
||||||
|
_paneViews: TwoPointDrawingPaneView[] = [];
|
||||||
|
|
||||||
|
protected _hovered: boolean = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
p1: Point,
|
||||||
|
p2: Point,
|
||||||
|
options?: Partial<DrawingOptions>
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
this.points.push(p1);
|
||||||
|
this.points.push(p2);
|
||||||
|
this._options = {
|
||||||
|
...defaultOptions,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setFirstPoint(point: Point) {
|
||||||
|
this.updatePoints(point);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSecondPoint(point: Point) {
|
||||||
|
this.updatePoints(null, point);
|
||||||
|
}
|
||||||
|
|
||||||
|
get p1() { return this.points[0]; }
|
||||||
|
get p2() { return this.points[1]; }
|
||||||
|
|
||||||
|
get hovered() { return this._hovered; }
|
||||||
|
}
|
||||||
15
src/example/example.ts
Normal file
15
src/example/example.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { generateCandleData } from '../sample-data';
|
||||||
|
import { Handler } from '../general/handler';
|
||||||
|
|
||||||
|
const handler = new Handler("sadasdas", 0.556, 0.5182, "left", true);
|
||||||
|
|
||||||
|
handler.createToolBox();
|
||||||
|
|
||||||
|
const data = generateCandleData();
|
||||||
|
if (handler.series)
|
||||||
|
handler.series.setData(data);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
27
src/example/index.html
Normal file
27
src/example/index.html
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Template Drawing Primitive Plugin Example</title>
|
||||||
|
<link rel="stylesheet" href="../general/styles.css">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
#container {
|
||||||
|
/*margin-inline: auto;
|
||||||
|
max-width: 800px;
|
||||||
|
height: 400px;
|
||||||
|
background-color: rgba(240, 243, 250, 1);
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;*/
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="container"></div>
|
||||||
|
<script type="module" src="./example.ts"></script>
|
||||||
|
<script type="module" src="../general/handler.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
60
src/general/global-params.ts
Normal file
60
src/general/global-params.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
export interface GlobalParams extends Window {
|
||||||
|
pane: paneStyle; // TODO shouldnt need this cause of css variables
|
||||||
|
handlerInFocus: string;
|
||||||
|
callbackFunction: Function;
|
||||||
|
containerDiv: HTMLElement;
|
||||||
|
setCursor: Function;
|
||||||
|
cursor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface paneStyle {
|
||||||
|
backgroundColor: string;
|
||||||
|
hoverBackgroundColor: string;
|
||||||
|
clickBackgroundColor: string;
|
||||||
|
activeBackgroundColor: string;
|
||||||
|
mutedBackgroundColor: string;
|
||||||
|
borderColor: string;
|
||||||
|
color: string;
|
||||||
|
activeColor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const paneStyleDefault: paneStyle = {
|
||||||
|
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',
|
||||||
|
}
|
||||||
|
|
||||||
|
declare const window: GlobalParams;
|
||||||
|
|
||||||
|
export function globalParamInit() {
|
||||||
|
window.pane = {
|
||||||
|
...paneStyleDefault,
|
||||||
|
}
|
||||||
|
window.containerDiv = document.getElementById("container") || document.createElement('div');
|
||||||
|
window.setCursor = (type: string | undefined) => {
|
||||||
|
if (type) window.cursor = type;
|
||||||
|
document.body.style.cursor = window.cursor;
|
||||||
|
}
|
||||||
|
window.cursor = 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setCursor = (type: string | undefined) => {
|
||||||
|
if (type) window.cursor = type;
|
||||||
|
document.body.style.cursor = window.cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// export interface SeriesHandler {
|
||||||
|
// type: string;
|
||||||
|
// series: ISeriesApi<SeriesType>;
|
||||||
|
// markers: SeriesMarker<"">[],
|
||||||
|
// horizontal_lines: HorizontalLine[],
|
||||||
|
// name?: string,
|
||||||
|
// precision: number,
|
||||||
|
// }
|
||||||
|
|
||||||
368
src/general/handler.ts
Normal file
368
src/general/handler.ts
Normal file
@ -0,0 +1,368 @@
|
|||||||
|
import {
|
||||||
|
ColorType,
|
||||||
|
CrosshairMode,
|
||||||
|
DeepPartial,
|
||||||
|
HistogramStyleOptions,
|
||||||
|
IChartApi,
|
||||||
|
ISeriesApi,
|
||||||
|
LineStyleOptions,
|
||||||
|
LogicalRange,
|
||||||
|
LogicalRangeChangeEventHandler,
|
||||||
|
MouseEventHandler,
|
||||||
|
MouseEventParams,
|
||||||
|
SeriesOptionsCommon,
|
||||||
|
SeriesType,
|
||||||
|
Time,
|
||||||
|
createChart
|
||||||
|
} from "lightweight-charts";
|
||||||
|
|
||||||
|
import { GlobalParams, globalParamInit } from "./global-params";
|
||||||
|
import { Legend } from "./legend";
|
||||||
|
import { ToolBox } from "./toolbox";
|
||||||
|
import { TopBar } from "./topbar";
|
||||||
|
|
||||||
|
|
||||||
|
export interface Scale{
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
globalParamInit();
|
||||||
|
declare const window: GlobalParams;
|
||||||
|
|
||||||
|
export class Handler {
|
||||||
|
public id: string;
|
||||||
|
public commandFunctions: Function[] = [];
|
||||||
|
|
||||||
|
public wrapper: HTMLDivElement;
|
||||||
|
public div: HTMLDivElement;
|
||||||
|
|
||||||
|
public chart: IChartApi;
|
||||||
|
public scale: Scale;
|
||||||
|
public precision: number = 2;
|
||||||
|
|
||||||
|
public series: ISeriesApi<SeriesType>;
|
||||||
|
public volumeSeries: ISeriesApi<SeriesType>;
|
||||||
|
|
||||||
|
public legend: Legend;
|
||||||
|
private _topBar: TopBar | undefined;
|
||||||
|
public toolBox: ToolBox | undefined;
|
||||||
|
public spinner: HTMLDivElement | undefined;
|
||||||
|
|
||||||
|
public _seriesList: ISeriesApi<SeriesType>[] = [];
|
||||||
|
|
||||||
|
// TODO find a better solution rather than the 'position' parameter
|
||||||
|
constructor(
|
||||||
|
chartId: string,
|
||||||
|
innerWidth: number,
|
||||||
|
innerHeight: number,
|
||||||
|
position: string,
|
||||||
|
autoSize: boolean
|
||||||
|
) {
|
||||||
|
this.reSize = this.reSize.bind(this)
|
||||||
|
|
||||||
|
this.id = chartId
|
||||||
|
this.scale = {
|
||||||
|
width: innerWidth,
|
||||||
|
height: innerHeight,
|
||||||
|
}
|
||||||
|
|
||||||
|
this.wrapper = document.createElement('div')
|
||||||
|
this.wrapper.classList.add("handler");
|
||||||
|
this.wrapper.style.float = position
|
||||||
|
|
||||||
|
this.div = document.createElement('div')
|
||||||
|
this.div.style.position = 'relative'
|
||||||
|
|
||||||
|
this.wrapper.appendChild(this.div);
|
||||||
|
window.containerDiv.append(this.wrapper)
|
||||||
|
|
||||||
|
this.chart = this._createChart();
|
||||||
|
this.series = this.createCandlestickSeries();
|
||||||
|
this.volumeSeries = this.createVolumeSeries();
|
||||||
|
|
||||||
|
this.legend = new Legend(this)
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (event) => {
|
||||||
|
for (let i = 0; i < this.commandFunctions.length; i++) {
|
||||||
|
if (this.commandFunctions[i](event)) break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
window.handlerInFocus = this.id;
|
||||||
|
this.wrapper.addEventListener('mouseover', () => window.handlerInFocus = this.id)
|
||||||
|
|
||||||
|
this.reSize()
|
||||||
|
if (!autoSize) return
|
||||||
|
window.addEventListener('resize', () => this.reSize())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
reSize() {
|
||||||
|
let topBarOffset = this.scale.height !== 0 ? this._topBar?._div.offsetHeight || 0 : 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}%`
|
||||||
|
|
||||||
|
// TODO definitely a better way to do this
|
||||||
|
if (this.scale.height === 0 || this.scale.width === 0) {
|
||||||
|
// if (this.legend.div.style.display == 'flex') this.legend.div.style.display = 'none'
|
||||||
|
if (this.toolBox) {
|
||||||
|
this.toolBox.div.style.display = 'none'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// this.legend.div.style.display = 'flex'
|
||||||
|
if (this.toolBox) {
|
||||||
|
this.toolBox.div.style.display = 'flex'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _createChart() {
|
||||||
|
return createChart(this.div, {
|
||||||
|
width: window.innerWidth * this.scale.width,
|
||||||
|
height: window.innerHeight * this.scale.height,
|
||||||
|
layout:{
|
||||||
|
textColor: window.pane.color,
|
||||||
|
background: {
|
||||||
|
color: '#000000',
|
||||||
|
type: ColorType.Solid,
|
||||||
|
},
|
||||||
|
fontSize: 12
|
||||||
|
},
|
||||||
|
rightPriceScale: {
|
||||||
|
scaleMargins: {top: 0.3, bottom: 0.25},
|
||||||
|
},
|
||||||
|
timeScale: {timeVisible: true, secondsVisible: false},
|
||||||
|
crosshair: {
|
||||||
|
mode: 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},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
createCandlestickSeries() {
|
||||||
|
const up = 'rgba(39, 157, 130, 100)'
|
||||||
|
const down = 'rgba(200, 97, 100, 100)'
|
||||||
|
const candleSeries = this.chart.addCandlestickSeries({
|
||||||
|
upColor: up, borderUpColor: up, wickUpColor: up,
|
||||||
|
downColor: down, borderDownColor: down, wickDownColor: down
|
||||||
|
});
|
||||||
|
candleSeries.priceScale().applyOptions({
|
||||||
|
scaleMargins: {top: 0.2, bottom: 0.2},
|
||||||
|
});
|
||||||
|
return candleSeries;
|
||||||
|
}
|
||||||
|
|
||||||
|
createVolumeSeries() {
|
||||||
|
const volumeSeries = this.chart.addHistogramSeries({
|
||||||
|
color: '#26a69a',
|
||||||
|
priceFormat: {type: 'volume'},
|
||||||
|
priceScaleId: 'volume_scale',
|
||||||
|
})
|
||||||
|
volumeSeries.priceScale().applyOptions({
|
||||||
|
scaleMargins: {top: 0.8, bottom: 0},
|
||||||
|
});
|
||||||
|
return volumeSeries;
|
||||||
|
}
|
||||||
|
|
||||||
|
createLineSeries(name: string, options: DeepPartial<LineStyleOptions & SeriesOptionsCommon>) {
|
||||||
|
const line = this.chart.addLineSeries({...options});
|
||||||
|
this._seriesList.push(line);
|
||||||
|
this.legend.makeSeriesRow(name, line)
|
||||||
|
return {
|
||||||
|
name: name,
|
||||||
|
series: line,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createHistogramSeries(name: string, options: DeepPartial<HistogramStyleOptions & SeriesOptionsCommon>) {
|
||||||
|
const line = this.chart.addHistogramSeries({...options});
|
||||||
|
this._seriesList.push(line);
|
||||||
|
this.legend.makeSeriesRow(name, line)
|
||||||
|
return {
|
||||||
|
name: name,
|
||||||
|
series: line,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createToolBox() {
|
||||||
|
this.toolBox = new ToolBox(this.id, this.chart, this.series, this.commandFunctions);
|
||||||
|
this.div.appendChild(this.toolBox.div);
|
||||||
|
}
|
||||||
|
|
||||||
|
createTopBar() {
|
||||||
|
this._topBar = new TopBar(this);
|
||||||
|
this.wrapper.prepend(this._topBar._div)
|
||||||
|
return this._topBar;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
// Exclude the chart attribute from serialization
|
||||||
|
const {chart, ...serialized} = this;
|
||||||
|
return serialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static syncCharts(childChart:Handler, parentChart: Handler, crosshairOnly = false) {
|
||||||
|
function crosshairHandler(chart: Handler, point: any) {//point: BarData | LineData) {
|
||||||
|
if (!point) {
|
||||||
|
chart.chart.clearCrosshairPosition()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// TODO fix any point ?
|
||||||
|
chart.chart.setCrosshairPosition(point.value || point!.close, point.time, chart.series);
|
||||||
|
chart.legend.legendHandler(point, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPoint(series: ISeriesApi<SeriesType>, param: MouseEventParams) {
|
||||||
|
if (!param.time) return null;
|
||||||
|
return param.seriesData.get(series) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const childTimeScale = childChart.chart.timeScale();
|
||||||
|
const parentTimeScale = parentChart.chart.timeScale();
|
||||||
|
|
||||||
|
const setChildRange = (timeRange: LogicalRange | null) => {
|
||||||
|
if(timeRange) childTimeScale.setVisibleLogicalRange(timeRange);
|
||||||
|
}
|
||||||
|
const setParentRange = (timeRange: LogicalRange | null) => {
|
||||||
|
if(timeRange) parentTimeScale.setVisibleLogicalRange(timeRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
const setParentCrosshair = (param: MouseEventParams) => {
|
||||||
|
crosshairHandler(parentChart, getPoint(childChart.series, param))
|
||||||
|
}
|
||||||
|
const setChildCrosshair = (param: MouseEventParams) => {
|
||||||
|
crosshairHandler(childChart, getPoint(parentChart.series, param))
|
||||||
|
}
|
||||||
|
|
||||||
|
let selected = parentChart
|
||||||
|
function addMouseOverListener(
|
||||||
|
thisChart: Handler,
|
||||||
|
otherChart: Handler,
|
||||||
|
thisCrosshair: MouseEventHandler<Time>,
|
||||||
|
otherCrosshair: MouseEventHandler<Time>,
|
||||||
|
thisRange: LogicalRangeChangeEventHandler,
|
||||||
|
otherRange: LogicalRangeChangeEventHandler)
|
||||||
|
{
|
||||||
|
thisChart.wrapper.addEventListener('mouseover', () => {
|
||||||
|
if (selected === thisChart) return
|
||||||
|
selected = thisChart
|
||||||
|
otherChart.chart.unsubscribeCrosshairMove(thisCrosshair)
|
||||||
|
thisChart.chart.subscribeCrosshairMove(otherCrosshair)
|
||||||
|
if (crosshairOnly) return;
|
||||||
|
otherChart.chart.timeScale().unsubscribeVisibleLogicalRangeChange(thisRange)
|
||||||
|
thisChart.chart.timeScale().subscribeVisibleLogicalRangeChange(otherRange)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
addMouseOverListener(
|
||||||
|
parentChart,
|
||||||
|
childChart,
|
||||||
|
setParentCrosshair,
|
||||||
|
setChildCrosshair,
|
||||||
|
setParentRange,
|
||||||
|
setChildRange
|
||||||
|
)
|
||||||
|
addMouseOverListener(
|
||||||
|
childChart,
|
||||||
|
parentChart,
|
||||||
|
setChildCrosshair,
|
||||||
|
setParentCrosshair,
|
||||||
|
setChildRange,
|
||||||
|
setParentRange
|
||||||
|
)
|
||||||
|
|
||||||
|
parentChart.chart.subscribeCrosshairMove(setChildCrosshair)
|
||||||
|
|
||||||
|
const parentRange = parentTimeScale.getVisibleLogicalRange()
|
||||||
|
if (parentRange) childTimeScale.setVisibleLogicalRange(parentRange)
|
||||||
|
|
||||||
|
if (crosshairOnly) return;
|
||||||
|
parentChart.chart.timeScale().subscribeVisibleLogicalRangeChange(setChildRange)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static makeSearchBox(chart: Handler) {
|
||||||
|
const searchWindow = document.createElement('div')
|
||||||
|
searchWindow.classList.add('searchbox');
|
||||||
|
searchWindow.style.display = 'none';
|
||||||
|
|
||||||
|
const 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>`
|
||||||
|
|
||||||
|
const sBox = document.createElement('input');
|
||||||
|
sBox.type = 'text';
|
||||||
|
|
||||||
|
searchWindow.appendChild(magnifyingGlass)
|
||||||
|
searchWindow.appendChild(sBox)
|
||||||
|
chart.div.appendChild(searchWindow);
|
||||||
|
|
||||||
|
chart.commandFunctions.push((event: KeyboardEvent) => {
|
||||||
|
if (window.handlerInFocus !== chart.id) 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static makeSpinner(chart: Handler) {
|
||||||
|
chart.spinner = document.createElement('div');
|
||||||
|
chart.spinner.classList.add('spinner');
|
||||||
|
chart.wrapper.appendChild(chart.spinner)
|
||||||
|
|
||||||
|
// TODO below can be css (animate)
|
||||||
|
let rotation = 0;
|
||||||
|
const speed = 10;
|
||||||
|
function animateSpinner() {
|
||||||
|
if (!chart.spinner) return;
|
||||||
|
rotation += speed
|
||||||
|
chart.spinner.style.transform = `translate(-50%, -50%) rotate(${rotation}deg)`
|
||||||
|
requestAnimationFrame(animateSpinner)
|
||||||
|
}
|
||||||
|
animateSpinner();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly _styleMap = {
|
||||||
|
'--bg-color': 'backgroundColor',
|
||||||
|
'--hover-bg-color': 'hoverBackgroundColor',
|
||||||
|
'--click-bg-color': 'clickBackgroundColor',
|
||||||
|
'--active-bg-color': 'activeBackgroundColor',
|
||||||
|
'--muted-bg-color': 'mutedBackgroundColor',
|
||||||
|
'--border-color': 'borderColor',
|
||||||
|
'--color': 'color',
|
||||||
|
'--active-color': 'activeColor',
|
||||||
|
}
|
||||||
|
public static setRootStyles(styles: any) {
|
||||||
|
const rootStyle = document.documentElement.style;
|
||||||
|
for (const [property, valueKey] of Object.entries(this._styleMap)) {
|
||||||
|
rootStyle.setProperty(property, styles[valueKey]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/general/index.ts
Normal file
9
src/general/index.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// TODO this won't be necessary with ws
|
||||||
|
|
||||||
|
export * from './handler';
|
||||||
|
export * from './global-params';
|
||||||
|
export * from './legend';
|
||||||
|
export * from './table';
|
||||||
|
export * from './toolbox';
|
||||||
|
export * from './topbar';
|
||||||
|
export * from '../horizontal-line/ray-line';
|
||||||
222
src/general/legend.ts
Normal file
222
src/general/legend.ts
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
import { ISeriesApi, LineData, Logical, MouseEventParams, PriceFormatBuiltIn, SeriesType } from "lightweight-charts";
|
||||||
|
import { Handler } from "./handler";
|
||||||
|
|
||||||
|
|
||||||
|
interface LineElement {
|
||||||
|
name: string;
|
||||||
|
div: HTMLDivElement;
|
||||||
|
row: HTMLDivElement;
|
||||||
|
toggle: HTMLDivElement,
|
||||||
|
series: ISeriesApi<SeriesType>,
|
||||||
|
solid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Legend {
|
||||||
|
private handler: Handler;
|
||||||
|
public div: HTMLDivElement;
|
||||||
|
|
||||||
|
private ohlcEnabled: boolean = false;
|
||||||
|
private percentEnabled: boolean = false;
|
||||||
|
private linesEnabled: boolean = false;
|
||||||
|
private colorBasedOnCandle: boolean = false;
|
||||||
|
|
||||||
|
private text: HTMLSpanElement;
|
||||||
|
private candle: HTMLDivElement;
|
||||||
|
public _lines: LineElement[] = [];
|
||||||
|
|
||||||
|
|
||||||
|
constructor(handler: Handler) {
|
||||||
|
this.legendHandler = this.legendHandler.bind(this)
|
||||||
|
|
||||||
|
this.handler = handler;
|
||||||
|
this.ohlcEnabled = false;
|
||||||
|
this.percentEnabled = false
|
||||||
|
this.linesEnabled = false
|
||||||
|
this.colorBasedOnCandle = false
|
||||||
|
|
||||||
|
this.div = document.createElement('div');
|
||||||
|
this.div.classList.add('legend');
|
||||||
|
this.div.style.maxWidth = `${(handler.scale.width * 100) - 8}vw`
|
||||||
|
this.div.style.display = 'none';
|
||||||
|
|
||||||
|
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)
|
||||||
|
handler.div.appendChild(this.div)
|
||||||
|
|
||||||
|
// this.makeSeriesRows(handler);
|
||||||
|
|
||||||
|
handler.chart.subscribeCrosshairMove(this.legendHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
// Exclude the chart attribute from serialization
|
||||||
|
const {_lines, handler, ...serialized} = this;
|
||||||
|
return serialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeSeriesRows(handler: Handler) {
|
||||||
|
// if (this.linesEnabled) handler._seriesList.forEach(s => this.makeSeriesRow(s))
|
||||||
|
// }
|
||||||
|
|
||||||
|
makeSeriesRow(name: string, series: ISeriesApi<SeriesType>) {
|
||||||
|
const strokeColor = '#FFF';
|
||||||
|
let openEye = `
|
||||||
|
<path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:${strokeColor};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:${strokeColor};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:${strokeColor};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.classList.add('legend-toggle-switch');
|
||||||
|
|
||||||
|
|
||||||
|
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', () => {
|
||||||
|
if (on) {
|
||||||
|
on = false
|
||||||
|
group.innerHTML = closedEye
|
||||||
|
series.applyOptions({
|
||||||
|
visible: false
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
on = true
|
||||||
|
series.applyOptions({
|
||||||
|
visible: true
|
||||||
|
})
|
||||||
|
group.innerHTML = openEye
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
svg.appendChild(group)
|
||||||
|
toggle.appendChild(svg);
|
||||||
|
row.appendChild(div)
|
||||||
|
row.appendChild(toggle)
|
||||||
|
this.div.appendChild(row)
|
||||||
|
|
||||||
|
const color = series.options().baseLineColor;
|
||||||
|
this._lines.push({
|
||||||
|
name: name,
|
||||||
|
div: div,
|
||||||
|
row: row,
|
||||||
|
toggle: toggle,
|
||||||
|
series: series,
|
||||||
|
solid: color.startsWith('rgba') ? color.replace(/[^,]+(?=\))/, '1') : color
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
legendItemFormat(num: number, decimal: number) { return num.toFixed(decimal).toString().padStart(8, ' ') }
|
||||||
|
|
||||||
|
shorthandFormat(num: number) {
|
||||||
|
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: MouseEventParams, usingPoint= false) {
|
||||||
|
if (!this.ohlcEnabled && !this.linesEnabled && !this.percentEnabled) return;
|
||||||
|
const options: any = this.handler.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: any;
|
||||||
|
let logical: Logical | null = null;
|
||||||
|
|
||||||
|
if (usingPoint) {
|
||||||
|
const timeScale = this.handler.chart.timeScale();
|
||||||
|
let coordinate = timeScale.timeToCoordinate(param.time)
|
||||||
|
if (coordinate)
|
||||||
|
logical = timeScale.coordinateToLogical(coordinate.valueOf())
|
||||||
|
if (logical)
|
||||||
|
data = this.handler.series.dataByIndex(logical.valueOf())
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
data = param.seriesData.get(this.handler.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.handler.precision)} `
|
||||||
|
str += `| H ${this.legendItemFormat(data.high, this.handler.precision)} `
|
||||||
|
str += `| L ${this.legendItemFormat(data.low, this.handler.precision)} `
|
||||||
|
str += `| C ${this.legendItemFormat(data.close, this.handler.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.handler.volumeSeries) {
|
||||||
|
let volumeData: any;
|
||||||
|
if (logical) {
|
||||||
|
volumeData = this.handler.volumeSeries.dataByIndex(logical)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
volumeData = param.seriesData.get(this.handler.volumeSeries)
|
||||||
|
}
|
||||||
|
if (volumeData) {
|
||||||
|
str += this.ohlcEnabled ? `<br>V ${this.shorthandFormat(volumeData.value)}` : ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.candle.innerHTML = str + '</span>'
|
||||||
|
|
||||||
|
this._lines.forEach((e) => {
|
||||||
|
if (!this.linesEnabled) {
|
||||||
|
e.row.style.display = 'none'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.row.style.display = 'flex'
|
||||||
|
|
||||||
|
let data
|
||||||
|
if (usingPoint && logical) {
|
||||||
|
data = e.series.dataByIndex(logical) as LineData
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
data = param.seriesData.get(e.series) as LineData
|
||||||
|
}
|
||||||
|
let price;
|
||||||
|
if (e.series.seriesType() == 'Histogram') {
|
||||||
|
price = this.shorthandFormat(data.value)
|
||||||
|
} else {
|
||||||
|
const format = e.series.options().priceFormat as PriceFormatBuiltIn
|
||||||
|
price = this.legendItemFormat(data.value, format.precision) // couldn't this just be line.options().precision?
|
||||||
|
}
|
||||||
|
e.div.innerHTML = `<span style="color: ${e.solid};">▨</span> ${e.name} : ${price}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/general/menu.ts
Normal file
59
src/general/menu.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { GlobalParams } from "./global-params";
|
||||||
|
|
||||||
|
declare const window: GlobalParams
|
||||||
|
|
||||||
|
export class Menu {
|
||||||
|
private div: HTMLDivElement;
|
||||||
|
private isOpen: boolean = false;
|
||||||
|
private widget: any;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private makeButton: Function,
|
||||||
|
private callbackName: string,
|
||||||
|
items: string[],
|
||||||
|
activeItem: string,
|
||||||
|
separator: boolean,
|
||||||
|
align: 'right'|'left') {
|
||||||
|
|
||||||
|
this.div = document.createElement('div')
|
||||||
|
this.div.classList.add('topbar-menu');
|
||||||
|
|
||||||
|
this.widget = this.makeButton(activeItem+' ↓', null, separator, true, align)
|
||||||
|
|
||||||
|
this.updateMenuItems(items)
|
||||||
|
|
||||||
|
this.widget.elem.addEventListener('click', () => {
|
||||||
|
this.isOpen = !this.isOpen;
|
||||||
|
if (!this.isOpen) {
|
||||||
|
this.div.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let rect = this.widget.elem.getBoundingClientRect()
|
||||||
|
this.div.style.display = 'flex'
|
||||||
|
this.div.style.flexDirection = 'column'
|
||||||
|
|
||||||
|
let center = rect.x+(rect.width/2)
|
||||||
|
this.div.style.left = center-(this.div.clientWidth/2)+'px'
|
||||||
|
this.div.style.top = rect.y+rect.height+'px'
|
||||||
|
})
|
||||||
|
document.body.appendChild(this.div)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMenuItems(items: string[]) {
|
||||||
|
this.div.innerHTML = '';
|
||||||
|
|
||||||
|
items.forEach(text => {
|
||||||
|
let button = this.makeButton(text, null, false, false)
|
||||||
|
button.elem.addEventListener('click', () => {
|
||||||
|
this.widget.elem.innerText = button.elem.innerText+' ↓'
|
||||||
|
window.callbackFunction(`${this.callbackName}_~_${button.elem.innerText}`)
|
||||||
|
this.div.style.display = 'none'
|
||||||
|
this.isOpen = false
|
||||||
|
});
|
||||||
|
button.elem.style.margin = '4px 4px'
|
||||||
|
button.elem.style.padding = '2px 2px'
|
||||||
|
this.div.appendChild(button.elem)
|
||||||
|
})
|
||||||
|
this.widget.elem.innerText = items[0]+' ↓';
|
||||||
|
}
|
||||||
|
}
|
||||||
234
src/general/styles.css
Normal file
234
src/general/styles.css
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
: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-textbox-input {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--color);
|
||||||
|
border: 1px solid 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: none;
|
||||||
|
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);
|
||||||
|
}
|
||||||
199
src/general/table.ts
Normal file
199
src/general/table.ts
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
import { GlobalParams } from "./global-params";
|
||||||
|
|
||||||
|
declare const window: GlobalParams
|
||||||
|
|
||||||
|
|
||||||
|
interface RowDictionary {
|
||||||
|
[key: number]: HTMLTableRowElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class Table {
|
||||||
|
private _div: HTMLDivElement;
|
||||||
|
private callbackName: string | null;
|
||||||
|
|
||||||
|
private borderColor: string;
|
||||||
|
private borderWidth: number;
|
||||||
|
private table: HTMLTableElement;
|
||||||
|
private rows: RowDictionary = {};
|
||||||
|
private headings: string[];
|
||||||
|
private widths: string[];
|
||||||
|
private alignments: string[];
|
||||||
|
|
||||||
|
public footer: HTMLDivElement[] | undefined;
|
||||||
|
public header: HTMLDivElement[] | undefined;
|
||||||
|
|
||||||
|
constructor(width: number, height: number, headings: string[], widths: number[], alignments: string[], position: string, draggable = false,
|
||||||
|
tableBackgroundColor: string, borderColor: string, borderWidth: number, textColors: string[], backgroundColors: string[]) {
|
||||||
|
this._div = document.createElement('div')
|
||||||
|
this.callbackName = null
|
||||||
|
this.borderColor = borderColor
|
||||||
|
this.borderWidth = borderWidth
|
||||||
|
|
||||||
|
if (draggable) {
|
||||||
|
this._div.style.position = 'absolute'
|
||||||
|
this._div.style.cursor = 'move'
|
||||||
|
} else {
|
||||||
|
this._div.style.position = 'relative'
|
||||||
|
this._div.style.float = position
|
||||||
|
}
|
||||||
|
this._div.style.zIndex = '2000'
|
||||||
|
this.reSize(width, height)
|
||||||
|
this._div.style.display = 'flex'
|
||||||
|
this._div.style.flexDirection = 'column'
|
||||||
|
// this._div.style.justifyContent = 'space-between'
|
||||||
|
|
||||||
|
this._div.style.borderRadius = '5px'
|
||||||
|
this._div.style.color = 'white'
|
||||||
|
this._div.style.fontSize = '12px'
|
||||||
|
this._div.style.fontVariantNumeric = 'tabular-nums'
|
||||||
|
|
||||||
|
this.table = document.createElement('table')
|
||||||
|
this.table.style.width = '100%'
|
||||||
|
this.table.style.borderCollapse = 'collapse'
|
||||||
|
this._div.style.overflow = 'hidden';
|
||||||
|
|
||||||
|
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._div.appendChild(overflowWrapper)
|
||||||
|
window.containerDiv.appendChild(this._div)
|
||||||
|
|
||||||
|
if (!draggable) return
|
||||||
|
|
||||||
|
let offsetX: number, offsetY: number;
|
||||||
|
|
||||||
|
let onMouseDown = (event: MouseEvent) => {
|
||||||
|
offsetX = event.clientX - this._div.offsetLeft;
|
||||||
|
offsetY = event.clientY - this._div.offsetTop;
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', onMouseMove);
|
||||||
|
document.addEventListener('mouseup', onMouseUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
let onMouseMove = (event: MouseEvent) => {
|
||||||
|
this._div.style.left = (event.clientX - offsetX) + 'px';
|
||||||
|
this._div.style.top = (event.clientY - offsetY) + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
let onMouseUp = () => {
|
||||||
|
// Remove the event listeners for dragging
|
||||||
|
document.removeEventListener('mousemove', onMouseMove);
|
||||||
|
document.removeEventListener('mouseup', onMouseUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._div.addEventListener('mousedown', onMouseDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
divToButton(div: HTMLDivElement, callbackString: string) {
|
||||||
|
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: number, 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]}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!returnClickedCell) {
|
||||||
|
this.divToButton(row, `${this.callbackName}_~_${id}`)
|
||||||
|
}
|
||||||
|
this.rows[id] = row
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteRow(id: number) {
|
||||||
|
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 = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getCell(rowId: number, column: string) {
|
||||||
|
return this.rows[rowId].cells[this.headings.indexOf(column)];
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCell(rowId: number, column: string, val: string) {
|
||||||
|
this._getCell(rowId, column).textContent = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
styleCell(rowId: number, column: string, styleAttribute: string, value: string) {
|
||||||
|
const style = this._getCell(rowId, column).style;
|
||||||
|
(style as any)[styleAttribute] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
makeSection(id: string, type: string, numBoxes: number, 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._div.appendChild(section) : this._div.prepend(section)
|
||||||
|
|
||||||
|
const textBoxes = []
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
textBoxes.push(textBox)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'footer') {
|
||||||
|
this.footer = textBoxes;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.header = textBoxes;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
reSize(width: number, height: number) {
|
||||||
|
this._div.style.width = width <= 1 ? width * 100 + '%' : width + 'px'
|
||||||
|
this._div.style.height = height <= 1 ? height * 100 + '%' : height + 'px'
|
||||||
|
}
|
||||||
|
}
|
||||||
180
src/general/toolbox.ts
Normal file
180
src/general/toolbox.ts
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
import { DrawingTool } from "../drawing/drawing-tool";
|
||||||
|
import { TrendLine } from "../trend-line/trend-line";
|
||||||
|
import { Box } from "../box/box";
|
||||||
|
import { Drawing } from "../drawing/drawing";
|
||||||
|
import { ContextMenu } from "../context-menu/context-menu";
|
||||||
|
import { GlobalParams } from "./global-params";
|
||||||
|
import { IChartApi, ISeriesApi, SeriesType } from "lightweight-charts";
|
||||||
|
import { HorizontalLine } from "../horizontal-line/horizontal-line";
|
||||||
|
import { RayLine } from "../horizontal-line/ray-line";
|
||||||
|
import { VerticalLine } from "../vertical-line/vertical-line";
|
||||||
|
|
||||||
|
|
||||||
|
interface Icon {
|
||||||
|
div: HTMLDivElement,
|
||||||
|
group: SVGGElement,
|
||||||
|
type: new (...args: any[]) => Drawing
|
||||||
|
}
|
||||||
|
|
||||||
|
declare const window: GlobalParams
|
||||||
|
|
||||||
|
export class ToolBox {
|
||||||
|
private static readonly TREND_SVG: string = '<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"/>';
|
||||||
|
private static readonly HORZ_SVG: string = '<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"/>';
|
||||||
|
private static readonly RAY_SVG: string = '<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"/>';
|
||||||
|
private static readonly BOX_SVG: string = '<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"/>';
|
||||||
|
private static readonly VERT_SVG: string = ToolBox.RAY_SVG;
|
||||||
|
|
||||||
|
div: HTMLDivElement;
|
||||||
|
private activeIcon: Icon | null = null;
|
||||||
|
|
||||||
|
private buttons: HTMLDivElement[] = [];
|
||||||
|
|
||||||
|
private _commandFunctions: Function[];
|
||||||
|
private _handlerID: string;
|
||||||
|
|
||||||
|
private _drawingTool: DrawingTool;
|
||||||
|
|
||||||
|
constructor(handlerID: string, chart: IChartApi, series: ISeriesApi<SeriesType>, commandFunctions: Function[]) {
|
||||||
|
this._handlerID = handlerID;
|
||||||
|
this._commandFunctions = commandFunctions;
|
||||||
|
this._drawingTool = new DrawingTool(chart, series, () => this.removeActiveAndSave());
|
||||||
|
this.div = this._makeToolBox()
|
||||||
|
new ContextMenu(this.saveDrawings, this._drawingTool);
|
||||||
|
|
||||||
|
commandFunctions.push((event: KeyboardEvent) => {
|
||||||
|
if ((event.metaKey || event.ctrlKey) && event.code === 'KeyZ') {
|
||||||
|
const drawingToDelete = this._drawingTool.drawings.pop();
|
||||||
|
if (drawingToDelete) this._drawingTool.delete(drawingToDelete)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
// Exclude the chart attribute from serialization
|
||||||
|
const { ...serialized} = this;
|
||||||
|
return serialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _makeToolBox() {
|
||||||
|
let div = document.createElement('div')
|
||||||
|
div.classList.add('toolbox');
|
||||||
|
this.buttons.push(this._makeToolBoxElement(TrendLine, 'KeyT', ToolBox.TREND_SVG))
|
||||||
|
this.buttons.push(this._makeToolBoxElement(HorizontalLine, 'KeyH', ToolBox.HORZ_SVG));
|
||||||
|
this.buttons.push(this._makeToolBoxElement(RayLine, 'KeyR', ToolBox.RAY_SVG));
|
||||||
|
this.buttons.push(this._makeToolBoxElement(Box, 'KeyB', ToolBox.BOX_SVG));
|
||||||
|
this.buttons.push(this._makeToolBoxElement(VerticalLine, 'KeyV', ToolBox.VERT_SVG, true));
|
||||||
|
for (const button of this.buttons) {
|
||||||
|
div.appendChild(button);
|
||||||
|
}
|
||||||
|
return div
|
||||||
|
}
|
||||||
|
|
||||||
|
private _makeToolBoxElement(DrawingType: new (...args: any[]) => Drawing, keyCmd: string, paths: string, rotate=false) {
|
||||||
|
const elem = document.createElement('div')
|
||||||
|
elem.classList.add("toolbox-button");
|
||||||
|
|
||||||
|
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||||
|
svg.setAttribute("width", "29");
|
||||||
|
svg.setAttribute("height", "29");
|
||||||
|
|
||||||
|
const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
||||||
|
group.innerHTML = paths
|
||||||
|
group.setAttribute("fill", window.pane.color)
|
||||||
|
|
||||||
|
svg.appendChild(group)
|
||||||
|
elem.appendChild(svg);
|
||||||
|
|
||||||
|
const icon: Icon = {div: elem, group: group, type: DrawingType}
|
||||||
|
|
||||||
|
elem.addEventListener('click', () => this._onIconClick(icon));
|
||||||
|
|
||||||
|
this._commandFunctions.push((event: KeyboardEvent) => {
|
||||||
|
if (this._handlerID !== window.handlerInFocus) return false;
|
||||||
|
|
||||||
|
if (event.altKey && event.code === keyCmd) {
|
||||||
|
event.preventDefault()
|
||||||
|
this._onIconClick(icon);
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
|
||||||
|
if (rotate == true) {
|
||||||
|
svg.style.transform = 'rotate(90deg)';
|
||||||
|
svg.style.transformBox = 'fill-box';
|
||||||
|
svg.style.transformOrigin = 'center';
|
||||||
|
}
|
||||||
|
|
||||||
|
return elem
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onIconClick(icon: Icon) {
|
||||||
|
if (this.activeIcon) {
|
||||||
|
|
||||||
|
this.activeIcon.div.classList.remove('active-toolbox-button');
|
||||||
|
window.setCursor('crosshair');
|
||||||
|
this._drawingTool?.stopDrawing()
|
||||||
|
if (this.activeIcon === icon) {
|
||||||
|
this.activeIcon = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.activeIcon = icon
|
||||||
|
this.activeIcon.div.classList.add('active-toolbox-button')
|
||||||
|
window.setCursor('crosshair');
|
||||||
|
this._drawingTool?.beginDrawing(this.activeIcon.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeActiveAndSave = () => {
|
||||||
|
window.setCursor('default');
|
||||||
|
if (this.activeIcon) this.activeIcon.div.classList.remove('active-toolbox-button')
|
||||||
|
this.activeIcon = null
|
||||||
|
this.saveDrawings()
|
||||||
|
}
|
||||||
|
|
||||||
|
addNewDrawing(d: Drawing) {
|
||||||
|
this._drawingTool.addNewDrawing(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearDrawings() {
|
||||||
|
this._drawingTool.clearDrawings();
|
||||||
|
}
|
||||||
|
|
||||||
|
saveDrawings = () => {
|
||||||
|
const drawingMeta = []
|
||||||
|
for (const d of this._drawingTool.drawings) {
|
||||||
|
drawingMeta.push({
|
||||||
|
type: d._type,
|
||||||
|
points: d.points,
|
||||||
|
options: d._options
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const string = JSON.stringify(drawingMeta);
|
||||||
|
window.callbackFunction(`save_drawings${this._handlerID}_~_${string}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
loadDrawings(drawings: any[]) { // TODO any
|
||||||
|
drawings.forEach((d) => {
|
||||||
|
switch (d.type) {
|
||||||
|
case "Box":
|
||||||
|
this._drawingTool.addNewDrawing(new Box(d.points[0], d.points[1], d.options));
|
||||||
|
break;
|
||||||
|
case "TrendLine":
|
||||||
|
this._drawingTool.addNewDrawing(new TrendLine(d.points[0], d.points[1], d.options));
|
||||||
|
break;
|
||||||
|
case "HorizontalLine":
|
||||||
|
this._drawingTool.addNewDrawing(new HorizontalLine(d.points[0], d.options));
|
||||||
|
break;
|
||||||
|
case "RayLine":
|
||||||
|
this._drawingTool.addNewDrawing(new RayLine(d.points[0], d.options));
|
||||||
|
break;
|
||||||
|
case "VerticalLine":
|
||||||
|
this._drawingTool.addNewDrawing(new VerticalLine(d.points[0], d.options));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
172
src/general/topbar.ts
Normal file
172
src/general/topbar.ts
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import { GlobalParams } from "./global-params";
|
||||||
|
import { Handler } from "./handler";
|
||||||
|
import { Menu } from "./menu";
|
||||||
|
|
||||||
|
declare const window: GlobalParams
|
||||||
|
|
||||||
|
interface Widget {
|
||||||
|
elem: HTMLDivElement;
|
||||||
|
callbackName: string;
|
||||||
|
intervalElements: HTMLButtonElement[];
|
||||||
|
onItemClicked: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TopBar {
|
||||||
|
private _handler: Handler;
|
||||||
|
public _div: HTMLDivElement;
|
||||||
|
|
||||||
|
private left: HTMLDivElement;
|
||||||
|
private right: HTMLDivElement;
|
||||||
|
|
||||||
|
constructor(handler: Handler) {
|
||||||
|
this._handler = handler;
|
||||||
|
|
||||||
|
this._div = document.createElement('div');
|
||||||
|
this._div.classList.add('topbar');
|
||||||
|
|
||||||
|
const createTopBarContainer = (justification: string) => {
|
||||||
|
const div = document.createElement('div')
|
||||||
|
div.classList.add('topbar-container')
|
||||||
|
div.style.justifyContent = justification
|
||||||
|
this._div.appendChild(div)
|
||||||
|
return div
|
||||||
|
}
|
||||||
|
this.left = createTopBarContainer('flex-start')
|
||||||
|
this.right = createTopBarContainer('flex-end')
|
||||||
|
}
|
||||||
|
|
||||||
|
makeSwitcher(items: string[], defaultItem: string, callbackName: string, align='left') {
|
||||||
|
const switcherElement = document.createElement('div');
|
||||||
|
switcherElement.style.margin = '4px 12px'
|
||||||
|
|
||||||
|
let activeItemEl: HTMLButtonElement;
|
||||||
|
|
||||||
|
const createAndReturnSwitcherButton = (itemName: string) => {
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.classList.add('topbar-button');
|
||||||
|
button.classList.add('switcher-button');
|
||||||
|
button.style.margin = '0px 2px';
|
||||||
|
button.innerText = itemName;
|
||||||
|
|
||||||
|
if (itemName == defaultItem) {
|
||||||
|
activeItemEl = button;
|
||||||
|
button.classList.add('active-switcher-button');
|
||||||
|
}
|
||||||
|
|
||||||
|
const buttonWidth = TopBar.getClientWidth(button)
|
||||||
|
button.style.minWidth = buttonWidth + 1 + 'px'
|
||||||
|
button.addEventListener('click', () => widget.onItemClicked(button))
|
||||||
|
|
||||||
|
switcherElement.appendChild(button);
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
const widget: Widget = {
|
||||||
|
elem: switcherElement,
|
||||||
|
callbackName: callbackName,
|
||||||
|
intervalElements: items.map(createAndReturnSwitcherButton),
|
||||||
|
onItemClicked: (item: HTMLButtonElement) => {
|
||||||
|
if (item == activeItemEl) return
|
||||||
|
activeItemEl.classList.remove('active-switcher-button');
|
||||||
|
item.classList.add('active-switcher-button');
|
||||||
|
activeItemEl = item;
|
||||||
|
window.callbackFunction(`${widget.callbackName}_~_${item.innerText}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.appendWidget(switcherElement, align, true)
|
||||||
|
return widget
|
||||||
|
}
|
||||||
|
|
||||||
|
makeTextBoxWidget(text: string, align='left', callbackName=null) {
|
||||||
|
if (callbackName) {
|
||||||
|
const textBox = document.createElement('input');
|
||||||
|
textBox.classList.add('topbar-textbox-input');
|
||||||
|
textBox.value = text
|
||||||
|
textBox.style.width = `${(textBox.value.length+2)}ch`
|
||||||
|
textBox.addEventListener('input', (e) => {
|
||||||
|
textBox.style.width = `${(textBox.value.length+2)}ch`;
|
||||||
|
});
|
||||||
|
textBox.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key == 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
textBox.blur();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
textBox.addEventListener('blur', () => {
|
||||||
|
window.callbackFunction(`${callbackName}_~_${textBox.value}`)
|
||||||
|
});
|
||||||
|
this.appendWidget(textBox, align, true)
|
||||||
|
return textBox
|
||||||
|
} else {
|
||||||
|
const textBox = document.createElement('div');
|
||||||
|
textBox.classList.add('topbar-textbox');
|
||||||
|
textBox.innerText = text
|
||||||
|
this.appendWidget(textBox, align, true)
|
||||||
|
return textBox
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
makeMenu(items: string[], activeItem: string, separator: boolean, callbackName: string, align: 'right'|'left') {
|
||||||
|
return new Menu(this.makeButton.bind(this), callbackName, items, activeItem, separator, align)
|
||||||
|
}
|
||||||
|
|
||||||
|
makeButton(defaultText: string, callbackName: string | null, separator: boolean, append=true, align='left', toggle=false) {
|
||||||
|
let button = document.createElement('button')
|
||||||
|
button.classList.add('topbar-button');
|
||||||
|
// button.style.color = window.pane.color
|
||||||
|
button.innerText = defaultText;
|
||||||
|
document.body.appendChild(button)
|
||||||
|
button.style.minWidth = button.clientWidth+1+'px'
|
||||||
|
document.body.removeChild(button)
|
||||||
|
|
||||||
|
let widget = {
|
||||||
|
elem: button,
|
||||||
|
callbackName: callbackName
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callbackName) {
|
||||||
|
let handler;
|
||||||
|
if (toggle) {
|
||||||
|
let state = false;
|
||||||
|
handler = () => {
|
||||||
|
state = !state
|
||||||
|
window.callbackFunction(`${widget.callbackName}_~_${state}`)
|
||||||
|
button.style.backgroundColor = state ? 'var(--active-bg-color)' : '';
|
||||||
|
button.style.color = state ? 'var(--active-color)' : '';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handler = () => window.callbackFunction(`${widget.callbackName}_~_${button.innerText}`)
|
||||||
|
}
|
||||||
|
button.addEventListener('click', handler);
|
||||||
|
}
|
||||||
|
if (append) this.appendWidget(button, align, separator)
|
||||||
|
return widget
|
||||||
|
}
|
||||||
|
|
||||||
|
makeSeparator(align='left') {
|
||||||
|
const separator = document.createElement('div')
|
||||||
|
separator.classList.add('topbar-seperator')
|
||||||
|
const div = align == 'left' ? this.left : this.right
|
||||||
|
div.appendChild(separator)
|
||||||
|
}
|
||||||
|
|
||||||
|
appendWidget(widget: HTMLElement, align: string, separator: boolean) {
|
||||||
|
const 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._handler.reSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getClientWidth(element: HTMLElement) {
|
||||||
|
document.body.appendChild(element);
|
||||||
|
const width = element.clientWidth;
|
||||||
|
document.body.removeChild(element);
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
33
src/helpers/assertions.ts
Normal file
33
src/helpers/assertions.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Ensures that value is defined.
|
||||||
|
* Throws if the value is undefined, returns the original value otherwise.
|
||||||
|
*
|
||||||
|
* @param value - The value, or undefined.
|
||||||
|
* @returns The passed value, if it is not undefined
|
||||||
|
*/
|
||||||
|
export function ensureDefined(value: undefined): never;
|
||||||
|
export function ensureDefined<T>(value: T | undefined): T;
|
||||||
|
export function ensureDefined<T>(value: T | undefined): T {
|
||||||
|
if (value === undefined) {
|
||||||
|
throw new Error('Value is undefined');
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that value is not null.
|
||||||
|
* Throws if the value is null, returns the original value otherwise.
|
||||||
|
*
|
||||||
|
* @param value - The value, or null.
|
||||||
|
* @returns The passed value, if it is not null
|
||||||
|
*/
|
||||||
|
export function ensureNotNull(value: null): never;
|
||||||
|
export function ensureNotNull<T>(value: T | null): T;
|
||||||
|
export function ensureNotNull<T>(value: T | null): T {
|
||||||
|
if (value === null) {
|
||||||
|
throw new Error('Value is null');
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
14
src/helpers/canvas-rendering.ts
Normal file
14
src/helpers/canvas-rendering.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { LineStyle } from "lightweight-charts";
|
||||||
|
|
||||||
|
export function setLineStyle(ctx: CanvasRenderingContext2D, style: LineStyle): void {
|
||||||
|
const dashPatterns = {
|
||||||
|
[LineStyle.Solid]: [],
|
||||||
|
[LineStyle.Dotted]: [ctx.lineWidth, ctx.lineWidth],
|
||||||
|
[LineStyle.Dashed]: [2 * ctx.lineWidth, 2 * ctx.lineWidth],
|
||||||
|
[LineStyle.LargeDashed]: [6 * ctx.lineWidth, 6 * ctx.lineWidth],
|
||||||
|
[LineStyle.SparseDotted]: [ctx.lineWidth, 4 * ctx.lineWidth],
|
||||||
|
};
|
||||||
|
|
||||||
|
const dashPattern = dashPatterns[style];
|
||||||
|
ctx.setLineDash(dashPattern);
|
||||||
|
}
|
||||||
6
src/helpers/dimensions/common.ts
Normal file
6
src/helpers/dimensions/common.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export interface BitmapPositionLength {
|
||||||
|
/** coordinate for use with a bitmap rendering scope */
|
||||||
|
position: number;
|
||||||
|
/** length for use with a bitmap rendering scope */
|
||||||
|
length: number;
|
||||||
|
}
|
||||||
23
src/helpers/dimensions/crosshair-width.ts
Normal file
23
src/helpers/dimensions/crosshair-width.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Default grid / crosshair line width in Bitmap sizing
|
||||||
|
* @param horizontalPixelRatio - horizontal pixel ratio
|
||||||
|
* @returns default grid / crosshair line width in Bitmap sizing
|
||||||
|
*/
|
||||||
|
export function gridAndCrosshairBitmapWidth(
|
||||||
|
horizontalPixelRatio: number
|
||||||
|
): number {
|
||||||
|
return Math.max(1, Math.floor(horizontalPixelRatio));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default grid / crosshair line width in Media sizing
|
||||||
|
* @param horizontalPixelRatio - horizontal pixel ratio
|
||||||
|
* @returns default grid / crosshair line width in Media sizing
|
||||||
|
*/
|
||||||
|
export function gridAndCrosshairMediaWidth(
|
||||||
|
horizontalPixelRatio: number
|
||||||
|
): number {
|
||||||
|
return (
|
||||||
|
gridAndCrosshairBitmapWidth(horizontalPixelRatio) / horizontalPixelRatio
|
||||||
|
);
|
||||||
|
}
|
||||||
29
src/helpers/dimensions/full-width.ts
Normal file
29
src/helpers/dimensions/full-width.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { BitmapPositionLength } from './common';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the position and width which will completely full the space for the bar.
|
||||||
|
* Useful if you want to draw something that will not have any gaps between surrounding bars.
|
||||||
|
* @param xMedia - x coordinate of the bar defined in media sizing
|
||||||
|
* @param halfBarSpacingMedia - half the width of the current barSpacing (un-rounded)
|
||||||
|
* @param horizontalPixelRatio - horizontal pixel ratio
|
||||||
|
* @returns position and width which will completely full the space for the bar
|
||||||
|
*/
|
||||||
|
export function fullBarWidth(
|
||||||
|
xMedia: number,
|
||||||
|
halfBarSpacingMedia: number,
|
||||||
|
horizontalPixelRatio: number
|
||||||
|
): BitmapPositionLength {
|
||||||
|
const fullWidthLeftMedia = xMedia - halfBarSpacingMedia;
|
||||||
|
const fullWidthRightMedia = xMedia + halfBarSpacingMedia;
|
||||||
|
const fullWidthLeftBitmap = Math.round(
|
||||||
|
fullWidthLeftMedia * horizontalPixelRatio
|
||||||
|
);
|
||||||
|
const fullWidthRightBitmap = Math.round(
|
||||||
|
fullWidthRightMedia * horizontalPixelRatio
|
||||||
|
);
|
||||||
|
const fullWidthBitmap = fullWidthRightBitmap - fullWidthLeftBitmap;
|
||||||
|
return {
|
||||||
|
position: fullWidthLeftBitmap,
|
||||||
|
length: fullWidthBitmap,
|
||||||
|
};
|
||||||
|
}
|
||||||
48
src/helpers/dimensions/positions.ts
Normal file
48
src/helpers/dimensions/positions.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { BitmapPositionLength } from './common';
|
||||||
|
|
||||||
|
function centreOffset(lineBitmapWidth: number): number {
|
||||||
|
return Math.floor(lineBitmapWidth * 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the bitmap position for an item with a desired length (height or width), and centred according to
|
||||||
|
* an position coordinate defined in media sizing.
|
||||||
|
* @param positionMedia - position coordinate for the bar (in media coordinates)
|
||||||
|
* @param pixelRatio - pixel ratio. Either horizontal for x positions, or vertical for y positions
|
||||||
|
* @param desiredWidthMedia - desired width (in media coordinates)
|
||||||
|
* @returns Position of of the start point and length dimension.
|
||||||
|
*/
|
||||||
|
export function positionsLine(
|
||||||
|
positionMedia: number,
|
||||||
|
pixelRatio: number,
|
||||||
|
desiredWidthMedia: number = 1,
|
||||||
|
widthIsBitmap?: boolean
|
||||||
|
): BitmapPositionLength {
|
||||||
|
const scaledPosition = Math.round(pixelRatio * positionMedia);
|
||||||
|
const lineBitmapWidth = widthIsBitmap
|
||||||
|
? desiredWidthMedia
|
||||||
|
: Math.round(desiredWidthMedia * pixelRatio);
|
||||||
|
const offset = centreOffset(lineBitmapWidth);
|
||||||
|
const position = scaledPosition - offset;
|
||||||
|
return { position, length: lineBitmapWidth };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the bitmap position and length for a dimension of a shape to be drawn.
|
||||||
|
* @param position1Media - media coordinate for the first point
|
||||||
|
* @param position2Media - media coordinate for the second point
|
||||||
|
* @param pixelRatio - pixel ratio for the corresponding axis (vertical or horizontal)
|
||||||
|
* @returns Position of of the start point and length dimension.
|
||||||
|
*/
|
||||||
|
export function positionsBox(
|
||||||
|
position1Media: number,
|
||||||
|
position2Media: number,
|
||||||
|
pixelRatio: number
|
||||||
|
): BitmapPositionLength {
|
||||||
|
const scaledPosition1 = Math.round(pixelRatio * position1Media);
|
||||||
|
const scaledPosition2 = Math.round(pixelRatio * position2Media);
|
||||||
|
return {
|
||||||
|
position: Math.min(scaledPosition1, scaledPosition2),
|
||||||
|
length: Math.abs(scaledPosition2 - scaledPosition1) + 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
34
src/helpers/time.ts
Normal file
34
src/helpers/time.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { Time, isUTCTimestamp, isBusinessDay } from 'lightweight-charts';
|
||||||
|
|
||||||
|
export function convertTime(t: Time): number {
|
||||||
|
if (isUTCTimestamp(t)) return t * 1000;
|
||||||
|
if (isBusinessDay(t)) return new Date(t.year, t.month, t.day).valueOf();
|
||||||
|
const [year, month, day] = t.split('-').map(parseInt);
|
||||||
|
return new Date(year, month, day).valueOf();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function displayTime(time: Time): string {
|
||||||
|
if (typeof time == 'string') return time;
|
||||||
|
const date = isBusinessDay(time)
|
||||||
|
? new Date(time.year, time.month, time.day)
|
||||||
|
: new Date(time * 1000);
|
||||||
|
return date.toLocaleDateString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formattedDateAndTime(timestamp: number | undefined): [string, string] {
|
||||||
|
if (!timestamp) return ['', ''];
|
||||||
|
const dateObj = new Date(timestamp);
|
||||||
|
|
||||||
|
// Format date string
|
||||||
|
const year = dateObj.getFullYear();
|
||||||
|
const month = dateObj.toLocaleString('default', { month: 'short' });
|
||||||
|
const date = dateObj.getDate().toString().padStart(2, '0');
|
||||||
|
const formattedDate = `${date} ${month} ${year}`;
|
||||||
|
|
||||||
|
// Format time string
|
||||||
|
const hours = dateObj.getHours().toString().padStart(2, '0');
|
||||||
|
const minutes = dateObj.getMinutes().toString().padStart(2, '0');
|
||||||
|
const formattedTime = `${hours}:${minutes}`;
|
||||||
|
|
||||||
|
return [formattedDate, formattedTime];
|
||||||
|
}
|
||||||
37
src/horizontal-line/axis-view.ts
Normal file
37
src/horizontal-line/axis-view.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { Coordinate, ISeriesPrimitiveAxisView, PriceFormatBuiltIn } from 'lightweight-charts';
|
||||||
|
import { HorizontalLine } from './horizontal-line';
|
||||||
|
|
||||||
|
export class HorizontalLineAxisView implements ISeriesPrimitiveAxisView {
|
||||||
|
_source: HorizontalLine;
|
||||||
|
_y: Coordinate | null = null;
|
||||||
|
_price: string | null = null;
|
||||||
|
|
||||||
|
constructor(source: HorizontalLine) {
|
||||||
|
this._source = source;
|
||||||
|
}
|
||||||
|
update() {
|
||||||
|
if (!this._source.series || !this._source._point) return;
|
||||||
|
this._y = this._source.series.priceToCoordinate(this._source._point.price);
|
||||||
|
const priceFormat = this._source.series.options().priceFormat as PriceFormatBuiltIn;
|
||||||
|
const precision = priceFormat.precision;
|
||||||
|
this._price = this._source._point.price.toFixed(precision).toString();
|
||||||
|
}
|
||||||
|
visible() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
tickVisible() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
coordinate() {
|
||||||
|
return this._y ?? 0;
|
||||||
|
}
|
||||||
|
text() {
|
||||||
|
return this._source._options.text || this._price || '';
|
||||||
|
}
|
||||||
|
textColor() {
|
||||||
|
return 'white';
|
||||||
|
}
|
||||||
|
backColor() {
|
||||||
|
return this._source._options.lineColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
99
src/horizontal-line/horizontal-line.ts
Normal file
99
src/horizontal-line/horizontal-line.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import {
|
||||||
|
DeepPartial,
|
||||||
|
MouseEventParams
|
||||||
|
} from "lightweight-charts";
|
||||||
|
import { Point } from "../drawing/data-source";
|
||||||
|
import { Drawing, InteractionState } from "../drawing/drawing";
|
||||||
|
import { DrawingOptions } from "../drawing/options";
|
||||||
|
import { HorizontalLinePaneView } from "./pane-view";
|
||||||
|
import { GlobalParams } from "../general/global-params";
|
||||||
|
import { HorizontalLineAxisView } from "./axis-view";
|
||||||
|
|
||||||
|
|
||||||
|
declare const window: GlobalParams;
|
||||||
|
|
||||||
|
export class HorizontalLine extends Drawing {
|
||||||
|
_type = 'HorizontalLine';
|
||||||
|
_paneViews: HorizontalLinePaneView[];
|
||||||
|
_point: Point;
|
||||||
|
private _callbackName: string | null;
|
||||||
|
_priceAxisViews: HorizontalLineAxisView[];
|
||||||
|
|
||||||
|
protected _startDragPoint: Point | null = null;
|
||||||
|
|
||||||
|
constructor(point: Point, options: DeepPartial<DrawingOptions>, callbackName=null) {
|
||||||
|
super(options)
|
||||||
|
this._point = point;
|
||||||
|
this._point.time = null; // time is null for horizontal lines
|
||||||
|
this._paneViews = [new HorizontalLinePaneView(this)];
|
||||||
|
this._priceAxisViews = [new HorizontalLineAxisView(this)];
|
||||||
|
|
||||||
|
this._callbackName = callbackName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get points() {
|
||||||
|
return [this._point];
|
||||||
|
}
|
||||||
|
|
||||||
|
public updatePoints(...points: (Point | null)[]) {
|
||||||
|
for (const p of points) if (p) this._point.price = p.price;
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAllViews() {
|
||||||
|
this._paneViews.forEach((pw) => pw.update());
|
||||||
|
this._priceAxisViews.forEach((tw) => tw.update());
|
||||||
|
}
|
||||||
|
|
||||||
|
priceAxisViews() {
|
||||||
|
return this._priceAxisViews;
|
||||||
|
}
|
||||||
|
|
||||||
|
_moveToState(state: InteractionState) {
|
||||||
|
switch(state) {
|
||||||
|
case InteractionState.NONE:
|
||||||
|
document.body.style.cursor = "default";
|
||||||
|
this._unsubscribe("mousedown", this._handleMouseDownInteraction);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case InteractionState.HOVERING:
|
||||||
|
document.body.style.cursor = "pointer";
|
||||||
|
this._unsubscribe("mouseup", this._childHandleMouseUpInteraction);
|
||||||
|
this._subscribe("mousedown", this._handleMouseDownInteraction)
|
||||||
|
this.chart.applyOptions({handleScroll: true});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case InteractionState.DRAGGING:
|
||||||
|
document.body.style.cursor = "grabbing";
|
||||||
|
this._subscribe("mouseup", this._childHandleMouseUpInteraction);
|
||||||
|
this.chart.applyOptions({handleScroll: false});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this._state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDrag(diff: any) {
|
||||||
|
this._addDiffToPoint(this._point, 0, diff.price);
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
_mouseIsOverDrawing(param: MouseEventParams, tolerance = 4) {
|
||||||
|
if (!param.point) return false;
|
||||||
|
const y = this.series.priceToCoordinate(this._point.price);
|
||||||
|
if (!y) return false;
|
||||||
|
return (Math.abs(y-param.point.y) < tolerance);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _onMouseDown() {
|
||||||
|
this._startDragPoint = null;
|
||||||
|
const hoverPoint = this._latestHoverPoint;
|
||||||
|
if (!hoverPoint) return;
|
||||||
|
return this._moveToState(InteractionState.DRAGGING);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _childHandleMouseUpInteraction = () => {
|
||||||
|
this._handleMouseUpInteraction();
|
||||||
|
if (!this._callbackName) return;
|
||||||
|
window.callbackFunction(`${this._callbackName}_~_${this._point.price.toFixed(8)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/horizontal-line/pane-renderer.ts
Normal file
35
src/horizontal-line/pane-renderer.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { CanvasRenderingTarget2D } from "fancy-canvas";
|
||||||
|
import { DrawingOptions } from "../drawing/options";
|
||||||
|
import { DrawingPaneRenderer } from "../drawing/pane-renderer";
|
||||||
|
import { ViewPoint } from "../drawing/pane-view";
|
||||||
|
import { setLineStyle } from "../helpers/canvas-rendering";
|
||||||
|
|
||||||
|
export class HorizontalLinePaneRenderer extends DrawingPaneRenderer {
|
||||||
|
_point: ViewPoint = {x: null, y: null};
|
||||||
|
|
||||||
|
constructor(point: ViewPoint, options: DrawingOptions) {
|
||||||
|
super(options);
|
||||||
|
this._point = point;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(target: CanvasRenderingTarget2D) {
|
||||||
|
target.useBitmapCoordinateSpace(scope => {
|
||||||
|
if (this._point.y == null) return;
|
||||||
|
const ctx = scope.context;
|
||||||
|
|
||||||
|
const scaledY = Math.round(this._point.y * scope.verticalPixelRatio);
|
||||||
|
const scaledX = this._point.x ? this._point.x * scope.horizontalPixelRatio : 0;
|
||||||
|
|
||||||
|
ctx.lineWidth = this._options.width;
|
||||||
|
ctx.strokeStyle = this._options.lineColor;
|
||||||
|
setLineStyle(ctx, this._options.lineStyle);
|
||||||
|
ctx.beginPath();
|
||||||
|
|
||||||
|
ctx.moveTo(scaledX, scaledY);
|
||||||
|
ctx.lineTo(scope.bitmapSize.width, scaledY);
|
||||||
|
|
||||||
|
ctx.stroke();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
31
src/horizontal-line/pane-view.ts
Normal file
31
src/horizontal-line/pane-view.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { HorizontalLinePaneRenderer } from './pane-renderer';
|
||||||
|
import { HorizontalLine } from './horizontal-line';
|
||||||
|
import { DrawingPaneView, ViewPoint } from '../drawing/pane-view';
|
||||||
|
|
||||||
|
|
||||||
|
export class HorizontalLinePaneView extends DrawingPaneView {
|
||||||
|
_source: HorizontalLine;
|
||||||
|
_point: ViewPoint = {x: null, y: null};
|
||||||
|
|
||||||
|
constructor(source: HorizontalLine) {
|
||||||
|
super(source);
|
||||||
|
this._source = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
const point = this._source._point;
|
||||||
|
const timeScale = this._source.chart.timeScale()
|
||||||
|
const series = this._source.series;
|
||||||
|
if (this._source._type == "RayLine") {
|
||||||
|
this._point.x = point.time ? timeScale.timeToCoordinate(point.time) : timeScale.logicalToCoordinate(point.logical);
|
||||||
|
}
|
||||||
|
this._point.y = series.priceToCoordinate(point.price);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer() {
|
||||||
|
return new HorizontalLinePaneRenderer(
|
||||||
|
this._point,
|
||||||
|
this._source._options
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/horizontal-line/ray-line.ts
Normal file
35
src/horizontal-line/ray-line.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import {
|
||||||
|
DeepPartial,
|
||||||
|
MouseEventParams
|
||||||
|
} from "lightweight-charts";
|
||||||
|
import { DiffPoint, Point } from "../drawing/data-source";
|
||||||
|
import { DrawingOptions } from "../drawing/options";
|
||||||
|
import { HorizontalLine } from "./horizontal-line";
|
||||||
|
|
||||||
|
export class RayLine extends HorizontalLine {
|
||||||
|
_type = 'RayLine';
|
||||||
|
|
||||||
|
constructor(point: Point, options: DeepPartial<DrawingOptions>) {
|
||||||
|
super({...point}, options);
|
||||||
|
this._point.time = point.time;
|
||||||
|
}
|
||||||
|
|
||||||
|
public updatePoints(...points: (Point | null)[]) {
|
||||||
|
for (const p of points) if (p) this._point = p;
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDrag(diff: DiffPoint) {
|
||||||
|
this._addDiffToPoint(this._point, diff.logical, diff.price);
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
_mouseIsOverDrawing(param: MouseEventParams, tolerance = 4) {
|
||||||
|
if (!param.point) return false;
|
||||||
|
const y = this.series.priceToCoordinate(this._point.price);
|
||||||
|
|
||||||
|
const x = this._point.time ? this.chart.timeScale().timeToCoordinate(this._point.time) : null;
|
||||||
|
if (!y || !x) return false;
|
||||||
|
return (Math.abs(y-param.point.y) < tolerance && param.point.x > x - tolerance);
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/index.ts
Normal file
6
src/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export * from './general';
|
||||||
|
export * from './horizontal-line/horizontal-line';
|
||||||
|
export * from './vertical-line/vertical-line';
|
||||||
|
export * from './box/box';
|
||||||
|
export * from './trend-line/trend-line';
|
||||||
|
export * from './vertical-line/vertical-line';
|
||||||
56
src/plugin-base.ts
Normal file
56
src/plugin-base.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import {
|
||||||
|
DataChangedScope,
|
||||||
|
IChartApi,
|
||||||
|
ISeriesApi,
|
||||||
|
ISeriesPrimitive,
|
||||||
|
SeriesAttachedParameter,
|
||||||
|
SeriesOptionsMap,
|
||||||
|
Time,
|
||||||
|
} from 'lightweight-charts';
|
||||||
|
import { ensureDefined } from './helpers/assertions';
|
||||||
|
|
||||||
|
//* PluginBase is a useful base to build a plugin upon which
|
||||||
|
//* already handles creating getters for the chart and series,
|
||||||
|
//* and provides a requestUpdate method.
|
||||||
|
export abstract class PluginBase implements ISeriesPrimitive<Time> {
|
||||||
|
private _chart: IChartApi | undefined = undefined;
|
||||||
|
private _series: ISeriesApi<keyof SeriesOptionsMap> | undefined = undefined;
|
||||||
|
|
||||||
|
protected dataUpdated?(scope: DataChangedScope): void;
|
||||||
|
protected requestUpdate(): void {
|
||||||
|
if (this._requestUpdate) this._requestUpdate();
|
||||||
|
}
|
||||||
|
private _requestUpdate?: () => void;
|
||||||
|
|
||||||
|
public attached({
|
||||||
|
chart,
|
||||||
|
series,
|
||||||
|
requestUpdate,
|
||||||
|
}: SeriesAttachedParameter<Time>) {
|
||||||
|
this._chart = chart;
|
||||||
|
this._series = series;
|
||||||
|
this._series.subscribeDataChanged(this._fireDataUpdated);
|
||||||
|
this._requestUpdate = requestUpdate;
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public detached() {
|
||||||
|
this._chart = undefined;
|
||||||
|
this._series = undefined;
|
||||||
|
this._requestUpdate = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get chart(): IChartApi {
|
||||||
|
return ensureDefined(this._chart);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get series(): ISeriesApi<keyof SeriesOptionsMap> {
|
||||||
|
return ensureDefined(this._series);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _fireDataUpdated(scope: DataChangedScope) {
|
||||||
|
if (this.dataUpdated) {
|
||||||
|
this.dataUpdated(scope);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/sample-data.ts
Normal file
59
src/sample-data.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import type { Time, } from 'lightweight-charts';
|
||||||
|
|
||||||
|
type LineData = {
|
||||||
|
time: Time;
|
||||||
|
value: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CandleData = {
|
||||||
|
time: Time;
|
||||||
|
high: number;
|
||||||
|
low: number;
|
||||||
|
close: number;
|
||||||
|
open: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
let randomFactor = 25 + Math.random() * 25;
|
||||||
|
const samplePoint = (i: number) =>
|
||||||
|
i *
|
||||||
|
(0.5 +
|
||||||
|
Math.sin(i / 10) * 0.2 +
|
||||||
|
Math.sin(i / 20) * 0.4 +
|
||||||
|
Math.sin(i / randomFactor) * 0.8 +
|
||||||
|
Math.sin(i / 500) * 0.5) +
|
||||||
|
200;
|
||||||
|
|
||||||
|
export function generateLineData(numberOfPoints: number = 500): LineData[] {
|
||||||
|
randomFactor = 25 + Math.random() * 25;
|
||||||
|
const res = [];
|
||||||
|
const date = new Date(Date.UTC(2018, 0, 1, 12, 0, 0, 0));
|
||||||
|
for (let i = 0; i < numberOfPoints; ++i) {
|
||||||
|
const time = (date.getTime() / 1000) as Time;
|
||||||
|
const value = samplePoint(i);
|
||||||
|
res.push({
|
||||||
|
time,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
|
||||||
|
date.setUTCDate(date.getUTCDate() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateCandleData(numberOfPoints: number = 250): CandleData[] {
|
||||||
|
const lineData = generateLineData(numberOfPoints);
|
||||||
|
return lineData.map((d, i) => {
|
||||||
|
const randomRanges = [-1 * Math.random(), Math.random(), Math.random()].map(
|
||||||
|
j => j * 10
|
||||||
|
);
|
||||||
|
const sign = Math.sin(Math.random() - 0.5);
|
||||||
|
return {
|
||||||
|
time: d.time,
|
||||||
|
low: d.value + randomRanges[0],
|
||||||
|
high: d.value + randomRanges[1],
|
||||||
|
open: d.value + sign * randomRanges[2],
|
||||||
|
close: samplePoint(i + 1),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
42
src/trend-line/pane-renderer.ts
Normal file
42
src/trend-line/pane-renderer.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { ViewPoint } from "./pane-view";
|
||||||
|
|
||||||
|
import { CanvasRenderingTarget2D } from "fancy-canvas";
|
||||||
|
import { TwoPointDrawingPaneRenderer } from "../drawing/pane-renderer";
|
||||||
|
import { DrawingOptions } from "../drawing/options";
|
||||||
|
import { setLineStyle } from "../helpers/canvas-rendering";
|
||||||
|
|
||||||
|
export class TrendLinePaneRenderer extends TwoPointDrawingPaneRenderer {
|
||||||
|
constructor(p1: ViewPoint, p2: ViewPoint, options: DrawingOptions, hovered: boolean) {
|
||||||
|
super(p1, p2, options, hovered);
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(target: CanvasRenderingTarget2D) {
|
||||||
|
target.useBitmapCoordinateSpace(scope => {
|
||||||
|
if (
|
||||||
|
this._p1.x === null ||
|
||||||
|
this._p1.y === null ||
|
||||||
|
this._p2.x === null ||
|
||||||
|
this._p2.y === null
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
const ctx = scope.context;
|
||||||
|
|
||||||
|
const scaled = this._getScaledCoordinates(scope);
|
||||||
|
if (!scaled) return;
|
||||||
|
|
||||||
|
ctx.lineWidth = this._options.width;
|
||||||
|
ctx.strokeStyle = this._options.lineColor;
|
||||||
|
setLineStyle(ctx, this._options.lineStyle);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(scaled.x1, scaled.y1);
|
||||||
|
ctx.lineTo(scaled.x2, scaled.y2);
|
||||||
|
ctx.stroke();
|
||||||
|
// this._drawTextLabel(scope, this._text1, x1Scaled, y1Scaled, true);
|
||||||
|
// this._drawTextLabel(scope, this._text2, x2Scaled, y2Scaled, false);
|
||||||
|
|
||||||
|
if (!this._hovered) return;
|
||||||
|
this._drawEndCircle(scope, scaled.x1, scaled.y1);
|
||||||
|
this._drawEndCircle(scope, scaled.x2, scaled.y2);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/trend-line/pane-view.ts
Normal file
24
src/trend-line/pane-view.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Coordinate, } from 'lightweight-charts';
|
||||||
|
import { TrendLine } from './trend-line';
|
||||||
|
import { TrendLinePaneRenderer } from './pane-renderer';
|
||||||
|
import { TwoPointDrawingPaneView } from '../drawing/pane-view';
|
||||||
|
|
||||||
|
export interface ViewPoint {
|
||||||
|
x: Coordinate | null;
|
||||||
|
y: Coordinate | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TrendLinePaneView extends TwoPointDrawingPaneView {
|
||||||
|
constructor(source: TrendLine) {
|
||||||
|
super(source)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer() {
|
||||||
|
return new TrendLinePaneRenderer(
|
||||||
|
this._p1,
|
||||||
|
this._p2,
|
||||||
|
this._source._options,
|
||||||
|
this._source.hovered,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
109
src/trend-line/trend-line.ts
Normal file
109
src/trend-line/trend-line.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import {
|
||||||
|
MouseEventParams,
|
||||||
|
} from 'lightweight-charts';
|
||||||
|
|
||||||
|
|
||||||
|
import { TrendLinePaneView } from './pane-view';
|
||||||
|
import { Point } from '../drawing/data-source';
|
||||||
|
import { InteractionState } from '../drawing/drawing';
|
||||||
|
import { DrawingOptions } from '../drawing/options';
|
||||||
|
import { TwoPointDrawing } from '../drawing/two-point-drawing';
|
||||||
|
|
||||||
|
|
||||||
|
export class TrendLine extends TwoPointDrawing {
|
||||||
|
_type = "TrendLine"
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
p1: Point,
|
||||||
|
p2: Point,
|
||||||
|
options?: Partial<DrawingOptions>
|
||||||
|
) {
|
||||||
|
super(p1, p2, options)
|
||||||
|
this._paneViews = [new TrendLinePaneView(this)];
|
||||||
|
}
|
||||||
|
|
||||||
|
_moveToState(state: InteractionState) {
|
||||||
|
switch(state) {
|
||||||
|
|
||||||
|
case InteractionState.NONE:
|
||||||
|
document.body.style.cursor = "default";
|
||||||
|
this._hovered = false;
|
||||||
|
this.requestUpdate();
|
||||||
|
this._unsubscribe("mousedown", this._handleMouseDownInteraction);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case InteractionState.HOVERING:
|
||||||
|
document.body.style.cursor = "pointer";
|
||||||
|
this._hovered = true;
|
||||||
|
this.requestUpdate();
|
||||||
|
this._subscribe("mousedown", this._handleMouseDownInteraction);
|
||||||
|
this._unsubscribe("mouseup", this._handleMouseDownInteraction);
|
||||||
|
this.chart.applyOptions({handleScroll: true});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case InteractionState.DRAGGINGP1:
|
||||||
|
case InteractionState.DRAGGINGP2:
|
||||||
|
case InteractionState.DRAGGING:
|
||||||
|
document.body.style.cursor = "grabbing";
|
||||||
|
this._subscribe("mouseup", this._handleMouseUpInteraction);
|
||||||
|
this.chart.applyOptions({handleScroll: false});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this._state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_onDrag(diff: any) {
|
||||||
|
if (this._state == InteractionState.DRAGGING || this._state == InteractionState.DRAGGINGP1) {
|
||||||
|
this._addDiffToPoint(this.p1, diff.logical, diff.price);
|
||||||
|
}
|
||||||
|
if (this._state == InteractionState.DRAGGING || this._state == InteractionState.DRAGGINGP2) {
|
||||||
|
this._addDiffToPoint(this.p2, diff.logical, diff.price);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _onMouseDown() {
|
||||||
|
this._startDragPoint = null;
|
||||||
|
const hoverPoint = this._latestHoverPoint;
|
||||||
|
if (!hoverPoint) return;
|
||||||
|
const p1 = this._paneViews[0]._p1;
|
||||||
|
const p2 = this._paneViews[0]._p2;
|
||||||
|
|
||||||
|
if (!p1.x || !p2.x || !p1.y || !p2.y) return this._moveToState(InteractionState.DRAGGING);
|
||||||
|
|
||||||
|
const tolerance = 10;
|
||||||
|
if (Math.abs(hoverPoint.x-p1.x) < tolerance && Math.abs(hoverPoint.y-p1.y) < tolerance) {
|
||||||
|
this._moveToState(InteractionState.DRAGGINGP1)
|
||||||
|
}
|
||||||
|
else if (Math.abs(hoverPoint.x-p2.x) < tolerance && Math.abs(hoverPoint.y-p2.y) < tolerance) {
|
||||||
|
this._moveToState(InteractionState.DRAGGINGP2)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this._moveToState(InteractionState.DRAGGING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _mouseIsOverDrawing(param: MouseEventParams, tolerance = 4) {
|
||||||
|
|
||||||
|
if (!param.point) return false;;
|
||||||
|
|
||||||
|
const x1 = this._paneViews[0]._p1.x;
|
||||||
|
const y1 = this._paneViews[0]._p1.y;
|
||||||
|
const x2 = this._paneViews[0]._p2.x;
|
||||||
|
const y2 = this._paneViews[0]._p2.y;
|
||||||
|
if (!x1 || !x2 || !y1 || !y2 ) return false;
|
||||||
|
|
||||||
|
const mouseX = param.point.x;
|
||||||
|
const mouseY = param.point.y;
|
||||||
|
|
||||||
|
if (mouseX <= Math.min(x1, x2) - tolerance ||
|
||||||
|
mouseX >= Math.max(x1, x2) + tolerance) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const distance = Math.abs((y2 - y1) * mouseX - (x2 - x1) * mouseY + x2 * y1 - y2 * x1
|
||||||
|
) / Math.sqrt((y2 - y1) ** 2 + (x2 - x1) ** 2);
|
||||||
|
|
||||||
|
return distance <= tolerance
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/vertical-line/axis-view.ts
Normal file
35
src/vertical-line/axis-view.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { Coordinate, ISeriesPrimitiveAxisView } from "lightweight-charts";
|
||||||
|
import { VerticalLine } from "./vertical-line";
|
||||||
|
|
||||||
|
export class VerticalLineTimeAxisView implements ISeriesPrimitiveAxisView {
|
||||||
|
_source: VerticalLine;
|
||||||
|
_x: Coordinate | null = null;
|
||||||
|
|
||||||
|
constructor(source: VerticalLine) {
|
||||||
|
this._source = source;
|
||||||
|
}
|
||||||
|
update() {
|
||||||
|
if (!this._source.chart|| !this._source._point) return;
|
||||||
|
const point = this._source._point;
|
||||||
|
const timeScale = this._source.chart.timeScale();
|
||||||
|
this._x = point.time ? timeScale.timeToCoordinate(point.time) : timeScale.logicalToCoordinate(point.logical);
|
||||||
|
}
|
||||||
|
visible() {
|
||||||
|
return !!this._source._options.text;
|
||||||
|
}
|
||||||
|
tickVisible() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
coordinate() {
|
||||||
|
return this._x ?? 0;
|
||||||
|
}
|
||||||
|
text() {
|
||||||
|
return this._source._options.text || '';
|
||||||
|
}
|
||||||
|
textColor() {
|
||||||
|
return "white";
|
||||||
|
}
|
||||||
|
backColor() {
|
||||||
|
return this._source._options.lineColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/vertical-line/pane-renderer.ts
Normal file
32
src/vertical-line/pane-renderer.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { CanvasRenderingTarget2D } from "fancy-canvas";
|
||||||
|
import { DrawingOptions } from "../drawing/options";
|
||||||
|
import { DrawingPaneRenderer } from "../drawing/pane-renderer";
|
||||||
|
import { ViewPoint } from "../drawing/pane-view";
|
||||||
|
import { setLineStyle } from "../helpers/canvas-rendering";
|
||||||
|
|
||||||
|
export class VerticalLinePaneRenderer extends DrawingPaneRenderer {
|
||||||
|
_point: ViewPoint = {x: null, y: null};
|
||||||
|
|
||||||
|
constructor(point: ViewPoint, options: DrawingOptions) {
|
||||||
|
super(options);
|
||||||
|
this._point = point;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(target: CanvasRenderingTarget2D) {
|
||||||
|
target.useBitmapCoordinateSpace(scope => {
|
||||||
|
if (this._point.x == null) return;
|
||||||
|
const ctx = scope.context;
|
||||||
|
const scaledX = this._point.x * scope.horizontalPixelRatio;
|
||||||
|
|
||||||
|
ctx.lineWidth = this._options.width;
|
||||||
|
ctx.strokeStyle = this._options.lineColor;
|
||||||
|
setLineStyle(ctx, this._options.lineStyle);
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(scaledX, 0);
|
||||||
|
ctx.lineTo(scaledX, scope.bitmapSize.height);
|
||||||
|
ctx.stroke();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
29
src/vertical-line/pane-view.ts
Normal file
29
src/vertical-line/pane-view.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { VerticalLinePaneRenderer } from './pane-renderer';
|
||||||
|
import { VerticalLine } from './vertical-line';
|
||||||
|
import { DrawingPaneView, ViewPoint } from '../drawing/pane-view';
|
||||||
|
|
||||||
|
|
||||||
|
export class VerticalLinePaneView extends DrawingPaneView {
|
||||||
|
_source: VerticalLine;
|
||||||
|
_point: ViewPoint = {x: null, y: null};
|
||||||
|
|
||||||
|
constructor(source: VerticalLine) {
|
||||||
|
super(source);
|
||||||
|
this._source = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
const point = this._source._point;
|
||||||
|
const timeScale = this._source.chart.timeScale()
|
||||||
|
const series = this._source.series;
|
||||||
|
this._point.x = point.time ? timeScale.timeToCoordinate(point.time) : timeScale.logicalToCoordinate(point.logical)
|
||||||
|
this._point.y = series.priceToCoordinate(point.price);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer() {
|
||||||
|
return new VerticalLinePaneRenderer(
|
||||||
|
this._point,
|
||||||
|
this._source._options
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
111
src/vertical-line/vertical-line.ts
Normal file
111
src/vertical-line/vertical-line.ts
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import {
|
||||||
|
DeepPartial,
|
||||||
|
MouseEventParams
|
||||||
|
} from "lightweight-charts";
|
||||||
|
import { Point } from "../drawing/data-source";
|
||||||
|
import { Drawing, InteractionState } from "../drawing/drawing";
|
||||||
|
import { DrawingOptions } from "../drawing/options";
|
||||||
|
import { VerticalLinePaneView } from "./pane-view";
|
||||||
|
import { GlobalParams } from "../general/global-params";
|
||||||
|
import { VerticalLineTimeAxisView } from "./axis-view";
|
||||||
|
|
||||||
|
|
||||||
|
declare const window: GlobalParams;
|
||||||
|
|
||||||
|
export class VerticalLine extends Drawing {
|
||||||
|
_type = 'VerticalLine';
|
||||||
|
_paneViews: VerticalLinePaneView[];
|
||||||
|
_timeAxisViews: VerticalLineTimeAxisView[];
|
||||||
|
_point: Point;
|
||||||
|
private _callbackName: string | null;
|
||||||
|
|
||||||
|
protected _startDragPoint: Point | null = null;
|
||||||
|
|
||||||
|
constructor(point: Point, options: DeepPartial<DrawingOptions>, callbackName=null) {
|
||||||
|
super(options)
|
||||||
|
this._point = point;
|
||||||
|
this._paneViews = [new VerticalLinePaneView(this)];
|
||||||
|
this._callbackName = callbackName;
|
||||||
|
|
||||||
|
this._timeAxisViews = [new VerticalLineTimeAxisView(this)]
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAllViews() {
|
||||||
|
this._paneViews.forEach(pw => pw.update());
|
||||||
|
this._timeAxisViews.forEach(tw => tw.update());
|
||||||
|
}
|
||||||
|
|
||||||
|
timeAxisViews() {
|
||||||
|
return this._timeAxisViews;
|
||||||
|
}
|
||||||
|
|
||||||
|
public updatePoints(...points: (Point | null)[]) {
|
||||||
|
for (const p of points) {
|
||||||
|
if (!p) continue;
|
||||||
|
if (!p.time && p.logical) {
|
||||||
|
p.time = this.series.dataByIndex(p.logical)?.time || null
|
||||||
|
}
|
||||||
|
this._point = p;
|
||||||
|
}
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
get points() {
|
||||||
|
return [this._point];
|
||||||
|
}
|
||||||
|
|
||||||
|
_moveToState(state: InteractionState) {
|
||||||
|
switch(state) {
|
||||||
|
case InteractionState.NONE:
|
||||||
|
document.body.style.cursor = "default";
|
||||||
|
this._unsubscribe("mousedown", this._handleMouseDownInteraction);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case InteractionState.HOVERING:
|
||||||
|
document.body.style.cursor = "pointer";
|
||||||
|
this._unsubscribe("mouseup", this._childHandleMouseUpInteraction);
|
||||||
|
this._subscribe("mousedown", this._handleMouseDownInteraction)
|
||||||
|
this.chart.applyOptions({handleScroll: true});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case InteractionState.DRAGGING:
|
||||||
|
document.body.style.cursor = "grabbing";
|
||||||
|
this._subscribe("mouseup", this._childHandleMouseUpInteraction);
|
||||||
|
this.chart.applyOptions({handleScroll: false});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this._state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDrag(diff: any) {
|
||||||
|
this._addDiffToPoint(this._point, diff.logical, 0);
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
_mouseIsOverDrawing(param: MouseEventParams, tolerance = 4) {
|
||||||
|
if (!param.point) return false;
|
||||||
|
const timeScale = this.chart.timeScale()
|
||||||
|
let x;
|
||||||
|
if (this._point.time) {
|
||||||
|
x = timeScale.timeToCoordinate(this._point.time);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
x = timeScale.logicalToCoordinate(this._point.logical);
|
||||||
|
}
|
||||||
|
if (!x) return false;
|
||||||
|
return (Math.abs(x-param.point.x) < tolerance);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _onMouseDown() {
|
||||||
|
this._startDragPoint = null;
|
||||||
|
const hoverPoint = this._latestHoverPoint;
|
||||||
|
if (!hoverPoint) return;
|
||||||
|
return this._moveToState(InteractionState.DRAGGING);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _childHandleMouseUpInteraction = () => {
|
||||||
|
this._handleMouseUpInteraction();
|
||||||
|
if (!this._callbackName) return;
|
||||||
|
window.callbackFunction(`${this._callbackName}_~_${this._point.price.toFixed(8)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
13
src/vite.config.js
Normal file
13
src/vite.config.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
const input = {
|
||||||
|
main: './src/example/index.html',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
input,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
43
test/drawings.json
Normal file
43
test/drawings.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"SYM": [
|
||||||
|
{
|
||||||
|
"type": "Box",
|
||||||
|
"points":
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"time": 1675036800,
|
||||||
|
"logical": 2928,
|
||||||
|
"price": 130.25483664317744
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"time": 1676332800,
|
||||||
|
"logical": 2939,
|
||||||
|
"price": 145.52389493914157
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"color": "#FFF",
|
||||||
|
"style": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "TrendLine",
|
||||||
|
"points": [
|
||||||
|
{
|
||||||
|
"time": 1669939200,
|
||||||
|
"logical": 2890,
|
||||||
|
"price": 196.12991672005123
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"time": 1673222400,
|
||||||
|
"logical": 2914,
|
||||||
|
"price": 223.17796284433055
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"color": "#FFF",
|
||||||
|
"style": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
22
test/run_tests.py
Normal file
22
test/run_tests.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from test_returns import TestReturns
|
||||||
|
from test_table import TestTable
|
||||||
|
from test_toolbox import TestToolBox
|
||||||
|
from test_topbar import TestTopBar
|
||||||
|
from test_chart import TestChart
|
||||||
|
|
||||||
|
|
||||||
|
TEST_CASES = [
|
||||||
|
TestReturns,
|
||||||
|
TestTable,
|
||||||
|
TestToolBox,
|
||||||
|
TestTopBar,
|
||||||
|
TestChart,
|
||||||
|
]
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
loader = unittest.TestLoader()
|
||||||
|
cases = [loader.loadTestsFromTestCase(module) for module in TEST_CASES]
|
||||||
|
suite = unittest.TestSuite(cases)
|
||||||
|
unittest.TextTestRunner(verbosity=2).run(suite)
|
||||||
21
test/test_chart.py
Normal file
21
test/test_chart.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import unittest
|
||||||
|
import pandas as pd
|
||||||
|
from util import BARS, Tester
|
||||||
|
from lightweight_charts import Chart
|
||||||
|
|
||||||
|
|
||||||
|
class TestChart(Tester):
|
||||||
|
def test_data_is_renamed(self):
|
||||||
|
uppercase_df = pd.DataFrame(BARS.copy()).rename({'date': 'Date', 'open': 'OPEN', 'high': 'HIgh', 'low': 'Low', 'close': 'close', 'volUME': 'volume'})
|
||||||
|
result = self.chart._df_datetime_format(uppercase_df)
|
||||||
|
self.assertEqual(list(result.columns), list(BARS.rename(columns={'date': 'time'}).columns))
|
||||||
|
|
||||||
|
def test_line_in_list(self):
|
||||||
|
result0 = self.chart.create_line()
|
||||||
|
result1 = self.chart.create_line()
|
||||||
|
self.assertEqual(result0, self.chart.lines()[0])
|
||||||
|
self.assertEqual(result1, self.chart.lines()[1])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
41
test/test_returns.py
Normal file
41
test/test_returns.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import unittest
|
||||||
|
import pandas as pd
|
||||||
|
from lightweight_charts import Chart
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from util import BARS, Tester
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TestReturns(Tester):
|
||||||
|
def test_screenshot_returns_value(self):
|
||||||
|
self.chart.set(BARS)
|
||||||
|
self.chart.show()
|
||||||
|
screenshot_data = self.chart.screenshot()
|
||||||
|
self.assertIsNotNone(screenshot_data)
|
||||||
|
|
||||||
|
def test_save_drawings(self):
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
asyncio.create_task(self.chart.show_async());
|
||||||
|
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
self.chart.toolbox.drawings.clear() # clear drawings in python
|
||||||
|
self.assertTrue(len(self.chart.toolbox.drawings) == 0)
|
||||||
|
self.chart.run_script(f'{self.chart.id}.toolBox.saveDrawings();')
|
||||||
|
await asyncio.sleep(1) # resave them, and assert they exist
|
||||||
|
self.assertTrue(len(self.chart.toolbox.drawings) > 0)
|
||||||
|
self.chart.exit()
|
||||||
|
|
||||||
|
self.chart = Chart(toolbox=True, width=100, height=100)
|
||||||
|
self.chart.set(BARS)
|
||||||
|
self.chart.topbar.textbox('symbol', 'SYM', align='right')
|
||||||
|
self.chart.toolbox.save_drawings_under(self.chart.topbar['symbol'])
|
||||||
|
self.chart.toolbox.import_drawings("drawings.json")
|
||||||
|
self.chart.toolbox.load_drawings("SYM")
|
||||||
|
asyncio.run(main())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
12
test/test_table.py
Normal file
12
test/test_table.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import unittest
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from lightweight_charts import Chart
|
||||||
|
|
||||||
|
|
||||||
|
class TestTable(unittest.TestCase):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
43
test/test_toolbox.py
Normal file
43
test/test_toolbox.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import unittest
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from lightweight_charts import Chart
|
||||||
|
from util import BARS, Tester
|
||||||
|
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
|
||||||
|
class TestToolBox(Tester):
|
||||||
|
def test_create_horizontal_line(self):
|
||||||
|
self.chart.set(BARS)
|
||||||
|
horz_line = self.chart.horizontal_line(200, width=4)
|
||||||
|
self.chart.show()
|
||||||
|
result = self.chart.win.run_script_and_get(f"{horz_line.id}._options");
|
||||||
|
self.assertTrue(result)
|
||||||
|
self.chart.exit()
|
||||||
|
|
||||||
|
def test_create_trend_line(self):
|
||||||
|
self.chart.set(BARS)
|
||||||
|
horz_line = self.chart.trend_line(BARS.iloc[-10]['date'], 180, BARS.iloc[-3]['date'], 190)
|
||||||
|
self.chart.show()
|
||||||
|
result = self.chart.win.run_script_and_get(f"{horz_line.id}._options");
|
||||||
|
self.assertTrue(result)
|
||||||
|
self.chart.exit()
|
||||||
|
|
||||||
|
def test_create_box(self):
|
||||||
|
self.chart.set(BARS)
|
||||||
|
horz_line = self.chart.box(BARS.iloc[-10]['date'], 180, BARS.iloc[-3]['date'], 190)
|
||||||
|
self.chart.show()
|
||||||
|
result = self.chart.win.run_script_and_get(f"{horz_line.id}._options");
|
||||||
|
self.assertTrue(result)
|
||||||
|
self.chart.exit()
|
||||||
|
|
||||||
|
def test_create_vertical_line(self):
|
||||||
|
...
|
||||||
|
|
||||||
|
def test_create_vertical_span(self):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
22
test/test_topbar.py
Normal file
22
test/test_topbar.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import unittest
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from lightweight_charts import Chart
|
||||||
|
from util import Tester
|
||||||
|
|
||||||
|
|
||||||
|
class TestTopBar(Tester):
|
||||||
|
def test_switcher_fires_event(self):
|
||||||
|
self.chart.topbar.switcher('a', ('1', '2'), func=lambda c: (self.assertEqual(c.topbar['a'].value, '2'), c.exit()))
|
||||||
|
self.chart.run_script(f'{self.chart.topbar["a"].id}.intervalElements[1].dispatchEvent(new Event("click"))')
|
||||||
|
self.chart.show(block=True)
|
||||||
|
|
||||||
|
def test_button_fires_event(self):
|
||||||
|
self.chart.topbar.button('a', '1', func=lambda c: (self.assertEqual(c.topbar['a'].value, '2'), c.exit()))
|
||||||
|
self.chart.topbar['a'].set('2')
|
||||||
|
self.chart.run_script(f'{self.chart.topbar["a"].id}.elem.dispatchEvent(new Event("click"))')
|
||||||
|
self.chart.show(block=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
20
test/util.py
Normal file
20
test/util.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import unittest
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from lightweight_charts import Chart
|
||||||
|
|
||||||
|
|
||||||
|
BARS = pd.read_csv('../examples/1_setting_data/ohlcv.csv')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Tester(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.chart: Chart = Chart(100, 100, 800, 100);
|
||||||
|
|
||||||
|
def tearDown(self) -> None:
|
||||||
|
self.chart.exit()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
22
tsconfig.json
Normal file
22
tsconfig.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ESNext", "DOM"],
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"strict": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationDir": "typings",
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
// "outDir": "./dist"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user