- 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:
louisnw
2023-06-10 23:15:10 +01:00
parent e537ae7e9d
commit adfc58a8af
7 changed files with 103 additions and 39 deletions

View File

@ -1,7 +1,7 @@
project = 'lightweight-charts-python'
copyright = '2023, louisnw'
author = 'louisnw'
release = '1.0.11'
release = '1.0.12'
extensions = ["myst_parser"]

View File

@ -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,10 +330,10 @@ 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')

View File

@ -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

View File

@ -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 '')

View File

@ -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

View File

@ -5,7 +5,7 @@ with open('README.md', 'r', encoding='utf-8') as f:
setup(
name='lightweight_charts',
version='1.0.11',
version='1.0.12',
packages=find_packages(),
python_requires='>=3.9',
install_requires=[