- Synced Subcharts will now have synced crosshairs.
- Fixed a bug causing Subcharts not to sync initially. - added `scale_margin_top` and `scale_margin_bottom` parameters to the `price_scale` method. - Added `price_line` and `price_label` parameters to `create_line`. - Lowered the default margins of the chart.
This commit is contained in:
@ -1,7 +1,7 @@
|
||||
project = 'lightweight-charts-python'
|
||||
copyright = '2023, louisnw'
|
||||
author = 'louisnw'
|
||||
release = '1.0.11'
|
||||
release = '1.0.12'
|
||||
|
||||
extensions = ["myst_parser"]
|
||||
|
||||
|
||||
@ -57,7 +57,7 @@ If `cumulative_volume` is used, the volume data given to this method will be add
|
||||
___
|
||||
|
||||
### `create_line`
|
||||
`color: str` | `width: int` | `-> Line`
|
||||
`color: str` | `width: int` | `price_line: bool` | `price_label: bool` | `-> Line`
|
||||
|
||||
Creates and returns a [Line](#line) object.
|
||||
___
|
||||
@ -100,7 +100,7 @@ Removes a horizontal line at the given price.
|
||||
___
|
||||
|
||||
### `price_scale`
|
||||
`mode: 'normal'/'logarithmic'/'percentage'/'index100'` | `align_labels: bool` | `border_visible: bool` | `border_color: str` | `text_color: str` | `entire_text_only: bool` | `ticks_visible: bool`
|
||||
`mode: 'normal'/'logarithmic'/'percentage'/'index100'` | `align_labels: bool` | `border_visible: bool` | `border_color: str` | `text_color: str` | `entire_text_only: bool` | `ticks_visible: bool` | `scale_margin_top: float` | `scale_margin_bottom: float`
|
||||
|
||||
Price scale options for the chart.
|
||||
___
|
||||
@ -108,7 +108,7 @@ ___
|
||||
### `time_scale`
|
||||
`right_offset: int` | `min_bar_spacing: float` | `visible: bool` | `time_visible: bool` | `seconds_visible: bool` | `border_visible: bool` | `border_color: str`
|
||||
|
||||
Time scale options for the chart.
|
||||
Timescale options for the chart.
|
||||
___
|
||||
|
||||
### `layout`
|
||||
@ -212,7 +212,7 @@ Creates and returns a [SubChart](#subchart) object, placing it adjacent to the d
|
||||
|
||||
`height` | `width`: Specifies the size of the `SubChart`, where `1` is the width/height of the window (100%)
|
||||
|
||||
`sync`: If given as `True`, the `SubChart`'s time scale will follow that of the declaring `Chart` or `SubChart`. If a `str` is passed, the `SubChart` will follow the panel with the given id. Chart ids can be accessed from the`chart.id` and `subchart.id` attributes.
|
||||
`sync`: If given as `True`, the `SubChart`'s timescale and crosshair will follow that of the declaring `Chart` or `SubChart`. If a `str` is passed, the `SubChart` will follow the panel with the given id. Chart ids can be accessed from the`chart.id` and `subchart.id` attributes.
|
||||
|
||||
```{important}
|
||||
`width` and `height` must be given as a number between 0 and 1.
|
||||
@ -330,9 +330,9 @@ from lightweight_charts import Chart
|
||||
|
||||
if __name__ == '__main__':
|
||||
chart = Chart(inner_width=1, inner_height=0.8)
|
||||
chart.time_scale(visible=False)
|
||||
|
||||
chart2 = chart.create_subchart(width=1, height=0.2, sync=True, volume_enabled=False)
|
||||
chart2.time_scale(visible=False)
|
||||
|
||||
df = pd.read_csv('ohlcv.csv')
|
||||
df2 = pd.read_csv('rsi.csv')
|
||||
|
||||
@ -123,17 +123,18 @@ class SeriesCommon:
|
||||
|
||||
|
||||
class Line(SeriesCommon):
|
||||
def __init__(self, parent, color, width):
|
||||
def __init__(self, parent, color, width, price_line, price_label):
|
||||
self._parent = parent
|
||||
self._rand = self._parent._rand
|
||||
self.id = f'window.{self._rand.generate()}'
|
||||
self.run_script = self._parent.run_script
|
||||
|
||||
self._parent.run_script(f'''
|
||||
self.run_script(f'''
|
||||
{self.id} = {{
|
||||
series: {self._parent.id}.chart.addLineSeries({{
|
||||
color: '{color}',
|
||||
lineWidth: {width},
|
||||
lastValueVisible: {_js_bool(price_label)},
|
||||
priceLineVisible: {_js_bool(price_line)},
|
||||
}}),
|
||||
markers: [],
|
||||
horizontal_lines: [],
|
||||
@ -240,6 +241,7 @@ class LWC(SeriesCommon):
|
||||
self.loaded = False
|
||||
self._html = HTML
|
||||
self._scripts = []
|
||||
self._final_scripts = []
|
||||
self._script_func = None
|
||||
self._last_bar = None
|
||||
self._interval = None
|
||||
@ -259,6 +261,7 @@ class LWC(SeriesCommon):
|
||||
return
|
||||
self.loaded = True
|
||||
[self.run_script(script) for script in self._scripts]
|
||||
[self.run_script(script) for script in self._final_scripts]
|
||||
|
||||
def _create_chart(self, autosize=True):
|
||||
self.run_script(f'''
|
||||
@ -270,11 +273,14 @@ class LWC(SeriesCommon):
|
||||
def _make_search_box(self):
|
||||
self.run_script(f'{self.id}.search = makeSearchBox({self.id}, {self._js_api_code})')
|
||||
|
||||
def run_script(self, script):
|
||||
def run_script(self, script, run_last=False):
|
||||
"""
|
||||
For advanced users; evaluates JavaScript within the Webview.
|
||||
"""
|
||||
self._script_func(script) if self.loaded else self._scripts.append(script)
|
||||
if self.loaded:
|
||||
self._script_func(script)
|
||||
return
|
||||
self._scripts.append(script) if not run_last else self._final_scripts.append(script)
|
||||
|
||||
def set(self, df: pd.DataFrame):
|
||||
"""
|
||||
@ -409,11 +415,12 @@ class LWC(SeriesCommon):
|
||||
bar['volume'] = 0
|
||||
self.update(bar, from_tick=True)
|
||||
|
||||
def create_line(self, color: str = 'rgba(214, 237, 255, 0.6)', width: int = 2) -> Line:
|
||||
def create_line(self, color: str = 'rgba(214, 237, 255, 0.6)', width: int = 2,
|
||||
price_line: bool = True, price_label: bool = True) -> Line:
|
||||
"""
|
||||
Creates and returns a Line object.)\n
|
||||
"""
|
||||
self._lines.append(Line(self, color, width))
|
||||
self._lines.append(Line(self, color, width, price_line, price_label))
|
||||
return self._lines[-1]
|
||||
|
||||
def lines(self):
|
||||
@ -424,9 +431,10 @@ class LWC(SeriesCommon):
|
||||
return self._lines
|
||||
|
||||
def price_scale(self, mode: PRICE_SCALE_MODE = 'normal', align_labels: bool = True, border_visible: bool = False,
|
||||
border_color: str = None, text_color: str = None, entire_text_only: bool = False, ticks_visible: bool = False):
|
||||
border_color: str = None, text_color: str = None, entire_text_only: bool = False,
|
||||
ticks_visible: bool = False, scale_margin_top: float = 0.2, scale_margin_bottom: float = 0.2):
|
||||
self.run_script(f'''
|
||||
{self.id}.chart.priceScale('right').applyOptions({{
|
||||
{self.id}.series.priceScale().applyOptions({{
|
||||
mode: {_price_scale_mode(mode)},
|
||||
alignLabels: {_js_bool(align_labels)},
|
||||
borderVisible: {_js_bool(border_visible)},
|
||||
@ -434,6 +442,7 @@ class LWC(SeriesCommon):
|
||||
{f'textColor: "{text_color}",' if text_color else ''}
|
||||
entireTextOnly: {_js_bool(entire_text_only)},
|
||||
ticksVisible: {_js_bool(ticks_visible)},
|
||||
scaleMargins: {{top: {scale_margin_top}, bottom: {scale_margin_bottom}}}
|
||||
}})''')
|
||||
|
||||
def time_scale(self, right_offset: int = 0, min_bar_spacing: float = 0.5,
|
||||
@ -639,8 +648,11 @@ class SubChart(LWC):
|
||||
{sync_parent_id}.chart.timeScale().subscribeVisibleLogicalRangeChange((timeRange) => {{
|
||||
{self.id}.chart.timeScale().setVisibleLogicalRange(timeRange)
|
||||
}});
|
||||
{self.id}.chart.timeScale().setVisibleLogicalRange({sync_parent_id}.chart.timeScale().getVisibleLogicalRange())
|
||||
syncCrosshairs({self.id}.chart, {sync_parent_id}.chart)
|
||||
''')
|
||||
self.run_script(f'''
|
||||
{self.id}.chart.timeScale().setVisibleLogicalRange({sync_parent_id}.chart.timeScale().getVisibleLogicalRange())
|
||||
''', run_last=True)
|
||||
|
||||
|
||||
SCRIPT = """
|
||||
@ -698,7 +710,10 @@ function makeChart(innerWidth, innerHeight, autoSize=true) {
|
||||
priceFormat: {type: 'volume'},
|
||||
priceScaleId: '',
|
||||
})
|
||||
chart.chart.priceScale('').applyOptions({
|
||||
chart.series.priceScale().applyOptions({
|
||||
scaleMargins: {top: 0.2, bottom: 0.2},
|
||||
});
|
||||
chart.volumeSeries.priceScale().applyOptions({
|
||||
scaleMargins: {top: 0.8, bottom: 0},
|
||||
});
|
||||
chart.legend.style.position = 'absolute'
|
||||
@ -751,6 +766,62 @@ function makeHorizontalLine(chart, lineId, price, color, width, style, axisLabel
|
||||
function legendItemFormat(num) {
|
||||
return num.toFixed(2).toString().padStart(8, ' ')
|
||||
}
|
||||
function syncCrosshairs(childChart, parentChart) {
|
||||
let parent = 0
|
||||
let child = 0
|
||||
|
||||
let parentCrosshairHandler = (e) => {
|
||||
parent ++
|
||||
if (parent < 10) {
|
||||
return
|
||||
}
|
||||
child = 0
|
||||
parentChart.applyOptions({crosshair: { horzLine: {
|
||||
visible: true,
|
||||
labelVisible: true,
|
||||
}}})
|
||||
childChart.applyOptions({crosshair: { horzLine: {
|
||||
visible: false,
|
||||
labelVisible: false,
|
||||
}}})
|
||||
|
||||
childChart.unsubscribeCrosshairMove(childCrosshairHandler)
|
||||
if (e.time !== undefined) {
|
||||
let xx = childChart.timeScale().timeToCoordinate(e.time);
|
||||
childChart.setCrosshairXY(xx,300,true);
|
||||
} else if (e.point !== undefined){
|
||||
childChart.setCrosshairXY(e.point.x,300,false);
|
||||
}
|
||||
childChart.subscribeCrosshairMove(childCrosshairHandler)
|
||||
}
|
||||
|
||||
let childCrosshairHandler = (e) => {
|
||||
child ++
|
||||
if (child < 10) {
|
||||
return
|
||||
}
|
||||
parent = 0
|
||||
childChart.applyOptions({crosshair: {horzLine: {
|
||||
visible: true,
|
||||
labelVisible: true,
|
||||
}}})
|
||||
parentChart.applyOptions({crosshair: {horzLine: {
|
||||
visible: false,
|
||||
labelVisible: false,
|
||||
}}})
|
||||
|
||||
parentChart.unsubscribeCrosshairMove(parentCrosshairHandler)
|
||||
if (e.time !== undefined) {
|
||||
let xx = parentChart.timeScale().timeToCoordinate(e.time);
|
||||
parentChart.setCrosshairXY(xx,300,true);
|
||||
} else if (e.point !== undefined){
|
||||
parentChart.setCrosshairXY(e.point.x,300,false);
|
||||
}
|
||||
parentChart.subscribeCrosshairMove(parentCrosshairHandler)
|
||||
}
|
||||
parentChart.subscribeCrosshairMove(parentCrosshairHandler)
|
||||
childChart.subscribeCrosshairMove(childCrosshairHandler)
|
||||
}
|
||||
"""
|
||||
|
||||
HTML = f"""
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -96,7 +96,7 @@ class PolygonAPI:
|
||||
for child in self._lasts.values():
|
||||
for subbed_chart in child['charts']:
|
||||
if subbed_chart == chart:
|
||||
self._send_q.put(('_unsubscribe', chart, sec_type, ticker))
|
||||
self._send_q.put(('_unsubscribe', chart, ticker))
|
||||
|
||||
df = pd.DataFrame(data['results'])
|
||||
columns = ['t', 'o', 'h', 'l', 'c']
|
||||
@ -262,16 +262,6 @@ class PolygonChart(Chart):
|
||||
|
||||
{self.id}.search.window.style.display = "block"
|
||||
{self.id}.search.box.focus()
|
||||
|
||||
window.stat = document.createElement('div')
|
||||
window.stat.style.position = 'absolute'
|
||||
window.stat.style.backgroundColor = '#E35C58'
|
||||
window.stat.style.borderRadius = '50%'
|
||||
window.stat.style.height = '8px'
|
||||
window.stat.style.width = '8px'
|
||||
window.stat.style.top = '10px'
|
||||
window.stat.style.right = '25px'
|
||||
{self.id}.topBar.appendChild(window.stat)
|
||||
''')
|
||||
|
||||
def show(self):
|
||||
@ -289,11 +279,12 @@ class PolygonChart(Chart):
|
||||
|
||||
mult, span = _convert_timeframe(self.topbar['timeframe'].value)
|
||||
delta = dt.timedelta(**{span + 's': int(mult)})
|
||||
short_delta = (delta < dt.timedelta(days=7))
|
||||
start_date = dt.datetime.now()
|
||||
remaining_bars = self.num_bars
|
||||
while remaining_bars > 0:
|
||||
start_date -= delta
|
||||
if start_date.weekday() > 4: # Monday to Friday (0 to 4)
|
||||
if start_date.weekday() > 4 and short_delta: # Monday to Friday (0 to 4)
|
||||
continue
|
||||
remaining_bars -= 1
|
||||
epoch = dt.datetime.fromtimestamp(0)
|
||||
@ -307,11 +298,7 @@ class PolygonChart(Chart):
|
||||
)
|
||||
self.spinner(False)
|
||||
self.crosshair(vert_visible=True, horz_visible=True) if success else None
|
||||
if not success:
|
||||
self.run_script(f'window.stat.style.backgroundColor = "#E35C58"')
|
||||
return False
|
||||
self.run_script(f'window.stat.style.backgroundColor = "#4CDE67"') if self.live else None
|
||||
return True
|
||||
return True if success else False
|
||||
|
||||
async def on_search(self, searched_string):
|
||||
self.topbar['symbol'].set(searched_string if self._polygon(searched_string) else '')
|
||||
|
||||
@ -116,12 +116,18 @@ class StaticLWC(LWC):
|
||||
self.height = height
|
||||
self._html = self._html.replace('</script>\n</body>\n</html>', '')
|
||||
|
||||
def run_script(self, script): self._html += '\n' + script
|
||||
def run_script(self, script, run_last=False):
|
||||
if run_last:
|
||||
self._final_scripts.append(script)
|
||||
else:
|
||||
self._html += '\n' + script
|
||||
|
||||
def load(self):
|
||||
if self.loaded:
|
||||
return
|
||||
self.loaded = True
|
||||
for script in self._final_scripts:
|
||||
self._html += '\n' + script
|
||||
self._load()
|
||||
|
||||
def _load(self): pass
|
||||
|
||||
Reference in New Issue
Block a user