From d43e7c24e7565d1d333bfa4ad2618d851860f6a5 Mon Sep 17 00:00:00 2001 From: louisnw Date: Sun, 24 Sep 2023 15:09:45 +0100 Subject: [PATCH] Enhancements: - Hotkeys can now use any character, and modifier keys are not required. - Refactored the colors of the topbar, searchbox, toolbox, and widgets for consistency. - Toolbox/interval refactoring and simplification. - Histograms now show up in the legend, and will use shorthand notation by default (e.g 34k rather than 34000). --- docs/source/conf.py | 2 +- docs/source/examples/subchart.md | 45 +++++ lightweight_charts/abstract.py | 75 +++++-- lightweight_charts/js/callback.js | 56 +++--- lightweight_charts/js/funcs.js | 39 +++- lightweight_charts/js/toolbox.js | 318 +++++++++++++----------------- lightweight_charts/polygon.py | 8 +- lightweight_charts/topbar.py | 14 +- setup.py | 2 +- 9 files changed, 303 insertions(+), 256 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 7959ad9..17f9989 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -3,7 +3,7 @@ import os.path project = 'lightweight-charts-python' copyright = '2023, louisnw' author = 'louisnw' -release = '1.0.17.2' +release = '1.0.17.6' extensions = [ "myst_parser", diff --git a/docs/source/examples/subchart.md b/docs/source/examples/subchart.md index 3ddd397..91ec837 100644 --- a/docs/source/examples/subchart.md +++ b/docs/source/examples/subchart.md @@ -27,6 +27,7 @@ if __name__ == '__main__': ``` ___ + ## Synced Line Chart ```python @@ -48,3 +49,47 @@ if __name__ == '__main__': chart.show(block=True) ``` +___ + +## Grid of 4 with maximize buttons + +```python +import pandas as pd +from lightweight_charts import Chart + +# ascii symbols +FULLSCREEN = '■' +CLOSE = '×' + + +def on_max(target_chart): + button = target_chart.topbar['max'] + if button.value == CLOSE: + [c.resize(0.5, 0.5) for c in charts] + button.set(FULLSCREEN) + else: + for chart in charts: + width, height = (1, 1) if chart == target_chart else (0, 0) + chart.resize(width, height) + button.set(CLOSE) + + +if __name__ == '__main__': + main_chart = Chart(inner_width=0.5, inner_height=0.5) + charts = [ + main_chart, + main_chart.create_subchart(position='top', width=0.5, height=0.5), + main_chart.create_subchart(position='left', width=0.5, height=0.5), + main_chart.create_subchart(position='right', width=0.5, height=0.5), + ] + + df = pd.read_csv('examples/1_setting_data/ohlcv.csv') + for i, c in enumerate(charts): + chart_number = str(i+1) + c.watermark(chart_number) + c.topbar.textbox('number', chart_number) + c.topbar.button('max', FULLSCREEN, False, align='right', func=on_max) + c.set(df) + + charts[0].show(block=True) +``` diff --git a/lightweight_charts/abstract.py b/lightweight_charts/abstract.py index a812a6a..fda20a2 100644 --- a/lightweight_charts/abstract.py +++ b/lightweight_charts/abstract.py @@ -105,6 +105,23 @@ class Window: ''', run_last=True) return subchart + def style(self, background_color: str = '#0c0d0f', hover_background_color: str = '#3c434c', + click_background_color: str = '#50565E', + active_background_color: str = 'rgba(0, 122, 255, 0.7)', + muted_background_color: str = 'rgba(0, 122, 255, 0.3)', + border_color: str = '#3C434C', color: str = '#d8d9db', active_color: str = '#ececed'): + self.run_script(f''' + window.pane = {{ + backgroundColor: '{background_color}', + hoverBackgroundColor: '{hover_background_color}', + clickBackgroundColor: '{click_background_color}', + activeBackgroundColor: '{active_background_color}', + mutedBackgroundColor: '{muted_background_color}', + borderColor: '{border_color}', + color: '{color}', + activeColor: '{active_color}', + }}''') + class SeriesCommon(Pane): def __init__(self, chart: 'AbstractChart', name: str = None): @@ -113,7 +130,7 @@ class SeriesCommon(Pane): if hasattr(chart, '_interval'): self._interval = chart._interval else: - self._interval = pd.Timedelta(seconds=1) + self._interval = 1 self._last_bar = None self.name = name self.num_decimals = 2 @@ -124,11 +141,36 @@ class SeriesCommon(Pane): common_interval = df['time'].diff().value_counts() if common_interval.empty: return - self._interval = common_interval.index[0] + self._interval = common_interval.index[0].total_seconds() + + units = [ + pd.Timedelta(microseconds=df['time'].dt.microsecond.value_counts().index[0]), + pd.Timedelta(seconds=df['time'].dt.second.value_counts().index[0]), + pd.Timedelta(minutes=df['time'].dt.minute.value_counts().index[0]), + pd.Timedelta(hours=df['time'].dt.hour.value_counts().index[0]), + pd.Timedelta(days=df['time'].dt.day.value_counts().index[0]), + ] + self.offset = 0 + for value in units: + value = value.total_seconds() + if value == 0: + continue + elif value > self._interval: + break + self.offset = value + break + self.run_script( - f'if ({self.id}.toolBox) {self.id}.interval = {self._interval.total_seconds() * 1000}' + f'if ({self.id}.toolBox) {self.id}.interval = {self._interval}' ) + def _push_to_legend(self): + self.run_script(f''' + {self._chart.id}.lines.push({self.id}) + if ('legend' in {self._chart.id}) {{ + {self._chart.id}.legend.lines.push({self._chart.id}.legend.makeLineRow({self.id})) + }}''') + @staticmethod def _format_labels(data, labels, index, exclude_lowercase): def rename(la, mapper): @@ -162,8 +204,7 @@ class SeriesCommon(Pane): def _single_datetime_format(self, arg): if isinstance(arg, (str, int, float)) or not pd.api.types.is_datetime64_any_dtype(arg): arg = pd.to_datetime(arg) - interval_seconds = self._interval.total_seconds() - arg = interval_seconds * (arg.timestamp() // interval_seconds) + arg = self._interval * (arg.timestamp() // self._interval)+self.offset return arg def set(self, df: pd.DataFrame = None, format_cols: bool = True): @@ -186,7 +227,6 @@ class SeriesCommon(Pane): self._last_bar = series self.run_script(f'{self.id}.series.update({js_data(series)})') - def marker(self, time: datetime = None, position: MARKER_POSITION = 'below', shape: MARKER_SHAPE = 'arrow_up', color: str = '#2196F3', text: str = '' ) -> str: @@ -362,8 +402,7 @@ class VerticalSpan(Pane): else: self.run_script(f''' {self.id}.setData(calculateTrendLine( - {start_time.timestamp()}, 1, {end_time.timestamp()}, 1, - {chart._interval.total_seconds() * 1000}, {chart.id})) + {start_time.timestamp()}, 1, {end_time.timestamp()}, 1, {chart.id})) ''') def delete(self): @@ -401,13 +440,6 @@ class Line(SeriesCommon): }} null''') - def _push_to_legend(self): - self.run_script(f''' - {self._chart.id}.lines.push({self.id}) - if ('legend' in {self._chart.id}) {{ - {self._chart.id}.legend.lines.push({self._chart.id}.legend.makeLineRow({self.id})) - }}''') - def _set_trend(self, start_time, start_value, end_time, end_value, ray=False, round=False): if round: start_time = self._single_datetime_format(start_time) @@ -418,9 +450,7 @@ class Line(SeriesCommon): self.run_script(f''' {self._chart.id}.chart.timeScale().applyOptions({{shiftVisibleRangeOnNewBar: false}}) {self.id}.series.setData( - calculateTrendLine({start_time}, {start_value}, - {end_time}, {end_value}, - {self._chart._interval.total_seconds() * 1000}, + calculateTrendLine({start_time}, {start_value}, {end_time}, {end_value}, {self._chart.id}, {jbool(ray)})) {self._chart.id}.chart.timeScale().applyOptions({{shiftVisibleRangeOnNewBar: true}}) ''') @@ -451,7 +481,8 @@ class Histogram(SeriesCommon): color: '{color}', lastValueVisible: {jbool(price_label)}, priceLineVisible: {jbool(price_line)}, - priceScaleId: '{self.id}' + priceScaleId: '{self.id}', + priceFormat: {{type: "volume"}}, }}), markers: [], horizontal_lines: [], @@ -681,7 +712,11 @@ class AbstractChart(Candlestick, Pane): """ Creates and returns a Histogram object. """ - return Histogram(self, name, color, price_line, price_label, scale_margin_top, scale_margin_bottom) + histogram = Histogram( + self, name, color, price_line, price_label, + scale_margin_top, scale_margin_bottom) + histogram._push_to_legend() + return histogram def lines(self) -> List[Line]: """ diff --git a/lightweight_charts/js/callback.js b/lightweight_charts/js/callback.js index f3bfd10..be7d0ed 100644 --- a/lightweight_charts/js/callback.js +++ b/lightweight_charts/js/callback.js @@ -1,16 +1,10 @@ if (!window.TopBar) { class TopBar { - constructor(chart, hoverBackgroundColor, clickBackgroundColor, activeBackgroundColor, textColor, activeTextColor) { + constructor(chart) { this.makeSwitcher = this.makeSwitcher.bind(this) - this.hoverBackgroundColor = hoverBackgroundColor - this.clickBackgroundColor = clickBackgroundColor - this.activeBackgroundColor = activeBackgroundColor - this.textColor = textColor - this.activeTextColor = activeTextColor - this.topBar = document.createElement('div') - this.topBar.style.backgroundColor = '#0c0d0f' - this.topBar.style.borderBottom = '2px solid #3C434C' + this.topBar.style.backgroundColor = pane.backgroundColor + this.topBar.style.borderBottom = '2px solid '+pane.borderColor this.topBar.style.display = 'flex' this.topBar.style.alignItems = 'center' @@ -45,17 +39,17 @@ if (!window.TopBar) { itemEl.style.margin = '0px 2px' itemEl.style.fontSize = '13px' itemEl.style.borderRadius = '4px' - itemEl.style.backgroundColor = item === activeItem ? this.activeBackgroundColor : 'transparent' - itemEl.style.color = item === activeItem ? this.activeTextColor : this.textColor + itemEl.style.backgroundColor = item === activeItem ? pane.activeBackgroundColor : 'transparent' + itemEl.style.color = item === activeItem ? pane.activeColor : pane.color itemEl.innerText = item; document.body.appendChild(itemEl) itemEl.style.minWidth = itemEl.clientWidth + 1 + 'px' document.body.removeChild(itemEl) - itemEl.addEventListener('mouseenter', () => itemEl.style.backgroundColor = item === activeItem ? this.activeBackgroundColor : this.hoverBackgroundColor) - itemEl.addEventListener('mouseleave', () => itemEl.style.backgroundColor = item === activeItem ? this.activeBackgroundColor : 'transparent') - itemEl.addEventListener('mousedown', () => itemEl.style.backgroundColor = item === activeItem ? this.activeBackgroundColor : this.clickBackgroundColor) - itemEl.addEventListener('mouseup', () => itemEl.style.backgroundColor = item === activeItem ? this.activeBackgroundColor : this.hoverBackgroundColor) + itemEl.addEventListener('mouseenter', () => itemEl.style.backgroundColor = item === activeItem ? pane.activeBackgroundColor : pane.hoverBackgroundColor) + itemEl.addEventListener('mouseleave', () => itemEl.style.backgroundColor = item === activeItem ? pane.activeBackgroundColor : 'transparent') + itemEl.addEventListener('mousedown', () => itemEl.style.backgroundColor = item === activeItem ? pane.activeBackgroundColor : pane.clickBackgroundColor) + itemEl.addEventListener('mouseup', () => itemEl.style.backgroundColor = item === activeItem ? pane.activeBackgroundColor : pane.hoverBackgroundColor) itemEl.addEventListener('click', () => onItemClicked(item)) switcherElement.appendChild(itemEl); @@ -66,8 +60,8 @@ if (!window.TopBar) { let onItemClicked = (item)=> { if (item === activeItem) return intervalElements.forEach((element, index) => { - element.style.backgroundColor = items[index] === item ? this.activeBackgroundColor : 'transparent' - element.style.color = items[index] === item ? this.activeTextColor : this.textColor + element.style.backgroundColor = items[index] === item ? pane.activeBackgroundColor : 'transparent' + element.style.color = items[index] === item ? pane.activeColor : pane.color element.style.fontWeight = items[index] === item ? '500' : 'normal' }) activeItem = item; @@ -80,7 +74,7 @@ if (!window.TopBar) { let textBox = document.createElement('div') textBox.style.margin = '0px 18px' textBox.style.fontSize = '16px' - textBox.style.color = 'rgb(220, 220, 220)' + textBox.style.color = pane.color textBox.innerText = text this.appendWidget(textBox, align, true) return textBox @@ -91,9 +85,9 @@ if (!window.TopBar) { menu.style.position = 'absolute' menu.style.display = 'none' menu.style.zIndex = '100000' - menu.style.backgroundColor = '#0c0d0f' + menu.style.backgroundColor = pane.backgroundColor menu.style.borderRadius = '2px' - menu.style.border = '2px solid #3C434C' + menu.style.border = '2px solid '+pane.borderColor menu.style.borderTop = 'none' menu.style.alignItems = 'flex-start' @@ -134,7 +128,7 @@ if (!window.TopBar) { button.style.margin = '4px 10px' button.style.fontSize = '13px' button.style.backgroundColor = 'transparent' - button.style.color = this.textColor + button.style.color = pane.color button.style.borderRadius = '4px' button.innerText = defaultText; document.body.appendChild(button) @@ -146,19 +140,19 @@ if (!window.TopBar) { callbackName: callbackName } - button.addEventListener('mouseenter', () => button.style.backgroundColor = this.hoverBackgroundColor) + button.addEventListener('mouseenter', () => button.style.backgroundColor = pane.hoverBackgroundColor) button.addEventListener('mouseleave', () => button.style.backgroundColor = 'transparent') if (callbackName) { button.addEventListener('click', () => window.callbackFunction(`${widget.callbackName}_~_${button.innerText}`)); } button.addEventListener('mousedown', () => { - button.style.backgroundColor = this.activeBackgroundColor - button.style.color = this.activeTextColor + button.style.backgroundColor = pane.activeBackgroundColor + button.style.color = pane.activeColor button.style.fontWeight = '500' }) button.addEventListener('mouseup', () => { - button.style.backgroundColor = this.hoverBackgroundColor - button.style.color = this.textColor + button.style.backgroundColor = pane.hoverBackgroundColor + button.style.color = pane.color button.style.fontWeight = 'normal' }) if (append) this.appendWidget(button, align, separator) @@ -169,7 +163,7 @@ if (!window.TopBar) { let seperator = document.createElement('div') seperator.style.width = '1px' seperator.style.height = '20px' - seperator.style.backgroundColor = '#3C434C' + seperator.style.backgroundColor = pane.borderColor let div = align === 'left' ? this.left : this.right div.appendChild(seperator) } @@ -201,7 +195,7 @@ function makeSearchBox(chart) { searchWindow.style.zIndex = '1000' searchWindow.style.alignItems = 'center' searchWindow.style.backgroundColor = 'rgba(30, 30, 30, 0.9)' - searchWindow.style.border = '2px solid #3C434C' + searchWindow.style.border = '2px solid '+pane.borderColor searchWindow.style.borderRadius = '5px' searchWindow.style.display = 'none' @@ -213,8 +207,8 @@ function makeSearchBox(chart) { sBox.style.textAlign = 'center' sBox.style.width = '100px' sBox.style.marginLeft = '10px' - sBox.style.backgroundColor = 'rgba(0, 122, 255, 0.3)' - sBox.style.color = 'rgb(240,240,240)' + sBox.style.backgroundColor = pane.mutedBackgroundColor + sBox.style.color = pane.activeColor sBox.style.fontSize = '20px' sBox.style.border = 'none' sBox.style.outline = 'none' @@ -260,7 +254,7 @@ function makeSpinner(chart) { chart.spinner.style.width = '30px' chart.spinner.style.height = '30px' chart.spinner.style.border = '4px solid rgba(255, 255, 255, 0.6)' - chart.spinner.style.borderTop = '4px solid rgba(0, 122, 255, 0.8)' + chart.spinner.style.borderTop = '4px solid '+pane.activeBackgroundColor chart.spinner.style.borderRadius = '50%' chart.spinner.style.position = 'absolute' chart.spinner.style.top = '50%' diff --git a/lightweight_charts/js/funcs.js b/lightweight_charts/js/funcs.js index 7db60de..364f063 100644 --- a/lightweight_charts/js/funcs.js +++ b/lightweight_charts/js/funcs.js @@ -1,4 +1,15 @@ if (!window.Chart) { + window.pane = { + backgroundColor: '#0c0d0f', + hoverBackgroundColor: '#3c434c', + clickBackgroundColor: '#50565E', + activeBackgroundColor: 'rgba(0, 122, 255, 0.7)', + mutedBackgroundColor: 'rgba(0, 122, 255, 0.3)', + borderColor: '#3C434C', + color: '#d8d9db', + activeColor: '#ececed', + } + class Chart { constructor(chartId, innerWidth, innerHeight, position, autoSize) { this.makeCandlestickSeries = this.makeCandlestickSeries.bind(this) @@ -17,7 +28,7 @@ if (!window.Chart) { width: window.innerWidth * innerWidth, height: window.innerHeight * innerHeight, layout: { - textColor: '#d1d4dc', + textColor: pane.color, background: { color: '#000000', type: LightweightCharts.ColorType.Solid, @@ -48,7 +59,7 @@ if (!window.Chart) { this.wrapper.style.position = 'relative' this.wrapper.style.float = position this.div.style.position = 'relative' - this.div.style.display = 'flex' + // this.div.style.display = 'flex' this.reSize() this.wrapper.appendChild(this.div) document.getElementById('wrapper').append(this.wrapper) @@ -196,9 +207,10 @@ if (!window.Chart) { let legendItemFormat = (num, decimal) => num.toFixed(decimal).toString().padStart(8, ' ') let shorthandFormat = (num) => { - if (num >= 1000000) { + const absNum = Math.abs(num) + if (absNum >= 1000000) { return (num / 1000000).toFixed(1) + 'M'; - } else if (num >= 1000) { + } else if (absNum >= 1000) { return (num / 1000).toFixed(1) + 'K'; } return num.toString().padStart(8, ' '); @@ -225,7 +237,14 @@ if (!window.Chart) { this.candle.innerHTML = finalString + '' this.lines.forEach((line) => { if (!param.seriesData.get(line.line.series)) return - let price = legendItemFormat(param.seriesData.get(line.line.series).value, line.line.precision) + let price = param.seriesData.get(line.line.series).value + + if (line.line.series._series._seriesType === 'Histogram') { + price = shorthandFormat(price) + } + else { + price = legendItemFormat(price, line.line.precision) + } line.div.innerHTML = ` ${line.line.name} : ${price}` }) @@ -381,7 +400,7 @@ function lastBar(obj) { return obj[obj.length-1] } -function calculateTrendLine(startDate, startValue, endDate, endValue, interval, chart, ray=false) { +function calculateTrendLine(startDate, startValue, endDate, endValue, chart, ray=false) { let reversed = false if (stampToDate(endDate).getTime() < stampToDate(startDate).getTime()) { reversed = true; @@ -406,7 +425,7 @@ function calculateTrendLine(startDate, startValue, endDate, endValue, interval, else { endIndex = chart.candleData.findIndex(item => stampToDate(item.time).getTime() === stampToDate(endDate).getTime()) if (endIndex === -1) { - let barsBetween = (stampToDate(endDate)-stampToDate(chart.candleData[chart.candleData.length-1].time))/interval + let barsBetween = (endDate-lastBar(chart.candleData).time)/chart.interval endIndex = chart.candleData.length-1+barsBetween } } @@ -422,7 +441,7 @@ function calculateTrendLine(startDate, startValue, endDate, endValue, interval, } else { iPastData ++ - currentDate = dateToStamp(new Date(stampToDate(chart.candleData[chart.candleData.length-1].time).getTime()+(iPastData*interval))) + currentDate = lastBar(chart.candleData).time+(iPastData*chart.interval) } const currentValue = reversed ? startValue + rate_of_change * (numBars - i) : startValue + rate_of_change * i; @@ -439,7 +458,7 @@ if (!window.ContextMenu) { this.menu.style.position = 'absolute' this.menu.style.zIndex = '10000' this.menu.style.background = 'rgb(50, 50, 50)' - this.menu.style.color = '#ececed' + this.menu.style.color = pane.activeColor this.menu.style.display = 'none' this.menu.style.borderRadius = '5px' this.menu.style.padding = '3px 3px' @@ -508,7 +527,7 @@ if (!window.ContextMenu) { separator.style.width = '90%' separator.style.height = '1px' separator.style.margin = '3px 0px' - separator.style.backgroundColor = '#3C434C' + separator.style.backgroundColor = pane.borderColor this.menu.appendChild(separator) } diff --git a/lightweight_charts/js/toolbox.js b/lightweight_charts/js/toolbox.js index e4181c5..f0bd2bf 100644 --- a/lightweight_charts/js/toolbox.js +++ b/lightweight_charts/js/toolbox.js @@ -11,11 +11,7 @@ if (!window.ToolBox) { this.chart.cursor = 'default' this.makingDrawing = false - this.activeBackgroundColor = 'rgba(0, 122, 255, 0.7)' - this.activeIconColor = 'rgb(240, 240, 240)' - this.iconColor = 'lightgrey' - this.backgroundColor = 'transparent' - this.hoverColor = 'rgba(80, 86, 94, 0.7)' + this.hoverBackgroundColor = 'rgba(80, 86, 94, 0.7)' this.clickBackgroundColor = 'rgba(90, 106, 104, 0.7)' this.elem = this.makeToolBox() @@ -35,9 +31,8 @@ if (!window.ToolBox) { toolBoxElem.style.display = 'flex' toolBoxElem.style.alignItems = 'center' toolBoxElem.style.top = '25%' - toolBoxElem.style.borderRight = '2px solid #3C434C' - toolBoxElem.style.borderTop = '2px solid #3C434C' - toolBoxElem.style.borderBottom = '2px solid #3C434C' + toolBoxElem.style.border = '2px solid '+pane.borderColor + toolBoxElem.style.borderLeft = 'none' toolBoxElem.style.borderTopRightRadius = '4px' toolBoxElem.style.borderBottomRightRadius = '4px' toolBoxElem.style.backgroundColor = 'rgba(25, 27, 30, 0.5)' @@ -48,11 +43,11 @@ if (!window.ToolBox) { let trend = this.makeToolBoxElement(this.onTrendSelect, 'KeyT', ``) let horz = this.makeToolBoxElement(this.onHorzSelect, 'KeyH', ``) let ray = this.makeToolBoxElement(this.onRaySelect, 'KeyR', ``) - //let testB = this.makeToolBoxElement(this.onTrendSelect, ``) + // let testB = this.makeToolBoxElement(this.onTrendSelect, 'KeyB', ``) toolBoxElem.appendChild(trend) toolBoxElem.appendChild(horz) toolBoxElem.appendChild(ray) - //toolBoxElem.appendChild(testB) + // toolBoxElem.appendChild(testB) this.chart.div.append(toolBoxElem) @@ -63,7 +58,7 @@ if (!window.ToolBox) { } this.chart.commandFunctions.push((event) => { if ((event.metaKey || event.ctrlKey) && event.code === 'KeyZ') { - commandZHandler(this.drawings[this.drawings.length - 1]) + commandZHandler(lastBar(this.drawings)) return true } }); @@ -72,13 +67,10 @@ if (!window.ToolBox) { } makeToolBoxElement(action, keyCmd, paths) { - let icon = { - elem: document.createElement('div'), - action: action, - } - icon.elem.style.margin = '3px' - icon.elem.style.borderRadius = '4px' - icon.elem.style.display = 'flex' + let elem = document.createElement('div') + elem.style.margin = '3px' + elem.style.borderRadius = '4px' + elem.style.display = 'flex' let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("width", "29"); @@ -86,27 +78,29 @@ if (!window.ToolBox) { let group = document.createElementNS("http://www.w3.org/2000/svg", "g"); group.innerHTML = paths - group.setAttribute("fill", this.iconColor) + group.setAttribute("fill", pane.color) svg.appendChild(group) - icon.elem.appendChild(svg); + elem.appendChild(svg); - icon.elem.addEventListener('mouseenter', () => { - icon.elem.style.backgroundColor = icon === this.chart.activeIcon ? this.activeBackgroundColor : this.hoverColor + let icon = {elem: elem, action: action} + + elem.addEventListener('mouseenter', () => { + elem.style.backgroundColor = icon === this.chart.activeIcon ? pane.activeBackgroundColor : this.hoverBackgroundColor }) - icon.elem.addEventListener('mouseleave', () => { - icon.elem.style.backgroundColor = icon === this.chart.activeIcon ? this.activeBackgroundColor : this.backgroundColor + elem.addEventListener('mouseleave', () => { + elem.style.backgroundColor = icon === this.chart.activeIcon ? pane.activeBackgroundColor : 'transparent' }) - icon.elem.addEventListener('mousedown', () => { - icon.elem.style.backgroundColor = icon === this.chart.activeIcon ? this.activeBackgroundColor : this.clickBackgroundColor + elem.addEventListener('mousedown', () => { + elem.style.backgroundColor = icon === this.chart.activeIcon ? pane.activeBackgroundColor : this.clickBackgroundColor }) - icon.elem.addEventListener('mouseup', () => { - icon.elem.style.backgroundColor = icon === this.chart.activeIcon ? this.activeBackgroundColor : 'transparent' + elem.addEventListener('mouseup', () => { + elem.style.backgroundColor = icon === this.chart.activeIcon ? pane.activeBackgroundColor : 'transparent' }) - icon.elem.addEventListener('click', () => { + elem.addEventListener('click', () => { if (this.chart.activeIcon) { - this.chart.activeIcon.elem.style.backgroundColor = this.backgroundColor - group.setAttribute("fill", this.iconColor) + this.chart.activeIcon.elem.style.backgroundColor = 'transparent' + group.setAttribute("fill", pane.color) document.body.style.cursor = 'crosshair' this.chart.cursor = 'crosshair' this.chart.activeIcon.action(false) @@ -115,8 +109,8 @@ if (!window.ToolBox) { } } this.chart.activeIcon = icon - group.setAttribute("fill", this.activeIconColor) - icon.elem.style.backgroundColor = this.activeBackgroundColor + group.setAttribute("fill", pane.activeColor) + elem.style.backgroundColor = pane.activeBackgroundColor document.body.style.cursor = 'crosshair' this.chart.cursor = 'crosshair' this.chart.activeIcon.action(true) @@ -125,76 +119,56 @@ if (!window.ToolBox) { if (event.altKey && event.code === keyCmd) { event.preventDefault() if (this.chart.activeIcon) { - this.chart.activeIcon.elem.style.backgroundColor = this.backgroundColor - group.setAttribute("fill", this.iconColor) + this.chart.activeIcon.elem.style.backgroundColor = 'transparent' + group.setAttribute("fill", pane.color) document.body.style.cursor = 'crosshair' this.chart.cursor = 'crosshair' this.chart.activeIcon.action(false) } this.chart.activeIcon = icon - group.setAttribute("fill", this.activeIconColor) - icon.elem.style.backgroundColor = this.activeBackgroundColor + group.setAttribute("fill", pane.activeColor) + elem.style.backgroundColor = pane.activeBackgroundColor document.body.style.cursor = 'crosshair' this.chart.cursor = 'crosshair' this.chart.activeIcon.action(true) return true } }) - return icon.elem + return elem + } + + removeActiveAndSave() { + document.body.style.cursor = 'default' + this.chart.cursor = 'default' + this.chart.activeIcon.elem.style.backgroundColor = 'transparent' + this.chart.activeIcon = null + this.saveDrawings() } onTrendSelect(toggle, ray = false) { - let trendLine = { - line: null, - color: 'rgb(15, 139, 237)', - markers: null, - data: null, - from: null, - to: null, - ray: ray, - } + let trendLine = null let firstTime = null let firstPrice = null let currentTime = null - if (!toggle) { - this.chart.chart.unsubscribeClick(this.clickHandler) - return + return this.chart.chart.unsubscribeClick(this.clickHandler) } let crosshairHandlerTrend = (param) => { this.chart.chart.unsubscribeCrosshairMove(crosshairHandlerTrend) if (!this.makingDrawing) return - this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: false}) - let logical = this.chart.chart.timeScale().getVisibleLogicalRange() - let lastCandleTime = this.chart.candleData[this.chart.candleData.length - 1].time currentTime = this.chart.chart.timeScale().coordinateToTime(param.point.x) if (!currentTime) { - let barsToMove = param.logical - this.chart.chart.timeScale().coordinateToLogical(this.chart.chart.timeScale().timeToCoordinate(lastCandleTime)) - currentTime = dateToStamp(new Date(stampToDate(this.chart.candleData[this.chart.candleData.length - 1].time).getTime() + (barsToMove * this.chart.interval))) + let barsToMove = param.logical - this.chart.candleData.length-1 + currentTime = lastBar(this.chart.candleData).time+(barsToMove*this.chart.interval) } let currentPrice = this.chart.series.coordinateToPrice(param.point.y) - if (!currentTime) return this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend) - let data = calculateTrendLine(firstTime, firstPrice, currentTime, currentPrice, this.chart.interval, this.chart, ray) - trendLine.from = [data[0].time, data[0].value] - trendLine.to = [data[data.length - 1].time, data[data.length-1].value] + trendLine.calculateAndSet(firstTime, firstPrice, currentTime, currentPrice) - trendLine.line.setData(data) - - this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: true}) - this.chart.chart.timeScale().setVisibleLogicalRange(logical) - - if (!ray) { - trendLine.markers = [ - {time: trendLine.from[0], position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1}, - {time: trendLine.to[0], position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1} - ] - trendLine.line.setMarkers(trendLine.markers) - } setTimeout(() => { this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend) }, 10); @@ -203,21 +177,9 @@ if (!window.ToolBox) { this.clickHandler = (param) => { if (!this.makingDrawing) { this.makingDrawing = true - trendLine.line = this.chart.chart.addLineSeries({ - color: 'rgb(15, 139, 237)', - lineWidth: 2, - lastValueVisible: false, - priceLineVisible: false, - crosshairMarkerVisible: false, - autoscaleInfoProvider: () => ({ - priceRange: { - minValue: 1_000_000_000, - maxValue: 0, - }, - }), - }) + trendLine = new TrendLine(this.chart, 'rgb(15, 139, 237)', ray) firstPrice = this.chart.series.coordinateToPrice(param.point.y) - firstTime = !ray ? this.chart.chart.timeScale().coordinateToTime(param.point.x) : this.chart.candleData[this.chart.candleData.length - 1].time + firstTime = !ray ? this.chart.chart.timeScale().coordinateToTime(param.point.x) : lastBar(this.chart.candleData).time this.chart.chart.applyOptions({handleScroll: false}) this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend) } @@ -228,30 +190,23 @@ if (!window.ToolBox) { this.drawings.push(trendLine) this.chart.chart.unsubscribeCrosshairMove(crosshairHandlerTrend) this.chart.chart.unsubscribeClick(this.clickHandler) - document.body.style.cursor = 'default' - this.chart.cursor = 'default' - this.chart.activeIcon.elem.style.backgroundColor = this.backgroundColor - this.chart.activeIcon = null - this.saveDrawings() + this.removeActiveAndSave() } } this.chart.chart.subscribeClick(this.clickHandler) } - clickHandlerHorz = (param) => { + onHorzSelect(toggle) { + let clickHandlerHorz = (param) => { let price = this.chart.series.coordinateToPrice(param.point.y) let lineStyle = LightweightCharts.LineStyle.Solid let line = new HorizontalLine(this.chart, 'toolBox', price,'red', 2, lineStyle, true) this.drawings.push(line) - this.chart.chart.unsubscribeClick(this.clickHandlerHorz) - document.body.style.cursor = 'default' - this.chart.cursor = 'default' - this.chart.activeIcon.elem.style.backgroundColor = this.backgroundColor - this.chart.activeIcon = null - this.saveDrawings() + this.chart.chart.unsubscribeClick(clickHandlerHorz) + this.removeActiveAndSave() } - onHorzSelect(toggle) { - !toggle ? this.chart.chart.unsubscribeClick(this.clickHandlerHorz) : this.chart.chart.subscribeClick(this.clickHandlerHorz) + if (toggle) this.chart.chart.subscribeClick(clickHandlerHorz) + else this.chart.chart.unsubscribeClick(clickHandlerHorz) } onRaySelect(toggle) { @@ -382,8 +337,8 @@ if (!window.ToolBox) { let priceDiff = priceAtCursor - originalPrice let barsToMove = param.logical - originalIndex - let startBarIndex = this.chart.candleData.findIndex(item => stampToDate(item.time).getTime() === stampToDate(hoveringOver.from[0]).getTime()) - let endBarIndex = this.chart.candleData.findIndex(item => stampToDate(item.time).getTime() === stampToDate(hoveringOver.to[0]).getTime()) + let startBarIndex = this.chart.candleData.findIndex(item => item.time === hoveringOver.from[0]) + let endBarIndex = this.chart.candleData.findIndex(item => item.time === hoveringOver.to[0]) let startDate let endBar @@ -395,27 +350,11 @@ if (!window.ToolBox) { endBar = endBarIndex === -1 ? null : this.chart.candleData[endBarIndex + barsToMove] } - let endDate = endBar ? endBar.time : dateToStamp(new Date(stampToDate(hoveringOver.to[0]).getTime() + (barsToMove * this.chart.interval))) + let endDate = endBar ? endBar.time : hoveringOver.to[0] + (barsToMove * this.chart.interval) let startValue = hoveringOver.from[1] + priceDiff let endValue = hoveringOver.to[1] + priceDiff - let data = calculateTrendLine(startDate, startValue, endDate, endValue, this.chart.interval, this.chart, hoveringOver.ray) - - let logical = this.chart.chart.timeScale().getVisibleLogicalRange() - - hoveringOver.from = [data[0].time, data[0].value] - hoveringOver.to = [data[data.length - 1].time, data[data.length - 1].value] - hoveringOver.line.setData(data) - - this.chart.chart.timeScale().setVisibleLogicalRange(logical) - - if (!hoveringOver.ray) { - hoveringOver.markers = [ - {time: hoveringOver.from[0], position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1}, - {time: hoveringOver.to[0], position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1} - ] - hoveringOver.line.setMarkers(hoveringOver.markers) - } + hoveringOver.calculateAndSet(startDate, startValue, endDate, endValue) originalIndex = param.logical originalPrice = priceAtCursor @@ -438,29 +377,12 @@ if (!window.ToolBox) { firstPrice = hoveringOver.to[1] } - this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: false}) - let logical = this.chart.chart.timeScale().getVisibleLogicalRange() - - let lastCandleTime = this.chart.candleData[this.chart.candleData.length - 1].time if (!currentTime) { - let barsToMove = param.logical - this.chart.chart.timeScale().coordinateToLogical(this.chart.chart.timeScale().timeToCoordinate(lastCandleTime)) - currentTime = dateToStamp(new Date(stampToDate(this.chart.candleData[this.chart.candleData.length - 1].time).getTime() + (barsToMove * this.chart.interval))) + let barsToMove = param.logical - this.chart.candleData.length-1 + currentTime = lastBar(this.chart.candleData).time + (barsToMove*this.chart.interval) } - let data = calculateTrendLine(firstTime, firstPrice, currentTime, currentPrice, this.chart.interval, this.chart) - hoveringOver.line.setData(data) - this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: true}) - this.chart.chart.timeScale().setVisibleLogicalRange(logical) - - hoveringOver.from = [data[0].time, data[0].value] - hoveringOver.to = [data[data.length - 1].time, data[data.length - 1].value] - - - hoveringOver.markers = [ - {time: hoveringOver.from[0], position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1}, - {time: hoveringOver.to[0], position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1} - ] - hoveringOver.line.setMarkers(hoveringOver.markers) + hoveringOver.calculateAndSet(firstTime, firstPrice, currentTime, currentPrice) setTimeout(() => { this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend) @@ -481,12 +403,10 @@ if (!window.ToolBox) { renderDrawings() { this.drawings.forEach((item) => { if ('price' in item) return - let startDate = dateToStamp(new Date(Math.round(stampToDate(item.from[0]).getTime() / this.chart.interval) * this.chart.interval)) - let endDate = dateToStamp(new Date(Math.round(stampToDate(item.to[0]).getTime() / this.chart.interval) * this.chart.interval)) - let data = calculateTrendLine(startDate, item.from[1], endDate, item.to[1], this.chart.interval, this.chart, item.ray) - item.from = [data[0].time, data[0].value] - item.to = [data[data.length - 1].time, data[data.length-1].value] - item.line.setData(data) + console.log('rendering') + let startDate = Math.round(item.from[0]/this.chart.interval)*this.chart.interval + let endDate = Math.round(item.to[0]/this.chart.interval)*this.chart.interval + item.calculateAndSet(startDate, item.from[1], endDate, item.to[1]) }) } @@ -524,55 +444,97 @@ if (!window.ToolBox) { } loadDrawings(drawings) { - this.drawings = drawings - this.chart.chart.applyOptions({handleScroll: false}) - this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: false}) - this.drawings.forEach((item) => { - let idx = this.drawings.indexOf(item) + this.drawings = [] + drawings.forEach((item) => { + let drawing = null if ('price' in item) { - this.drawings[idx] = new HorizontalLine(this.chart, 'toolBox', item.priceLine.price, item.priceLine.color, 2, item.priceLine.lineStyle, item.priceLine.axisLabelVisible) + drawing = new HorizontalLine(this.chart, 'toolBox', + item.priceLine.price, item.priceLine.color, 2, + item.priceLine.lineStyle, item.priceLine.axisLabelVisible) } else { - this.drawings[idx].line = this.chart.chart.addLineSeries({ - lineWidth: 2, - color: this.drawings[idx].color, - lastValueVisible: false, - priceLineVisible: false, - crosshairMarkerVisible: false, - autoscaleInfoProvider: () => ({ - priceRange: { - minValue: 1_000_000_000, - maxValue: 0, - }, - }), - }) - let startDate = dateToStamp(new Date(Math.round(stampToDate(item.from[0]).getTime() / this.chart.interval) * this.chart.interval)) - let endDate = dateToStamp(new Date(Math.round(stampToDate(item.to[0]).getTime() / this.chart.interval) * this.chart.interval)) - let data = calculateTrendLine(startDate, item.from[1], endDate, item.to[1], this.chart.interval, this.chart, item.ray) - item.from = [data[0].time, data[0].value] - item.to = [data[data.length - 1].time, data[data.length-1].value] - item.line.setData(data) + let startDate = Math.round(item.from[0]/this.chart.interval)*this.chart.interval + let endDate = Math.round(item.to[0]/this.chart.interval)*this.chart.interval + + drawing = new TrendLine(this.chart, item.color, item.ray) + drawing.calculateAndSet(startDate, item.from[1], endDate, item.to[1]) } + this.drawings.push(drawing) }) - this.chart.chart.applyOptions({handleScroll: true}) - this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: true}) } } window.ToolBox = ToolBox + + class TrendLine { + constructor(chart, color, ray) { + this.calculateAndSet = this.calculateAndSet.bind(this) + + this.line = chart.chart.addLineSeries({ + color: color, + lineWidth: 2, + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: false, + autoscaleInfoProvider: () => ({ + priceRange: { + minValue: 1_000_000_000, + maxValue: 0, + }, + }), + }) + this.color = color + this.markers = null + this.data = null + this.from = null + this.to = null + this.ray = ray + this.chart = chart + } + + toJSON() { + // Exclude the chart attribute from serialization + const {chart, ...serialized} = this; + return serialized; + } + + calculateAndSet(firstTime, firstPrice, currentTime, currentPrice) { + let data = calculateTrendLine(firstTime, firstPrice, currentTime, currentPrice, this.chart, this.ray) + this.from = [data[0].time, data[0].value] + this.to = [data[data.length - 1].time, data[data.length-1].value] + + this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: false}) + let logical = this.chart.chart.timeScale().getVisibleLogicalRange() + + this.line.setData(data) + + this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: true}) + this.chart.chart.timeScale().setVisibleLogicalRange(logical) + + if (!this.ray) { + this.markers = [ + {time: this.from[0], position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1}, + {time: this.to[0], position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1} + ] + this.line.setMarkers(this.markers) + } + } + } + window.TrendLine = TrendLine + class ColorPicker { constructor(saveDrawings) { this.saveDrawings = saveDrawings this.container = document.createElement('div') this.container.style.maxWidth = '170px' - this.container.style.backgroundColor = '#191B1E' + this.container.style.backgroundColor = pane.backgroundColor this.container.style.position = 'absolute' this.container.style.zIndex = '10000' this.container.style.display = 'none' this.container.style.flexDirection = 'column' this.container.style.alignItems = 'center' - this.container.style.border = '2px solid #3C434C' + this.container.style.border = '2px solid '+pane.borderColor this.container.style.borderRadius = '8px' this.container.style.cursor = 'default' @@ -593,7 +555,7 @@ if (!window.ToolBox) { colors.forEach((color) => colorPicker.appendChild(this.makeColorBox(color))) let separator = document.createElement('div') - separator.style.backgroundColor = '#3C434C' + separator.style.backgroundColor = pane.borderColor separator.style.height = '1px' separator.style.width = '130px' @@ -696,7 +658,7 @@ if (!window.ToolBox) { this.container.style.position = 'absolute' this.container.style.zIndex = '10000' this.container.style.background = 'rgb(50, 50, 50)' - this.container.style.color = '#ececed' + this.container.style.color = pane.activeColor this.container.style.display = 'none' this.container.style.borderRadius = '5px' this.container.style.padding = '3px 3px' @@ -727,7 +689,7 @@ if (!window.ToolBox) { item.style.borderRadius = '3px' item.innerText = text - item.addEventListener('mouseover', (event) => item.style.backgroundColor = 'rgba(0, 122, 255, 0.3)') + item.addEventListener('mouseover', (event) => item.style.backgroundColor = pane.mutedBackgroundColor) item.addEventListener('mouseout', (event) => item.style.backgroundColor = 'transparent') item.addEventListener('click', (event) => { @@ -760,5 +722,5 @@ if (!window.ToolBox) { this.container.style.display = 'none' } } - window.ColorPicker = ColorPicker + window.StylePicker = StylePicker } diff --git a/lightweight_charts/polygon.py b/lightweight_charts/polygon.py index ff56c8e..7d5c69f 100644 --- a/lightweight_charts/polygon.py +++ b/lightweight_charts/polygon.py @@ -411,21 +411,21 @@ class PolygonChart(Chart): self.end_date = end_date self.limit = limit self.live = live - + self.win.style( + active_background_color='rgba(91, 98, 246, 0.8)', + muted_background_color='rgba(91, 98, 246, 0.5)' + ) self.polygon.api_key(api_key) self.events.search += self.on_search self.legend(True) self.grid(False, False) self.crosshair(vert_visible=False, horz_visible=False) - self.topbar.active_background_color = 'rgb(91, 98, 246)' self.topbar.textbox('symbol') self.topbar.switcher('timeframe', timeframe_options, func=self._on_timeframe_selection) self.topbar.switcher('security', security_options, func=self._on_security_selection) self.run_script(f''' - {self.id}.search.box.style.backgroundColor = 'rgba(91, 98, 246, 0.5)' - {self.id}.spinner.style.borderTop = '4px solid rgba(91, 98, 246, 0.8)' {self.id}.search.window.style.display = "flex" {self.id}.search.box.focus() ''') diff --git a/lightweight_charts/topbar.py b/lightweight_charts/topbar.py index d321882..084c624 100644 --- a/lightweight_charts/topbar.py +++ b/lightweight_charts/topbar.py @@ -50,7 +50,8 @@ class MenuWidget(Widget): class ButtonWidget(Widget): def __init__(self, topbar, button, separator, align, func): super().__init__(topbar, value=button, func=func) - self.run_script(f'{self.id} = {topbar.id}.makeButton("{button}", "{self.id}", {jbool(separator)}, "{align}")') + self.run_script( + f'{self.id} = {topbar.id}.makeButton("{button}", "{self.id}", {jbool(separator)}, true, "{align}")') def set(self, string): self.value = string @@ -62,12 +63,6 @@ class TopBar(Pane): super().__init__(chart.win) self._chart = chart self._widgets: Dict[str, Widget] = {} - - self.click_bg_color = '#50565E' - self.hover_bg_color = '#3c434c' - self.active_bg_color = 'rgba(0, 122, 255, 0.7)' - self.active_text_color = '#ececed' - self.text_color = '#d8d9db' self._created = False def _create(self): @@ -76,10 +71,7 @@ class TopBar(Pane): from lightweight_charts.abstract import JS self._created = True self.run_script(JS['callback']) - self.run_script(f''' - {self.id} = new TopBar( {self._chart.id}, '{self.hover_bg_color}', '{self.click_bg_color}', - '{self.active_bg_color}', '{self.text_color}', '{self.active_text_color}') - ''') + self.run_script(f'{self.id} = new TopBar({self._chart.id})') def __getitem__(self, item): if widget := self._widgets.get(item): diff --git a/setup.py b/setup.py index c2ff4ef..03f3867 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ with open('README.md', 'r', encoding='utf-8') as f: setup( name='lightweight_charts', - version='1.0.17.5', + version='1.0.17.6', packages=find_packages(), python_requires='>=3.8', install_requires=[