Enhancements and Bug Fixes
Tables Feature - Added the `create_table` method, which returns a `Table` object. This can be used to display watchlists, order windows, position windows and more. - See the new page on the docs for more information! Bugs - Fixed a bug preventing the named column of a line to not work as a label of the series. - Fixed a bug causing drawings loaded from the minute timeframe to not show on a daily timeframe. - Fixed a bug causing `chart.exit` to not work. - Fixed a bug preventing the chart from being moved after placing a ray. - Fixed the ‘price in hoveringOver’ web console error. Enhancements - The date/time column can also be the `name` of the passed series object. - Added the `label` method to `HorizontalLine`, allowing for the price line label of horizontal lines to be updated. - `None` or an empty DataFrame can now be passed to `line.set` as a means to clear it. - Seperate Chart objects will now run on the same pywebview instance. This means that any Chart objects created after the first will inherit the first Chart’s API. - Reorganized the documentation for clarity.
This commit is contained in:
@ -5,6 +5,7 @@ from base64 import b64decode
|
||||
import pandas as pd
|
||||
from typing import Union, Literal, Dict, List
|
||||
|
||||
from lightweight_charts.table import Table
|
||||
from lightweight_charts.util import LINE_STYLE, MARKER_POSITION, MARKER_SHAPE, CROSSHAIR_MODE, _crosshair_mode, \
|
||||
_line_style, \
|
||||
MissingColumn, _js_bool, _price_scale_mode, PRICE_SCALE_MODE, _marker_position, _marker_shape, IDGen
|
||||
@ -12,7 +13,7 @@ from lightweight_charts.util import LINE_STYLE, MARKER_POSITION, MARKER_SHAPE, C
|
||||
|
||||
JS = {}
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
for file in ('pkg', 'funcs', 'callback', 'toolbox'):
|
||||
for file in ('pkg', 'funcs', 'callback', 'toolbox', 'table'):
|
||||
with open(os.path.join(current_dir, 'js', f'{file}.js'), 'r', encoding='utf-8') as f:
|
||||
JS[file] = f.read()
|
||||
|
||||
@ -41,6 +42,7 @@ HTML = f"""
|
||||
<div id="wrapper"></div>
|
||||
<script>
|
||||
{JS['funcs']}
|
||||
{JS['table']}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -89,6 +91,8 @@ class SeriesCommon:
|
||||
self._rename(series, {exclude_lowercase.lower(): exclude_lowercase}, False)
|
||||
if 'date' in series.index:
|
||||
self._rename(series, {'date': 'time'}, False)
|
||||
elif 'time' not in series.index:
|
||||
series['time'] = series.name
|
||||
series['time'] = self._datetime_format(series['time'])
|
||||
return series
|
||||
|
||||
@ -219,6 +223,9 @@ class HorizontalLine:
|
||||
"""
|
||||
self._chart.run_script(f'{self.id}.updatePrice({price})')
|
||||
|
||||
def label(self, text: str):
|
||||
self._chart.run_script(f'{self.id}.updateLabel("{text}")')
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Irreversibly deletes the horizontal line.
|
||||
@ -258,7 +265,7 @@ class Line(SeriesCommon):
|
||||
{self._chart.id}.lines.push({self.id})
|
||||
if ('legend' in {self._chart.id}) {{
|
||||
{self._chart.id}.legend.makeLines({self._chart.id})
|
||||
}}
|
||||
}}
|
||||
''')
|
||||
|
||||
|
||||
@ -268,6 +275,9 @@ class Line(SeriesCommon):
|
||||
:param data: If the name parameter is not used, the columns should be named: date/time, value.
|
||||
:param name: The column of the DataFrame to use as the line value. When used, the Line will be named after this column.
|
||||
"""
|
||||
if data.empty or data is None:
|
||||
self.run_script(f'{self.id}.series.setData([]); {self.id}.name = "{name}"')
|
||||
return
|
||||
df = self._df_datetime_format(data, exclude_lowercase=name)
|
||||
if name:
|
||||
if name not in data:
|
||||
@ -282,7 +292,9 @@ class Line(SeriesCommon):
|
||||
Updates the line data.\n
|
||||
:param series: labels: date/time, value
|
||||
"""
|
||||
series = self._series_datetime_format(series)
|
||||
series = self._series_datetime_format(series, exclude_lowercase=self.name)
|
||||
if self.name in series.index:
|
||||
series.rename({self.name: 'value'}, inplace=True)
|
||||
self._last_bar = series
|
||||
self.run_script(f'{self.id}.series.update({series.to_dict()})')
|
||||
|
||||
@ -413,7 +425,7 @@ class ToolBox:
|
||||
class LWC(SeriesCommon):
|
||||
def __init__(self, volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0, dynamic_loading: bool = False,
|
||||
scale_candles_only: bool = False, topbar: bool = False, searchbox: bool = False, toolbox: bool = False,
|
||||
_js_api_code: str = '""', autosize=True, _run_script=None):
|
||||
_js_api_code: str = None, autosize=True, _run_script=None):
|
||||
self.volume_enabled = volume_enabled
|
||||
self._scale_candles_only = scale_candles_only
|
||||
self._inner_width = inner_width
|
||||
@ -433,7 +445,7 @@ class LWC(SeriesCommon):
|
||||
self._interval = None
|
||||
self._charts = {self.id: self}
|
||||
self._lines = []
|
||||
self._js_api_code = _js_api_code
|
||||
self.run_script(f'window.callbackFunction = {_js_api_code}') if _js_api_code else None
|
||||
self._methods = {}
|
||||
self._return_q = None
|
||||
|
||||
@ -445,7 +457,7 @@ class LWC(SeriesCommon):
|
||||
self.polygon: PolygonAPI = PolygonAPI(self)
|
||||
|
||||
self.run_script(f'''
|
||||
{self.id} = makeChart({self._js_api_code}, {self._inner_width}, {self._inner_height}, autoSize={_js_bool(autosize)})
|
||||
{self.id} = makeChart({self._inner_width}, {self._inner_height}, autoSize={_js_bool(autosize)})
|
||||
{self.id}.id = '{self.id}'
|
||||
{self.id}.wrapper.style.float = "{self._position}"
|
||||
''')
|
||||
@ -820,7 +832,7 @@ class LWC(SeriesCommon):
|
||||
canvas.toBlob(function(blob) {{
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(event) {{
|
||||
{self._js_api_code}(`return_~_{self.id}_~_${{event.target.result}}`)
|
||||
window.callbackFunction(`return_~_{self.id}_~_${{event.target.result}}`)
|
||||
}};
|
||||
reader.readAsDataURL(blob);
|
||||
}})
|
||||
@ -837,12 +849,17 @@ class LWC(SeriesCommon):
|
||||
{self.id}.commandFunctions.unshift((event) => {{
|
||||
if (event.{modifier_key + 'Key'} && event.code === '{key_code}') {{
|
||||
event.preventDefault()
|
||||
{self.id}.callbackFunction(`{str(method)}_~_{self.id}_~_{key}`)
|
||||
window.callbackFunction(`{str(method)}_~_{self.id}_~_{key}`)
|
||||
return true
|
||||
}}
|
||||
else return false
|
||||
}})''')
|
||||
|
||||
def create_table(self, width: Union[float, int], height: Union[float, int], headings: tuple, widths: tuple = None, alignments: tuple = None,
|
||||
position: str = 'left', draggable: bool = False, method: object = None):
|
||||
self._methods[str(method)] = method
|
||||
return Table(self, width, height, headings, widths, alignments, position, draggable, method)
|
||||
|
||||
def create_subchart(self, volume_enabled: bool = True, position: Literal['left', 'right', 'top', 'bottom'] = 'left',
|
||||
width: float = 0.5, height: float = 0.5, sync: Union[bool, str] = False, dynamic_loading: bool = False,
|
||||
scale_candles_only: bool = False, topbar: bool = False, searchbox: bool = False, toolbox: bool = False):
|
||||
@ -854,8 +871,7 @@ class LWC(SeriesCommon):
|
||||
class SubChart(LWC):
|
||||
def __init__(self, parent, volume_enabled, position, width, height, sync, dynamic_loading, scale_candles_only, topbar, searchbox, toolbox):
|
||||
self._chart = parent._chart if isinstance(parent, SubChart) else parent
|
||||
super().__init__(volume_enabled, width, height, dynamic_loading, scale_candles_only, topbar, searchbox, toolbox,
|
||||
self._chart._js_api_code, _run_script=self._chart.run_script)
|
||||
super().__init__(volume_enabled, width, height, dynamic_loading, scale_candles_only, topbar, searchbox, toolbox, _run_script=self._chart.run_script)
|
||||
self._parent = parent
|
||||
self._position = position
|
||||
self._return_q = self._chart._return_q
|
||||
|
||||
@ -5,6 +5,10 @@ import webview
|
||||
from lightweight_charts.abstract import LWC
|
||||
|
||||
|
||||
chart = None
|
||||
num_charts = 0
|
||||
|
||||
|
||||
class CallbackAPI:
|
||||
def __init__(self, emit_queue, return_queue):
|
||||
self.emit_q, self.return_q = emit_queue, return_queue
|
||||
@ -17,33 +21,43 @@ class CallbackAPI:
|
||||
|
||||
|
||||
class PyWV:
|
||||
def __init__(self, q, exit, loaded, html, width, height, x, y, on_top, maximize, debug, emit_queue, return_queue):
|
||||
def __init__(self, q, start: mp.Event, exit, loaded, html, width, height, x, y, on_top, maximize, debug, emit_queue, return_queue):
|
||||
if maximize:
|
||||
width, height = webview.screens[0].width, webview.screens[0].height
|
||||
self.queue = q
|
||||
self.exit = exit
|
||||
self.loaded = loaded
|
||||
self.debug = debug
|
||||
js_api = CallbackAPI(emit_queue, return_queue)
|
||||
self.webview = webview.create_window('', html=html, on_top=on_top, js_api=js_api, width=width, height=height,
|
||||
x=x, y=y, background_color='#000000')
|
||||
self.webview.events.loaded += self.on_js_load
|
||||
self.loop()
|
||||
self.callback_api = CallbackAPI(emit_queue, return_queue)
|
||||
self.loaded: list = loaded
|
||||
|
||||
def loop(self):
|
||||
self.windows = []
|
||||
self.create_window(html, on_top, width, height, x, y)
|
||||
|
||||
start.wait()
|
||||
webview.start(debug=debug)
|
||||
self.exit.set()
|
||||
|
||||
def create_window(self, html, on_top, width, height, x, y):
|
||||
self.windows.append(webview.create_window(
|
||||
'', html=html, on_top=on_top, js_api=self.callback_api,
|
||||
width=width, height=height, x=x, y=y, background_color='#000000'))
|
||||
self.windows[-1].events.loaded += lambda: self.loop(self.loaded[len(self.windows)-1])
|
||||
|
||||
def loop(self, loaded):
|
||||
loaded.set()
|
||||
while 1:
|
||||
arg = self.queue.get()
|
||||
if arg in ('start', 'show', 'hide', 'exit'):
|
||||
webview.start(debug=self.debug) if arg == 'start' else getattr(self.webview, arg)()
|
||||
self.exit.set() if arg in ('start', 'exit') else None
|
||||
i, arg = self.queue.get()
|
||||
if i == 'create_window':
|
||||
self.create_window(*arg)
|
||||
elif arg in ('show', 'hide'):
|
||||
getattr(self.windows[i], arg)()
|
||||
elif arg == 'exit':
|
||||
self.exit.set()
|
||||
else:
|
||||
try:
|
||||
self.webview.evaluate_js(arg)
|
||||
self.windows[i].evaluate_js(arg)
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
def on_js_load(self): self.loaded.set(), self.loop()
|
||||
|
||||
|
||||
class Chart(LWC):
|
||||
def __init__(self, volume_enabled: bool = True, width: int = 800, height: int = 600, x: int = None, y: int = None,
|
||||
@ -51,14 +65,30 @@ class Chart(LWC):
|
||||
api: object = None, topbar: bool = False, searchbox: bool = False, toolbox: bool = False,
|
||||
inner_width: float = 1.0, inner_height: float = 1.0, dynamic_loading: bool = False, scale_candles_only: bool = False):
|
||||
super().__init__(volume_enabled, inner_width, inner_height, dynamic_loading, scale_candles_only, topbar, searchbox, toolbox, 'pywebview.api.callback')
|
||||
self._q, self._emit_q, self._return_q = (mp.Queue() for _ in range(3))
|
||||
self._exit, self._loaded = mp.Event(), mp.Event()
|
||||
self._script_func = self._q.put
|
||||
self._api = api
|
||||
self._process = mp.Process(target=PyWV, args=(self._q, self._exit, self._loaded, self._html,
|
||||
width, height, x, y, on_top, maximize, debug,
|
||||
self._emit_q, self._return_q), daemon=True)
|
||||
self._process.start()
|
||||
global chart, num_charts
|
||||
|
||||
if chart:
|
||||
self._q, self._exit, self._start, self._process = chart._q, chart._exit, chart._start, chart._process
|
||||
self._emit_q, self._return_q = mp.Queue(), mp.Queue()
|
||||
chart._charts[self.id] = self
|
||||
self._api = chart._api
|
||||
self._loaded = chart._loaded_list[num_charts]
|
||||
self._q.put(('create_window', (self._html, on_top, width, height, x, y)))
|
||||
else:
|
||||
self._api = api
|
||||
self._q, self._emit_q, self._return_q = (mp.Queue() for _ in range(3))
|
||||
self._loaded_list = [mp.Event() for _ in range(10)]
|
||||
self._loaded = self._loaded_list[0]
|
||||
self._exit, self._start = (mp.Event() for _ in range(2))
|
||||
self._process = mp.Process(target=PyWV, args=(self._q, self._start, self._exit, self._loaded_list, self._html,
|
||||
width, height, x, y, on_top, maximize, debug,
|
||||
self._emit_q, self._return_q), daemon=True)
|
||||
self._process.start()
|
||||
chart = self
|
||||
|
||||
self.i = num_charts
|
||||
num_charts += 1
|
||||
self._script_func = lambda s: self._q.put((self.i, s))
|
||||
|
||||
def show(self, block: bool = False):
|
||||
"""
|
||||
@ -66,11 +96,11 @@ class Chart(LWC):
|
||||
:param block: blocks execution until the chart is closed.
|
||||
"""
|
||||
if not self.loaded:
|
||||
self._q.put('start')
|
||||
self._start.set()
|
||||
self._loaded.wait()
|
||||
self._on_js_load()
|
||||
else:
|
||||
self._q.put('show')
|
||||
self._q.put((self.i, 'show'))
|
||||
if block:
|
||||
asyncio.run(self.show_async(block=True))
|
||||
|
||||
@ -88,13 +118,15 @@ class Chart(LWC):
|
||||
return
|
||||
elif not self._emit_q.empty():
|
||||
name, chart_id, arg = self._emit_q.get()
|
||||
self._api.chart = self._charts[chart_id]
|
||||
if name == 'save_drawings':
|
||||
self._api.chart.toolbox._save_drawings(arg)
|
||||
continue
|
||||
fixed_callbacks = ('on_search', 'on_horizontal_line_move')
|
||||
func = self._methods[name] if name not in fixed_callbacks else getattr(self._api, name)
|
||||
if hasattr(self._api.chart, 'topbar') and (widget := self._api.chart.topbar._widget_with_method(name)):
|
||||
if self._api:
|
||||
self._api.chart = self._charts[chart_id]
|
||||
if self._api and name == 'save_drawings':
|
||||
func = self._api.chart.toolbox._save_drawings
|
||||
elif name in ('on_search', 'on_horizontal_line_move'):
|
||||
func = getattr(self._api, name)
|
||||
else:
|
||||
func = self._methods[name]
|
||||
if self._api and hasattr(self._api.chart, 'topbar') and (widget := self._api.chart.topbar._widget_with_method(name)):
|
||||
widget.value = arg
|
||||
await func() if asyncio.iscoroutinefunction(func) else func()
|
||||
else:
|
||||
@ -106,18 +138,22 @@ class Chart(LWC):
|
||||
except KeyboardInterrupt:
|
||||
return
|
||||
|
||||
|
||||
def hide(self):
|
||||
"""
|
||||
Hides the chart window.\n
|
||||
"""
|
||||
self._q.put('hide')
|
||||
self._q.put((self.i, 'hide'))
|
||||
|
||||
def exit(self):
|
||||
"""
|
||||
Exits and destroys the chart window.\n
|
||||
"""
|
||||
self._q.put('exit')
|
||||
self._exit.wait()
|
||||
if not self.loaded:
|
||||
global num_charts, chart
|
||||
chart = None
|
||||
num_charts = 0
|
||||
else:
|
||||
self._q.put((self.i, 'exit'))
|
||||
self._exit.wait()
|
||||
self._process.terminate()
|
||||
del self
|
||||
|
||||
@ -11,7 +11,6 @@ function makeSearchBox(chart) {
|
||||
searchWindow.style.padding = '5px'
|
||||
searchWindow.style.zIndex = '1000'
|
||||
searchWindow.style.alignItems = 'center'
|
||||
searchWindow.style.alignItems = 'center'
|
||||
searchWindow.style.backgroundColor = 'rgba(30, 30, 30, 0.9)'
|
||||
searchWindow.style.border = '2px solid #3C434C'
|
||||
searchWindow.style.borderRadius = '5px'
|
||||
@ -60,7 +59,7 @@ function makeSearchBox(chart) {
|
||||
else return false
|
||||
}
|
||||
else if (event.key === 'Enter') {
|
||||
chart.callbackFunction(`on_search_~_${chart.id}_~_${sBox.value}`)
|
||||
window.callbackFunction(`on_search_~_${chart.id}_~_${sBox.value}`)
|
||||
searchWindow.style.display = 'none'
|
||||
sBox.value = ''
|
||||
return true
|
||||
@ -145,7 +144,7 @@ function makeSwitcher(chart, items, activeItem, callbackName, activeBackgroundCo
|
||||
element.style.color = items[index] === item ? 'activeColor' : inactiveColor
|
||||
});
|
||||
activeItem = item;
|
||||
chart.callbackFunction(`${callbackName}_~_${chart.id}_~_${item}`);
|
||||
window.callbackFunction(`${callbackName}_~_${chart.id}_~_${item}`);
|
||||
}
|
||||
chart.topBar.appendChild(switcherElement)
|
||||
makeSeperator(chart.topBar)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
function makeChart(callbackFunction, innerWidth, innerHeight, autoSize=true) {
|
||||
function makeChart(innerWidth, innerHeight, autoSize=true) {
|
||||
let chart = {
|
||||
markers: [],
|
||||
horizontal_lines: [],
|
||||
@ -9,7 +9,6 @@ function makeChart(callbackFunction, innerWidth, innerHeight, autoSize=true) {
|
||||
width: innerWidth,
|
||||
height: innerHeight,
|
||||
},
|
||||
callbackFunction: callbackFunction,
|
||||
candleData: [],
|
||||
commandFunctions: []
|
||||
}
|
||||
@ -64,6 +63,7 @@ function makeChart(callbackFunction, innerWidth, innerHeight, autoSize=true) {
|
||||
chart.wrapper.style.height = `${100*innerHeight}%`
|
||||
chart.wrapper.style.display = 'flex'
|
||||
chart.wrapper.style.flexDirection = 'column'
|
||||
chart.wrapper.style.position = 'relative'
|
||||
|
||||
chart.div.style.position = 'relative'
|
||||
chart.div.style.display = 'flex'
|
||||
@ -119,6 +119,12 @@ if (!window.HorizontalLine) {
|
||||
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)
|
||||
}
|
||||
|
||||
deleteLine() {
|
||||
this.chart.series.removePriceLine(this.line)
|
||||
this.chart.horizontal_lines.splice(this.chart.horizontal_lines.indexOf(this))
|
||||
|
||||
139
lightweight_charts/js/table.js
Normal file
139
lightweight_charts/js/table.js
Normal file
@ -0,0 +1,139 @@
|
||||
if (!window.Table) {
|
||||
class Table {
|
||||
constructor(width, height, headings, widths, alignments, position, draggable = false, pythonMethod, chart) {
|
||||
this.container = document.createElement('div')
|
||||
this.pythonMethod = pythonMethod
|
||||
this.chart = chart
|
||||
|
||||
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.container.style.width = width <= 1 ? width * 100 + '%' : width + 'px'
|
||||
this.container.style.height = height <= 1 ? height * 100 + '%' : height + 'px'
|
||||
this.container.style.display = 'flex'
|
||||
this.container.style.flexDirection = 'column'
|
||||
this.container.style.justifyContent = 'space-between'
|
||||
|
||||
this.container.style.backgroundColor = 'rgb(45, 45, 45)'
|
||||
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.table.style.border = '1px solid rgb(70, 70, 70)';
|
||||
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.textAlign = 'center'
|
||||
row.appendChild(th)
|
||||
th.style.border = '1px solid rgb(70, 70, 70)'
|
||||
}
|
||||
|
||||
this.container.appendChild(this.table)
|
||||
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);
|
||||
|
||||
|
||||
}
|
||||
|
||||
newRow(vals, id) {
|
||||
let row = this.table.insertRow()
|
||||
row.style.cursor = 'default'
|
||||
|
||||
for (let i = 0; i < vals.length; i++) {
|
||||
row[this.headings[i]] = row.insertCell()
|
||||
row[this.headings[i]].textContent = vals[i]
|
||||
row[this.headings[i]].style.width = this.widths[i];
|
||||
row[this.headings[i]].style.textAlign = this.alignments[i];
|
||||
row[this.headings[i]].style.border = '1px solid rgb(70, 70, 70)'
|
||||
|
||||
}
|
||||
row.addEventListener('mouseover', () => row.style.backgroundColor = 'rgba(60, 60, 60, 0.6)')
|
||||
row.addEventListener('mouseout', () => row.style.backgroundColor = 'transparent')
|
||||
row.addEventListener('mousedown', () => {
|
||||
row.style.backgroundColor = 'rgba(60, 60, 60)'
|
||||
window.callbackFunction(`${this.pythonMethod}_~_${this.chart.id}_~_${id}`)
|
||||
})
|
||||
row.addEventListener('mouseup', () => row.style.backgroundColor = 'rgba(60, 60, 60, 0.6)')
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
makeFooter(numBoxes) {
|
||||
let footer = document.createElement('div')
|
||||
footer.style.display = 'flex'
|
||||
footer.style.width = '100%'
|
||||
footer.style.padding = '3px 0px'
|
||||
footer.style.backgroundColor = 'rgb(30, 30, 30)'
|
||||
this.container.appendChild(footer)
|
||||
|
||||
this.footer = []
|
||||
for (let i = 0; i < numBoxes; i++) {
|
||||
this.footer.push(document.createElement('div'))
|
||||
footer.appendChild(this.footer[i])
|
||||
this.footer[i].style.flex = '1'
|
||||
this.footer[i].style.textAlign = 'center'
|
||||
}
|
||||
}
|
||||
}
|
||||
window.Table = Table
|
||||
}
|
||||
@ -184,12 +184,12 @@ if (!window.ToolBox) {
|
||||
trendLine.line.setData(data)
|
||||
|
||||
if (logical) {
|
||||
this.chart.chart.applyOptions({handleScroll: true})
|
||||
this.chart.chart.applyOptions({handleScroll: false})
|
||||
setTimeout(() => {
|
||||
this.chart.chart.timeScale().setVisibleLogicalRange(logical)
|
||||
}, 1)
|
||||
setTimeout(() => {
|
||||
this.chart.chart.applyOptions({handleScroll: false})
|
||||
this.chart.chart.applyOptions({handleScroll: true})
|
||||
}, 50)
|
||||
}
|
||||
if (!ray) {
|
||||
@ -309,6 +309,10 @@ if (!window.ToolBox) {
|
||||
document.body.style.cursor = this.chart.cursor
|
||||
hoveringOver = null
|
||||
contextMenu.listen(false)
|
||||
if (!mouseDown) {
|
||||
document.removeEventListener('mousedown', checkForClick)
|
||||
document.removeEventListener('mouseup', checkForRelease)
|
||||
}
|
||||
}
|
||||
})
|
||||
this.chart.chart.subscribeCrosshairMove(hoverOver)
|
||||
@ -327,8 +331,6 @@ if (!window.ToolBox) {
|
||||
|
||||
this.chart.chart.unsubscribeCrosshairMove(hoverOver)
|
||||
|
||||
// let [x, y] = [event.clientX, event.clientY]
|
||||
// if ('topBar' in this.chart) y = y - this.chart.topBar.offsetHeight
|
||||
if ('price' in hoveringOver) {
|
||||
originalPrice = hoveringOver.price
|
||||
this.chart.chart.subscribeCrosshairMove(crosshairHandlerHorz)
|
||||
@ -352,7 +354,7 @@ if (!window.ToolBox) {
|
||||
|
||||
this.chart.chart.applyOptions({handleScroll: true})
|
||||
if (hoveringOver && 'price' in hoveringOver && hoveringOver.id !== 'toolBox') {
|
||||
this.chart.callbackFunction(`on_horizontal_line_move_~_${this.chart.id}_~_${hoveringOver.id};;;${hoveringOver.price.toFixed(8)}`);
|
||||
window.callbackFunction(`on_horizontal_line_move_~_${this.chart.id}_~_${hoveringOver.id};;;${hoveringOver.price.toFixed(8)}`);
|
||||
}
|
||||
hoveringOver = null
|
||||
document.removeEventListener('mousedown', checkForClick)
|
||||
@ -472,6 +474,8 @@ if (!window.ToolBox) {
|
||||
let startDate = dateToChartTime(new Date(Math.round(chartTimeToDate(item.from[0]).getTime() / this.interval) * this.interval), this.interval)
|
||||
let endDate = dateToChartTime(new Date(Math.round(chartTimeToDate(item.to[0]).getTime() / this.interval) * this.interval), this.interval)
|
||||
let data = calculateTrendLine(startDate, item.from[1], endDate, item.to[1], this.interval, this.chart, item.ray)
|
||||
item.from = [data[0].time, data[0].value]
|
||||
item.to = [data[data.length - 1].time, data[data.length-1].value]
|
||||
item.line.setData(data)
|
||||
})
|
||||
//this.chart.chart.timeScale().setVisibleLogicalRange(logical)
|
||||
@ -508,7 +512,7 @@ if (!window.ToolBox) {
|
||||
}
|
||||
return value;
|
||||
});
|
||||
this.chart.callbackFunction(`save_drawings_~_${this.chart.id}_~_${drawingsString}`)
|
||||
window.callbackFunction(`save_drawings_~_${this.chart.id}_~_${drawingsString}`)
|
||||
}
|
||||
|
||||
loadDrawings(drawings) {
|
||||
@ -537,6 +541,8 @@ if (!window.ToolBox) {
|
||||
let startDate = dateToChartTime(new Date(Math.round(chartTimeToDate(item.from[0]).getTime() / this.interval) * this.interval), this.interval)
|
||||
let endDate = dateToChartTime(new Date(Math.round(chartTimeToDate(item.to[0]).getTime() / this.interval) * this.interval), this.interval)
|
||||
let data = calculateTrendLine(startDate, item.from[1], endDate, item.to[1], this.interval, this.chart, item.ray)
|
||||
item.from = [data[0].time, data[0].value]
|
||||
item.to = [data[data.length - 1].time, data[data.length-1].value]
|
||||
item.line.setData(data)
|
||||
}
|
||||
})
|
||||
@ -546,6 +552,5 @@ if (!window.ToolBox) {
|
||||
this.chart.chart.timeScale().setVisibleLogicalRange(logical)
|
||||
}
|
||||
}
|
||||
|
||||
window.ToolBox = ToolBox
|
||||
}
|
||||
77
lightweight_charts/table.py
Normal file
77
lightweight_charts/table.py
Normal file
@ -0,0 +1,77 @@
|
||||
import random
|
||||
from typing import Union
|
||||
|
||||
from lightweight_charts.util import _js_bool
|
||||
|
||||
|
||||
class Footer:
|
||||
def __init__(self, table):
|
||||
self._table = table
|
||||
self._chart = table._chart
|
||||
|
||||
def __setitem__(self, key, value): self._chart.run_script(f'{self._table.id}.footer[{key}].innerText = "{value}"')
|
||||
|
||||
def __call__(self, number_of_text_boxes): self._chart.run_script(f'{self._table.id}.makeFooter({number_of_text_boxes})')
|
||||
|
||||
|
||||
class Row(dict):
|
||||
def __init__(self, table, id, items):
|
||||
super().__init__()
|
||||
self._table = table
|
||||
self._chart = table._chart
|
||||
self.id = id
|
||||
self.meta = {}
|
||||
self._table._chart.run_script(f'''{self._table.id}.newRow({list(items.values())}, '{self.id}')''')
|
||||
for key, val in items.items():
|
||||
self[key] = val
|
||||
|
||||
def __setitem__(self, column, value):
|
||||
str_value = str(value)
|
||||
if column in self._table._formatters:
|
||||
str_value = self._table._formatters[column].replace(self._table.VALUE, str_value)
|
||||
self._chart.run_script(f'''{self._table.id}.updateCell('{self.id}', '{column}', '{str_value}')''')
|
||||
|
||||
return super().__setitem__(column, value)
|
||||
|
||||
def background_color(self, column, color):
|
||||
self._chart.run_script(f"{self._table.id}.rows[{self.id}]['{column}'].style.backgroundColor = '{color}'")
|
||||
|
||||
def delete(self):
|
||||
self._chart.run_script(f"{self._table.id}.deleteRow('{self.id}')")
|
||||
self._table.pop(self.id)
|
||||
|
||||
class Table(dict):
|
||||
VALUE = 'CELL__~__VALUE__~__PLACEHOLDER'
|
||||
|
||||
def __init__(self, chart, width, height, headings, widths=None, alignments=None, position='left', draggable=False, method=None):
|
||||
super().__init__()
|
||||
self._chart = chart
|
||||
self.headings = headings
|
||||
self._formatters = {}
|
||||
self.is_shown = True
|
||||
|
||||
self.id = self._chart._rand.generate()
|
||||
self._chart.run_script(f'''
|
||||
{self.id} = new Table({width}, {height}, {list(headings)}, {list(widths)}, {list(alignments)}, '{position}', {_js_bool(draggable)}, '{method}', {chart.id})
|
||||
''')
|
||||
self.footer = Footer(self)
|
||||
|
||||
def new_row(self, *values, id=None) -> Row:
|
||||
row_id = random.randint(0, 99_999_999) if not id else id
|
||||
self[row_id] = Row(self, row_id, {heading: item for heading, item in zip(self.headings, values)})
|
||||
return self[row_id]
|
||||
|
||||
def clear(self): self._chart.run_script(f"{self.id}.clearRows()"), super().clear()
|
||||
|
||||
def get(self, __key: Union[int, str]) -> Row: return super().get(int(__key))
|
||||
|
||||
def __getitem__(self, item): return super().__getitem__(int(item))
|
||||
|
||||
def format(self, column: str, format_str: str): self._formatters[column] = format_str
|
||||
|
||||
def visible(self, visible: bool):
|
||||
self.is_shown = visible
|
||||
self._chart.run_script(f"""
|
||||
{self.id}.container.style.display = '{'block' if visible else 'none'}'
|
||||
{self.id}.container.{'add' if visible else 'remove'}EventListener('mousedown', {self.id}.onMouseDown)
|
||||
""")
|
||||
Reference in New Issue
Block a user