from __future__ import annotations
import datetime
from typing import TYPE_CHECKING, Callable, Any
import uuid
try:
from typing import Unpack
except ImportError:
from typing_extensions import Unpack
from lightningchart.instance import Instance
from lightningchart.ui.axis import DefaultAxis, DefaultAxis3D, Axis
from lightningchart.ui.cursor import Cursor2D, Cursor3D, CursorXY
from lightningchart.ui.legend import Legend
from lightningchart.ui.text_box import TextBox, PointableTextBox
from lightningchart.ui.check_box import CheckBox
from lightningchart.ui.button_box import ButtonBox
from lightningchart.utils import convert_to_base64, convert_color_to_hex
from lightningchart.utils.utils import ColorInput, LegendOptions, PaddingKwargs
if TYPE_CHECKING:
from lightningchart.ui.cursor import CursorXY, Cursor2D, Cursor3D
[docs]
class Chart:
def __init__(self, instance: Instance):
self.id = str(uuid.uuid4()).split('-')[0]
self.instance = instance
self._legend = None
@property
def legend(self):
"""Access to chart's default legend."""
if self._legend is None:
self._legend = Legend(self)
return self._legend
[docs]
def open(
self,
method: str = None,
live=False,
width: int | str = '100%',
height: int | str = 600,
):
"""Open the rendering view.
Method "browser" will open the chart in your browser.
Method "notebook" will display the chart in a notebook environment with an IFrame component.
Method "link" will return a URL of the chart that can be used to embed it in external applications.
Args:
method (str): "browser" | "notebook"
live (bool): Whether to use real-time rendering or not.
width (int): The width of the IFrame component in pixels.
height (int): The height of the IFrame component in pixels.
Returns:
self | str: Returns a URL string if method is "link", otherwise returns the class instance.
"""
return self.instance.open(method=method, live=live, width=width, height=height) or self
[docs]
def close(self):
"""Close the connection to a chart with real-time display mode.
Note: This will terminate the current Python instance!
"""
self.instance.close()
return self
[docs]
def set_data_preservation(self, enabled: bool):
"""Enable or disable server-side data preservation for real-time visualization use cases.
Enabled by default!
Args:
enabled (bool): Boolean flag.
Returns:
The instance of the class for fluent interface.
"""
self.instance.set_data_preservation(enabled)
return self
[docs]
class GeneralMethods(Chart):
[docs]
def save_to_file(
self,
file_name: str = None,
image_format: str = 'image/png',
image_quality: float = 0.92,
scale: float = None,
):
"""Save the current rendering view as a screenshot.
Args:
file_name (str): Name of prompted download file as string. File extension shouldn't be included as it is
automatically detected from 'type'-argument.
image_format (str): A DOMString indicating the image format. The default format type is image/png.
image_quality (float): A Number between 0 and 1 indicating the image quality to use for image formats that
use lossy compression such as image/jpeg and image/webp. If this argument is anything else,
the default value for image quality is used. The default value is 0.92.
scale (float): Convenience output scaling factor. This doesn't actually stretch the result, but instead draws an altered scaled version and captures that.
Returns:
The instance of the class for fluent interface.
"""
if file_name is None:
file_name = f'LightningChart_Python_{datetime.datetime.now().strftime("%Y%m%d%H%M%S")}'
self.instance.send(
self.id,
'saveToFile',
{
'fileName': file_name,
'type': image_format,
'encoderOptions': image_quality,
'scale': scale,
},
)
return self
[docs]
def dispose(self):
"""Permanently destroy the component."""
self.instance.send(self.id, 'dispose')
[docs]
def set_animations_enabled(self, enabled: bool = True):
"""Disable/Enable all animations of the Chart.
Args:
enabled (bool): Boolean value to enable or disable animations.
Returns:
The instance of the class for fluent interface.
"""
self.instance.send(self.id, 'setAnimationsEnabled', {'enabled': enabled})
return self
[docs]
def set_padding(self, *args, **kwargs: Unpack[PaddingKwargs]):
"""Set padding around the chart 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_background_color(self, color: ColorInput | None):
"""Set the background color of the chart.
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, 'setBackgroundFillStyle', {'color': color})
return self
[docs]
def get_background_color(self) -> dict:
"""Get chart background color.
Returns:
dict with 'color', 'colorHex', 'colorRgb'.
"""
return self.instance.get(self.id, 'getChartBackgroundColor', {})
[docs]
def set_background_stroke(self, thickness: int | float, color: ColorInput | None = None):
"""Set the background stroke style of the chart.
Args:
thickness (int | float): Thickness of the stroke.
color (Color): The 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,
'setBackgroundStrokeStyle',
{'thickness': thickness, 'color': color},
)
return self
[docs]
def add_legend(self, **options: Unpack[LegendOptions]) -> Legend:
"""Add a new user-managed legend to the chart.
Args:
**options: Legend configuration options including:
visible (bool): Whether legend should be visible (default: True)
position: Legend position (LegendPosition enum or custom position dict)
title (str): Legend title
title_font (dict): Title font settings
title_fill_style: Title color/fill style
orientation: Legend orientation (LegendOrientation.Horizontal/Vertical)
render_on_top (bool): Whether to render legend on top of chart
background_visible (bool): Whether background should be visible
background_fill_style: Background fill style
background_stroke_style: Background stroke style
padding: Legend content padding
margin_inner: Margin from chart to legend
margin_outer: Margin from legend to chart edge
entry_margin: Margin between legend entries
auto_hide_threshold (float): Auto-hide threshold (0.0-1.0)
add_entries_automatically (bool): Whether to add entries automatically (default: False for user legends)
entries (dict): Default entry options
Returns:
New user-managed legend instance
Examples:
Basic user-managed legend
>>> legend = chart.add_legend(title="Custom Legend")
Positioned legend
>>> legend = chart.add_legend(
... position='TopRight',
... orientation='Horizontal',
... background_visible=True
)
"""
legend = Legend(self, is_user_legend=True)
legend_id = str(uuid.uuid4())
legend.id = legend_id
self.instance.send(legend_id, 'addLegend', {'chart': self.id})
if options:
legend.set_options(**options)
return legend
[docs]
def add_textbox(
self,
text: str = None,
x: int = None,
y: int = None,
position_scale: str = 'axis',
):
"""Add text box to the chart.
Args:
text (str): Text of the text box.
x (int): X position in percentages (0-100).
y (int): Y position in percentages (0-100).
position_scale (str): "percentage" | "pixel" | "axis"
Returns:
Reference to Text Box class.
"""
return TextBox(chart=self, text=text, x=x, y=y, position_scale=position_scale)
textbox = add_textbox
[docs]
def add_pointable_textbox(
self,
text: str = None,
x: int = None,
y: int = None,
position_scale: str = 'axis',
):
"""Add pointable text box to the chart.
Args:
text (str): Text of the text box.
x (int): X position.
y (int): Y position.
position_scale (str): "percentage" | "pixel" | "axis"
Returns:
Reference to PointableTextBox class.
"""
return PointableTextBox(
chart=self,
text=text,
x=x,
y=y,
position_scale=position_scale,
)
pointable_textbox = add_pointable_textbox
[docs]
def add_checkbox(
self,
text: str = None,
x: int = None,
y: int = None,
position_scale: str = 'axis',
):
"""Add checkbox to the chart.
Args:
text (str): Label text of the checkbox.
x (int): X position.
y (int): Y position.
position_scale (str): "percentage" | "pixel" | "axis"
Returns:
Reference to CheckBox class.
"""
return CheckBox(
chart=self,
text=text,
x=x,
y=y,
position_scale=position_scale,
)
checkbox = add_checkbox
buttonbox = add_buttonbox
[docs]
class TitleMethods(Chart):
[docs]
def set_title(self, title: str):
"""Set text of Chart title.
Args:
title (str): Chart title as a string.
Returns:
The instance of the class for fluent interface.
"""
self.instance.send(self.id, 'setTitle', {'title': title})
return self
[docs]
def hide_title(self):
"""Hide title and remove padding around it.
Returns:
The instance of the class for fluent interface.
"""
self.instance.send(self.id, 'hideTitle')
return self
[docs]
def set_title_color(self, color: ColorInput | None):
"""Set color of Chart title.
Args:
color (Color): Color of the title. 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, 'setTitleColor', {'color': color})
return self
[docs]
def set_title_font(
self,
size: int | float,
family: str = 'Segoe UI, -apple-system, Verdana, Helvetica',
style: str = 'normal',
weight: str = 'normal',
):
"""Set font of Chart title.
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,
'setTitleFont',
{'family': family, 'size': size, 'weight': weight, 'style': style},
)
return self
[docs]
def set_title_margin(self, *args, **kwargs: Unpack[PaddingKwargs]):
"""Specifies padding after chart title.
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_title_margin(5)`: Sets uniform padding for all sides (integer or float).
- `set_title_margin(left=10, top=15)`: Sets padding for specific sides only.
- `set_title_margin(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_title_margin(5): Uniform margin for all sides.\n'
'- set_title_margin(left=10, top=15): Specify individual sides.\n'
'- set_title_margin(left=10, top=15, right=20, bottom=25): Full margin definition.'
)
self.instance.send(self.id, 'setTitleMargin', {'margin': padding})
return self
[docs]
def set_title_rotation(self, degrees: int | float):
"""Set rotation of Chart title.
Args:
degrees (int | float): Rotation in degrees.
Returns:
The instance of the class for fluent interface.
"""
self.instance.send(self.id, 'setTitleRotation', {'value': degrees})
return self
[docs]
def set_title_effect(self, enabled: bool = True):
"""Set theme effect enabled on component or disabled.
Args:
enabled (bool): Theme effect enabled.
Returns:
The instance of the class for fluent interface.
"""
self.instance.send(self.id, 'setTitleEffect', {'enabled': enabled})
return self
[docs]
class ChartWithXYAxis(Chart):
def __init__(self):
self.default_x_axis = DefaultAxis(self, 'x')
self.default_y_axis = DefaultAxis(self, 'y')
[docs]
def get_default_x_axis(self) -> Axis:
"""Get the reference to the default x-axis of the chart.
Returns:
Reference to Axis class.
"""
return self.default_x_axis
[docs]
def get_default_y_axis(self) -> Axis:
"""Get the reference to the default y-axis of the chart.
Returns:
Reference to Axis class.
"""
return self.default_y_axis
[docs]
def synchronize_axis_intervals(self, axis_array: list[Axis]):
"""Convenience function for synchronizing the intervals of n amount of ´Axis´.
Args:
axis_array (list[Axis]): List of Axis to synchronize.
Returns:
The instance of the class for fluent interface.
"""
axis_array = [axis.id for axis in axis_array]
self.instance.send(self.id, 'synchronizeAxes', {'axes': axis_array})
return self
[docs]
class ChartWithXYZAxis(Chart):
def __init__(self):
self.default_x_axis = DefaultAxis3D(self, 'x')
self.default_y_axis = DefaultAxis3D(self, 'y')
self.default_z_axis = DefaultAxis3D(self, 'z')
[docs]
def get_default_x_axis(self) -> DefaultAxis3D:
"""Get the reference to the default x-axis of the chart.
Returns:
Reference to Axis3D class.
"""
return self.default_x_axis
[docs]
def get_default_y_axis(self) -> DefaultAxis3D:
"""Get the reference to the default y-axis of the chart.
Returns:
Reference to Axis3D class.
"""
return self.default_y_axis
[docs]
def get_default_z_axis(self) -> DefaultAxis3D:
"""Get the reference to the default z-axis of the chart.
Returns:
Reference to Axis3D class.
"""
return self.default_z_axis
[docs]
class ChartWithSeries(Chart):
def __init__(self, instance: Instance):
Chart.__init__(self, instance)
self.series_list = []
self._auto_color_index_counter = 0
def _resolve_auto_color_index(self, automatic_color_index: int | None):
"""Return the user-specified automatic_color_index or generate a new one."""
if automatic_color_index is not None:
return automatic_color_index
idx = self._auto_color_index_counter
self._auto_color_index_counter += 1
return idx
[docs]
def set_series_background_effect(self, enabled: bool = True):
"""Set theme effect enabled on component or disabled.
Args:
enabled (bool): Theme effect enabled.
Returns:
The instance of the class for fluent interface.
"""
self.instance.send(self.id, 'setSeriesBackgroundEffect', {'enabled': enabled})
return self
[docs]
def set_series_background_color(self, color: ColorInput | None):
"""Set the color of chart series background.
Args:
color (Color): Color of the series 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, 'setSeriesBackgroundFillStyle', {'color': color})
return self
[docs]
def get_series_background_color(self) -> dict:
"""Get series-background color of the chart.
Returns:
dict with 'color' (uint32 RGBA), 'colorHex' (#rrggbbaa), 'colorRgb' (rgb/rgba).
Notes:
Call this in live mode, e.g. ``chart.open(live=True)``
"""
return self.instance.get(self.id, 'getSeriesBackgroundColor', {})
[docs]
def set_series_background_stroke(self, thickness: int | float, color: ColorInput | None = None):
"""
Set the stroke (border) of the Series Background (plot area).
Use thickness=0 or color=None/'transparent' to hide it.
"""
color = convert_color_to_hex(color) if color is not None else None
self.instance.send(
self.id,
'setSeriesBackgroundStrokeStyle',
{'thickness': thickness, 'color': color},
)
return self
[docs]
def set_engine_background_color(self, color: ColorInput | None):
"""
Set the background color of the chart's ENGINE (canvas behind the chart). Use 'transparent' or None to hide.
"""
color = convert_color_to_hex(color) if color is not None else None
self.instance.send(self.id, 'setEngineBackgroundFillStyle', {'color': color})
return self
[docs]
class ChartWithLUT(Chart):
[docs]
def set_lookup_table(
self,
steps: list[dict[str, any]],
interpolate: bool = True,
percentage_values: bool = False,
):
"""Attach lookup table (LUT) to fill the slices with Colors based on value.
Args:
steps (list[dict]): List of {"value": number, "color": Color, 'label': 'Label'} dictionaries.
interpolate (bool): Whether color interpolation is used
percentage_values (bool): Whether values represent percentages or explicit values.
Returns:
The instance of the class for fluent interface.
"""
steps = [dict(s, color=convert_color_to_hex(s['color'])) if 'color' in s else s for s in steps]
self.instance.send(
self.id,
'setLUT',
{
'steps': steps,
'interpolate': interpolate,
'percentageValues': percentage_values,
},
)
return self
[docs]
class ChartWithLabelStyling(Chart):
[docs]
def set_label_color(self, color: ColorInput | None):
"""Set the color of Slice Labels.
Args:
color (Color): Color of the labels. 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, 'setLabelColor', {'color': color})
return self
[docs]
def set_label_font(
self,
size: int | float,
family: str = 'Segoe UI, -apple-system, Verdana, Helvetica',
style: str = 'normal',
weight: str = 'normal',
):
"""Set font of Slice Labels.
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,
'setLabelFont',
{'family': family, 'size': size, 'weight': weight, 'style': style},
)
return self
[docs]
def set_label_effect(self, enabled: bool):
"""Set theme effect enabled on label or disabled. A theme can specify an Effect to add extra visual
oomph to chart applications, like Glow effects around data or other components.
Args:
enabled (bool): Theme effect enabled.
Returns:
The instance of the class for fluent interface.
"""
self.instance.send(self.id, 'setLabelEffect', {'enabled': enabled})
return self
[docs]
def set_slice_effect(self, enabled: bool):
"""Set theme effect enabled on slice or disabled. A theme can specify an Effect to add extra visual
oomph to chart applications, like Glow effects around data or other components.
Args:
enabled (bool): Theme effect enabled.
Returns:
The instance of the class for fluent interface.
"""
self.instance.send(self.id, 'setSliceEffect', {'enabled': enabled})
return self
[docs]
def set_slice_colors(self, color_list: list[any]):
"""Set the colors of all slices at once.
Args:
color_list (list[Color]): list of Colors. The length must match the current number of slices! Use 'transparent' or None to hide.
Returns:
The instance of the class for fluent interface.
"""
hex_array = []
for color in color_list:
hex_array.append(convert_color_to_hex(color))
self.instance.send(self.id, 'setSliceFuncFillStyle', {'hexArray': hex_array})
return self
[docs]
def set_slice_sorter(self, sorter: str):
"""Define the sorting logic for slices.
Args:
sorter (str): "name" | "valueAscending" | "valueDescending" | "none"
Returns:
The instance of the class for fluent interface.
"""
slice_sorters = ('name', 'valueAscending', 'valueDescending', 'none')
if sorter not in slice_sorters:
raise ValueError(f"Expected sorter to be one of {slice_sorters}, but got '{sorter}'.")
self.instance.send(self.id, 'setSliceSorter', {'sorter': sorter})
return self
[docs]
def set_label_connector_style(self, style: str, thickness: int | float, color: ColorInput | None = None):
"""Set style of Label connector lines.
Args:
style (str): "solid" | "dashed" | "empty"
thickness (int | float): Thickness of the connector line.
color (Color): Color of the connector line. 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
styles = ('solid', 'dashed', 'empty')
if style not in styles:
raise ValueError(f"Expected style to be one of {styles}, but got '{style}'.")
self.instance.send(
self.id,
'setLabelConnectorStyle',
{'style': style, 'thickness': thickness, 'color': color},
)
return self
[docs]
def get_slice_color(self, category: str) -> dict:
"""Get the fill color of a slice (Pie / Funnel / Pyramid).
Args:
category: Slice name.
Returns:
dict with 'color', 'colorHex', 'colorRgb'.
Notes:
Call this in live mode, e.g. ``chart.open(live=True)``
"""
return self.instance.get(self.id, 'getSliceFillStyle', {'category': category})
[docs]
class BackgroundChartStyle(Chart):
[docs]
def set_chart_background_image(
self,
source: str,
fit_mode: str = 'Stretch',
surrounding_color=None,
source_missing_color=None,
):
"""
Set the chart background image.
Args:
source (str): The image source. This can be:
- A URL (remote image).
- A local file path.
- An already Base64-encoded image string.
fit_mode (str, optional): Fit mode for the image. Options:
- "Stretch" (default)
- "Fill"
- "Fit"
- "Tile"
- "Center"
surrounding_color (Color, optional): Color for areas outside the image.
source_missing_color (Color, optional): Color when the image fails to load.
Returns:
self: The instance of the class for method chaining.
Raises:
ValueError: If the source is invalid.
Example:
>>> chart.set_chart_background_image("D:/path/to/local_image.png")
>>> chart.set_chart_background_image("https://example.com/image.jpg")
"""
if not source:
raise ValueError('Image source is required.')
if not source.startswith('data:'):
source = convert_to_base64(source)
args = {
'source': source,
'fitMode': fit_mode,
'surroundingColor': convert_color_to_hex(surrounding_color) if surrounding_color else None,
'sourceMissingColor': convert_color_to_hex(source_missing_color) if source_missing_color else None,
}
for fit_mode_option in ['Stretch', 'Fill', 'Fit', 'Tile', 'Center']:
if fit_mode_option.lower() == fit_mode.lower():
args['fitMode'] = fit_mode_option
break
self.instance.send(self.id, 'setChartBackgroundImage', args)
return self
[docs]
def set_chart_background_video(
self, video_source: str, fit_mode: str = 'fit', surrounding_color: ColorInput | None = None, source_missing_color: ColorInput | None = None
):
"""
Sets the chart background to a video.
Args:
video_source (str): Path to the video file (MP4 or WEBM).
fit_mode (str): Fit mode ('Fit', 'Stretch', 'Fill', 'Center', 'Tile').
surrounding_color (Color, optional): Color for areas outside the video.
source_missing_color (Color, optional): Color when video fails to load.
Returns:
The instance of the class for method chaining.
Example:
>>> chart.set_chart_background_video("D:/path/to/local_video.mp4")
>>> chart.set_chart_background_video("https://example.com/video.mp4")
"""
if not video_source:
raise ValueError('Video source is required.')
video_data_uri = convert_to_base64(video_source)
surrounding_color = convert_color_to_hex(surrounding_color) if surrounding_color is not None else None
source_missing_color = convert_color_to_hex(source_missing_color) if source_missing_color is not None else None
args = {
'videoSource': video_data_uri,
'fitMode': fit_mode,
'surroundingColor': surrounding_color if surrounding_color else None,
'sourceMissingColor': source_missing_color if source_missing_color else None,
}
for fit_mode_option in ['Stretch', 'Fill', 'Fit', 'Tile', 'Center']:
if fit_mode_option.lower() == fit_mode.lower():
args['fitMode'] = fit_mode_option
break
self.instance.send(self.id, 'setChartBackgroundVideo', args)
return self
[docs]
def set_series_background_image(
self,
source: str,
fit_mode: str = 'Stretch',
surrounding_color=None,
source_missing_color=None,
):
"""
Set the series background image.
Args:
source (str): The image source. This can be:
- A URL (remote image).
- A local file path.
- An already Base64-encoded image string.
fit_mode (str, optional): Fit mode for the image. Options:
- "Stretch" (default)
- "Fill"
- "Fit"
- "Tile"
- "Center"
surrounding_color (Color, optional): Color for areas outside the image.
source_missing_color (Color, optional): Color when the image fails to load.
Returns:
self: The instance of the class for method chaining.
Raises:
ValueError: If the source is invalid.
Example:
>>> chart.set_series_background_image("D:/path/to/local_image.png")
>>> chart.set_series_background_image("https://example.com/image.jpg")
"""
if not source:
raise ValueError('Image source is required.')
if not source.startswith('data:'):
source = convert_to_base64(source)
args = {
'source': source,
'fitMode': fit_mode,
'surroundingColor': convert_color_to_hex(surrounding_color) if surrounding_color else None,
'sourceMissingColor': convert_color_to_hex(source_missing_color) if source_missing_color else None,
}
for fit_mode_option in ['Stretch', 'Fill', 'Fit', 'Tile', 'Center']:
if fit_mode_option.lower() == fit_mode.lower():
args['fitMode'] = fit_mode_option
break
self.instance.send(self.id, 'setSeriesBackgroundImage', args)
return self
[docs]
def set_series_background_video(
self,
video_source: str,
fit_mode: str = 'fit',
surrounding_color: ColorInput | None = None,
source_missing_color: ColorInput | None = None,
):
"""
Sets the series background to a video.
Args:
video_source (str): Path to the video file (MP4 or WEBM).
fit_mode (str): Fit mode ('Fit', 'Stretch', 'Fill', 'Center', 'Tile').
surrounding_color (Color, optional): Color for areas outside the video.
source_missing_color (Color, optional): Color when video fails to load.
Returns:
The instance of the class for method chaining.
Example:
>>> chart.set_series_background_video("D:/path/to/local_video.mp4")
>>> chart.set_series_background_video("https://example.com/video.mp4")
"""
if not video_source:
raise ValueError('Video source is required.')
video_data_uri = convert_to_base64(video_source)
surrounding_color = convert_color_to_hex(surrounding_color) if surrounding_color is not None else None
source_missing_color = convert_color_to_hex(source_missing_color) if source_missing_color is not None else None
args = {
'videoSource': video_data_uri,
'fitMode': fit_mode,
'surroundingColor': surrounding_color if surrounding_color else None,
'sourceMissingColor': source_missing_color if source_missing_color else None,
}
for fit_mode_option in ['Stretch', 'Fill', 'Fit', 'Tile', 'Center']:
if fit_mode_option.lower() == fit_mode.lower():
args['fitMode'] = fit_mode_option
break
self.instance.send(self.id, 'setSeriesBackgroundVideo', args)
return self
[docs]
class FunnelPyramidLabelConnectorMethods:
[docs]
def set_label_connector_gap_before_label(self, gap: int | float):
"""Set gap before label in label connector.
Args:
gap (int | float): Gap in pixels.
Returns:
The instance of the class for fluent interface.
"""
self.instance.send(self.id, 'setLabelConnectorGapBeforeLabel', {'gap': gap})
return self
[docs]
def set_label_connector_gap_before_slice(self, gap: int | float):
"""Set gap before slice in label connector.
Args:
gap (int | float): Gap in pixels.
Returns:
The instance of the class for fluent interface.
"""
self.instance.send(self.id, 'setLabelConnectorGapBeforeSlice', {'gap': gap})
return self
[docs]
def set_label_connector_length_after_slice(self, length: int | float):
"""Set length after slice in label connector.
Args:
length (int | float): Length in pixels.
Returns:
The instance of the class for fluent interface.
"""
self.instance.send(self.id, 'setLabelConnectorLengthAfterSlice', {'length': length})
return self
[docs]
def set_label_connector_min_length_before_slice(self, length: int | float):
"""Set minimum length before slice in label connector.
Args:
length (int | float): Minimum length in pixels.
Returns:
The instance of the class for fluent interface.
"""
self.instance.send(self.id, 'setLabelConnectorMinLengthBeforeSlice', {'length': length})
return self
[docs]
class ChartsWithAddEventListener:
[docs]
def add_event_listener(
self,
event: str,
handler: callable | None = None,
xy_chart=None,
throttle_ms: int = 0,
once: bool = False,
target: str = 'auto',
) -> str:
"""
Add event and (optionally) bind a ChartXY overlay to stay
in sync with a Map/Bar view.
Args:
event : str
Event name emitted by the target. Common options include:
- Interaction: 'click', 'pointermove', 'pointerdown', 'pointerup',
'pointerenter', 'pointerleave', 'dblclick'
- Cursor: 'cursortargetchange' (reports cursor hits / mouse location)
- Lifecycle/Layout: 'ready', 'layoutchange'
- Map/Bar view: 'viewchange' (latitude/longitude + margins)
handler : Python callback receiving event data
xy_chart : ChartXY | None, keyword-only
When `event == 'viewchange'`, pass a `ChartXY` here to keep its axes
and padding bound to the Map/Bar view. Ignored for other events.
throttle_ms : Minimum delay between callbacks in milliseconds
once : bool, default False, keyword-only
If True, listener removes itself after first trigger
target : {'auto','seriesBackground','background','title','axisXTitle','axisYTitle','chart'}, default 'auto'
Which DOM element to attach to:
- 'auto' : For mouse/pointer events, uses plot area
(`seriesBackground`) if available, otherwise the chart.
- 'seriesBackground': Plot area (inside axes) — best for data-space clicks/moves.
- 'background' : Chart background (outside axes).
- 'title' : Chart title element.
- 'axisXTitle' : X-axis title element.
- 'axisYTitle' : Y-axis title element.
- 'axisZTitle' : Z-axis title element (3D charts only).
- 'chart' : Chart object itself (e.g., 'layoutchange', 'ready',
or chart-level cursor events).
Returns:
`callback_id` that identifies the registered handler (empty string if
no `handler` was supplied).
Examples:
# 1) Clicks in the plot area of a ChartXY
>>> def on_click(ev): print('[click]', ev)
>>> xy.add_event_listener('click', handler=on_click, target='seriesBackground')
# 2) Keep an overlayed XY chart glued to a Map view (pan/zoom/resize)
>>> map_chart.add_event_listener('viewchange', xy_chart=xy_overlay)
# 3) Listen cursor hits (hover) at chart level
>>> def on_cursor(ev): print(ev.get('hits'))
>>> xy.add_event_listener('cursortargetchange', handler=on_cursor, target='chart', throttle_ms=50)
# 4) Run code when a chart signals it is ready
>>> def on_ready(_): print('chart ready')
>>> map_chart.add_event_listener('ready', handler=on_ready, target='chart')
"""
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,
'xyId': getattr(xy_chart, 'id', None),
'throttleMs': int(throttle_ms) if throttle_ms else 0,
'options': {'once': bool(once)},
'target': target,
},
)
return callback_id
[docs]
class GeneralGetMethods:
[docs]
def get_title(self) -> str | None:
"""Get text of Chart title.
Returns:
The instance of the class for fluent interface.
Notes:
Call this in live mode, e.g. ``chart.open(live=True)``
"""
return self.instance.get(self.id, 'getTitle', {})
[docs]
def get_title_effect(self) -> bool:
"""Get whether theme effect is enabled on axis title.
A theme can specify an Effect to add extra visual elements to chart
applications, like Glow effects around data or other components.
Returns:
bool: True if theme effect is enabled, False otherwise.
Notes:
Call this in live mode, e.g. ``chart.open(live=True)``
Examples:
>>> effect_enabled = chart.get_title_effect()
"""
return self.instance.get(self.id, 'getTitleEffect', {})
[docs]
def get_title_fill_style(self) -> str | None:
"""Get chart title fill style (color).
Returns:
str | None: Color as rgba string, or None if transparent.
Notes:
Call this in live mode, e.g. ``chart.open(live=True)``
Examples:
>>> color = chart.get_title_fill_style()
"""
return self.instance.get(self.id, 'getTitleFillStyle', {})
[docs]
def get_title_font(self) -> dict:
"""Get font settings of chart title.
Returns:
dict: Font settings with keys:
- size (float): Font size
- family (str): Font family
- weight (str): Font weight (e.g., 'normal', 'bold')
- style (str): Font style (e.g., 'normal', 'italic')
Notes:
Call this in live mode, e.g. ``chart.open(live=True)``
Examples:
>>> font = chart.get_title_font()
>>> print(f"Size: {font['size']}, Family: {font['family']}")
"""
return self.instance.get(self.id, 'getTitleFont', {})
[docs]
def get_title_rotation(self) -> float:
"""Get rotation of chart title.
Returns:
float: Rotation in degrees.
Notes:
Call this in live mode, e.g. ``chart.open(live=True)``
Examples:
>>> rotation = chart.get_title_rotation()
>>> print(f"Title rotation: {rotation} degrees")
"""
return self.instance.get(self.id, 'getTitleRotation', {})
[docs]
def get_size_pixels(self):
"""Get size of control as pixels.
Returns:
Control size in pixels.
Notes:
Call this in live mode, e.g. ``chart.open(live=True)``
"""
result = self.instance.get(self.id, 'getSizePixels', {})
return result
[docs]
def get_animations_enabled(self) -> bool:
"""Get animations disable/enable state."""
return self.instance.get(self.id, 'getAnimationsEnabled', {})
[docs]
def get_background_fill_style(self) -> str | None:
"""Get chart background fill style color as rgba string (or None if transparent/unavailable)."""
return self.instance.get(self.id, 'getBackgroundFillStyle', {})
[docs]
def get_background_stroke_style(self) -> dict | None:
"""Get chart background stroke style.
Returns:
dict | None: {'thickness': number|None, 'color': 'rgba(...)'|None}
"""
return self.instance.get(self.id, 'getBackgroundStrokeStyle', {})
[docs]
def get_is_in_view(self) -> bool:
"""True when chart/panel is considered to be in browser viewport."""
return self.instance.get(self.id, 'getIsInView', {})
[docs]
def get_minimum_size(self) -> dict | None:
"""Get minimum size of the panel.
Returns:
dict | None: {'x': number, 'y': number} or None
"""
return self.instance.get(self.id, 'getMinimumSize', {})
[docs]
def get_padding(self) -> dict:
"""Get padding around chart in pixels.
Returns:
dict: Margin-like object (e.g. {'top':..,'right':..,'bottom':..,'left':..})
"""
return self.instance.get(self.id, 'getPadding', {})
[docs]
def get_theme(self) -> dict:
"""Get the Theme currently being used.
Returns:
dict: Theme object.
"""
return self.instance.get(self.id, 'getTheme', {})
[docs]
def get_title_margin(self) -> dict:
"""Get padding/margin after chart title.
Returns:
dict: Margin-like object (e.g. {'top':..,'right':..,'bottom':..,'left':..})
"""
return self.instance.get(self.id, 'getTitleMargin', {})
[docs]
def get_title_size(self) -> dict:
"""Get chart title size in pixels.
Returns:
dict: {'x': number, 'y': number}
"""
return self.instance.get(self.id, 'getTitleSize', {})
[docs]
class CursorBehaviorMixin:
"""Shared cursor behavior utilities for charts with built-in cursors."""
_cursor_modes = (
'disabled',
'show-all',
'show-all-interpolated',
'show-nearest',
'show-nearest-interpolated',
'show-pointed',
'show-pointed-interpolated',
)
_custom_cursor_callback_id: str | None = None
_cursor_formatter_callback_id: str | None = None
_cursor_formatter_handler: Callable[[Any], Any] | None = None
[docs]
def set_cursor_mode(self, mode: str | None):
"""Set built-in cursor mode.
Args:
mode (str | None): One of "disabled", "show-nearest", "show-nearest-interpolated",
"show-pointed", "show-pointed-interpolated", "show-all", "show-all-interpolated".
``None`` is treated as ``"disabled"``.
Returns:
The instance of the class for fluent interface.
"""
if mode is None:
mode_normalized = 'disabled'
elif isinstance(mode, str):
mode_normalized = mode.strip().lower()
else:
raise ValueError('mode must be a string or None.')
if mode_normalized not in self._cursor_modes:
raise ValueError(f"Expected mode to be one of {self._cursor_modes}, but got '{mode}'.")
self.instance.send(self.id, 'setCursorMode', {'mode': mode_normalized})
return self
[docs]
def get_cursor_mode(self) -> str:
"""Get active cursor mode (returns 'disabled' when cursor is off)."""
mode = self.instance.get(self.id, 'getCursorMode', {})
if isinstance(mode, str) and mode:
return mode
return 'disabled'
[docs]
def get_cursor(self):
"""Returns the managed cursor instance."""
return self.set_cursor()
[docs]
def set_cursor_dynamic_behavior(
self,
match_point_marker_shape: bool | None = None,
point_marker_fill: str | None = None,
point_marker_stroke: dict | None = None,
point_marker_size: int | float | tuple[float, float] | dict | None = None,
):
"""
Configure automatic cursor styling.
Args:
match_point_marker_shape (bool, optional): Match marker shape to hovered series marker.
point_marker_fill (str, optional): Hex/RGB color or ``'match-data'`` to use series color.
point_marker_stroke (dict, optional): {'color': '#fff'|'match-data', 'thickness': float}
point_marker_size (float | tuple | dict, optional): Uniform size or {'x','y'} mapping.
Returns:
The instance of the class for fluent interface.
"""
params = {}
if match_point_marker_shape is None and point_marker_fill is None and point_marker_stroke is None and point_marker_size is None:
params['enabled'] = False
self.instance.send(self.id, 'setCursorDynamicBehavior', params)
return self
params['enabled'] = True
if match_point_marker_shape is not None:
params['matchShape'] = bool(match_point_marker_shape)
if point_marker_fill is not None:
if isinstance(point_marker_fill, str) and point_marker_fill.replace('_', '-').lower() == 'match-data':
params['fillMode'] = 'data'
else:
params['fillMode'] = 'solid'
params['fillColor'] = convert_color_to_hex(point_marker_fill)
stroke_opts = point_marker_stroke or {}
if stroke_opts:
stroke_color = stroke_opts.get('color')
if stroke_color is None and stroke_opts.get('thickness') is not None:
stroke_color = 'match-data'
if isinstance(stroke_color, str) and stroke_color.replace('_', '-').lower() == 'match-data':
params['strokeMode'] = 'data'
elif stroke_color is not None:
params['strokeMode'] = 'solid'
params['strokeColor'] = convert_color_to_hex(stroke_color)
if stroke_opts.get('thickness') is not None:
params['strokeThickness'] = stroke_opts['thickness']
if point_marker_size is not None:
if isinstance(point_marker_size, (int, float)):
params['size'] = {'x': point_marker_size, 'y': point_marker_size}
elif isinstance(point_marker_size, (tuple, list)):
params['size'] = {'x': point_marker_size[0], 'y': point_marker_size[1]}
else:
params['size'] = point_marker_size
self.instance.send(self.id, 'setCursorDynamicBehavior', params)
return self
[docs]
def set_custom_cursor(self, handler: Callable[[dict], Any] | None = None, throttle_ms: int = 0):
"""
Replace the built-in cursor with a custom handler.
Args:
handler (callable, optional): Receives the same payload as the chart event cursortargetchange.
throttle_ms (int, optional): Minimum delay between handler invocations.
"""
if self._custom_cursor_callback_id:
self.instance.event_handlers.pop(self._custom_cursor_callback_id, None)
self._custom_cursor_callback_id = None
if handler is None:
self.instance.send(self.id, 'setCustomCursor', {'callbackId': None})
return self
callback_id = str(uuid.uuid4()).split('-')[0]
self.instance.event_handlers[callback_id] = handler
self._custom_cursor_callback_id = callback_id
self.instance.send(
self.id,
'setCustomCursor',
{'callbackId': callback_id, 'throttleMs': int(throttle_ms) if throttle_ms else 0},
)
return self
[docs]
class ManualCursorXY(CursorXY):
def __init__(self, chart, cursor_id: str):
super().__init__(chart, target_id=cursor_id)
[docs]
def dispose(self):
self.instance.send(self._target_id, 'dispose', {})
return self
[docs]
class ManualCursor2D(Cursor2D):
def __init__(self, chart, cursor_id: str):
super().__init__(chart, target_id=cursor_id)
[docs]
def dispose(self):
self.instance.send(self._target_id, 'dispose', {})
return self
[docs]
class ManualCursor3D(Cursor3D):
def __init__(self, chart, cursor_id: str):
super().__init__(chart, target_id=cursor_id)
[docs]
def dispose(self):
self.instance.send(self._target_id, 'dispose', {})
return self
[docs]
class ChartWithCursorXY(CursorBehaviorMixin):
_cursor_obj: 'CursorXY | None' = None
[docs]
def set_cursor(self) -> 'CursorXY':
if self._cursor_obj is None:
from lightningchart.ui.cursor import CursorXY
self._cursor_obj = CursorXY(self)
self.instance.send(self.id, 'cursorInit', {})
return self._cursor_obj
[docs]
def add_cursor(self) -> 'ManualCursorXY':
"""
Create a manual cursor (independent from the built-in cursor modes).
Notes:
- Manual cursors do NOT use chart.set_cursor_mode(...) hover/nearest logic.
You control their position via cursor.set_position(...).
- The result table is not auto-populated for manual cursors.
To display it, you must provide content via cursor.set_result_table(content=...),
e.g. content=[["X", "60"], ["Y", "60"]].
Otherwise the result table may remain empty/invisible even if visible=True.
Returns:
ManualCursorXY: The created manual cursor instance.
Examples:
>>> c = chart.add_cursor()
>>> c.set_position(60, 60)
>>> c.set_result_table(visible=True, content=[["X", "60"], ["Y", "60"]])
"""
cursor_id = str(uuid.uuid4()).split('-')[0]
self.instance.send(self.id, 'addCursor', {'cursorId': cursor_id})
return ManualCursorXY(self, cursor_id)
[docs]
def set_cursor_enabled_during_axis_animation(self, enabled: bool):
"""Disable/Enable Cursor during Axis Animations. Axis Animations are Axis Scale changes that are animated,
such as Zooming and Scrolling done by using API (such as Axis.set_interval)
or by using the mouse to click & drag on the Chart.
Args:
enabled (bool): Boolean value to enable or disable Cursor during Axis Animations.
Returns:
The instance of the class for fluent interface.
"""
self.instance.send(self.id, 'setCursorEnabledDuringAxisAnimation', {'enabled': enabled})
return self
[docs]
def get_cursor_enabled_during_axis_animation(self) -> bool:
"""Get whether the built-in cursor is enabled during axis animations (pan/zoom).
Returns:
bool: True if enabled during axis animations, False otherwise.
"""
return self.instance.get(self.id, 'getCursorEnabledDuringAxisAnimation', {})
[docs]
class ChartWithCursor2D(CursorBehaviorMixin):
_cursor_obj: 'Cursor2D | None' = None
[docs]
def set_cursor(self) -> 'Cursor2D':
if self._cursor_obj is None:
from lightningchart.ui.cursor import Cursor2D
self._cursor_obj = Cursor2D(self)
self.instance.send(self.id, 'cursorInit', {})
return self._cursor_obj
[docs]
def add_cursor(self) -> 'ManualCursor2D':
"""Create a manual cursor."""
cursor_id = str(uuid.uuid4()).split('-')[0]
self.instance.send(self.id, 'addCursor', {'cursorId': cursor_id})
return ManualCursor2D(self, cursor_id)
[docs]
class ChartWithCursor3D(CursorBehaviorMixin):
_cursor_obj: 'Cursor3D | None' = None
[docs]
def set_cursor(self) -> 'Cursor3D':
if self._cursor_obj is None:
from lightningchart.ui.cursor import Cursor3D
self._cursor_obj = Cursor3D(self)
self.instance.send(self.id, 'cursorInit', {})
return self._cursor_obj
[docs]
def add_cursor(self) -> 'ManualCursor3D':
"""Create a manual cursor."""
cursor_id = str(uuid.uuid4()).split('-')[0]
self.instance.send(self.id, 'addCursor', {'cursorId': cursor_id})
return ManualCursor3D(self, cursor_id)