From 84e2d6f299a342b98bcbc7a6543d98c907e06c2f Mon Sep 17 00:00:00 2001 From: DIMITRIOS CHRYSOCHERIS Date: Fri, 27 Sep 2024 21:56:52 +0300 Subject: [PATCH 01/18] topbar -> button & ButtonWidget || Added disabled feature --- lightweight_charts/topbar.py | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/lightweight_charts/topbar.py b/lightweight_charts/topbar.py index bef9978..8cbb360 100644 --- a/lightweight_charts/topbar.py +++ b/lightweight_charts/topbar.py @@ -76,15 +76,37 @@ def update_items(self, *items: str): class ButtonWidget(Widget): - def __init__(self, topbar, button, separator, align, toggle, func): + def __init__(self, topbar, button, separator, align, toggle, disabled: bool = False, func=None): super().__init__(topbar, value=False, func=func, convert_boolean=toggle) + self.disabled = disabled self.run_script( - f'{self.id} = {topbar.id}.makeButton("{button}", "{self.id}", {jbool(separator)}, true, "{align}", {jbool(toggle)})') + f'{self.id} = {topbar.id}.makeButton("{button}", "{self.id}", {jbool(separator)}, true, "{align}", {jbool(toggle)})' + ) + self.update_disabled() def set(self, string): - # self.value = string self.run_script(f'{self.id}.elem.innerText = "{string}"') + def update_disabled(self): + """Update the button's disabled state and text opacity in the UI.""" + # Generate a unique name for the button element in JavaScript + unique_button_elem = f'buttonElem_{self.id.replace(".", "_")}' # Replace '.' to avoid issues in variable naming + self.run_script(f''' + const {unique_button_elem} = {self.id}.elem; // Unique reference for each button + {unique_button_elem}.disabled = {jbool(self.disabled)}; + {unique_button_elem}.style.opacity = {0.5 if self.disabled else 1}; + ''') + + def disable(self): + """Disable the button.""" + self.disabled = True + self.update_disabled() + + def enable(self): + """Enable the button.""" + self.disabled = False + self.update_disabled() + class TopBar(Pane): def __init__(self, chart): @@ -123,6 +145,6 @@ def textbox(self, name: str, initial_text: str = '', self._widgets[name] = TextWidget(self, initial_text, align, func) def button(self, name, button_text: str, separator: bool = True, - align: ALIGN = 'left', toggle: bool = False, func: callable = None): + align: ALIGN = 'left', toggle: bool = False, disabled: bool = False, func: callable = None): self._create() - self._widgets[name] = ButtonWidget(self, button_text, separator, align, toggle, func) + self._widgets[name] = ButtonWidget(self, button_text, separator, align, toggle, disabled, func) From 8ba9f305868db4e925d7673a74f5e615aab4bc86 Mon Sep 17 00:00:00 2001 From: DIMITRIOS CHRYSOCHERIS Date: Fri, 27 Sep 2024 22:18:42 +0300 Subject: [PATCH 02/18] topbar -> button & ButtonWidget || Added text font size --- lightweight_charts/topbar.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lightweight_charts/topbar.py b/lightweight_charts/topbar.py index 8cbb360..d58d54b 100644 --- a/lightweight_charts/topbar.py +++ b/lightweight_charts/topbar.py @@ -76,9 +76,10 @@ def update_items(self, *items: str): class ButtonWidget(Widget): - def __init__(self, topbar, button, separator, align, toggle, disabled: bool = False, func=None): + def __init__(self, topbar, button, separator, align, toggle, disabled: bool = False, font_size: str = '16px', func=None): super().__init__(topbar, value=False, func=func, convert_boolean=toggle) self.disabled = disabled + self.font_size = font_size # Store font size self.run_script( f'{self.id} = {topbar.id}.makeButton("{button}", "{self.id}", {jbool(separator)}, true, "{align}", {jbool(toggle)})' ) @@ -95,6 +96,7 @@ def update_disabled(self): const {unique_button_elem} = {self.id}.elem; // Unique reference for each button {unique_button_elem}.disabled = {jbool(self.disabled)}; {unique_button_elem}.style.opacity = {0.5 if self.disabled else 1}; + {unique_button_elem}.style.fontSize = "{self.font_size}"; // Set the font size ''') def disable(self): @@ -145,6 +147,6 @@ def textbox(self, name: str, initial_text: str = '', self._widgets[name] = TextWidget(self, initial_text, align, func) def button(self, name, button_text: str, separator: bool = True, - align: ALIGN = 'left', toggle: bool = False, disabled: bool = False, func: callable = None): + align: ALIGN = 'left', toggle: bool = False, disabled: bool = False, font_size: str = '16px', func: callable = None): self._create() - self._widgets[name] = ButtonWidget(self, button_text, separator, align, toggle, disabled, func) + self._widgets[name] = ButtonWidget(self, button_text, separator, align, toggle, disabled, font_size, func) From 604cbd851f448cec63b3917216ce7390a35c80a5 Mon Sep 17 00:00:00 2001 From: DIMITRIOS CHRYSOCHERIS Date: Fri, 27 Sep 2024 22:19:15 +0300 Subject: [PATCH 03/18] Update topbar.py --- lightweight_charts/topbar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lightweight_charts/topbar.py b/lightweight_charts/topbar.py index d58d54b..7167386 100644 --- a/lightweight_charts/topbar.py +++ b/lightweight_charts/topbar.py @@ -79,7 +79,7 @@ class ButtonWidget(Widget): def __init__(self, topbar, button, separator, align, toggle, disabled: bool = False, font_size: str = '16px', func=None): super().__init__(topbar, value=False, func=func, convert_boolean=toggle) self.disabled = disabled - self.font_size = font_size # Store font size + self.font_size = font_size self.run_script( f'{self.id} = {topbar.id}.makeButton("{button}", "{self.id}", {jbool(separator)}, true, "{align}", {jbool(toggle)})' ) From cdfdc8bf464d63bb21cc64df9d04a62c273af7a8 Mon Sep 17 00:00:00 2001 From: DIMITRIOS CHRYSOCHERIS Date: Fri, 27 Sep 2024 22:49:20 +0300 Subject: [PATCH 04/18] Added button right click action --- lightweight_charts/topbar.py | 56 +++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/lightweight_charts/topbar.py b/lightweight_charts/topbar.py index 7167386..2bebe75 100644 --- a/lightweight_charts/topbar.py +++ b/lightweight_charts/topbar.py @@ -8,30 +8,40 @@ class Widget(Pane): - def __init__(self, topbar, value, func: callable = None, convert_boolean=False): + def __init__(self, topbar, value, func: callable = None, right_click_func: callable = None, font_size: str = '16px', convert_boolean=False): super().__init__(topbar.win) self.value = value + if func and not callable(func): + raise TypeError(f"The provided 'func' is not callable: {func}") + + # Left-click wrapper def wrapper(v): if convert_boolean: self.value = False if v == 'false' else True else: self.value = v - func(topbar._chart) - + if func: + func(topbar._chart) + + # Right-click wrapper + def right_click_wrapper(v): + if right_click_func: + right_click_func(topbar._chart) + async def async_wrapper(v): self.value = v await func(topbar._chart) - + self.win.handlers[self.id] = async_wrapper if asyncio.iscoroutinefunction(func) else wrapper + if right_click_func: + self.win.handlers[self.id + '_right_click'] = right_click_wrapper class TextWidget(Widget): def __init__(self, topbar, initial_text, align, func): super().__init__(topbar, value=initial_text, func=func) - callback_name = f'"{self.id}"' if func else '' - self.run_script(f'{self.id} = {topbar.id}.makeTextBoxWidget("{initial_text}", "{align}", {callback_name})') def set(self, string): @@ -76,29 +86,48 @@ def update_items(self, *items: str): class ButtonWidget(Widget): - def __init__(self, topbar, button, separator, align, toggle, disabled: bool = False, font_size: str = '16px', func=None): + def __init__(self, topbar, button, separator, align, toggle, disabled: bool = False, func=None, right_click_func=None, font_size: str = '16px'): super().__init__(topbar, value=False, func=func, convert_boolean=toggle) self.disabled = disabled self.font_size = font_size + self.right_click_func = right_click_func + self.run_script( f'{self.id} = {topbar.id}.makeButton("{button}", "{self.id}", {jbool(separator)}, true, "{align}", {jbool(toggle)})' ) self.update_disabled() + if right_click_func: + self.enable_right_click() def set(self, string): self.run_script(f'{self.id}.elem.innerText = "{string}"') def update_disabled(self): """Update the button's disabled state and text opacity in the UI.""" - # Generate a unique name for the button element in JavaScript - unique_button_elem = f'buttonElem_{self.id.replace(".", "_")}' # Replace '.' to avoid issues in variable naming + unique_button_elem = f'buttonElem_{self.id.replace(".", "_")}' # Unique reference for each button self.run_script(f''' - const {unique_button_elem} = {self.id}.elem; // Unique reference for each button + const {unique_button_elem} = {self.id}.elem; {unique_button_elem}.disabled = {jbool(self.disabled)}; {unique_button_elem}.style.opacity = {0.5 if self.disabled else 1}; - {unique_button_elem}.style.fontSize = "{self.font_size}"; // Set the font size + {unique_button_elem}.style.fontSize = "{self.font_size}"; ''') + def enable_right_click(self): + """Enable right-click functionality for the button.""" + unique_button_elem = f'buttonElem_{self.id.replace(".", "_right_")}' # Unique reference for each button + + self.run_script(f''' + const {unique_button_elem} = {self.id}.elem; + {unique_button_elem}.addEventListener('contextmenu', (event) => {{ + event.preventDefault(); // Prevent the default right-click context menu + if ({unique_button_elem}.disabled) return; // Ignore right-click if button is disabled + window.callbackFunction(`right_click_{self.id}_~_` + event.button); + }}); + ''') + + # Set up a handler for the right-click event + self.win.handlers[f'right_click_{self.id}'] = self.right_click_func + def disable(self): """Disable the button.""" self.disabled = True @@ -147,6 +176,7 @@ def textbox(self, name: str, initial_text: str = '', self._widgets[name] = TextWidget(self, initial_text, align, func) def button(self, name, button_text: str, separator: bool = True, - align: ALIGN = 'left', toggle: bool = False, disabled: bool = False, font_size: str = '16px', func: callable = None): + align: ALIGN = 'left', toggle: bool = False, disabled: bool = False, + font_size: str = '16px', func: callable = None, right_click_func: callable = None): self._create() - self._widgets[name] = ButtonWidget(self, button_text, separator, align, toggle, disabled, font_size, func) + self._widgets[name] = ButtonWidget(self, button_text, separator, align, toggle, disabled, func, right_click_func, font_size) From 7f7e8ea380a9161327996fbfd72dc72243b353a5 Mon Sep 17 00:00:00 2001 From: DIMITRIOS CHRYSOCHERIS Date: Fri, 27 Sep 2024 23:22:06 +0300 Subject: [PATCH 05/18] topbar -> button & ButtonWidget || right-click improvements --- lightweight_charts/topbar.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lightweight_charts/topbar.py b/lightweight_charts/topbar.py index 2bebe75..cb83283 100644 --- a/lightweight_charts/topbar.py +++ b/lightweight_charts/topbar.py @@ -35,7 +35,7 @@ async def async_wrapper(v): self.win.handlers[self.id] = async_wrapper if asyncio.iscoroutinefunction(func) else wrapper if right_click_func: - self.win.handlers[self.id + '_right_click'] = right_click_wrapper + self.win.handlers[self.id + '_right'] = right_click_wrapper class TextWidget(Widget): @@ -111,10 +111,9 @@ def update_disabled(self): {unique_button_elem}.style.opacity = {0.5 if self.disabled else 1}; {unique_button_elem}.style.fontSize = "{self.font_size}"; ''') - def enable_right_click(self): """Enable right-click functionality for the button.""" - unique_button_elem = f'buttonElem_{self.id.replace(".", "_right_")}' # Unique reference for each button + unique_button_elem = f'buttonElem_{self.id.replace(".", "_")}_right' # Unique reference for each button self.run_script(f''' const {unique_button_elem} = {self.id}.elem; @@ -125,8 +124,9 @@ def enable_right_click(self): }}); ''') - # Set up a handler for the right-click event - self.win.handlers[f'right_click_{self.id}'] = self.right_click_func + # Check if right_click_func is callable before assigning + if callable(self.right_click_func): + self.win.handlers[f'right_click_{self.id}'] = self.right_click_func def disable(self): """Disable the button.""" From 33bcdf2d31df625820520cb21714f880d83927e6 Mon Sep 17 00:00:00 2001 From: DIMITRIOS CHRYSOCHERIS Date: Sat, 28 Sep 2024 16:42:18 +0300 Subject: [PATCH 06/18] util -> Added hex_to_rgb function --- lightweight_charts/util.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lightweight_charts/util.py b/lightweight_charts/util.py index 5f9df42..f1063bb 100644 --- a/lightweight_charts/util.py +++ b/lightweight_charts/util.py @@ -189,3 +189,16 @@ def __exit__(self, *args): def add_script(self, script): self.scripts.append(script) + +def hex_to_rgb(hex_color: str): + """ + Convert a hex color string to an RGB tuple. + + Args: + hex_color (str): Hex color string (e.g., '#FFFFFF'). + + Returns: + tuple: A tuple containing the RGB values (r, g, b). + """ + hex_color = hex_color.lstrip('#') + return tuple(int(hex_color[i:i + 2], 16) for i in (0, 2, 4)) From 4809f235aa4c8d9b78361ba6e766c2404b9d7503 Mon Sep 17 00:00:00 2001 From: DIMITRIOS CHRYSOCHERIS Date: Sat, 28 Sep 2024 16:47:21 +0300 Subject: [PATCH 07/18] topbar -> button & ButtonWidget || Added toggle select feature. --- lightweight_charts/topbar.py | 66 +++++++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 8 deletions(-) diff --git a/lightweight_charts/topbar.py b/lightweight_charts/topbar.py index cb83283..7b2aec7 100644 --- a/lightweight_charts/topbar.py +++ b/lightweight_charts/topbar.py @@ -1,19 +1,16 @@ import asyncio from typing import Dict, Literal -from .util import jbool, Pane +from .util import jbool, Pane, hex_to_rgb ALIGN = Literal['left', 'right'] class Widget(Pane): - def __init__(self, topbar, value, func: callable = None, right_click_func: callable = None, font_size: str = '16px', convert_boolean=False): + def __init__(self, topbar, value, func: callable = None, right_click_func: callable = None, convert_boolean=False): super().__init__(topbar.win) self.value = value - - if func and not callable(func): - raise TypeError(f"The provided 'func' is not callable: {func}") # Left-click wrapper def wrapper(v): @@ -86,11 +83,16 @@ def update_items(self, *items: str): class ButtonWidget(Widget): - def __init__(self, topbar, button, separator, align, toggle, disabled: bool = False, func=None, right_click_func=None, font_size: str = '16px'): + def __init__(self, topbar, button, separator, align, toggle, disabled: bool = False, + func=None, right_click_func=None, font_size: str = '16px', + selected_bg: str = '#ffffff', # Default color with full opacity + ): super().__init__(topbar, value=False, func=func, convert_boolean=toggle) self.disabled = disabled self.font_size = font_size self.right_click_func = right_click_func + self.selected_bg = selected_bg # Store the selected background color and opacity + self.is_selected = False # State to track if the button is toggled on self.run_script( f'{self.id} = {topbar.id}.makeButton("{button}", "{self.id}", {jbool(separator)}, true, "{align}", {jbool(toggle)})' @@ -111,6 +113,7 @@ def update_disabled(self): {unique_button_elem}.style.opacity = {0.5 if self.disabled else 1}; {unique_button_elem}.style.fontSize = "{self.font_size}"; ''') + def enable_right_click(self): """Enable right-click functionality for the button.""" unique_button_elem = f'buttonElem_{self.id.replace(".", "_")}_right' # Unique reference for each button @@ -138,6 +141,53 @@ def enable(self): self.disabled = False self.update_disabled() + def toggle_select(self): + """Select the button, changing its background color and text color based on brightness.""" + if not self.is_selected: + self.is_selected = True + unique_button_elem = f'{self.id}.elem' + color = self.selected_bg # Unpack the color + + # Convert hex color to RGB + r, g, b = hex_to_rgb(color) + + # Calculate brightness using the formula + brightness = r * 0.299 + g * 0.587 + b * 0.114 + + # Determine text color based on brightness + text_color = '#000000' if brightness > 186 else '#ffffff' + + # Store the original text color to revert later + self.run_script(f''' + const elem = {unique_button_elem}; + if (elem) {{ + elem._originalTextColor = elem.style.color; // Store original text color + elem.style.backgroundColor = "{color}"; + elem.style.color = "{text_color}"; // Set text color based on brightness + }} + ''') + + def toggle_deselect(self): + """Deselect the button, restoring the original background and text color.""" + if self.is_selected: + self.is_selected = False + unique_button_elem = f'{self.id}.elem' + + # Restore the original text color and remove the selected background color + self.run_script(f''' + const elem = {unique_button_elem}; + if (elem) {{ + elem.style.backgroundColor = ""; // Remove background color + elem.style.color = elem._originalTextColor; // Restore original text color + }} + ''') + + @property + def is_toggle_selected(self): + """Return whether the button is currently selected.""" + return self.is_selected + + class TopBar(Pane): def __init__(self, chart): @@ -177,6 +227,6 @@ def textbox(self, name: str, initial_text: str = '', def button(self, name, button_text: str, separator: bool = True, align: ALIGN = 'left', toggle: bool = False, disabled: bool = False, - font_size: str = '16px', func: callable = None, right_click_func: callable = None): + font_size: str = '16px', func: callable = None, right_click_func: callable = None, selected_bg: str = '#ffffff'): self._create() - self._widgets[name] = ButtonWidget(self, button_text, separator, align, toggle, disabled, func, right_click_func, font_size) + self._widgets[name] = ButtonWidget(self, button_text, separator, align, toggle, disabled, func, right_click_func, font_size, selected_bg) From a3d84d94da7875c89bc3889f32bf87f8dc6962c9 Mon Sep 17 00:00:00 2001 From: DIMITRIOS CHRYSOCHERIS Date: Sat, 28 Sep 2024 16:55:11 +0300 Subject: [PATCH 08/18] Fixed toggle select feature If you don't set a button's selected_bg it will disable the toggle select feature. --- lightweight_charts/topbar.py | 54 ++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/lightweight_charts/topbar.py b/lightweight_charts/topbar.py index 7b2aec7..fffae7c 100644 --- a/lightweight_charts/topbar.py +++ b/lightweight_charts/topbar.py @@ -85,13 +85,12 @@ def update_items(self, *items: str): class ButtonWidget(Widget): def __init__(self, topbar, button, separator, align, toggle, disabled: bool = False, func=None, right_click_func=None, font_size: str = '16px', - selected_bg: str = '#ffffff', # Default color with full opacity - ): + selected_bg: str = None): # Change default to None super().__init__(topbar, value=False, func=func, convert_boolean=toggle) self.disabled = disabled self.font_size = font_size self.right_click_func = right_click_func - self.selected_bg = selected_bg # Store the selected background color and opacity + self.selected_bg = selected_bg # Now it can be None if not provided self.is_selected = False # State to track if the button is toggled on self.run_script( @@ -143,29 +142,30 @@ def enable(self): def toggle_select(self): """Select the button, changing its background color and text color based on brightness.""" - if not self.is_selected: - self.is_selected = True - unique_button_elem = f'{self.id}.elem' - color = self.selected_bg # Unpack the color - - # Convert hex color to RGB - r, g, b = hex_to_rgb(color) - - # Calculate brightness using the formula - brightness = r * 0.299 + g * 0.587 + b * 0.114 - - # Determine text color based on brightness - text_color = '#000000' if brightness > 186 else '#ffffff' - - # Store the original text color to revert later - self.run_script(f''' - const elem = {unique_button_elem}; - if (elem) {{ - elem._originalTextColor = elem.style.color; // Store original text color - elem.style.backgroundColor = "{color}"; - elem.style.color = "{text_color}"; // Set text color based on brightness - }} - ''') + if self.selected_bg: # Only proceed if selected_bg is set + if not self.is_selected: + self.is_selected = True + unique_button_elem = f'{self.id}.elem' + color = self.selected_bg # Unpack the color + + # Convert hex color to RGB + r, g, b = hex_to_rgb(color) + + # Calculate brightness using the formula + brightness = r * 0.299 + g * 0.587 + b * 0.114 + + # Determine text color based on brightness + text_color = '#000000' if brightness > 186 else '#ffffff' + + # Store the original text color to revert later + self.run_script(f''' + const elem = {unique_button_elem}; + if (elem) {{ + elem._originalTextColor = elem.style.color; // Store original text color + elem.style.backgroundColor = "{color}"; + elem.style.color = "{text_color}"; // Set text color based on brightness + }} + ''') def toggle_deselect(self): """Deselect the button, restoring the original background and text color.""" @@ -227,6 +227,6 @@ def textbox(self, name: str, initial_text: str = '', def button(self, name, button_text: str, separator: bool = True, align: ALIGN = 'left', toggle: bool = False, disabled: bool = False, - font_size: str = '16px', func: callable = None, right_click_func: callable = None, selected_bg: str = '#ffffff'): + font_size: str = '16px', func: callable = None, right_click_func: callable = None, selected_bg: str = None): self._create() self._widgets[name] = ButtonWidget(self, button_text, separator, align, toggle, disabled, func, right_click_func, font_size, selected_bg) From 04fb233ab6817d8998161f83f49d6800a7583c39 Mon Sep 17 00:00:00 2001 From: DIMITRIOS CHRYSOCHERIS Date: Mon, 30 Sep 2024 00:03:52 +0300 Subject: [PATCH 09/18] abstract -> SeriesCommon | Added toggle_data Same functionality with hide_data and show_data, but if the data is visible, it will be hidden, and vice versa without the need of adding ifs and variables in your code. --- lightweight_charts/abstract.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lightweight_charts/abstract.py b/lightweight_charts/abstract.py index 0091d3f..04e7420 100644 --- a/lightweight_charts/abstract.py +++ b/lightweight_charts/abstract.py @@ -399,6 +399,14 @@ def hide_data(self): def show_data(self): self._toggle_data(True) + def toggle_data(self): + """ + Toggles the visibility of the data and its volume if applicable. + if the data is visible, it will be hidden, and vice versa. + """ + self._toggle_data(not self._data_visible) + self.visible = not self._data_visible + def _toggle_data(self, arg): self.run_script(f''' {self.id}.series.applyOptions({{visible: {jbool(arg)}}}) From 143e822cdc48911a7cb427aac7916f4e83237f58 Mon Sep 17 00:00:00 2001 From: DIMITRIOS CHRYSOCHERIS Date: Mon, 30 Sep 2024 22:53:20 +0300 Subject: [PATCH 10/18] All imports from r4gn4r's library fix --- examples/1_setting_data/setting_data.py | 2 +- examples/2_live_data/live_data.py | 2 +- examples/3_tick_data/tick_data.py | 2 +- examples/4_line_indicators/line_indicators.py | 2 +- examples/5_styling/styling.py | 2 +- examples/6_callbacks/callbacks.py | 2 +- lightweight_charts/chart.py | 4 ++-- lightweight_charts/util.py | 2 +- lightweight_charts/widgets.py | 2 +- test/test_chart.py | 2 +- test/test_returns.py | 2 +- test/test_table.py | 2 +- test/test_toolbox.py | 2 +- test/test_topbar.py | 2 +- test/util.py | 2 +- 15 files changed, 16 insertions(+), 16 deletions(-) diff --git a/examples/1_setting_data/setting_data.py b/examples/1_setting_data/setting_data.py index c0f0cd4..14e348f 100644 --- a/examples/1_setting_data/setting_data.py +++ b/examples/1_setting_data/setting_data.py @@ -1,5 +1,5 @@ import pandas as pd -from lightweight_charts import Chart +from lightweight_charts_r4gn4r.lightweight_charts import Chart if __name__ == '__main__': chart = Chart() diff --git a/examples/2_live_data/live_data.py b/examples/2_live_data/live_data.py index 2563fc5..31b3873 100644 --- a/examples/2_live_data/live_data.py +++ b/examples/2_live_data/live_data.py @@ -1,6 +1,6 @@ import pandas as pd from time import sleep -from lightweight_charts import Chart +from lightweight_charts_r4gn4r.lightweight_charts import Chart if __name__ == '__main__': diff --git a/examples/3_tick_data/tick_data.py b/examples/3_tick_data/tick_data.py index 5d3217d..7893050 100644 --- a/examples/3_tick_data/tick_data.py +++ b/examples/3_tick_data/tick_data.py @@ -1,6 +1,6 @@ import pandas as pd from time import sleep -from lightweight_charts import Chart +from lightweight_charts_r4gn4r.lightweight_charts import Chart if __name__ == '__main__': diff --git a/examples/4_line_indicators/line_indicators.py b/examples/4_line_indicators/line_indicators.py index 44ecbbf..29fa244 100644 --- a/examples/4_line_indicators/line_indicators.py +++ b/examples/4_line_indicators/line_indicators.py @@ -1,5 +1,5 @@ import pandas as pd -from lightweight_charts import Chart +from lightweight_charts_r4gn4r.lightweight_charts import Chart def calculate_sma(df, period: int = 50): diff --git a/examples/5_styling/styling.py b/examples/5_styling/styling.py index 7be3c89..ee4b6c9 100644 --- a/examples/5_styling/styling.py +++ b/examples/5_styling/styling.py @@ -1,5 +1,5 @@ import pandas as pd -from lightweight_charts import Chart +from lightweight_charts_r4gn4r.lightweight_charts import Chart if __name__ == '__main__': diff --git a/examples/6_callbacks/callbacks.py b/examples/6_callbacks/callbacks.py index 85b85c0..d730e86 100644 --- a/examples/6_callbacks/callbacks.py +++ b/examples/6_callbacks/callbacks.py @@ -1,5 +1,5 @@ import pandas as pd -from lightweight_charts import Chart +from lightweight_charts_r4gn4r.lightweight_charts import Chart def get_bar_data(symbol, timeframe): diff --git a/lightweight_charts/chart.py b/lightweight_charts/chart.py index 305534c..2ff608c 100644 --- a/lightweight_charts/chart.py +++ b/lightweight_charts/chart.py @@ -5,7 +5,7 @@ import webview from webview.errors import JavascriptException -from lightweight_charts import abstract +from lightweight_charts_r4gn4r.lightweight_charts import abstract from .util import parse_event_message, FLOAT import os @@ -204,7 +204,7 @@ def show(self, block: bool = False): async def show_async(self): self.show(block=False) try: - from lightweight_charts import polygon + from lightweight_charts_r4gn4r.lightweight_charts import polygon [asyncio.create_task(self.polygon.async_set(*args)) for args in polygon._set_on_load] while 1: while Chart.WV.emit_queue.empty() and self.is_alive: diff --git a/lightweight_charts/util.py b/lightweight_charts/util.py index 3372792..46b944d 100644 --- a/lightweight_charts/util.py +++ b/lightweight_charts/util.py @@ -9,7 +9,7 @@ class Pane: def __init__(self, window): - from lightweight_charts import Window + from lightweight_charts_r4gn4r.lightweight_charts import Window self.win: Window = window self.run_script = window.run_script self.bulk_run = window.bulk_run diff --git a/lightweight_charts/widgets.py b/lightweight_charts/widgets.py index c347c45..2430943 100644 --- a/lightweight_charts/widgets.py +++ b/lightweight_charts/widgets.py @@ -2,7 +2,7 @@ import html from .util import parse_event_message -from lightweight_charts import abstract +from lightweight_charts_r4gn4r.lightweight_charts import abstract try: import wx.html2 diff --git a/test/test_chart.py b/test/test_chart.py index 42db0a7..126e977 100644 --- a/test/test_chart.py +++ b/test/test_chart.py @@ -1,7 +1,7 @@ import unittest import pandas as pd from util import BARS, Tester -from lightweight_charts import Chart +from lightweight_charts_r4gn4r.lightweight_charts import Chart class TestChart(Tester): diff --git a/test/test_returns.py b/test/test_returns.py index a06da8d..94a16c7 100644 --- a/test/test_returns.py +++ b/test/test_returns.py @@ -1,6 +1,6 @@ import unittest import pandas as pd -from lightweight_charts import Chart +from lightweight_charts_r4gn4r.lightweight_charts import Chart import asyncio from util import BARS, Tester diff --git a/test/test_table.py b/test/test_table.py index 2e811d0..fb00d3c 100644 --- a/test/test_table.py +++ b/test/test_table.py @@ -1,7 +1,7 @@ import unittest import pandas as pd -from lightweight_charts import Chart +from lightweight_charts_r4gn4r.lightweight_charts import Chart class TestTable(unittest.TestCase): diff --git a/test/test_toolbox.py b/test/test_toolbox.py index 2dee1ba..634cb28 100644 --- a/test/test_toolbox.py +++ b/test/test_toolbox.py @@ -1,7 +1,7 @@ import unittest import pandas as pd -from lightweight_charts import Chart +from lightweight_charts_r4gn4r.lightweight_charts import Chart from util import BARS, Tester from time import sleep diff --git a/test/test_topbar.py b/test/test_topbar.py index e1ba108..a21342a 100644 --- a/test/test_topbar.py +++ b/test/test_topbar.py @@ -1,7 +1,7 @@ import unittest import pandas as pd -from lightweight_charts import Chart +from lightweight_charts_r4gn4r.lightweight_charts import Chart from util import Tester diff --git a/test/util.py b/test/util.py index 41705d1..4874e33 100644 --- a/test/util.py +++ b/test/util.py @@ -1,7 +1,7 @@ import unittest import pandas as pd -from lightweight_charts import Chart +from lightweight_charts_r4gn4r.lightweight_charts import Chart BARS = pd.read_csv('../examples/1_setting_data/ohlcv.csv') From 504974389f55597d9a747f6926ed471475a83c4e Mon Sep 17 00:00:00 2001 From: DIMITRIOS CHRYSOCHERIS Date: Mon, 30 Sep 2024 22:55:06 +0300 Subject: [PATCH 11/18] abstract -> SeriesCommon || Added remove_markers If you create a variable of marker_list(), you may do markers = chart.marker_list(m_list) chart.remove_markers(markers) --- lightweight_charts/abstract.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lightweight_charts/abstract.py b/lightweight_charts/abstract.py index 04e7420..6a236a9 100644 --- a/lightweight_charts/abstract.py +++ b/lightweight_charts/abstract.py @@ -307,6 +307,16 @@ def remove_marker(self, marker_id: str): self.markers.pop(marker_id) self._update_markers() + def remove_markers(self, marker_ids: list): + """ + Removes multiple markers with the given ids. + + :param marker_ids: A list of marker ids to be removed. + """ + for marker_id in marker_ids: + self.markers.pop(marker_id, None) # Use pop with default to avoid KeyError + self._update_markers() + def horizontal_line(self, price: NUM, color: str = 'rgb(122, 146, 202)', width: int = 2, style: LINE_STYLE = 'solid', text: str = '', axis_label_visible: bool = True, func: Optional[Callable] = None From c2b2a159dc99cdf3a5dfd4721d2d355af108309d Mon Sep 17 00:00:00 2001 From: DIMITRIOS CHRYSOCHERIS Date: Mon, 30 Sep 2024 23:02:24 +0300 Subject: [PATCH 12/18] abstract -> SeriesCommon || fixed toggle_data feature --- lightweight_charts/abstract.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lightweight_charts/abstract.py b/lightweight_charts/abstract.py index 6a236a9..2edbbcf 100644 --- a/lightweight_charts/abstract.py +++ b/lightweight_charts/abstract.py @@ -153,6 +153,7 @@ def __init__(self, chart: 'AbstractChart', name: str = ''): self.offset = 0 self.data = pd.DataFrame() self.markers = {} + self._visible = True def _set_interval(self, df: pd.DataFrame): if not pd.api.types.is_datetime64_any_dtype(df['time']): @@ -414,8 +415,8 @@ def toggle_data(self): Toggles the visibility of the data and its volume if applicable. if the data is visible, it will be hidden, and vice versa. """ - self._toggle_data(not self._data_visible) - self.visible = not self._data_visible + self._toggle_data(not self._visible) + self._visible = not self._visible def _toggle_data(self, arg): self.run_script(f''' From 936896615b22fecbb53ef5f0b259f812591be455 Mon Sep 17 00:00:00 2001 From: DIMITRIOS CHRYSOCHERIS Date: Tue, 1 Oct 2024 01:12:23 +0300 Subject: [PATCH 13/18] chart -> Added better debugging --- .gitignore | 1 + lightweight_charts/chart.py | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 0ea6d05..929d00c 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ pyrightconfig.json working/ .zed/ +.vscode/settings.json diff --git a/lightweight_charts/chart.py b/lightweight_charts/chart.py index 2ff608c..f7c1e49 100644 --- a/lightweight_charts/chart.py +++ b/lightweight_charts/chart.py @@ -61,7 +61,6 @@ def create_window( self.windows[-1].events.loaded += lambda: self.loaded_event.set() - def loop(self): # self.loaded_event.set() while self.is_alive: @@ -88,10 +87,21 @@ def loop(self): else: window.evaluate_js(arg) except KeyError as e: + # Handle KeyError if it occurs in the evaluation + print(f"KeyError encountered: {e}") return except JavascriptException as e: - msg = eval(str(e)) - raise JavascriptException(f"\n\nscript -> '{arg}',\nerror -> {msg['name']}[{msg['line']}:{msg['column']}]\n{msg['message']}") + # Enhanced error handling + msg = eval(str(e)) # Ensure msg is a dictionary + line_info = msg.get('line', 'unknown line') # Use 'unknown line' as default + column_info = msg.get('column', 'unknown column') # Use 'unknown column' as default + + # Raise a new JavascriptException with detailed info + raise JavascriptException( + f"\n\nscript -> '{arg}',\n" + f"error -> {msg.get('name', 'unknown error name')}[{line_info}:{column_info}]\n" + f"{msg.get('message', 'No message available')}" + ) class WebviewHandler(): From 1f3ebc58060f92ae48fa1de0c95385acb71b3e21 Mon Sep 17 00:00:00 2001 From: DIMITRIOS CHRYSOCHERIS Date: Tue, 1 Oct 2024 18:23:34 +0300 Subject: [PATCH 14/18] fix | use my library instead of original --- lightweight_charts/abstract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lightweight_charts/abstract.py b/lightweight_charts/abstract.py index 2edbbcf..ce52d05 100644 --- a/lightweight_charts/abstract.py +++ b/lightweight_charts/abstract.py @@ -720,7 +720,7 @@ def __init__(self, window: Window, width: float = 1.0, height: float = 1.0, self._height = height self.events: Events = Events(self) - from lightweight_charts.polygon import PolygonAPI + from lightweight_charts_r4gn4r.lightweight_charts.polygon import PolygonAPI self.polygon: PolygonAPI = PolygonAPI(self) self.run_script( From 461051ac6f6ac41c1d2932ea1c28774f4ab784a0 Mon Sep 17 00:00:00 2001 From: DIMITRIOS CHRYSOCHERIS Date: Sat, 5 Oct 2024 13:02:11 +0300 Subject: [PATCH 15/18] drawings -> Drawing || Added show/hide logic --- lightweight_charts/drawings.py | 38 ++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/lightweight_charts/drawings.py b/lightweight_charts/drawings.py index 36a7305..d624e95 100644 --- a/lightweight_charts/drawings.py +++ b/lightweight_charts/drawings.py @@ -24,6 +24,7 @@ class Drawing(Pane): def __init__(self, chart, func=None): super().__init__(chart.win) self.chart = chart + self._visible = True def update(self, *points): formatted_points = [] @@ -45,6 +46,43 @@ def options(self, color='#1E80F0', style='solid', width=4): width: {width}, }})''') + def show_data(self): + """ + Shows the drawing. + """ + if not self._visible: + self.run_script(f'{self.chart.id}.series.attachPrimitive({self.id})') + self._visible = True + + def hide_data(self): + """ + Hides the drawing. + """ + if self._visible: + self.run_script(f'{self.id}.detach()') + self._visible = False + + + def _toggle_data(self, visible: bool): + """ + Toggles the visibility of the drawing. If visible is True, attach the drawing to the chart. + If False, detach the drawing (effectively hiding it). + """ + if visible: + # Re-attach the drawing to make it visible again + self.run_script(f'{self.chart.id}.series.attachPrimitive({self.id})') + else: + # Detach the drawing to hide it + self.run_script(f'{self.id}.detach()') + self._visible = visible + + def toggle_data(self): + """ + Toggles the visibility of the drawing. + """ + self._toggle_data(not self._visible) + + class TwoPointDrawing(Drawing): def __init__( self, From d6a2ce33320f56729827438033543e5e457d6206 Mon Sep 17 00:00:00 2001 From: DIMITRIOS CHRYSOCHERIS Date: Sun, 6 Oct 2024 00:58:45 +0300 Subject: [PATCH 16/18] drawings -> Drawing || fixed delete() --- lightweight_charts/drawings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lightweight_charts/drawings.py b/lightweight_charts/drawings.py index d624e95..6963aac 100644 --- a/lightweight_charts/drawings.py +++ b/lightweight_charts/drawings.py @@ -37,7 +37,7 @@ def delete(self): """ Irreversibly deletes the drawing. """ - self.run_script(f'{self.id}.detach()') + self.run_script(f'{self.chart.id}.series.detachPrimitive({self.id})') def options(self, color='#1E80F0', style='solid', width=4): self.run_script(f'''{self.id}.applyOptions({{ From 2ec1de0068c6ef25874db0ad3c502d51bac261f3 Mon Sep 17 00:00:00 2001 From: DIMITRIOS CHRYSOCHERIS Date: Sun, 6 Oct 2024 16:35:20 +0300 Subject: [PATCH 17/18] topbar -> MenuWidget || execute option's function directly through MenuWidget instead of Widget --- lightweight_charts/topbar.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/lightweight_charts/topbar.py b/lightweight_charts/topbar.py index 1580c9a..2e02861 100644 --- a/lightweight_charts/topbar.py +++ b/lightweight_charts/topbar.py @@ -63,11 +63,15 @@ class MenuWidget(Widget): def __init__(self, topbar, options, default, separator, align, func): super().__init__(topbar, value=default, func=func) self.options = list(options) + self.func = func # Store the function for later use self.run_script(f''' - {self.id} = {topbar.id}.makeMenu({list(options)}, "{default}", {jbool(separator)}, "{self.id}", "{align}") + {self.id} = {topbar.id}.makeMenu({self.options}, "{default}", {jbool(separator)}, "{self.id}", "{align}"); + {self.id}.onItemClicked = function(option) {{ + // Call the Python function associated with the clicked option + py_callback("{self.id}", option); + }}; ''') - # TODO this will probably need to be fixed def set(self, option): if option not in self.options: raise ValueError(f"Option {option} not in menu options ({self.options})") @@ -75,12 +79,22 @@ def set(self, option): self.run_script(f''' {self.id}._clickHandler("{option}") ''') - # self.win.handlers[self.id](option) + + # Execute the function associated with the selected option + if self.func: + self.func(option) # Call the function with the selected option def update_items(self, *items: str): self.options = list(items) self.run_script(f'{self.id}.updateMenuItems({self.options})') + # This method receives the callback from JavaScript to trigger the function + def py_callback(self, menu_id, option): + if option in self.options: + self.func(option) # Call the function associated with the selected option + else: + print(f"No function assigned to the option: {option}") + class ButtonWidget(Widget): def __init__(self, topbar, button, separator, align, toggle, disabled: bool = False, From c0d681a91894ab5dc9986a297f04bb5d33f90413 Mon Sep 17 00:00:00 2001 From: DIMITRIOS CHRYSOCHERIS Date: Wed, 9 Oct 2024 19:33:09 +0300 Subject: [PATCH 18/18] abstract -> SeriesCommon || Added marker show/hide feature --- lightweight_charts/abstract.py | 46 ++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/lightweight_charts/abstract.py b/lightweight_charts/abstract.py index ce52d05..d9aa10d 100644 --- a/lightweight_charts/abstract.py +++ b/lightweight_charts/abstract.py @@ -266,6 +266,7 @@ def marker_list(self, markers: list): "time": self._single_datetime_format(marker['time']), "position": marker_position(marker['position']), "color": marker['color'], + "original_color": marker['color'], # Store the original color "shape": marker_shape(marker['shape']), "text": marker['text'], } @@ -295,6 +296,7 @@ def marker(self, time: Optional[datetime] = None, position: MARKER_POSITION = 'b "time": formatted_time, "position": marker_position(position), "color": color, + "original_color": color, "shape": marker_shape(shape), "text": text, } @@ -318,6 +320,50 @@ def remove_markers(self, marker_ids: list): self.markers.pop(marker_id, None) # Use pop with default to avoid KeyError self._update_markers() + def hide_marker(self, marker_id: str): + """ + Hides the marker with the given id by setting its color to 'transparent'. + :param marker_id: The id of the marker to hide. + """ + if marker_id in self.markers: + self.markers[marker_id]['color'] = 'transparent' # Set marker color to transparent + self._update_markers() + + def show_marker(self, marker_id: str): + """ + Shows the marker with the given id by restoring its color. + :param marker_id: The id of the marker to show. + :param color: The color to restore to the marker. + """ + if marker_id in self.markers: + self.markers[marker_id]['color'] = self.markers[marker_id]['original_color'] # Restore original or specified color + self._update_markers() + + def toggle_marker(self, marker_id: str): + """ + Toggles the visibility of the marker with the given id. + If the marker is currently visible, it will be hidden. + If the marker is currently hidden, it will be shown. + :param marker_id: The id of the marker to toggle. + """ + if marker_id in self.markers: + # Get the current color and original color of the marker + current_color = self.markers[marker_id]['color'] + original_color = self.markers[marker_id]['original_color'] + + # Determine if the marker is currently visible or hidden + if current_color == 'transparent': + # If the marker is hidden, restore the original color + self.markers[marker_id]['color'] = original_color + else: + # If the marker is visible, hide it + self.markers[marker_id]['color'] = 'transparent' + + # Update the markers in the chart + self._update_markers() + + + def horizontal_line(self, price: NUM, color: str = 'rgb(122, 146, 202)', width: int = 2, style: LINE_STYLE = 'solid', text: str = '', axis_label_visible: bool = True, func: Optional[Callable] = None