Ability to save drawings
- Added `toolbox` to the common methods. - `toolbox.save_drawings_under` can save drawings under a specific `topbar` widget. eg `chart.toolbox.save_drawings_under(chart.topbar[’symbol’]`) - `toolbox.load_drawings` will load and display drawings stored under the tag/string given. - `toolbox.export_drawings` will export all currently saved drawings to the given file path. - `toolbox.import_drawings` will import the drawings stored at the given file path. Fixes/Enhancements: - `update` methods are no longer case sensitive. - HorizontalLines no longer throw cyclic structure errors in the web console. - `API` methods can now be normal methods or coroutines.
This commit is contained in:
4
.github/FUNDING.yml
vendored
4
.github/FUNDING.yml
vendored
@ -1,4 +1,2 @@
|
|||||||
# These are supported funding model platforms
|
|
||||||
|
|
||||||
github: louisnw01
|
github: louisnw01
|
||||||
custom: https://www.buymeacoffee.com/7wzcr2p9vxM
|
custom: https://www.buymeacoffee.com/7wzcr2p9vxM/
|
||||||
|
|||||||
@ -30,6 +30,8 @@ ___
|
|||||||
7. Direct integration of market data through [Polygon.io's](https://polygon.io/?utm_source=affiliate&utm_campaign=pythonlwcharts) market data API.
|
7. Direct integration of market data through [Polygon.io's](https://polygon.io/?utm_source=affiliate&utm_campaign=pythonlwcharts) market data API.
|
||||||
|
|
||||||
__Supports:__ Jupyter Notebooks, PyQt, wxPython, Streamlit, and asyncio.
|
__Supports:__ Jupyter Notebooks, PyQt, wxPython, Streamlit, and asyncio.
|
||||||
|
|
||||||
|
PartTimeLarry: [Interactive Brokers API and TradingView Charts in Python](https://www.youtube.com/watch?v=TlhDI3PforA)
|
||||||
___
|
___
|
||||||
|
|
||||||
### 1. Display data from a csv:
|
### 1. Display data from a csv:
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
project = 'lightweight-charts-python'
|
project = 'lightweight-charts-python'
|
||||||
copyright = '2023, louisnw'
|
copyright = '2023, louisnw'
|
||||||
author = 'louisnw'
|
author = 'louisnw'
|
||||||
release = '1.0.14.1'
|
release = '1.0.14.2'
|
||||||
|
|
||||||
extensions = ["myst_parser"]
|
extensions = ["myst_parser"]
|
||||||
|
|
||||||
|
|||||||
@ -325,7 +325,7 @@ ___
|
|||||||
|
|
||||||
Sets the data for the line.
|
Sets the data for the line.
|
||||||
|
|
||||||
When not using the `name` parameter, the columns should be named: `time | value`.
|
When not using the `name` parameter, the columns should be named: `time | value` (Not case sensitive).
|
||||||
|
|
||||||
Otherwise, the method will use the column named after the string given in `name`. This name will also be used within the legend of the chart. For example:
|
Otherwise, the method will use the column named after the string given in `name`. This name will also be used within the legend of the chart. For example:
|
||||||
```python
|
```python
|
||||||
@ -469,6 +469,7 @@ The ID shown above will change depending upon which pane was used to search, due
|
|||||||
|
|
||||||
```{important}
|
```{important}
|
||||||
* Search callbacks will always be emitted to a method named `on_search`
|
* Search callbacks will always be emitted to a method named `on_search`
|
||||||
|
* `API` class methods can be either coroutines or normal methods.
|
||||||
```
|
```
|
||||||
___
|
___
|
||||||
|
|
||||||
@ -568,7 +569,33 @@ The following hotkeys can also be used when the Toolbox is enabled:
|
|||||||
* Alt+H: Horizontal Line
|
* Alt+H: Horizontal Line
|
||||||
* Alt+R: Ray Line
|
* Alt+R: Ray Line
|
||||||
* Meta+Z or Ctrl+Z: Undo
|
* Meta+Z or Ctrl+Z: Undo
|
||||||
|
___
|
||||||
|
|
||||||
|
### `save_drawings_under`
|
||||||
|
`widget: Widget`
|
||||||
|
|
||||||
|
Saves drawings under a specific `topbar` text widget. For example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
chart.toolbox.save_drawings_under(chart.topbar['symbol'])
|
||||||
|
```
|
||||||
|
___
|
||||||
|
|
||||||
|
### `load_drawings`
|
||||||
|
`tag: str`
|
||||||
|
|
||||||
|
Loads and displays drawings stored under the tag given.
|
||||||
|
___
|
||||||
|
### `import_drawings`
|
||||||
|
`file_path: str`
|
||||||
|
|
||||||
|
Imports the drawings stored at the JSON file given in `file_path`.
|
||||||
|
|
||||||
|
___
|
||||||
|
### `export_drawings`
|
||||||
|
`file_path: str`
|
||||||
|
|
||||||
|
Exports all currently saved drawings to the JSON file given in `file_path`.
|
||||||
___
|
___
|
||||||
|
|
||||||
## QtChart
|
## QtChart
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import json
|
||||||
import os
|
import os
|
||||||
from datetime import timedelta, datetime
|
from datetime import timedelta, datetime
|
||||||
from base64 import b64decode
|
from base64 import b64decode
|
||||||
@ -59,23 +60,35 @@ class SeriesCommon:
|
|||||||
}}
|
}}
|
||||||
''')
|
''')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _rename(data, mapper, is_dataframe):
|
||||||
|
if is_dataframe:
|
||||||
|
data.columns = [mapper[key] if key in mapper else key for key in data.columns]
|
||||||
|
else:
|
||||||
|
data.index = [mapper[key] if key in mapper else key for key in data.index]
|
||||||
|
|
||||||
def _df_datetime_format(self, df: pd.DataFrame, exclude_lowercase=None):
|
def _df_datetime_format(self, df: pd.DataFrame, exclude_lowercase=None):
|
||||||
df = df.copy()
|
df = df.copy()
|
||||||
|
if 'date' not in df.columns and 'time' not in df.columns:
|
||||||
df.columns = df.columns.str.lower()
|
df.columns = df.columns.str.lower()
|
||||||
if exclude_lowercase:
|
if exclude_lowercase:
|
||||||
df[exclude_lowercase] = df[exclude_lowercase.lower()]
|
df[exclude_lowercase] = df[exclude_lowercase.lower()]
|
||||||
if 'date' in df.columns:
|
if 'date' in df.columns:
|
||||||
df = df.rename(columns={'date': 'time'})
|
self._rename(df, {'date': 'time'}, True)
|
||||||
elif 'time' not in df.columns:
|
elif 'time' not in df.columns:
|
||||||
df['time'] = df.index
|
df['time'] = df.index
|
||||||
self._set_interval(df)
|
self._set_interval(df)
|
||||||
df['time'] = self._datetime_format(df['time'])
|
df['time'] = self._datetime_format(df['time'])
|
||||||
return df
|
return df
|
||||||
|
|
||||||
def _series_datetime_format(self, series):
|
def _series_datetime_format(self, series: pd.Series, exclude_lowercase=None):
|
||||||
series = series.copy()
|
series = series.copy()
|
||||||
if 'date' in series.keys():
|
if 'date' not in series.index and 'time' not in series.index:
|
||||||
series = series.rename({'date': 'time'})
|
series.index = series.index.str.lower()
|
||||||
|
if exclude_lowercase:
|
||||||
|
self._rename(series, {exclude_lowercase.lower(): exclude_lowercase}, False)
|
||||||
|
if 'date' in series.index:
|
||||||
|
self._rename(series, {'date': 'time'}, False)
|
||||||
series['time'] = self._datetime_format(series['time'])
|
series['time'] = self._datetime_format(series['time'])
|
||||||
return series
|
return series
|
||||||
|
|
||||||
@ -353,6 +366,47 @@ class TopBar:
|
|||||||
return widget
|
return widget
|
||||||
|
|
||||||
|
|
||||||
|
class ToolBox:
|
||||||
|
def __init__(self, chart):
|
||||||
|
self.run_script = chart.run_script
|
||||||
|
self.id = chart.id
|
||||||
|
self._return_q = chart._return_q
|
||||||
|
|
||||||
|
self._saved_drawings = {}
|
||||||
|
|
||||||
|
def save_drawings_under(self, widget: Widget):
|
||||||
|
"""
|
||||||
|
Drawings made on charts will be saved under the widget given. eg `chart.toolbox.save_drawings_under(chart.topbar['symbol'])`.
|
||||||
|
"""
|
||||||
|
self._save_under = widget
|
||||||
|
|
||||||
|
def load_drawings(self, tag: str):
|
||||||
|
"""
|
||||||
|
Loads and displays the drawings on the chart stored under the tag given.
|
||||||
|
"""
|
||||||
|
if not self._saved_drawings.get(tag):
|
||||||
|
return
|
||||||
|
self.run_script(f'if ("toolBox" in {self.id}) {self.id}.toolBox.loadDrawings({json.dumps(self._saved_drawings[tag])})')
|
||||||
|
|
||||||
|
def import_drawings(self, file_path):
|
||||||
|
"""
|
||||||
|
Imports a list of drawings stored at the given file path.
|
||||||
|
"""
|
||||||
|
with open(file_path, 'r') as f:
|
||||||
|
json_data = json.load(f)
|
||||||
|
self._saved_drawings = json_data
|
||||||
|
|
||||||
|
def export_drawings(self, file_path):
|
||||||
|
"""
|
||||||
|
Exports the current list of drawings to the given file path.
|
||||||
|
"""
|
||||||
|
with open(file_path, 'w+') as f:
|
||||||
|
json.dump(self._saved_drawings, f)
|
||||||
|
|
||||||
|
def _save_drawings(self, drawings):
|
||||||
|
self._saved_drawings[self._save_under.value] = json.loads(drawings)
|
||||||
|
|
||||||
|
|
||||||
class LWC(SeriesCommon):
|
class LWC(SeriesCommon):
|
||||||
def __init__(self, volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0, dynamic_loading: bool = False,
|
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,
|
scale_candles_only: bool = False, topbar: bool = False, searchbox: bool = False, toolbox: bool = False,
|
||||||
@ -394,6 +448,7 @@ class LWC(SeriesCommon):
|
|||||||
if toolbox:
|
if toolbox:
|
||||||
self.run_script(JS['toolbox'])
|
self.run_script(JS['toolbox'])
|
||||||
self.run_script(f'{self.id}.toolBox = new ToolBox({self.id})')
|
self.run_script(f'{self.id}.toolBox = new ToolBox({self.id})')
|
||||||
|
self.toolbox: ToolBox = ToolBox(self)
|
||||||
if not topbar and not searchbox:
|
if not topbar and not searchbox:
|
||||||
return
|
return
|
||||||
self.run_script(JS['callback'])
|
self.run_script(JS['callback'])
|
||||||
|
|||||||
@ -87,13 +87,17 @@ class Chart(LWC):
|
|||||||
self._exit.clear()
|
self._exit.clear()
|
||||||
return
|
return
|
||||||
elif not self._emit_q.empty():
|
elif not self._emit_q.empty():
|
||||||
key, chart_id, arg = self._emit_q.get()
|
name, chart_id, arg = self._emit_q.get()
|
||||||
self._api.chart = self._charts[chart_id]
|
self._api.chart = self._charts[chart_id]
|
||||||
if widget := self._api.chart.topbar._widget_with_method(key):
|
if name == 'save_drawings':
|
||||||
|
self._api.chart.toolbox._save_drawings(arg)
|
||||||
|
continue
|
||||||
|
method = getattr(self._api, name)
|
||||||
|
if hasattr(self._api.chart, 'topbar') and (widget := self._api.chart.topbar._widget_with_method(name)):
|
||||||
widget.value = arg
|
widget.value = arg
|
||||||
await getattr(self._api, key)()
|
await method() if asyncio.iscoroutinefunction(method) else method()
|
||||||
else:
|
else:
|
||||||
await getattr(self._api, key)(*arg.split(';;;'))
|
await method(*arg.split(';;;')) if asyncio.iscoroutinefunction(method) else method(arg)
|
||||||
continue
|
continue
|
||||||
value = self.polygon._q.get()
|
value = self.polygon._q.get()
|
||||||
func, args = value[0], value[1:]
|
func, args = value[0], value[1:]
|
||||||
|
|||||||
@ -42,7 +42,7 @@ function makeSearchBox(chart) {
|
|||||||
yPrice = param.point.y;
|
yPrice = param.point.y;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let selectedChart = true
|
let selectedChart = false
|
||||||
chart.wrapper.addEventListener('mouseover', (event) => {
|
chart.wrapper.addEventListener('mouseover', (event) => {
|
||||||
selectedChart = true
|
selectedChart = true
|
||||||
})
|
})
|
||||||
@ -50,6 +50,7 @@ function makeSearchBox(chart) {
|
|||||||
selectedChart = false
|
selectedChart = false
|
||||||
})
|
})
|
||||||
chart.commandFunctions.push((event) => {
|
chart.commandFunctions.push((event) => {
|
||||||
|
if (!selectedChart) return
|
||||||
if (searchWindow.style.display === 'none') {
|
if (searchWindow.style.display === 'none') {
|
||||||
if (/^[a-zA-Z0-9]$/.test(event.key)) {
|
if (/^[a-zA-Z0-9]$/.test(event.key)) {
|
||||||
searchWindow.style.display = 'flex';
|
searchWindow.style.display = 'flex';
|
||||||
|
|||||||
@ -106,6 +106,12 @@ if (!window.HorizontalLine) {
|
|||||||
this.chart.horizontal_lines.push(this)
|
this.chart.horizontal_lines.push(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
// Exclude the chart attribute from serialization
|
||||||
|
const {chart, line, ...serialized} = this;
|
||||||
|
return serialized;
|
||||||
|
}
|
||||||
|
|
||||||
updatePrice(price) {
|
updatePrice(price) {
|
||||||
this.chart.series.removePriceLine(this.line)
|
this.chart.series.removePriceLine(this.line)
|
||||||
this.price = price
|
this.price = price
|
||||||
@ -361,36 +367,3 @@ function calculateTrendLine(startDate, startValue, endDate, endValue, interval,
|
|||||||
}
|
}
|
||||||
return trendData;
|
return trendData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
let customMenu = document.createElement('div')
|
|
||||||
customMenu.style.position = 'absolute'
|
|
||||||
customMenu.style.zIndex = '10000'
|
|
||||||
customMenu.style.background = 'rgba(25, 25, 25, 0.7)'
|
|
||||||
customMenu.style.color = 'lightgrey'
|
|
||||||
customMenu.style.display = 'none'
|
|
||||||
customMenu.style.borderRadius = '5px'
|
|
||||||
customMenu.style.padding = '5px 10px'
|
|
||||||
document.body.appendChild(customMenu)
|
|
||||||
|
|
||||||
function menuItem(text) {
|
|
||||||
let elem = document.createElement('div')
|
|
||||||
elem.innerText = text
|
|
||||||
customMenu.appendChild(elem)
|
|
||||||
}
|
|
||||||
menuItem('Delete drawings')
|
|
||||||
menuItem('Hide all indicators')
|
|
||||||
menuItem('Save Chart State')
|
|
||||||
|
|
||||||
let closeMenu = (event) => {if (!customMenu.contains(event.target)) customMenu.style.display = 'none';}
|
|
||||||
document.addEventListener('contextmenu', function (event) {
|
|
||||||
event.preventDefault(); // Prevent default right-click menu
|
|
||||||
customMenu.style.left = event.clientX + 'px';
|
|
||||||
customMenu.style.top = event.clientY + 'px';
|
|
||||||
customMenu.style.display = 'block';
|
|
||||||
document.removeEventListener('click', closeMenu)
|
|
||||||
document.addEventListener('click', closeMenu)
|
|
||||||
});
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|||||||
@ -66,7 +66,8 @@ if (!window.ToolBox) {
|
|||||||
this.chart.chart.removeSeries(toDelete.line);
|
this.chart.chart.removeSeries(toDelete.line);
|
||||||
if (toDelete.ray) this.chart.chart.timeScale().setVisibleLogicalRange(logical)
|
if (toDelete.ray) this.chart.chart.timeScale().setVisibleLogicalRange(logical)
|
||||||
}
|
}
|
||||||
this.drawings.splice(this.drawings.length - 1)
|
this.drawings.splice(this.drawings.indexOf(toDelete))
|
||||||
|
this.saveDrawings()
|
||||||
}
|
}
|
||||||
this.chart.commandFunctions.push((event) => {
|
this.chart.commandFunctions.push((event) => {
|
||||||
if ((event.metaKey || event.ctrlKey) && event.code === 'KeyZ') {
|
if ((event.metaKey || event.ctrlKey) && event.code === 'KeyZ') {
|
||||||
@ -248,6 +249,7 @@ if (!window.ToolBox) {
|
|||||||
this.chart.cursor = 'default'
|
this.chart.cursor = 'default'
|
||||||
this.chart.activeIcon.elem.style.backgroundColor = this.backgroundColor
|
this.chart.activeIcon.elem.style.backgroundColor = this.backgroundColor
|
||||||
this.chart.activeIcon = null
|
this.chart.activeIcon = null
|
||||||
|
this.saveDrawings()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.chart.chart.subscribeClick(this.clickHandler)
|
this.chart.chart.subscribeClick(this.clickHandler)
|
||||||
@ -263,6 +265,7 @@ if (!window.ToolBox) {
|
|||||||
this.chart.cursor = 'default'
|
this.chart.cursor = 'default'
|
||||||
this.chart.activeIcon.elem.style.backgroundColor = this.backgroundColor
|
this.chart.activeIcon.elem.style.backgroundColor = this.backgroundColor
|
||||||
this.chart.activeIcon = null
|
this.chart.activeIcon = null
|
||||||
|
this.saveDrawings()
|
||||||
}
|
}
|
||||||
onHorzSelect(toggle) {
|
onHorzSelect(toggle) {
|
||||||
!toggle ? this.chart.chart.unsubscribeClick(this.clickHandlerHorz) : this.chart.chart.subscribeClick(this.clickHandlerHorz)
|
!toggle ? this.chart.chart.unsubscribeClick(this.clickHandlerHorz) : this.chart.chart.subscribeClick(this.clickHandlerHorz)
|
||||||
@ -319,7 +322,6 @@ if (!window.ToolBox) {
|
|||||||
let mouseDown = false
|
let mouseDown = false
|
||||||
let clickedEnd = false
|
let clickedEnd = false
|
||||||
let checkForClick = (event) => {
|
let checkForClick = (event) => {
|
||||||
//if (!hoveringOver) return
|
|
||||||
mouseDown = true
|
mouseDown = true
|
||||||
document.body.style.cursor = 'grabbing'
|
document.body.style.cursor = 'grabbing'
|
||||||
this.chart.chart.applyOptions({
|
this.chart.chart.applyOptions({
|
||||||
@ -345,11 +347,11 @@ if (!window.ToolBox) {
|
|||||||
this.chart.chart.subscribeCrosshairMove(checkForDrag)
|
this.chart.chart.subscribeCrosshairMove(checkForDrag)
|
||||||
}
|
}
|
||||||
originalIndex = this.chart.chart.timeScale().coordinateToLogical(x)
|
originalIndex = this.chart.chart.timeScale().coordinateToLogical(x)
|
||||||
this.chart.chart.unsubscribeClick(checkForClick)
|
document.removeEventListener('mousedown', checkForClick)
|
||||||
}
|
}
|
||||||
let checkForRelease = (event) => {
|
let checkForRelease = (event) => {
|
||||||
mouseDown = false
|
mouseDown = false
|
||||||
document.body.style.cursor = 'pointer'
|
document.body.style.cursor = this.chart.cursor
|
||||||
|
|
||||||
this.chart.chart.applyOptions({handleScroll: true})
|
this.chart.chart.applyOptions({handleScroll: true})
|
||||||
if (hoveringOver && 'price' in hoveringOver && hoveringOver.id !== 'toolBox') {
|
if (hoveringOver && 'price' in hoveringOver && hoveringOver.id !== 'toolBox') {
|
||||||
@ -359,6 +361,7 @@ if (!window.ToolBox) {
|
|||||||
document.removeEventListener('mousedown', checkForClick)
|
document.removeEventListener('mousedown', checkForClick)
|
||||||
document.removeEventListener('mouseup', checkForRelease)
|
document.removeEventListener('mouseup', checkForRelease)
|
||||||
this.chart.chart.subscribeCrosshairMove(hoverOver)
|
this.chart.chart.subscribeCrosshairMove(hoverOver)
|
||||||
|
this.saveDrawings()
|
||||||
}
|
}
|
||||||
let checkForDrag = (param) => {
|
let checkForDrag = (param) => {
|
||||||
if (!param.point) return
|
if (!param.point) return
|
||||||
@ -486,6 +489,54 @@ if (!window.ToolBox) {
|
|||||||
})
|
})
|
||||||
this.drawings = []
|
this.drawings = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveDrawings() {
|
||||||
|
let drawingsString = JSON.stringify(this.drawings, (key, value) => {
|
||||||
|
if (key === '' && Array.isArray(value)) {
|
||||||
|
return value.filter(item => !(item && typeof item === 'object' && 'priceLine' in item && item.id !== 'toolBox'));
|
||||||
|
} else if (key === 'line' || (value && typeof value === 'object' && 'priceLine' in value && value.id !== 'toolBox')) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
this.chart.callbackFunction(`save_drawings__${this.chart.id}__${drawingsString}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
loadDrawings(drawings) {
|
||||||
|
this.drawings = drawings
|
||||||
|
this.chart.chart.applyOptions({
|
||||||
|
handleScroll: false
|
||||||
|
})
|
||||||
|
let logical = this.chart.chart.timeScale().getVisibleLogicalRange()
|
||||||
|
this.drawings.forEach((item) => {
|
||||||
|
if ('price' in item) {
|
||||||
|
this.drawings[this.drawings.indexOf(item)] = new HorizontalLine(this.chart, 'toolBox', item.priceLine.price, item.priceLine.color, 2, item.priceLine.lineStyle, item.priceLine.axisLabelVisible)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.drawings[this.drawings.indexOf(item)].line = this.chart.chart.addLineSeries({
|
||||||
|
lineWidth: 2,
|
||||||
|
lastValueVisible: false,
|
||||||
|
priceLineVisible: false,
|
||||||
|
crosshairMarkerVisible: false,
|
||||||
|
autoscaleInfoProvider: () => ({
|
||||||
|
priceRange: {
|
||||||
|
minValue: 1_000_000_000,
|
||||||
|
maxValue: 0,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
let startDate = dateToChartTime(new Date(Math.round(chartTimeToDate(item.from).getTime() / this.interval) * this.interval), this.interval)
|
||||||
|
let endDate = dateToChartTime(new Date(Math.round(chartTimeToDate(item.to).getTime() / this.interval) * this.interval), this.interval)
|
||||||
|
let data = calculateTrendLine(startDate, item.data[0].value, endDate, item.data[item.data.length - 1].value, this.interval, this.chart, item.ray)
|
||||||
|
if (data.length !== 0) item.data = data
|
||||||
|
item.line.setData(data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.chart.chart.applyOptions({
|
||||||
|
handleScroll: true
|
||||||
|
})
|
||||||
|
this.chart.chart.timeScale().setVisibleLogicalRange(logical)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.ToolBox = ToolBox
|
window.ToolBox = ToolBox
|
||||||
|
|||||||
2
setup.py
2
setup.py
@ -5,7 +5,7 @@ with open('README.md', 'r', encoding='utf-8') as f:
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='lightweight_charts',
|
name='lightweight_charts',
|
||||||
version='1.0.14.1',
|
version='1.0.14.2',
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
python_requires='>=3.8',
|
python_requires='>=3.8',
|
||||||
install_requires=[
|
install_requires=[
|
||||||
|
|||||||
Reference in New Issue
Block a user