- Added the ChartAsync class, allowing for more sophisticated Charts and SubCharts. - Symbol searching, timeframe selectors, and more is now possible with this varation of Chart. `QtChart` and `WxChart` have access to all the methods that `ChartAsync` has, however they utilize their own respective event loops rather than asyncio. New Feature: `StreamlitChart` - Chart window that can display static data within a Streamlit application. Removed the `subscribe_click` method.
247 lines
9.3 KiB
Python
247 lines
9.3 KiB
Python
import asyncio
|
|
import multiprocessing as mp
|
|
from typing import Literal, Union
|
|
|
|
from lightweight_charts import LWC
|
|
from lightweight_charts.chart import PyWV
|
|
|
|
|
|
class LWCAsync(LWC):
|
|
def __init__(self, volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0, dynamic_loading: bool = False):
|
|
super().__init__(volume_enabled, inner_width, inner_height, dynamic_loading)
|
|
self._charts = {self.id: self}
|
|
|
|
def _make_search_box(self):
|
|
self.run_script(f'makeSearchBox({self.id}, {self._js_api_code})')
|
|
|
|
def corner_text(self, text: str):
|
|
self.run_script(f'{self.id}.cornerText.innerText = "{text}"')
|
|
|
|
def create_switcher(self, method, *options, default=None):
|
|
self.run_script(f'''
|
|
makeSwitcher({self.id}, {list(options)}, '{default if default else options[0]}', {self._js_api_code}, '{method.__name__}')
|
|
{self.id}.chart.resize(window.innerWidth*{self._inner_width}, (window.innerHeight*{self._inner_height})-{self.id}.topBar.offsetHeight)
|
|
''')
|
|
|
|
def create_subchart(self, top_bar: bool = True, volume_enabled: bool = True, position: Literal['left', 'right', 'top', 'bottom'] = 'left',
|
|
width: float = 0.5, height: float = 0.5, sync: Union[bool, str] = False):
|
|
subchart = SubChartAsync(self, top_bar, volume_enabled, position, width, height, sync)
|
|
self._charts[subchart.id] = subchart
|
|
return subchart
|
|
|
|
|
|
class ChartAsync(LWCAsync):
|
|
def __init__(self, api: object, top_bar: bool = True, search_box: bool = True, volume_enabled: bool = True, width: int = 800, height: int = 600, x: int = None, y: int = None,
|
|
on_top: bool = False, debug: bool = False,
|
|
inner_width: float = 1.0, inner_height: float = 1.0, dynamic_loading: bool = False):
|
|
super().__init__(volume_enabled, inner_width, inner_height, dynamic_loading)
|
|
self.api = api
|
|
|
|
self._js_api_code = 'pywebview.api.callback'
|
|
self._emit = mp.Queue()
|
|
|
|
self._q = mp.Queue()
|
|
self._script_func = self._q.put
|
|
self._exit = mp.Event()
|
|
self._loaded = mp.Event()
|
|
self._process = mp.Process(target=PyWV, args=(self._q, self._exit, self._loaded, self._html,
|
|
width, height, x, y, on_top, debug, self._emit), daemon=True)
|
|
self._process.start()
|
|
|
|
self.run_script(ASYNC_SCRIPT)
|
|
self._create_chart(top_bar)
|
|
self._make_search_box() if search_box else None
|
|
|
|
async def show(self, block=False):
|
|
if not self.loaded:
|
|
self._q.put('start')
|
|
self._loaded.wait()
|
|
self._on_js_load()
|
|
else:
|
|
self._q.put('show')
|
|
if block:
|
|
try:
|
|
while 1:
|
|
while self._emit.empty() and not self._exit.is_set():
|
|
await asyncio.sleep(0.1)
|
|
if self._exit.is_set():
|
|
return
|
|
key, chart_id, args = self._emit.get()
|
|
self.api.chart = self._charts[chart_id]
|
|
await getattr(self.api, key)(args)
|
|
|
|
except KeyboardInterrupt:
|
|
return
|
|
asyncio.create_task(self.show(block=True))
|
|
|
|
|
|
class SubChartAsync(LWCAsync):
|
|
def __init__(self, parent, top_bar, volume_enabled, position, width, height, sync):
|
|
super().__init__(volume_enabled, width, height)
|
|
self._chart = parent._chart if isinstance(parent, SubChartAsync) else parent
|
|
self._parent = parent
|
|
self._position = position
|
|
self._rand = self._chart._rand
|
|
self.id = f'window.{self._rand.generate()}'
|
|
self._js_api_code = self._chart._js_api_code
|
|
self.run_script = self._chart.run_script
|
|
self._charts = self._chart._charts
|
|
self._create_chart(top_bar)
|
|
self._make_search_box()
|
|
if not sync:
|
|
return
|
|
sync_parent_var = self._parent.id if isinstance(sync, bool) else sync
|
|
self.run_script(f'''
|
|
{sync_parent_var}.chart.timeScale().subscribeVisibleLogicalRangeChange((timeRange) => {{
|
|
{self.id}.chart.timeScale().setVisibleLogicalRange(timeRange)
|
|
}});
|
|
''')
|
|
|
|
|
|
ASYNC_SCRIPT = '''
|
|
function makeSearchBox(chart, callbackFunction) {
|
|
let searchWindow = document.createElement('div')
|
|
searchWindow.style.position = 'absolute'
|
|
searchWindow.style.top = '30%'
|
|
searchWindow.style.left = '50%'
|
|
searchWindow.style.transform = 'translate(-50%, -30%)'
|
|
searchWindow.style.width = '200px'
|
|
searchWindow.style.height = '200px'
|
|
searchWindow.style.backgroundColor = 'rgba(30, 30, 30, 0.9)'
|
|
searchWindow.style.zIndex = '1000'
|
|
searchWindow.style.display = 'none'
|
|
searchWindow.style.borderRadius = '10px'
|
|
|
|
let sBox = document.createElement('input');
|
|
sBox.type = 'text';
|
|
sBox.placeholder = 'search';
|
|
sBox.style.position = 'absolute';
|
|
sBox.style.zIndex = '1000';
|
|
|
|
sBox.style.textAlign = 'center'
|
|
sBox.style.left = '50%';
|
|
sBox.style.top = '30%';
|
|
sBox.style.transform = 'translate(-50%, -30%)'
|
|
sBox.style.width = '100px'
|
|
|
|
sBox.style.backgroundColor = 'rgba(0, 122, 255, 0.2)'
|
|
sBox.style.color = 'white'
|
|
sBox.style.fontSize = '20px'
|
|
sBox.style.border = 'none'
|
|
sBox.style.borderRadius = '5px'
|
|
|
|
searchWindow.appendChild(sBox)
|
|
chart.div.appendChild(searchWindow);
|
|
|
|
let yPrice = null
|
|
chart.chart.subscribeCrosshairMove((param) => {
|
|
if (param.point){
|
|
yPrice = param.point.y;
|
|
}
|
|
});
|
|
|
|
let selectedChart = false
|
|
chart.wrapper.addEventListener('mouseover', (event) => {
|
|
selectedChart = true
|
|
})
|
|
chart.wrapper.addEventListener('mouseout', (event) => {
|
|
selectedChart = false
|
|
})
|
|
document.addEventListener('keydown', function(event) {
|
|
if (!selectedChart) {return}
|
|
if (event.altKey && event.code === 'KeyH') {
|
|
let price = chart.series.coordinateToPrice(yPrice)
|
|
makeHorizontalLine(chart, price, '#FFFFFF', 1, LightweightCharts.LineStyle.Solid, true, '')
|
|
}
|
|
if (searchWindow.style.display === 'none') {
|
|
if (/^[a-zA-Z0-9]$/.test(event.key)) {
|
|
searchWindow.style.display = 'block';
|
|
sBox.focus();
|
|
}
|
|
}
|
|
else if (event.key === 'Enter') {
|
|
callbackFunction(`on_search__${chart.id}__${sBox.value}`)
|
|
searchWindow.style.display = 'none'
|
|
sBox.value = ''
|
|
}
|
|
else if (event.key === 'Escape') {
|
|
searchWindow.style.display = 'none'
|
|
sBox.value = ''
|
|
}
|
|
});
|
|
sBox.addEventListener('input', function() {
|
|
sBox.value = sBox.value.toUpperCase();
|
|
});
|
|
}
|
|
|
|
function makeSwitcher(chart, items, activeItem, callbackFunction, callbackName) {
|
|
let switcherElement = document.createElement('div');
|
|
switcherElement.style.margin = '4px 18px'
|
|
switcherElement.style.zIndex = '1000'
|
|
|
|
let intervalElements = items.map(function(item) {
|
|
let itemEl = document.createElement('button');
|
|
itemEl.style.cursor = 'pointer'
|
|
itemEl.style.padding = '3px 6px'
|
|
itemEl.style.margin = '0px 4px'
|
|
itemEl.style.fontSize = '14px'
|
|
itemEl.style.color = 'lightgrey'
|
|
itemEl.style.backgroundColor = item === activeItem ? 'rgba(0, 122, 255, 0.7)' : 'transparent'
|
|
|
|
|
|
itemEl.style.border = 'none'
|
|
itemEl.style.borderRadius = '4px'
|
|
itemEl.addEventListener('mouseenter', function() {
|
|
itemEl.style.backgroundColor = item === activeItem ? 'rgba(0, 122, 255, 0.7)' : 'rgb(19, 40, 84)'
|
|
})
|
|
itemEl.addEventListener('mouseleave', function() {
|
|
itemEl.style.backgroundColor = item === activeItem ? 'rgba(0, 122, 255, 0.7)' : 'transparent'
|
|
})
|
|
itemEl.innerText = item;
|
|
itemEl.addEventListener('click', function() {
|
|
onItemClicked(item);
|
|
});
|
|
switcherElement.appendChild(itemEl);
|
|
return itemEl;
|
|
});
|
|
function onItemClicked(item) {
|
|
if (item === activeItem) {
|
|
return;
|
|
}
|
|
intervalElements.forEach(function(element, index) {
|
|
element.style.backgroundColor = items[index] === item ? 'rgba(0, 122, 255, 0.7)' : 'transparent'
|
|
});
|
|
activeItem = item;
|
|
callbackFunction(`${callbackName}__${chart.id}__${item}`);
|
|
}
|
|
chart.topBar.appendChild(switcherElement)
|
|
makeSeperator(chart.topBar)
|
|
return switcherElement;
|
|
}
|
|
function makeTopBar(chart) {
|
|
chart.topBar = document.createElement('div')
|
|
chart.topBar.style.backgroundColor = '#191B1E'
|
|
chart.topBar.style.borderBottom = '3px solid #3C434C'
|
|
chart.topBar.style.borderRadius = '2px'
|
|
chart.topBar.style.display = 'flex'
|
|
chart.topBar.style.alignItems = 'center'
|
|
chart.wrapper.prepend(chart.topBar)
|
|
|
|
chart.cornerText = document.createElement('div')
|
|
chart.cornerText.style.margin = '0px 18px'
|
|
chart.cornerText.style.position = 'relative'
|
|
chart.cornerText.style.fontFamily = 'SF Pro'
|
|
chart.cornerText.style.color = 'lightgrey'
|
|
chart.topBar.appendChild(chart.cornerText)
|
|
|
|
makeSeperator(chart.topBar)
|
|
}
|
|
function makeSeperator(topBar) {
|
|
let seperator = document.createElement('div')
|
|
seperator.style.width = '1px'
|
|
seperator.style.height = '20px'
|
|
seperator.style.backgroundColor = '#3C434C'
|
|
topBar.appendChild(seperator)
|
|
}
|
|
'''
|