from __future__ import annotations
import uuid
try:
from typing import Unpack
except ImportError:
from typing_extensions import Unpack
from lightningchart.ui.text_box import TextBox
from lightningchart.utils import convert_color_to_hex
from lightningchart.utils.utils import ColorInput, LegendEntryOptions, LegendOptions
[docs]
class Legend:
"""Legend property accessor for chart legends."""
def __init__(self, chart, is_user_legend=False):
self.chart = chart
self.instance = chart.instance
self.chart_id = chart.id
self.id = None
self.is_user_legend = is_user_legend
def _get_target_id(self):
"""Get the correct ID to use for operations."""
return self.id if (self.is_user_legend and self.id) else self.chart_id
[docs]
def set_options(self, **kwargs: Unpack[LegendOptions]):
"""Set legend options.
Args:
visible (bool): Whether legend should be visible
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
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. marginOuter might only be visible with certain positions or when the legend is at chart edges.
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
entries (dict): Default entry options
Returns:
The chart instance for fluent interface.
Examples:
Basic chart with simple legend
>>> chart.legend.set_options(
... visible=True,
... title='My Legend',
... position={'x': 200, 'y': 300, 'origin': 'LeftTop'},
... orientation='Horizontal',
... )
Styled legend with background and custom entries
>>> chart.legend.set_options(
... visible=True,
... title='My Legend',
... background_visible=True,
... background_fill_style="#E08585",
... background_stroke_style={'thickness': 2, 'color': "#F054D3"},
... entries={
... 'button_shape': 'Triangle',
... 'button_size': 20,
... 'button_fill_style': '#00FF00',
... 'button_stroke_style': {'thickness': 5, 'color': '#000000'},
... }
... )
"""
options = {}
if 'visible' in kwargs:
options['visible'] = kwargs['visible']
if 'position' in kwargs:
position = kwargs['position']
if isinstance(position, str):
options['position'] = position
elif hasattr(position, 'value'):
options['position'] = position.value
else:
options['position'] = position
if 'title' in kwargs:
options['title'] = kwargs['title']
if 'title_font' in kwargs:
options['titleFont'] = kwargs['title_font']
if 'title_fill_style' in kwargs:
options['titleFillStyle'] = convert_color_to_hex(kwargs['title_fill_style'])
if 'orientation' in kwargs:
orientation = kwargs['orientation']
options['orientation'] = getattr(orientation, 'value', orientation)
if 'render_on_top' in kwargs:
options['renderOnTop'] = kwargs['render_on_top']
if 'background_visible' in kwargs:
options['backgroundVisible'] = kwargs['background_visible']
if 'background_fill_style' in kwargs:
options['backgroundFillStyle'] = convert_color_to_hex(kwargs['background_fill_style'])
if 'background_stroke_style' in kwargs:
stroke = kwargs['background_stroke_style']
if isinstance(stroke, dict) and 'color' in stroke:
stroke = {**stroke, 'color': convert_color_to_hex(stroke['color'])}
options['backgroundStrokeStyle'] = stroke
if 'padding' in kwargs:
options['padding'] = kwargs['padding']
if 'margin_inner' in kwargs:
options['marginInner'] = kwargs['margin_inner']
if 'margin_outer' in kwargs:
options['marginOuter'] = kwargs['margin_outer']
if 'entry_margin' in kwargs:
options['entryMargin'] = kwargs['entry_margin']
if 'auto_hide_threshold' in kwargs:
options['autoHideThreshold'] = kwargs['auto_hide_threshold']
if 'add_entries_automatically' in kwargs:
options['addEntriesAutomatically'] = kwargs['add_entries_automatically']
if 'entries' in kwargs and kwargs['entries'] is not None:
entries_options = {}
entries = kwargs['entries']
if 'button_shape' in entries:
entries_options['buttonShape'] = entries['button_shape']
if 'button_size' in entries:
entries_options['buttonSize'] = entries['button_size']
if 'button_fill_style' in entries:
entries_options['buttonFillStyle'] = convert_color_to_hex(entries['button_fill_style'])
if 'button_stroke_style' in entries:
bs = entries['button_stroke_style']
if isinstance(bs, dict) and 'color' in bs:
bs = {**bs, 'color': convert_color_to_hex(bs['color'])}
entries_options['buttonStrokeStyle'] = bs
if 'button_rotation' in entries:
entries_options['buttonRotation'] = entries['button_rotation']
if 'text' in entries:
entries_options['text'] = entries['text']
if 'text_font' in entries:
entries_options['textFont'] = entries['text_font']
if 'text_fill_style' in entries:
entries_options['textFillStyle'] = convert_color_to_hex(entries['text_fill_style'])
if 'show' in entries:
entries_options['show'] = entries['show']
if 'match_style_exactly' in entries:
entries_options['matchStyleExactly'] = entries['match_style_exactly']
if 'highlight' in entries:
entries_options['highlight'] = entries['highlight']
if 'lut' in entries:
entries_options['lut'] = entries['lut']
if 'lut_length' in entries:
entries_options['lutLength'] = entries['lut_length']
if 'lut_thickness' in entries:
entries_options['lutThickness'] = entries['lut_thickness']
if 'lut_display_proportional_steps' in entries:
entries_options['lutDisplayProportionalSteps'] = entries['lut_display_proportional_steps']
options['entries'] = entries_options
self.instance.send(self._get_target_id(), 'setLegendOptions', {'options': options})
return self.chart
[docs]
def set_entry_options(self, component, **kwargs: Unpack[LegendEntryOptions]):
"""Set options for specific legend entry.
Args:
component: The series/component to configure
show (bool): Whether to show entry
text (str): Entry text
button_shape (str): 'Arrow', 'Diamond', 'Plus', 'Triangle', 'Circle', 'Square', 'Cross', 'Minus' and 'Star'.
button_size (int | dict): Size in pixels or {'x': 20, 'y': 15}.
button_fill_style: Button fill style
button_stroke_style: Button stroke style
button_rotation: Button rotation in degrees
text_font (dict): Text font settings
text_fill_style: Text fill style
match_style_exactly (bool): Whether to match component style exactly
highlight (bool): Whether highlighting on hover is enabled.
lut: LUT element (None to disable)
lut_length: LUT length in pixels
lut_thickness: LUT thickness in pixels
lut_display_proportional_steps (bool): LUT step display mode
Returns:
The chart instance for fluent interface.
Examples:
Configure individual entries
>>> chart.legend.set_entry_options(
... series,
... text="Primary Data",
... button_size=5,
... show=True
... )
Custom legend entries
>>> chart.legend.set_entry_options(
... series,
... show=True,
... text="Series A — Custom",
... button_shape='Triangle',
... button_size=25,
... button_fill_style="#CB5B15",
... button_stroke_style={'thickness': 3, 'color': '#003300'},
... button_rotation=45,
... text_font={'size': 18, 'style': 'italic'},
... text_fill_style='#0000FF',
... match_style_exactly=False,
... )
"""
options = {}
if 'show' in kwargs:
options['show'] = kwargs['show']
if 'text' in kwargs:
options['text'] = kwargs['text']
if 'button_shape' in kwargs:
options['buttonShape'] = kwargs['button_shape']
if 'button_size' in kwargs:
options['buttonSize'] = kwargs['button_size']
if 'button_fill_style' in kwargs:
options['buttonFillStyle'] = convert_color_to_hex(kwargs['button_fill_style'])
if 'button_stroke_style' in kwargs:
bs = kwargs['button_stroke_style']
if isinstance(bs, dict) and 'color' in bs:
bs = {**bs, 'color': convert_color_to_hex(bs['color'])}
options['buttonStrokeStyle'] = bs
if 'button_rotation' in kwargs:
options['buttonRotation'] = kwargs['button_rotation']
if 'text_font' in kwargs:
options['textFont'] = kwargs['text_font']
if 'text_fill_style' in kwargs:
options['textFillStyle'] = convert_color_to_hex(kwargs['text_fill_style'])
if 'match_style_exactly' in kwargs:
options['matchStyleExactly'] = kwargs['match_style_exactly']
if 'highlight' in kwargs:
options['highlight'] = kwargs['highlight']
if 'lut' in kwargs:
options['lut'] = kwargs['lut']
if 'lut_length' in kwargs:
options['lutLength'] = kwargs['lut_length']
if 'lut_thickness' in kwargs:
options['lutThickness'] = kwargs['lut_thickness']
if 'lut_display_proportional_steps' in kwargs:
options['lutDisplayProportionalSteps'] = kwargs['lut_display_proportional_steps']
self.instance.send(self._get_target_id(), 'setLegendEntryOptions', {'component': component.id, 'options': options})
return self.chart
[docs]
def get_options(self):
"""Get current legend options.
Notes:
Call this in live mode, e.g. ``chart.open(live=True)``
"""
return self.instance.get(self._get_target_id(), 'getLegendOptions', {})
[docs]
def get_entry_options(self, component):
"""Get options for specific entry.
Notes:
Call this in live mode, e.g. ``chart.open(live=True)``
"""
return self.instance.get(self._get_target_id(), 'getLegendEntryOptions', {'component': component.id})
[docs]
def add(self, component, options: LegendEntryOptions = None):
"""Add standalone legend entry.
Args:
component: The series/component to configure
options (dict): Legend entry options
Examples:
Add standalone legend entry
>>> legend = chart.add_legend()
... series = chart.add_line_series()
... legend.add(
... component=series,
... options={
... 'text': 'Series A',
... 'button_shape': 'Triangle',
... 'button_fill_style': '#00FF00'
... })
"""
options = options or {}
component_id = component.id if component is not None else None
converted_options = {}
if 'text' in options:
converted_options['text'] = options['text']
if 'button_shape' in options:
converted_options['buttonShape'] = options['button_shape']
if 'button_size' in options:
converted_options['buttonSize'] = options['button_size']
if 'button_fill_style' in options:
converted_options['buttonFillStyle'] = convert_color_to_hex(options['button_fill_style'])
if 'button_stroke_style' in options:
bs = options['button_stroke_style']
if isinstance(bs, dict) and 'color' in bs:
bs = {**bs, 'color': convert_color_to_hex(bs['color'])}
converted_options['buttonStrokeStyle'] = bs
if 'button_rotation' in options:
converted_options['buttonRotation'] = options['button_rotation']
if 'text_font' in options:
converted_options['textFont'] = options['text_font']
if 'text_fill_style' in options:
converted_options['textFillStyle'] = convert_color_to_hex(options['text_fill_style'])
if 'show' in options:
converted_options['show'] = options['show']
if 'match_style_exactly' in options:
converted_options['matchStyleExactly'] = options['match_style_exactly']
if 'highlight' in options:
converted_options['highlight'] = options['highlight']
if 'lut' in options:
converted_options['lut'] = options['lut']
if 'lut_length' in options:
converted_options['lutLength'] = options['lut_length']
if 'lut_thickness' in options:
converted_options['lutThickness'] = options['lut_thickness']
if 'lut_display_proportional_steps' in options:
converted_options['lutDisplayProportionalSteps'] = options['lut_display_proportional_steps']
entry_id = str(uuid.uuid4())
message = {
'component': component_id,
'options': converted_options,
'entryId': entry_id,
}
self.instance.send(self._get_target_id(), 'legendAdd', message)
return {'type': 'standalone_entry', 'id': entry_id}
[docs]
def remove(self, component):
"""Remove component from legend."""
if isinstance(component, dict) and component.get('type') == 'standalone_entry':
self.instance.send(self._get_target_id(), 'legendRemove', {'entryId': component['id']})
else:
self.instance.send(self._get_target_id(), 'legendRemove', {'component': component.id})
return self.chart
[docs]
def clear(self):
"""Clear all legend entries."""
self.instance.send(self._get_target_id(), 'legendClear', {})
return self.chart
[docs]
def dispose(self):
"""Permanently destroy the legend.
Returns:
The chart instance for fluent interface.
"""
self.instance.send(self._get_target_id(), 'legendDispose', {})
return self.chart
[docs]
class LegendPanelMethods:
[docs]
def add_textbox(
self,
text: str = None,
x: int = None,
y: int = None,
position_scale: str = 'axis',
):
"""Add a text box to the legend panel.
Args:
text (str): Text of the text box.
x: X position. Interpretation depends on position_scale:
- 'axis': axis coordinates
- 'percentage': 0–100
- 'pixel': pixels
y: Y position, same scale as x.
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 dispose(self):
"""Permanently destroy the component."""
self.instance.send(self.id, 'dispose')
return self
[docs]
def set_background_color(self, color: ColorInput | None):
"""Set the background color of the legend panel.
Args:
color (Color): Color of the background. Use 'transparent' or None to hide.
Use 'transparent' for overlays.
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 set_background_stroke(self, thickness: int | float, color: ColorInput | None = None):
"""Set the background stroke style of the legend panel.
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 set_minimum_size(self, width: float, height: float | None = None):
"""Set minimum size (pixels) for the panel (affects dashboard splitter resize).
Args:
width: Minimum width in pixels.
height: Minimum height in pixels. If None, equals width.
Returns:
self (for chaining).
"""
if height is None:
height = float(width)
self.instance.send(self.id, 'setMinimumSize', {'Point': {'x': float(width), 'y': float(height)}})
return self
[docs]
class LegendPanelDashboard(LegendPanelMethods):
def __init__(self, instance, dashboard_id, column, row, colspan, rowspan, legend):
self.instance = instance
self.id = str(uuid.uuid4())
self.dashboard_id = dashboard_id
self._opts = {'column': column, 'row': row, 'colspan': colspan, 'rowspan': rowspan, 'legendConfig': legend or {}}
self.instance.send(
self.id,
'createLegendPanel',
{
'db': self.dashboard_id,
**self._opts,
},
)
[docs]
def add(self, component):
"""Add component legend entry to panel."""
self.instance.send(self.id, 'legendPanelAdd', {'component': component.id})
return self
[docs]
class LegendPanelContainer(LegendPanelMethods):
def __init__(self, instance, container, column, row, colspan, rowspan, legend):
self.instance = instance
self.id = str(uuid.uuid4())
self.container = container
self.instance.send(
self.id,
'createLegendPanelContainer',
{'containerId': container.id, 'column': column, 'row': row, 'colspan': colspan, 'rowspan': rowspan, 'legendConfig': legend or {}},
)
[docs]
def add(self, component):
"""Add component legend entry to panel."""
self.instance.send(self.id, 'legendPanelContainerAdd', {'component': component.id})
return self