Skip to content

Commit e5c26d4

Browse files
committed
Umprove support of big datasets
1 parent 8e6488a commit e5c26d4

File tree

5 files changed

+133
-7
lines changed

5 files changed

+133
-7
lines changed

bn_lightweight_charts/abstract.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from .util import (
1414
BulkRunScript, Pane, Events, IDGen, as_enum, jbool, js_json, TIME, NUM, FLOAT,
1515
LINE_STYLE, MARKER_POSITION, MARKER_SHAPE, CROSSHAIR_MODE,
16-
PRICE_SCALE_MODE, marker_position, marker_shape, js_data,
16+
PRICE_SCALE_MODE, marker_position, marker_shape, js_data, js_zipdata
1717
)
1818

1919
current_dir = os.path.dirname(os.path.abspath(__file__))
@@ -56,7 +56,7 @@ def on_js_load(self):
5656
self.scripts.extend(self.final_scripts)
5757
for script in self.scripts:
5858
initial_script += f'\n{script}'
59-
self.script_func(initial_script)
59+
self.script_func(f'(async ()=> {{ {initial_script} }})();')
6060

6161
def run_script(self, script: str, run_last: bool = False):
6262
"""
@@ -217,6 +217,21 @@ def _single_datetime_format(self, arg) -> float:
217217
arg = self._interval * (arg.timestamp() // self._interval)+self.offset
218218
return arg
219219

220+
def set_zipped(self, df: Optional[pd.DataFrame] = None, format_cols: bool = True):
221+
if df is None or df.empty:
222+
self.run_script(f'{self.id}.series.setData([])')
223+
self.data = pd.DataFrame()
224+
return
225+
if format_cols:
226+
df = self._df_datetime_format(df, exclude_lowercase=self.name)
227+
if self.name:
228+
if self.name not in df:
229+
raise NameError(f'No column named "{self.name}".')
230+
df = df.rename(columns={self.name: 'value'})
231+
self.data = df.copy()
232+
self._last_bar = df.iloc[-1]
233+
self.run_script(f'{self.id}.series.setData(await decodeGzJSON("{js_zipdata(df)}")); ')
234+
220235
def set(self, df: Optional[pd.DataFrame] = None, format_cols: bool = True):
221236
if df is None or df.empty:
222237
self.run_script(f'{self.id}.series.setData([])')
@@ -594,6 +609,47 @@ def set(self, df: Optional[pd.DataFrame] = None, keep_drawings=False):
594609
else:
595610
self.run_script(f"{self._chart.id}.toolBox?.clearDrawings()")
596611

612+
def set_zipped(self, df: Optional[pd.DataFrame] = None, keep_drawings=False):
613+
"""
614+
Sets the initial data for the chart.\n
615+
:param df: columns: date/time, open, high, low, close, volume (if volume enabled).
616+
:param keep_drawings: keeps any drawings made through the toolbox. Otherwise, they will be deleted.
617+
"""
618+
if df is None or df.empty:
619+
self.run_script(f'{self.id}.series.setData([])')
620+
self.run_script(f'{self.id}.volumeSeries.setData([])')
621+
self.candle_data = pd.DataFrame()
622+
return
623+
df = self._df_datetime_format(df)
624+
df_copy = df.copy()
625+
self.candle_data = df_copy[['time', 'open', 'high', 'low', 'close']]
626+
self._last_bar = df.iloc[-1]
627+
candle_js_data = js_zipdata(df[['time', 'open', 'high', 'low', 'close']])
628+
self.run_script(f'{self.id}.series.setData(await decodeGzJSON("{candle_js_data}"))')
629+
630+
if 'volume' not in df:
631+
return
632+
volume = df[['time', 'volume']].rename(columns={'volume': 'value'})
633+
volume['color'] = self._volume_down_color
634+
volume.loc[df['close'] > df['open'], 'color'] = self._volume_up_color
635+
volume_js_data = js_zipdata(volume)
636+
self.run_script(f'{self.id}.volumeSeries.setData(await decodeGzJSON("{volume_js_data}"))')
637+
638+
for line in self._lines:
639+
if line.name not in df.columns:
640+
continue
641+
line.set(df[['time', line.name]], format_cols=False)
642+
# set autoScale to true in case the user has dragged the price scale
643+
self.run_script(f'''
644+
if (!{self.id}.chart.priceScale("right").options.autoScale)
645+
{self.id}.chart.priceScale("right").applyOptions({{autoScale: true}})
646+
''')
647+
# TODO keep drawings doesn't work consistenly w
648+
if keep_drawings:
649+
self.run_script(f'{self._chart.id}.toolBox?._drawingTool.repositionOnTime()')
650+
else:
651+
self.run_script(f"{self._chart.id}.toolBox?.clearDrawings()")
652+
597653
def update(self, series: pd.Series, _from_tick=False):
598654
"""
599655
Updates the data from a bar;

bn_lightweight_charts/js/index.html

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,34 @@
2020
<body>
2121
<div id="container"></div>
2222
<script src="./bundle.js"></script>
23+
<script>
24+
async function decodeGzipBase64(b64) {
25+
// base64 -> Uint8Array
26+
const bytes = Uint8Array.from(
27+
atob(b64),
28+
c => c.charCodeAt(0)
29+
);
30+
31+
// gunzip
32+
const stream = new DecompressionStream("gzip");
33+
const decompressedStream = new Blob([bytes])
34+
.stream()
35+
.pipeThrough(stream);
36+
37+
// Uint8Array -> string
38+
const decompressed = await new Response(decompressedStream).arrayBuffer();
39+
return new TextDecoder("utf-8").decode(decompressed);
40+
}
41+
42+
async function decodeGzJSON(b64) {
43+
let s = await decodeGzipBase64(b64)
44+
try {
45+
return JSON.parse(s)
46+
} catch {
47+
return []
48+
}
49+
}
50+
</script>
2351
</body>
2452
</html>
2553

bn_lightweight_charts/js/index_bn.html

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,34 @@ <h6>Performance Report for [ <span id="strategy-title"></span> ] </h6>
138138
</div>
139139

140140
<script src="./bundle.js"></script>
141+
<script>
142+
async function decodeGzipBase64(b64) {
143+
// base64 -> Uint8Array
144+
const bytes = Uint8Array.from(
145+
atob(b64),
146+
c => c.charCodeAt(0)
147+
);
148+
149+
// gunzip
150+
const stream = new DecompressionStream("gzip");
151+
const decompressedStream = new Blob([bytes])
152+
.stream()
153+
.pipeThrough(stream);
154+
155+
// Uint8Array -> string
156+
const decompressed = await new Response(decompressedStream).arrayBuffer();
157+
return new TextDecoder("utf-8").decode(decompressed);
158+
}
159+
160+
async function decodeGzJSON(b64) {
161+
let s = await decodeGzipBase64(b64)
162+
try {
163+
return JSON.parse(s)
164+
} catch {
165+
return []
166+
}
167+
}
168+
</script>
141169
</body>
142170
</html>
143171

bn_lightweight_charts/util.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import asyncio
22
import json
3+
import gzip
4+
import base64
35
from datetime import datetime
46
from zoneinfo import ZoneInfo
57
from tzlocal import get_localzone_name
@@ -79,6 +81,18 @@ def js_data(data: Union[pd.DataFrame, pd.Series]):
7981
return json.dumps(filtered_records)
8082

8183

84+
def js_zipdata(data: Union[pd.DataFrame, pd.Series]):
85+
if isinstance(data, pd.DataFrame):
86+
d = data.to_dict(orient='records')
87+
filtered_records = [{k: v for k, v in record.items() if v is not None and not pd.isna(v)} for record in d]
88+
else:
89+
d = data.to_dict()
90+
filtered_records = {k: v for k, v in d.items()}
91+
raw = json.dumps(filtered_records, ensure_ascii=False).encode("utf-8")
92+
compressed = gzip.compress(raw)
93+
return base64.b64encode(compressed).decode("ascii")
94+
95+
8296
def snake_to_camel(s: str):
8397
components = s.split('_')
8498
return components[0] + ''.join(x.title() for x in components[1:])

bn_lightweight_charts/widgets.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ def __init__(self, width=None, height=None, inner_width=1, inner_height=1, scale
177177
def _load(self):
178178
if sthtml is None:
179179
raise ModuleNotFoundError('streamlit.components.v1.html was not found, and must be installed to use StreamlitChart.')
180-
sthtml(f'{self._html_init} {self._html}</script></body></html>', width=self.width, height=self.height)
180+
sthtml(f'{self._html_init} (async ()=> {{\n{self._html}\n}})();\n </script></body></html>', width=self.width, height=self.height)
181181

182182

183183
class JupyterChart(StaticLWC):
@@ -188,7 +188,7 @@ def __init__(self, width: int = 800, height=350, inner_width=1, inner_height=1,
188188
def _load(self):
189189
if HTML is None:
190190
raise ModuleNotFoundError('IPython.display.HTML was not found, and must be installed to use JupyterChart.')
191-
html_code = html.escape(f"{self._html_init} {self._html}</script></body></html>")
191+
html_code = html.escape(f"{self._html_init} (async ()=> {{\n{self._html}\n}})();\n </script></body></html>")
192192
iframe = f'<iframe width="{self.width}" height="{self.height}" frameBorder="0" srcdoc="{html_code}"></iframe>'
193193
display(HTML(iframe))
194194

@@ -200,13 +200,13 @@ def __init__(self, width: int = 800, height=350, inner_width=1, inner_height=1,
200200
self.filename = filename
201201

202202
def _load(self):
203-
html_code = f"{self._html_init} {self._html}</script></body></html>"
203+
html_code = f"{self._html_init} (async ()=> {{\n {self._html}\n}})();\n </script></body></html>"
204204
with open(self.filename, 'w') as file:
205205
file.write(html_code)
206206

207207

208208
class HTMLChart_BN(StaticLWC):
209-
def __init__(self, width: int = 800, height=350, inner_width=1, inner_height=1,
209+
def __init__(self, width: int = 800, height=350, inner_width=1, inner_height=1,
210210
scale_candles_only: bool = False, toolbox: bool = False, filename = "bn_charts.html"):
211211
super().__init__(width=width, height=height, inner_width=inner_width, inner_height=inner_height,
212212
scale_candles_only=scale_candles_only, toolbox=toolbox, autosize=True,
@@ -237,7 +237,7 @@ def _prepare_html(self):
237237
const perf_metrics = {json.dumps(self.performance)};
238238
const strategy_titles = {json.dumps(self.strat_titles)};
239239
240-
function updateChart(id){{
240+
async function updateChart(id){{
241241
document.querySelector('#nav-home-tab')?.click();
242242
{func_code}
243243
}}

0 commit comments

Comments
 (0)