bn-lightweight-charts 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,470 @@
1
+ import asyncio
2
+ import logging
3
+ import datetime as dt
4
+ import re
5
+ import json
6
+ import urllib.request
7
+ from typing import Literal, Union, List
8
+ import pandas as pd
9
+
10
+ from .chart import Chart
11
+
12
+ try:
13
+ import websockets
14
+ except ImportError:
15
+ websockets = None
16
+
17
+ SEC_TYPE = Literal['stocks', 'options', 'indices', 'forex', 'crypto']
18
+
19
+ ch = logging.StreamHandler()
20
+ ch.setFormatter(logging.Formatter('%(asctime)s | [polygon.io] %(levelname)s: %(message)s', datefmt='%H:%M:%S'))
21
+ ch.setLevel(logging.DEBUG)
22
+ _log = logging.getLogger('polygon')
23
+ _log.setLevel(logging.ERROR)
24
+ _log.addHandler(ch)
25
+
26
+ api_key = ''
27
+ _tickers = {}
28
+ _set_on_load = []
29
+
30
+ _lasts = {}
31
+ _ws = {'stocks': None, 'options': None, 'indices': None, 'crypto': None, 'forex': None}
32
+ _subscription_type = {
33
+ 'stocks': ('Q', 'A'),
34
+ 'options': ('Q', 'A'),
35
+ 'indices': ('V', None),
36
+ 'forex': ('C', 'CA'),
37
+ 'crypto': ('XQ', 'XA'),
38
+ }
39
+
40
+
41
+ def _convert_timeframe(timeframe):
42
+ spans = {
43
+ 'min': 'minute',
44
+ 'H': 'hour',
45
+ 'D': 'day',
46
+ 'W': 'week',
47
+ 'M': 'month',
48
+ }
49
+ try:
50
+ multiplier = re.findall(r'\d+', timeframe)[0]
51
+ except IndexError:
52
+ return 1, spans[timeframe]
53
+ timespan = spans[timeframe.replace(multiplier, '')]
54
+ return multiplier, timespan
55
+
56
+
57
+ def _get_sec_type(ticker):
58
+ if '/' in ticker:
59
+ return 'forex'
60
+ for prefix, security_type in zip(('O:', 'I:', 'C:', 'X:'), ('options', 'indices', 'forex', 'crypto')):
61
+ if ticker.startswith(prefix):
62
+ return security_type
63
+ else:
64
+ return 'stocks'
65
+
66
+
67
+ def _polygon_request(query_url):
68
+ query_url = 'https://api.polygon.io'+query_url
69
+ query_url += f'&apiKey={api_key}'
70
+
71
+ request = urllib.request.Request(query_url, headers={'User-Agent': 'lightweight_charts/1.0'})
72
+ with urllib.request.urlopen(request) as response:
73
+ if response.status != 200:
74
+ error = response.json()
75
+ _log.error(f'({response.status}) Request failed: {error["error"]}')
76
+ return
77
+ data = json.loads(response.read())
78
+ if 'results' not in data:
79
+ _log.error(f'No results for {query_url}')
80
+ return
81
+ return data['results']
82
+
83
+
84
+ def get_bar_data(ticker: str, timeframe: str, start_date: str, end_date: str, limit: int = 5_000):
85
+ end_date = dt.datetime.now().strftime('%Y-%m-%d') if end_date == 'now' else end_date
86
+ mult, span = _convert_timeframe(timeframe)
87
+ if '-' in ticker:
88
+ ticker = ticker.replace('-', '')
89
+
90
+ query_url = f"/v2/aggs/ticker/{ticker}/range/{mult}/{span}/{start_date}/{end_date}?limit={limit}"
91
+ results = _polygon_request(query_url)
92
+ if not results:
93
+ return None
94
+
95
+ df = pd.DataFrame(results)
96
+ df['t'] = pd.to_datetime(df['t'], unit='ms')
97
+
98
+ rename = {'o': 'open', 'h': 'high', 'l': 'low', 'c': 'close', 't': 'time'}
99
+ if not ticker.startswith('I:'):
100
+ rename['v'] = 'volume'
101
+
102
+ return df[rename.keys()].rename(columns=rename)
103
+
104
+
105
+ async def async_get_bar_data(ticker: str, timeframe: str, start_date: str, end_date: str, limit: int = 5_000):
106
+ loop = asyncio.get_running_loop()
107
+ return await loop.run_in_executor(None, get_bar_data, ticker, timeframe, start_date, end_date, limit)
108
+
109
+
110
+ async def _send(sec_type: SEC_TYPE, action: str, params: str):
111
+ ws = _ws[sec_type]
112
+ while ws is None:
113
+ await asyncio.sleep(0.05)
114
+ ws = _ws[sec_type]
115
+ await ws.send(json.dumps({'action': action, 'params': params}))
116
+
117
+
118
+ async def subscribe(ticker: str, sec_type: SEC_TYPE, func, args, precision=2):
119
+ if not _ws[sec_type]:
120
+ asyncio.create_task(_websocket_connect(sec_type))
121
+
122
+ if sec_type in ('forex', 'crypto'):
123
+ key = ticker[ticker.index(':')+1:]
124
+ key = key.replace('-', '/') if sec_type == 'forex' else key
125
+ else:
126
+ key = ticker
127
+
128
+ if not _lasts.get(key):
129
+ _lasts[key] = {
130
+ 'price': 0,
131
+ 'funcs': [],
132
+ 'precision': precision
133
+ }
134
+ if sec_type != 'indices':
135
+ _lasts[key]['volume'] = 0
136
+
137
+ data = _lasts[key]
138
+
139
+ quotes, aggs = _subscription_type[sec_type]
140
+ await _send(sec_type, 'subscribe', f'{quotes}.{ticker}')
141
+ await _send(sec_type, 'subscribe', f'{aggs}.{ticker}') if aggs else None
142
+
143
+ if func in data['funcs']:
144
+ return
145
+ data['funcs'].append((func, args))
146
+
147
+
148
+ async def unsubscribe(func):
149
+ for key, data in _lasts.items():
150
+ if val := next(((f, args) for f, args in data['funcs'] if f == func), None):
151
+ break
152
+ else:
153
+ return
154
+ data['funcs'].remove(val)
155
+
156
+ if data['funcs']:
157
+ return
158
+ sec_type = _get_sec_type(key)
159
+ quotes, aggs = _subscription_type[sec_type]
160
+ await _send(sec_type, 'unsubscribe', f'{quotes}.{key}')
161
+ await _send(sec_type, 'unsubscribe', f'{aggs}.{key}')
162
+
163
+
164
+ async def _websocket_connect(sec_type):
165
+ if websockets is None:
166
+ raise ImportError('The "websockets" library was not found, and must be installed to pull live data.')
167
+ ticker_key = {
168
+ 'stocks': 'sym',
169
+ 'options': 'sym',
170
+ 'indices': 'T',
171
+ 'forex': 'p',
172
+ 'crypto': 'pair',
173
+ }[sec_type]
174
+ async with websockets.connect(f'wss://socket.polygon.io/{sec_type}') as ws:
175
+ _ws[sec_type] = ws
176
+ await _send(sec_type, 'auth', api_key)
177
+ while 1:
178
+ response = await ws.recv()
179
+ data_list: List[dict] = json.loads(response)
180
+ for i, data in enumerate(data_list):
181
+ if data['ev'] == 'status':
182
+ _log.info(f'{data["message"]}')
183
+ continue
184
+ _ticker_key = {
185
+ 'stocks': 'sym',
186
+ 'options': 'sym',
187
+ 'indices': 'T',
188
+ 'forex': 'p',
189
+ 'crypto': 'pair',
190
+ }
191
+ await _handle_tick(data[ticker_key], data)
192
+
193
+
194
+ async def _handle_tick(ticker, data):
195
+ lasts = _lasts[ticker]
196
+ sec_type = _get_sec_type(ticker)
197
+
198
+ if data['ev'] in ('Q', 'V', 'C', 'XQ'):
199
+ if sec_type == 'forex':
200
+ data['bp'] = data.pop('b')
201
+ data['ap'] = data.pop('a')
202
+ price = (data['bp'] + data['ap']) / 2 if sec_type != 'indices' else data['val']
203
+ if abs(price - lasts['price']) < (1/(10**lasts['precision'])):
204
+ return
205
+ lasts['price'] = price
206
+
207
+ if sec_type != 'indices':
208
+ lasts['volume'] = 0
209
+
210
+ if 't' not in data:
211
+ lasts['time'] = pd.to_datetime(data.pop('s'), unit='ms')
212
+ else:
213
+ lasts['time'] = pd.to_datetime(data['t'], unit='ms')
214
+
215
+ elif data['ev'] in ('A', 'CA', 'XA'):
216
+ lasts['volume'] = data['v']
217
+ if not lasts.get('time'):
218
+ return
219
+ lasts['symbol'] = ticker
220
+ for func, args in lasts['funcs']:
221
+ func(pd.Series(lasts), *args)
222
+
223
+
224
+ class PolygonAPI:
225
+ """
226
+ Offers direct access to Polygon API data within all Chart objects.
227
+
228
+ It is not designed to be initialized by the user, and should be utilised
229
+ through the `polygon` method of `AbstractChart` (chart.polygon.<method>).
230
+ """
231
+ _set_on_load = []
232
+
233
+ def __init__(self, chart):
234
+ self._chart = chart
235
+
236
+ def set(self, *args):
237
+ if asyncio.get_event_loop().is_running():
238
+ asyncio.create_task(self.async_set(*args))
239
+ return True
240
+ else:
241
+ _set_on_load.append(args)
242
+ return False
243
+
244
+ async def async_set(self, sec_type: Literal['stocks', 'options', 'indices', 'forex', 'crypto'], ticker, timeframe,
245
+ start_date, end_date, limit, live):
246
+ await unsubscribe(self._chart.update_from_tick)
247
+
248
+ df = await async_get_bar_data(ticker, timeframe, start_date, end_date, limit)
249
+
250
+ self._chart.set(df, keep_drawings=_tickers.get(self._chart) == ticker)
251
+ _tickers[self._chart] = ticker
252
+
253
+ if not live:
254
+ return True
255
+ await subscribe(ticker, sec_type, self._chart.update_from_tick, (True,), self._chart.num_decimals)
256
+ return True
257
+
258
+ def stock(
259
+ self, symbol: str, timeframe: str, start_date: str, end_date='now',
260
+ limit: int = 5_000, live: bool = False
261
+ ) -> bool:
262
+ """
263
+ Requests and displays stock data pulled from Polygon.io.\n
264
+ :param symbol: Ticker to request.
265
+ :param timeframe: Timeframe to request (1min, 5min, 2H, 1D, 1W, 2M, etc).
266
+ :param start_date: Start date of the data (YYYY-MM-DD).
267
+ :param end_date: End date of the data (YYYY-MM-DD). If left blank, this will be set to today.
268
+ :param limit: The limit of base aggregates queried to create the timeframe given (max 50_000).
269
+ :param live: If true, the data will be updated in real-time.
270
+ """
271
+ return self.set('stocks', symbol, timeframe, start_date, end_date, limit, live)
272
+
273
+ def option(
274
+ self, symbol: str, timeframe: str, start_date: str, expiration: str = None,
275
+ right: Literal['C', 'P'] = None, strike: Union[int, float] = None,
276
+ end_date: str = 'now', limit: int = 5_000, live: bool = False
277
+ ) -> bool:
278
+ """
279
+ Requests and displays option data pulled from Polygon.io.\n
280
+ :param symbol: The underlying ticker to request.
281
+ A formatted option ticker can also be given instead of using the expiration, right, and strike parameters.
282
+ :param timeframe: Timeframe to request (1min, 5min, 2H, 1D, 1W, 2M, etc).
283
+ :param start_date: Start date of the data (YYYY-MM-DD).
284
+ :param expiration: Expiration of the option (YYYY-MM-DD).
285
+ :param right: Right of the option (C, P).
286
+ :param strike: The strike price of the option.
287
+ :param end_date: End date of the data (YYYY-MM-DD). If left blank, this will be set to today.
288
+ :param limit: The limit of base aggregates queried to create the timeframe given (max 50_000).
289
+ :param live: If true, the data will be updated in real-time.
290
+ """
291
+ if any((expiration, right, strike)):
292
+ expiration = dt.datetime.strptime(expiration, "%Y-%m-%d").strftime("%y%m%d")
293
+ symbol = f'{symbol}{expiration}{right}{strike * 1000:08d}'
294
+ return self.set('options', f'O:{symbol}', timeframe, start_date, end_date, limit, live)
295
+
296
+ def index(
297
+ self, symbol: str, timeframe: str, start_date: str, end_date: str = 'now',
298
+ limit: int = 5_000, live: bool = False
299
+ ) -> bool:
300
+ """
301
+ Requests and displays index data pulled from Polygon.io.\n
302
+ :param symbol: Ticker to request.
303
+ :param timeframe: Timeframe to request (1min, 5min, 2H, 1D, 1W, 2M, etc).
304
+ :param start_date: Start date of the data (YYYY-MM-DD).
305
+ :param end_date: End date of the data (YYYY-MM-DD). If left blank, this will be set to today.
306
+ :param limit: The limit of base aggregates queried to create the timeframe given (max 50_000).
307
+ :param live: If true, the data will be updated in real-time.
308
+ """
309
+ return self.set('indices', f'I:{symbol}', timeframe, start_date, end_date, limit, live)
310
+
311
+ def forex(
312
+ self, fiat_pair: str, timeframe: str, start_date: str, end_date: str = 'now',
313
+ limit: int = 5_000, live: bool = False
314
+ ) -> bool:
315
+ """
316
+ Requests and displays forex data pulled from Polygon.io.\n
317
+ :param fiat_pair: The fiat pair to request. (USD-CAD, GBP-JPY etc.)
318
+ :param timeframe: Timeframe to request (1min, 5min, 2H, 1D, 1W, 2M, etc).
319
+ :param start_date: Start date of the data (YYYY-MM-DD).
320
+ :param end_date: End date of the data (YYYY-MM-DD). If left blank, this will be set to today.
321
+ :param limit: The limit of base aggregates queried to create the timeframe given (max 50_000).
322
+ :param live: If true, the data will be updated in real-time.
323
+ """
324
+ return self.set('forex', f'C:{fiat_pair}', timeframe, start_date, end_date, limit, live)
325
+
326
+ def crypto(
327
+ self, crypto_pair: str, timeframe: str, start_date: str, end_date: str = 'now',
328
+ limit: int = 5_000, live: bool = False
329
+ ) -> bool:
330
+ """
331
+ Requests and displays crypto data pulled from Polygon.io.\n
332
+ :param crypto_pair: The crypto pair to request. (BTC-USD, ETH-BTC etc.)
333
+ :param timeframe: Timeframe to request (1min, 5min, 2H, 1D, 1W, 2M, etc).
334
+ :param start_date: Start date of the data (YYYY-MM-DD).
335
+ :param end_date: End date of the data (YYYY-MM-DD). If left blank, this will be set to today.
336
+ :param limit: The limit of base aggregates queried to create the timeframe given (max 50_000).
337
+ :param live: If true, the data will be updated in real-time.
338
+ """
339
+ return self.set('crypto', f'X:{crypto_pair}', timeframe, start_date, end_date, limit, live)
340
+
341
+ async def async_stock(
342
+ self, symbol: str, timeframe: str, start_date: str, end_date: str = 'now',
343
+ limit: int = 5_000, live: bool = False
344
+ ) -> bool:
345
+ return await self.async_set('stocks', symbol, timeframe, start_date, end_date, limit, live)
346
+
347
+ async def async_option(
348
+ self, symbol: str, timeframe: str, start_date: str, expiration: str = None,
349
+ right: Literal['C', 'P'] = None, strike: Union[int, float] = None,
350
+ end_date: str = 'now', limit: int = 5_000, live: bool = False
351
+ ) -> bool:
352
+ if any((expiration, right, strike)):
353
+ expiration = dt.datetime.strptime(expiration, "%Y-%m-%d").strftime("%y%m%d")
354
+ symbol = f'{symbol}{expiration}{right}{strike * 1000:08d}'
355
+ return await self.async_set('options', f'O:{symbol}', timeframe, start_date, end_date, limit, live)
356
+
357
+ async def async_index(
358
+ self, symbol: str, timeframe: str, start_date: str, end_date: str = 'now',
359
+ limit: int = 5_000, live: bool = False
360
+ ) -> bool:
361
+ return await self.async_set('indices', f'I:{symbol}', timeframe, start_date, end_date, limit, live)
362
+
363
+ async def async_forex(
364
+ self, fiat_pair: str, timeframe: str, start_date: str, end_date: str = 'now',
365
+ limit: int = 5_000, live: bool = False
366
+ ) -> bool:
367
+ return await self.async_set('forex', f'C:{fiat_pair}', timeframe, start_date, end_date, limit, live)
368
+
369
+ async def async_crypto(
370
+ self, crypto_pair: str, timeframe: str, start_date: str, end_date: str = 'now',
371
+ limit: int = 5_000, live: bool = False
372
+ ) -> bool:
373
+ return await self.async_set('crypto', f'X:{crypto_pair}', timeframe, start_date, end_date, limit, live)
374
+
375
+ @staticmethod
376
+ def log(info: bool):
377
+ """
378
+ Streams informational messages related to Polygon.io.
379
+ """
380
+ _log.setLevel(logging.INFO) if info else _log.setLevel(logging.ERROR)
381
+
382
+ @staticmethod
383
+ def api_key(key: str):
384
+ """
385
+ Sets the API key to be used with Polygon.io.
386
+ """
387
+ global api_key
388
+ api_key = key
389
+
390
+
391
+ class PolygonChart(Chart):
392
+ """
393
+ A prebuilt callback chart object allowing for a standalone, plug-and-play
394
+ experience of Polygon.io's API.
395
+
396
+ Tickers, security types and timeframes are to be defined within the chart window.
397
+
398
+ If using the standard `show` method, the `block` parameter must be set to True.
399
+ `show_async` can also be used.
400
+ """
401
+ def __init__(
402
+ self, api_key: str, live: bool = False, num_bars: int = 200, end_date: str = 'now', limit: int = 5_000,
403
+ timeframe_options: tuple = ('1min', '5min', '30min', 'D', 'W'),
404
+ security_options: tuple = ('Stock', 'Option', 'Index', 'Forex', 'Crypto'),
405
+ toolbox: bool = True, width: int = 800, height: int = 600, x: int = None, y: int = None,
406
+ on_top: bool = False, maximize: bool = False, debug: bool = False,
407
+ title: str = '', screen: int = None,
408
+ ):
409
+ super().__init__(width, height, x, y, title, screen, on_top, maximize, debug, toolbox)
410
+
411
+ self.num_bars = num_bars
412
+ self.end_date = end_date
413
+ self.limit = limit
414
+ self.live = live
415
+ self.win.style(
416
+ active_background_color='rgba(91, 98, 246, 0.8)',
417
+ muted_background_color='rgba(91, 98, 246, 0.5)'
418
+ )
419
+ self.polygon.api_key(api_key)
420
+ self.events.search += self.on_search
421
+ self.legend(True)
422
+ self.grid(False, False)
423
+ self.crosshair(vert_visible=False, horz_visible=False)
424
+
425
+ self.topbar.textbox('symbol')
426
+ self.topbar.switcher('timeframe', timeframe_options, func=self._on_timeframe_selection)
427
+ self.topbar.switcher('security', security_options, func=self._on_security_selection)
428
+
429
+ self.run_script(f'''
430
+ {self.id}.search.window.style.display = "flex"
431
+ {self.id}.search.box.focus()
432
+ ''')
433
+
434
+ async def _polygon(self, symbol):
435
+ self.spinner(True)
436
+ self.set(pd.DataFrame(), True)
437
+ self.crosshair(vert_visible=False, horz_visible=False)
438
+
439
+ mult, span = _convert_timeframe(self.topbar['timeframe'].value)
440
+ delta = dt.timedelta(**{span + 's': int(mult)})
441
+ short_delta = (delta < dt.timedelta(days=7))
442
+ start_date = dt.datetime.now() if self.end_date == 'now' else dt.datetime.strptime(self.end_date, '%Y-%m-%d')
443
+ remaining_bars = self.num_bars
444
+ while remaining_bars > 0:
445
+ start_date -= delta
446
+ if start_date.weekday() > 4 and short_delta: # Monday to Friday (0 to 4)
447
+ continue
448
+ remaining_bars -= 1
449
+ epoch = dt.datetime.fromtimestamp(0)
450
+ start_date = epoch if start_date < epoch else start_date
451
+ success = await getattr(self.polygon, 'async_'+self.topbar['security'].value.lower())(
452
+ symbol,
453
+ timeframe=self.topbar['timeframe'].value,
454
+ start_date=start_date.strftime('%Y-%m-%d'),
455
+ end_date=self.end_date,
456
+ limit=self.limit,
457
+ live=self.live
458
+ )
459
+ self.spinner(False)
460
+ self.crosshair() if success else None
461
+ return success
462
+
463
+ async def on_search(self, chart, searched_string):
464
+ chart.topbar['symbol'].set(searched_string if await self._polygon(searched_string) else '')
465
+
466
+ async def _on_timeframe_selection(self, chart):
467
+ await self._polygon(chart.topbar['symbol'].value) if chart.topbar['symbol'].value else None
468
+
469
+ async def _on_security_selection(self, chart):
470
+ self.precision(5 if chart.topbar['security'].value == 'Forex' else 2)
@@ -0,0 +1,138 @@
1
+ import asyncio
2
+ import random
3
+ from typing import Union, Optional, Callable
4
+
5
+ from .util import jbool, Pane, NUM
6
+
7
+
8
+ class Section(Pane):
9
+ def __init__(self, table, section_type):
10
+ super().__init__(table.win)
11
+ self._table = table
12
+ self.type = section_type
13
+
14
+ def __call__(self, number_of_text_boxes: int, func: Optional[Callable] = None):
15
+ if func is not None:
16
+ self.win.handlers[self.id] = lambda boxId: func(self._table, int(boxId))
17
+ self.run_script(f'''
18
+ {self._table.id}.makeSection("{self.id}", "{self.type}", {number_of_text_boxes}, {"true" if func else ""})
19
+ ''')
20
+
21
+ def __setitem__(self, key, value):
22
+ self.run_script(f'{self._table.id}.{self.type}[{key}].innerText = "{value}"')
23
+
24
+
25
+ class Row(dict):
26
+ def __init__(self, table, id, items):
27
+ super().__init__()
28
+ self.run_script = table.run_script
29
+ self._table = table
30
+ self.id = id
31
+ self.meta = {}
32
+ self.run_script(f'{self._table.id}.newRow("{self.id}", {jbool(table.return_clicked_cells)})')
33
+ for key, val in items.items():
34
+ self[key] = val
35
+
36
+ def __setitem__(self, column, value):
37
+ if isinstance(column, tuple):
38
+ [self.__setitem__(col, val) for col, val in zip(column, value)]
39
+ return
40
+ original_value = value
41
+ if column in self._table._formatters:
42
+ value = self._table._formatters[column].replace(self._table.VALUE, str(value))
43
+ self.run_script(f'{self._table.id}.updateCell("{self.id}", "{column}", "{value}")')
44
+ return super().__setitem__(column, original_value)
45
+
46
+ def background_color(self, column, color): self._style('backgroundColor', column, color)
47
+
48
+ def text_color(self, column, color): self._style('textColor', column, color)
49
+
50
+ def _style(self, style, column, arg):
51
+ self.run_script(f"{self._table.id}.styleCell({self.id}, '{column}', '{style}', '{arg}')")
52
+
53
+ def delete(self):
54
+ self.run_script(f"{self._table.id}.deleteRow('{self.id}')")
55
+ self._table.pop(self.id)
56
+
57
+
58
+ class Table(Pane, dict):
59
+ VALUE = 'CELL__~__VALUE__~__PLACEHOLDER'
60
+
61
+ def __init__(
62
+ self,
63
+ window,
64
+ width: NUM,
65
+ height: NUM,
66
+ headings: tuple,
67
+ widths: Optional[tuple] = None,
68
+ alignments: Optional[tuple] = None,
69
+ position='left',
70
+ draggable: bool = False,
71
+ background_color: str = '#121417',
72
+ border_color: str = 'rgb(70, 70, 70)',
73
+ border_width: int = 1,
74
+ heading_text_colors: Optional[tuple] = None,
75
+ heading_background_colors: Optional[tuple] = None,
76
+ return_clicked_cells: bool = False,
77
+ func: Optional[Callable] = None
78
+ ):
79
+ dict.__init__(self)
80
+ Pane.__init__(self, window)
81
+ self._formatters = {}
82
+ self.headings = headings
83
+ self.is_shown = True
84
+ def wrapper(rId, cId=None):
85
+ if return_clicked_cells:
86
+ func(self[rId], cId)
87
+ else:
88
+ func(self[rId])
89
+
90
+ async def async_wrapper(rId, cId=None):
91
+ if return_clicked_cells:
92
+ await func(self[rId], cId)
93
+ else:
94
+ await func(self[rId])
95
+
96
+ self.win.handlers[self.id] = async_wrapper if asyncio.iscoroutinefunction(func) else wrapper
97
+ self.return_clicked_cells = return_clicked_cells
98
+
99
+ self.run_script(f'''
100
+ {self.id} = new Lib.Table(
101
+ {width},
102
+ {height},
103
+ {list(headings)},
104
+ {list(widths) if widths else []},
105
+ {list(alignments) if alignments else []},
106
+ '{position}',
107
+ {jbool(draggable)},
108
+ '{background_color}',
109
+ '{border_color}',
110
+ {border_width},
111
+ {list(heading_text_colors) if heading_text_colors else []},
112
+ {list(heading_background_colors) if heading_background_colors else []}
113
+ )''')
114
+ self.run_script(f'{self.id}.callbackName = "{self.id}"') if func else None
115
+ self.footer = Section(self, 'footer')
116
+ self.header = Section(self, 'header')
117
+
118
+ def new_row(self, *values, id=None) -> Row:
119
+ row_id = random.randint(0, 99_999_999) if not id else id
120
+ self[row_id] = Row(self, row_id, {heading: item for heading, item in zip(self.headings, values)})
121
+ return self[row_id]
122
+
123
+ def clear(self): self.run_script(f"{self.id}.clearRows()"), super().clear()
124
+
125
+ def get(self, __key: Union[int, str]) -> Row: return super().get(int(__key))
126
+
127
+ def __getitem__(self, item): return super().__getitem__(int(item))
128
+
129
+ def format(self, column: str, format_str: str): self._formatters[column] = format_str
130
+
131
+ def resize(self, width: NUM, height: NUM): self.run_script(f'{self.id}.reSize({width}, {height})')
132
+
133
+ def visible(self, visible: bool):
134
+ self.is_shown = visible
135
+ self.run_script(f"""
136
+ {self.id}._div.style.display = '{'flex' if visible else 'none'}'
137
+ {self.id}._div.{'add' if visible else 'remove'}EventListener('mousedown', {self.id}.onMouseDown)
138
+ """)
@@ -0,0 +1,45 @@
1
+ import json
2
+
3
+
4
+ class ToolBox:
5
+ def __init__(self, chart):
6
+ self.run_script = chart.run_script
7
+ self.id = chart.id
8
+ self._save_under = None
9
+ self.drawings = {}
10
+ chart.win.handlers[f'save_drawings{self.id}'] = self._save_drawings
11
+ self.run_script(f'{self.id}.createToolBox()')
12
+
13
+ def save_drawings_under(self, widget: 'Widget'):
14
+ """
15
+ Drawings made on charts will be saved under the widget given. eg `chart.toolbox.save_drawings_under(chart.topbar['symbol'])`.
16
+ """
17
+ self._save_under = widget
18
+
19
+ def load_drawings(self, tag: str):
20
+ """
21
+ Loads and displays the drawings on the chart stored under the tag given.
22
+ """
23
+ if not self.drawings.get(tag):
24
+ return
25
+ self.run_script(f'if ({self.id}.toolBox) {self.id}.toolBox.loadDrawings({json.dumps(self.drawings[tag])})')
26
+
27
+ def import_drawings(self, file_path):
28
+ """
29
+ Imports a list of drawings stored at the given file path.
30
+ """
31
+ with open(file_path, 'r') as f:
32
+ json_data = json.load(f)
33
+ self.drawings = json_data
34
+
35
+ def export_drawings(self, file_path):
36
+ """
37
+ Exports the current list of drawings to the given file path.
38
+ """
39
+ with open(file_path, 'w+') as f:
40
+ json.dump(self.drawings, f, indent=4)
41
+
42
+ def _save_drawings(self, drawings):
43
+ if not self._save_under:
44
+ return
45
+ self.drawings[self._save_under.value] = json.loads(drawings)