New Feature: ChartAsync

- 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.
This commit is contained in:
louisnw
2023-05-29 21:31:13 +01:00
parent 39e40334d5
commit a58f1e306c
24 changed files with 19390 additions and 3143 deletions

View File

@ -1,4 +1,5 @@
from .chart import Chart
from .js import LWC
from .chartasync import ChartAsync

View File

@ -4,15 +4,25 @@ import multiprocessing as mp
from lightweight_charts.js import LWC
class CallbackAPI:
def __init__(self, emit): self.emit = emit
def callback(self, message: str):
messages = message.split('__')
name, chart_id = messages[:2]
args = messages[2:]
self.emit.put((name, chart_id, *args))
class PyWV:
def __init__(self, q, exit, loaded, html, js_api, width, height, x, y, on_top, debug):
def __init__(self, q, exit, loaded, html, width, height, x, y, on_top, debug, emit=None):
self.queue = q
self.exit = exit
self.loaded = loaded
self.debug = debug
self.js_api = js_api
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')
js_api = CallbackAPI(emit) if emit else None
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()
@ -22,9 +32,6 @@ class PyWV:
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
elif arg == 'subscribe':
func, c_id = (self.queue.get() for _ in range(2))
self.js_api.click_funcs[str(c_id)] = func
else:
try:
self.webview.evaluate_js(arg)
@ -40,12 +47,11 @@ class Chart(LWC):
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._js_api_code = 'pywebview.api.onClick'
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, self._js_api,
self._process = mp.Process(target=PyWV, args=(self._q, self._exit, self._loaded, self._html,
width, height, x, y, on_top, debug,), daemon=True)
self._process.start()
self._create_chart()
@ -83,9 +89,3 @@ class Chart(LWC):
self._process.terminate()
del self
def subscribe_click(self, function: object):
self._q.put('subscribe')
self._q.put(function)
self._q.put(self.id)
super().subscribe_click(function)

View File

@ -0,0 +1,246 @@
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)
}
'''

View File

@ -26,7 +26,7 @@ class SeriesCommon:
series['time'] = self._datetime_format(series['time'])
return series
def _datetime_format(self, arg):
def _datetime_format(self, arg: Union[pd.Series, str]):
arg = pd.to_datetime(arg)
if self._interval != timedelta(days=1):
arg = arg.astype('int64') // 10 ** 9 if isinstance(arg, pd.Series) else arg.timestamp()
@ -80,21 +80,9 @@ class SeriesCommon:
"""
Creates a horizontal line at the given price.\n
"""
var = self._rand.generate()
self.run_script(f"""
let priceLine{var} = {{
price: {price},
color: '{color}',
lineWidth: {width},
lineStyle: {_line_style(style)},
axisLabelVisible: {_js_bool(axis_label_visible)},
title: '{text}',
}};
let line{var} = {{
line: {self.id}.series.createPriceLine(priceLine{var}),
price: {price},
}};
{self.id}.horizontal_lines.push(line{var})""")
makeHorizontalLine({self.id}, {price}, '{color}', {width}, {_line_style(style)}, {_js_bool(axis_label_visible)}, '{text}')
""")
def remove_horizontal_line(self, price: Union[float, int]):
"""
@ -116,11 +104,11 @@ class Line(SeriesCommon):
def __init__(self, parent, color, width):
self._parent = parent
self._rand = self._parent._rand
self.id = self._rand.generate()
self.id = f'window.{self._rand.generate()}'
self.run_script = self._parent.run_script
self._parent.run_script(f'''
var {self.id} = {{
{self.id} = {{
series: {self._parent.id}.chart.addLineSeries({{
color: '{color}',
lineWidth: {width},
@ -149,19 +137,6 @@ class Line(SeriesCommon):
self.run_script(f'{self.id}.series.update({series.to_dict()})')
class API:
def __init__(self):
self.click_funcs = {}
def onClick(self, data):
click_func = self.click_funcs[data['id']]
if isinstance(data['time'], int):
data['time'] = datetime.fromtimestamp(data['time'])
else:
data['time'] = datetime.strptime(data['time'], '%Y-%m-%d')
click_func(data) if click_func else None
class LWC(SeriesCommon):
def __init__(self, volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0, dynamic_loading: bool = False):
self._volume_enabled = volume_enabled
@ -170,35 +145,30 @@ class LWC(SeriesCommon):
self._dynamic_loading = dynamic_loading
self._rand = IDGen()
self.id = self._rand.generate()
self.id = f'window.{self._rand.generate()}'
self._position = 'left'
self.loaded = False
self._html = HTML
self._append_js = f'document.body.append({self.id}.div)'
self._scripts = []
self._script_func = None
self._last_bar = None
self._interval = None
self._js_api = API()
self._js_api_code = None
self._background_color = '#000000'
self._volume_up_color = 'rgba(83,141,131,0.8)'
self._volume_down_color = 'rgba(200,127,130,0.8)'
def _on_js_load(self):
if self.loaded:
return
self.loaded = True
for script in self._scripts:
self.run_script(script)
[self.run_script(script) for script in self._scripts]
def _create_chart(self):
def _create_chart(self, top_bar=False):
self.run_script(f'''
{self.id} = makeChart({self._inner_width}, {self._inner_height})
{self.id}.div.style.float = "{self._position}"
{self._append_js}
window.addEventListener('resize', function() {{
{self.id}.chart.resize(window.innerWidth*{self.id}.scale.width, window.innerHeight*{self.id}.scale.height)
}});
{self.id} = makeChart({self._inner_width}, {self._inner_height}, topBar={_js_bool(top_bar)})
{self.id}.id = '{self.id}'
{self.id}.wrapper.style.float = "{self._position}"
''')
def run_script(self, script):
@ -498,11 +468,11 @@ class LWC(SeriesCommon):
const data = param.seriesData.get({self.id}.series);
if (!data) {{return}}
let percentMove = ((data.close-data.open)/data.open)*100
let ohlc = `open: ${{legendItemFormat(data.open)}}
| high: ${{legendItemFormat(data.high)}}
| low: ${{legendItemFormat(data.low)}}
| close: ${{legendItemFormat(data.close)}} `
let percent = `| daily: ${{percentMove >= 0 ? '+' : ''}}${{percentMove.toFixed(2)}} %`
let ohlc = `O ${{legendItemFormat(data.open)}}
| H ${{legendItemFormat(data.high)}}
| L ${{legendItemFormat(data.low)}}
| C ${{legendItemFormat(data.close)}} `
let percent = `| ${{percentMove >= 0 ? '+' : ''}}${{percentMove.toFixed(2)}} %`
let finalString = ''
{'finalString += ohlc' if ohlc else ''}
{'finalString += percent' if percent else ''}
@ -513,28 +483,6 @@ class LWC(SeriesCommon):
}}
}});''')
def subscribe_click(self, function: object):
"""
Subscribes the given function to a chart click event.
The event returns a dictionary containing the bar object at the time clicked, and the price at the crosshair.
"""
self._js_api.click_funcs[self.id] = function
self.run_script(f'''
{self.id}.chart.subscribeClick((param) => {{
if (!param.point) {{return}}
let prices = param.seriesData.get({self.id}.series);
let data = {{
time: param.time,
open: prices.open,
high: prices.high,
low: prices.low,
close: prices.close,
hover: {self.id}.series.coordinateToPrice(param.point.y),
id: '{self.id}'
}}
{self._js_api_code}(data)
}})''')
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):
return SubChart(self, volume_enabled, position, width, height, sync)
@ -548,9 +496,6 @@ class SubChart(LWC):
self._position = position
self._rand = self._chart._rand
self.id = f'window.{self._rand.generate()}'
self._append_js = f'{self._parent.id}.div.parentNode.insertBefore({self.id}.div, {self._parent.id}.div.nextSibling)'
self._js_api = self._chart._js_api
self._js_api_code = self._chart._js_api_code
self.run_script = self._chart.run_script
self._create_chart()
if not sync:
@ -569,22 +514,30 @@ const up = 'rgba(39, 157, 130, 100)'
const down = 'rgba(200, 97, 100, 100)'
const wrapper = document.createElement('div')
wrapper.className = 'wrapper'
document.body.appendChild(wrapper)
function makeChart(innerWidth, innerHeight) {
function makeChart(innerWidth, innerHeight, topBar=false) {
let chart = {
markers: [],
horizontal_lines: [],
div: document.createElement('div'),
wrapper: document.createElement('div'),
legend: document.createElement('div'),
scale: {
width: innerWidth,
height: innerHeight
},
}
let topBarOffset = 0
if (topBar) {
makeTopBar(chart)
topBarOffset = chart.topBar.offsetHeight
}
chart.chart = LightweightCharts.createChart(chart.div, {
width: window.innerWidth*innerWidth,
height: window.innerHeight*innerHeight,
height: (window.innerHeight*innerHeight)-topBarOffset,
layout: {
textColor: '#d1d4dc',
background: {
@ -612,6 +565,12 @@ function makeChart(innerWidth, innerHeight) {
},
handleScroll: {vertTouchDrag: true},
})
window.addEventListener('resize', function() {
if (topBar) {
topBarOffset = chart.topBar.offsetHeight
}
chart.chart.resize(window.innerWidth*innerWidth, (window.innerHeight*innerHeight)-topBarOffset)
});
chart.series = chart.chart.addCandlestickSeries({color: 'rgb(0, 120, 255)', upColor: up, borderUpColor: up, wickUpColor: up,
downColor: down, borderDownColor: down, wickDownColor: down, lineWidth: 2,
})
@ -631,9 +590,34 @@ function makeChart(innerWidth, innerHeight) {
chart.legend.style.fontFamily = 'Monaco'
chart.legend.style.fontSize = '11px'
chart.legend.style.color = 'rgb(191, 195, 203)'
chart.wrapper.style.width = `${100*innerWidth}%`
chart.wrapper.style.height = `${100*innerHeight}%`
chart.div.style.position = 'relative'
chart.wrapper.style.display = 'flex'
chart.wrapper.style.flexDirection = 'column'
chart.div.appendChild(chart.legend)
chart.wrapper.appendChild(chart.div)
wrapper.append(chart.wrapper)
return chart
}
function makeHorizontalLine(chart, price, color, width, style, axisLabelVisible, text) {
let priceLine = {
price: price,
color: color,
lineWidth: width,
lineStyle: style,
axisLabelVisible: axisLabelVisible,
title: text,
};
let line = {
line: chart.series.createPriceLine(priceLine),
price: price,
};
chart.horizontal_lines.push(line)
}
function legendItemFormat(num) {
return num.toFixed(2).toString().padStart(8, ' ')
}

View File

@ -5,49 +5,115 @@ except ImportError:
try:
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtWebChannel import QWebChannel
from PyQt5.QtCore import QObject
from PyQt5.QtCore import QObject, pyqtSlot
class Bridge(QObject):
def __init__(self, chart):
super().__init__()
self.chart = chart
@pyqtSlot(str)
def callback(self, message):
_widget_message(self.chart, message)
except ImportError:
pass
try:
from streamlit.components.v1 import html
except ImportError:
pass
from lightweight_charts.chartasync import LWCAsync, ASYNC_SCRIPT
from lightweight_charts.js import LWC
class WxChart(LWC):
def __init__(self, parent, volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0):
def _widget_message(chart, string):
messages = string.split('__')
name, chart_id = messages[:2]
args = messages[2:]
chart.api.chart = chart._charts[chart_id]
getattr(chart.api, name)(*args)
class WxChart(LWCAsync):
def __init__(self, parent, api: object = None, top_bar: bool = False, search_box: bool = False,
volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0):
try:
self.webview: wx.html2.WebView = wx.html2.WebView.New(parent)
except NameError:
raise ModuleNotFoundError('wx.html2 was not found, and must be installed to use WxChart.')
super().__init__(volume_enabled, inner_width=inner_width, inner_height=inner_height)
self.api = api
self._script_func = self.webview.RunScript
self._js_api_code = 'window.wx_msg.postMessage'
self._js_api_code = 'window.wx_msg.postMessage.bind(window.wx_msg)'
self.webview.Bind(wx.html2.EVT_WEBVIEW_LOADED, lambda e: wx.CallLater(200, self._on_js_load))
self.webview.Bind(wx.html2.EVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, lambda e: _widget_message(self, e.GetString()))
self.webview.AddScriptMessageHandler('wx_msg')
self.webview.Bind(wx.html2.EVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, lambda e: self._js_api.onClick(eval(e.GetString())))
self.webview.Bind(wx.html2.EVT_WEBVIEW_LOADED, self._on_js_load)
self.webview.SetPage(self._html, '')
self._create_chart()
def _on_js_load(self, e): super()._on_js_load()
self.webview.SetPage(self._html, '')
self.webview.AddUserScript(ASYNC_SCRIPT)
self._create_chart(top_bar)
self._make_search_box() if search_box else None
def get_webview(self): return self.webview
class QtChart(LWC):
def __init__(self, widget=None, volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0):
class QtChart(LWCAsync):
def __init__(self, widget=None, api: object = None, top_bar: bool = False, search_box: bool = False,
volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0):
try:
self.webview = QWebEngineView(widget)
except NameError:
raise ModuleNotFoundError('QWebEngineView was not found, and must be installed to use QtChart.')
super().__init__(volume_enabled, inner_width=inner_width, inner_height=inner_height)
self.api = api
self._script_func = self.webview.page().runJavaScript
self._js_api_code = 'window.pythonObject.callback'
self.web_channel = QWebChannel()
self.bridge = Bridge(self)
self.web_channel.registerObject('bridge', self.bridge)
self.webview.page().setWebChannel(self.web_channel)
self.webview.loadFinished.connect(self._on_js_load)
self._html = f'''
{self._html[: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>
{self._html[85:]}
'''
self.webview.page().setHtml(self._html)
self._create_chart()
self.run_script(ASYNC_SCRIPT)
self._create_chart(top_bar)
self._make_search_box() if search_box else None
def get_webview(self): return self.webview
class StreamlitChart(LWC):
def __init__(self, volume_enabled=True, width=None, height=None, inner_width=1, inner_height=1):
super().__init__(volume_enabled, inner_width, inner_height)
self.width = width
self.height = height
self._html = self._html.replace('</script>\n</body>\n</html>', '')
self._create_chart()
def run_script(self, script): self._html += '\n' + script
def load(self):
if self.loaded:
return
self.loaded = True
try:
html(f'{self._html}</script></body></html>', width=self.width, height=self.height)
except NameError:
raise ModuleNotFoundError('streamlit.components.v1.html was not found, and must be installed to use StreamlitChart.')