from __future__ import annotations
import uuid
try:
from typing import Unpack
except ImportError:
from typing_extensions import Unpack
from lightningchart.ui import UIEWithPosition, UIElement, UIElementsWithAutoDispose
from lightningchart.utils import convert_to_base64, convert_color_to_hex
from lightningchart.utils.utils import ColorInput, PaddingKwargs
[docs]
class TextBox(UIEWithPosition, UIElementsWithAutoDispose):
"""UI Element for adding text annotations on top of the chart."""
def __init__(
self,
chart,
text: str = None,
x: int = None,
y: int = None,
position_scale: str = 'axis',
):
UIElement.__init__(self, chart)
self.instance.send(
self.id,
'textBox',
{'chart': self.chart.id, 'positionScale': position_scale},
)
if text:
self.set_text(text)
if x is not None and y is not None:
self.set_position(x, y)
[docs]
def set_text(self, text: str):
"""Set the content of the text box.
Args:
text (str): Text string.
Returns:
The instance of the class for fluent interface.
"""
self.instance.send(self.id, 'setText', {'text': text})
return self
[docs]
def set_padding(self, *args, **kwargs: Unpack[PaddingKwargs]):
"""Set padding around object in pixels.
Args:
*args: A single numeric value (int or float) for uniform padding on all sides.
**kwargs: Optional named arguments to specify padding for individual sides:
- `left` (int or float): Padding for the left side.
- `right` (int or float): Padding for the right side.
- `top` (int or float): Padding for the top side.
- `bottom` (int or float): Padding for the bottom side.
Examples:
- `set_padding(5)`: Sets uniform padding for all sides (integer or float).
- `set_padding(left=10, top=15)`: Sets padding for specific sides only.
- `set_padding(left=10, top=15, right=20, bottom=25)`: Fully define padding for all sides.
Returns:
The instance of the class for fluent interface.
"""
if len(args) == 1 and isinstance(args[0], (int, float)):
padding = args[0]
elif kwargs:
padding = {}
for key in ['left', 'right', 'bottom', 'top']:
if key in kwargs:
padding[key] = kwargs[key]
else:
raise ValueError(
'Invalid arguments. Use one of the following formats:\n'
'- set_padding(5): Uniform padding for all sides.\n'
'- set_padding(left=10, top=15): Specify individual sides.\n'
'- set_padding(left=10, top=15, right=20, bottom=25): Full padding definition.'
)
self.instance.send(self.id, 'setPadding', {'padding': padding})
return self
[docs]
def set_text_fill_style(self, color: ColorInput | None):
"""Set the color of the text.
Args:
color (Color): Color of the text. Use 'transparent' or None to hide.
Returns:
The instance of the class for fluent interface.
"""
color = convert_color_to_hex(color) if color is not None else None
self.instance.send(self.id, 'setTextFillStyle', {'color': color})
return self
[docs]
def set_text_font(
self,
size: int | float,
family: str = 'Segoe UI, -apple-system, Verdana, Helvetica',
style: str = 'normal',
weight: str = 'normal',
):
"""Set the font style of the text.
Args:
size (int | float): CSS font size. For example, 16.
family (str): CSS font family. For example, 'Arial, Helvetica, sans-serif'.
weight (str): CSS font weight. For example, 'bold'.
style (str): CSS font style. For example, 'italic'
Returns:
The instance of the class for fluent interface.
"""
self.instance.send(
self.id,
'setTextFont',
{'family': family, 'size': size, 'weight': weight, 'style': style},
)
return self
[docs]
def set_text_rotation(self, rotation: int | float):
"""Set the rotation of the text.
Args:
rotation (int | float): Rotation in degrees.
Returns:
The instance of the class for fluent interface.
"""
self.instance.send(self.id, 'setTextRotation', {'rotation': rotation})
return self
[docs]
def set_background_color(self, color: ColorInput | None):
"""Set the background color of the text box.
Args:
color (Color): Color of the background. Use 'transparent' or None to hide.
Returns:
The instance of the class for fluent interface.
"""
color = convert_color_to_hex(color) if color is not None else None
self.instance.send(self.id, 'setBackgroundFill', {'color': color})
return self
[docs]
def set_stroke(self, thickness: int | float, color: ColorInput | None = None):
"""Set the text box stroke style.
Args:
thickness (int | float): Thickness of the stroke.
color (Color): Color of the stroke. Use 'transparent' or None to hide.
Returns:
The instance of the class for fluent interface.
"""
color = convert_color_to_hex(color) if color is not None else None
self.instance.send(
self.id,
'setBackgroundStroke',
{'thickness': thickness, 'color': color},
)
return self
[docs]
def add_video(
self,
video_source: str,
fit_mode: str = 'fit',
size: dict = None,
):
"""
Updates the background of the current TextBox UI element to use a video
(provided as a Base64-encoded data URI) as its fill.
Args:
video_source (str): Path to the video file (MP4 or WEBM) or URL.
fit_mode (str): How the video should fit ('Fit', 'Stretch', 'Fill', 'Tile', 'Center').
size (dict, optional): Desired size of the video display, e.g. {"width": 150, "height": 150}.
This controls the UI element's padding. Defaults to 100x100.
Returns:
self: The instance for fluent interfacing.
Example:
>>> textbox.add_video("D:/path/to/local_video.mp4")
>>> textbox.add_video("https://example.com/video.mp4")
"""
if not video_source:
raise ValueError('Video source is required.')
video_data_uri = convert_to_base64(video_source)
args = {
'videoSource': video_data_uri,
'fitMode': fit_mode,
'size': size or {'width': 100, 'height': 100},
}
for fit_mode_option in [
'Fit',
'Stretch',
'Fill',
'Tile',
'Center',
]:
if fit_mode.lower() == fit_mode_option.lower():
args['fitMode'] = fit_mode_option
break
self.instance.send(self.id, 'addCustomVideo', args)
return self
[docs]
def add_image(
self,
source: str,
fit_mode: str = 'Stretch',
size: dict = None,
):
"""
Updates the background of the current TextBox UI element to use an image
(provided as a Base64-encoded data URI) as its fill.
Args:
source (str): Path to the image file or URL.
fit_mode (str): How the image should fit ('Stretch', 'Fill', 'Fit', 'Tile', 'Center').
size (dict, optional): Desired size of the image display, e.g. {"width": 100, "height": 100}.
Defaults to 100x100.
Returns:
self: The instance for fluent interfacing.
Example:
>>> textbox.add_image("D:/path/to/local_image.png")
>>> textbox.add_image("https://example.com/image.jpg")
"""
if not source:
raise ValueError('Image source is required.')
image_data_uri = convert_to_base64(source)
args = {
'source': image_data_uri,
'fitMode': fit_mode,
'size': size or {'width': 100, 'height': 100},
}
for fit_mode_option in [
'Stretch',
'Fill',
'Fit',
'Tile',
'Center',
]:
if fit_mode.lower() == fit_mode_option.lower():
args['fitMode'] = fit_mode_option
break
self.instance.send(self.id, 'addCustomImage', args)
return self
[docs]
def set_effect(self, enabled: bool):
"""Set theme effect enabled on component or disabled.
Args:
enabled: Boolean flag.
Returns:
The instance of the class for fluent interface.
"""
self.instance.send(self.id, 'setEffect', {'enabled': enabled})
return self
[docs]
def add_event_listener(
self,
event: str,
handler: callable | None = None,
throttle_ms: int = 0,
once: bool = False,
) -> str:
"""Attach an event listener to the TextBox.
Args:
event: Event name, e.g. 'pointerdown', 'pointermove', 'click'.
handler: Optional Python callback that receives the event payload.
throttle_ms: Minimum delay between handler invocations.
once: When True, the listener removes itself after the first trigger.
Returns:
The callback id that identifies this listener.
"""
callback_id = str(uuid.uuid4()).split('-')[0] if handler else ''
if handler is not None:
self.instance.event_handlers[callback_id] = handler
self.instance.send(
self.id,
'addEventListener',
{
'event': event,
'callbackId': callback_id or None,
'throttleMs': int(throttle_ms) if throttle_ms else 0,
'options': {'once': bool(once)},
},
)
return callback_id
[docs]
def get_text(self) -> str | None:
"""Get the text content of the text box.
Returns:
str | None: The text content of the text box.
"""
return self.instance.get(self.id, 'textBoxGetText', {})
[docs]
def get_text_font(self) -> dict | None:
"""Get font settings of the text box.
Returns:
dict | None: Font settings with keys:
- size (float): Font size
- family (str): Font family
- weight (str): Font weight
- style (str): Font style
"""
return self.instance.get(self.id, 'textBoxGetTextFont', {})
[docs]
def get_text_rotation(self) -> float | None:
"""Get the rotation of the text in degrees.
Returns:
float | None: Rotation in degrees.
"""
return self.instance.get(self.id, 'textBoxGetTextRotation', {})
[docs]
def get_text_fill_style(self) -> str | None:
"""Get the text fill style (color).
Returns:
str | None: Color as rgba string.
"""
return self.instance.get(self.id, 'textBoxGetTextFillStyle', {})
[docs]
def get_padding(self) -> dict | None:
"""Get padding around the text box in pixels.
Returns:
dict | None: Margin datastructure with padding values.
"""
return self.instance.get(self.id, 'textBoxGetPadding', {})
[docs]
def get_margin(self) -> dict | None:
"""Get margin around the text box in pixels.
Returns:
dict | None: Margin datastructure with margin values.
"""
return self.instance.get(self.id, 'textBoxGetMargin', {})
[docs]
def get_background(self) -> dict | None:
"""Get the text box background styling.
Returns:
dict | None: Contains `type`, `fill_style`, and `stroke_style`.
"""
return self.instance.get(self.id, 'textBoxGetBackground', {})
[docs]
def get_effect(self) -> bool | None:
"""Get theme effect enabled state on the text box.
Returns:
bool | None: True if theme effect is enabled, False otherwise.
"""
return self.instance.get(self.id, 'textBoxGetEffect', {})
[docs]
def get_highlight(self) -> float | None:
"""Get state of component highlighting.
Returns:
float | None: Number between 0 and 1, where 1 is fully highlighted.
"""
return self.instance.get(self.id, 'textBoxGetHighlight', {})
[docs]
def get_pointer_events(self) -> bool | None:
"""Get the mouse interactions state.
Returns:
bool | None: Mouse interactions state.
"""
return self.instance.get(self.id, 'textBoxGetPointerEvents', {})
[docs]
def is_disposed(self) -> bool | None:
"""Check whether the text box is disposed.
Returns:
bool | None: True if object is disposed.
"""
return self.instance.get(self.id, 'textBoxIsDisposed', {})
[docs]
class PointableTextBox(TextBox):
"""Interactive TextBox variant with built-in pointer support and drag handles."""
def __init__(
self,
chart,
text: str = None,
x: int = None,
y: int = None,
position_scale: str = 'axis',
):
UIElement.__init__(self, chart)
self.instance.send(
self.id,
'pointableTextBox',
{'chart': self.chart.id, 'positionScale': position_scale},
)
if text:
self.set_text(text)
if x is not None and y is not None:
self.set_position(x, y)
[docs]
def set_direction(self, direction: str):
"""Set pointable direction."""
direction_map = {
'up': 0,
'right': 1,
'down': 2,
'left': 3,
}
value = direction
if isinstance(direction, str):
value = direction_map.get(direction.lower(), direction)
self.instance.send(self.id, 'pointableTextBoxSetDirection', {'direction': value})
return self
[docs]
def get_direction(self):
"""Get current pointable direction."""
return self.instance.get(self.id, 'pointableTextBoxGetDirection', {})
[docs]
def set_pointer_length(self, length: int | float):
"""Set pointer head length in pixels."""
self.instance.send(self.id, 'pointableTextBoxSetPointerLength', {'length': length})
return self
[docs]
def get_pointer_length(self) -> float | None:
"""Get pointer head length in pixels."""
return self.instance.get(self.id, 'pointableTextBoxGetPointerLength', {})
[docs]
def set_tick_label_padding(self, padding: int | float):
"""Set padding between pointer and label."""
self.instance.send(self.id, 'pointableTextBoxSetTickLabelPadding', {'padding': padding})
return self
[docs]
def get_tick_label_padding(self) -> float | None:
"""Get padding between pointer and label."""
return self.instance.get(self.id, 'pointableTextBoxGetTickLabelPadding', {})