from typing import Optional, Self
from lightningchart import Themes, conf
from lightningchart.series.parallel_coordinate_series import ParallelCoordinateSeries
from lightningchart.charts import (
ChartWithCursor2D,
ChartWithSeries,
ChartsWithAddEventListener,
ChartsWithCoordinateTransforms,
TitleMethods,
GeneralMethods,
Chart,
)
from lightningchart.instance import Instance
from lightningchart.ui.axis import GenericAxis
from lightningchart.ui import UserInteractions
from lightningchart.ui.parallel_coordinate_custom_tick import ParallelCoordinateCustomTick
from lightningchart.utils import convert_color_to_hex
import uuid
from lightningchart.utils.utils import ColorInput, LegendOptions, apply_post_legend_config, build_legend_config
[docs]
class ParallelCoordinateChart(
ChartWithSeries, ChartWithCursor2D, TitleMethods, GeneralMethods, UserInteractions, ChartsWithCoordinateTransforms, ChartsWithAddEventListener
):
"""Chart for visualizing data in a parallel coordinate system."""
def __init__(
self,
theme: Themes = Themes.Light,
theme_scale: float = 1.0,
title: str = None,
license: str = None,
license_information: str = None,
html_text_rendering: bool = True,
legend: Optional[LegendOptions] = None,
):
"""Initialize a Parallel Coordinate Chart with a theme and optional title.
Args:
theme (Themes): Theme for the chart. Defaults to `Themes.White`.
title (str, optional): Title of the chart. Defaults to None.
license (str): License key.
theme_scale: To up or downscale font sizes as well as tick lengths, element paddings, etc. to make font sizes sit in nicely.
html_text_rendering: Can be enabled for sharper text display where required with drawback of weaker performance.
legend (dict): Legend configuration dictionary with the following options:
visible (bool): Show/hide legend.
position: Position (TopRight, RightCenter, etc.).
title (str): Legend title text.
title_font (dict): Title font settings
title_fill_style: Title color/fill style
orientation: Horizontal or Vertical orientation.
render_on_top (bool): Render above chart (default: False).
background_visible (bool): Show legend background.
background_fill_style (str): Background color ("#ff0000").
background_stroke_style (dict): Border style {'thickness': 2, 'color': '#000'}.
padding (int | dict): Padding around legend content.
margin_inner (int): Space between chart and legend.
margin_outer (int): Space from legend to chart edge.
entry_margin (int): Space between legend entries.
auto_hide_threshold (float): Auto-hide when legend takes >X of chart (0.0-1.0).
add_entries_automatically (bool): Auto-add series to legend.
entries (dict): Default styling for all legend entries with options:
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 (str): Button color ("#ff0000").
button_stroke_style (dict): Button border {'thickness': 1, 'color': '#000'}.
button_rotation (float): Button rotation in degrees.
text (str): Override default series name.
text_font (dict): Font settings {'size': 16, 'family': 'Arial', 'weight': 'bold'}.
text_fill_style (str): Text color ("#000000").
show (bool): Show/hide this entry.
match_style_exactly (bool): Match series style exactly vs simplified.
highlight (bool): Whether highlighting on hover is enabled.
lut: LUT element for legends (None to disable).
lut_length (int): LUT bar length for heatmap legends.
lut_thickness (int): LUT bar thickness for heatmap legends.
lut_display_proportional_steps (bool): LUT step display mode.
Returns:
Reference to ParallelCoordinateChart class.
Examples:
Basic chart with simple legend
>>> chart = lc.ParallelCoordinateChart(
... title='My Chart',
... legend={
... 'visible': True,
... 'position': 'RightCenter',
... 'title': "Data Series"
... }
... )
Styled legend with background and custom entries
>>> chart = lc.ParallelCoordinateChart(
... title='Styled Chart',
... legend={
... 'visible': True,
... 'position': 'RightCenter',
... 'background_visible': True,
... 'background_fill_style': "#e01212",
... 'background_stroke_style': {'thickness': 3, 'color': '#003300'},
... 'entries': {
... 'button_shape': 'Circle',
... 'button_size': 20,
... 'text_font': {'size': 16},
... 'text_fill_style': "#000080"
... }
... }
... )
Custom positioned legend
>>> chart = lc.ParallelCoordinateChart(
... title='Custom Legend',
... legend={
... 'position': 'RightCenter',
... 'orientation': 'Horizontal',
... 'render_on_top': True,
... 'padding': 15,
... 'margin_inner': 10
... }
... )
"""
instance = Instance()
super().__init__(instance)
self.theme = theme
self.axes = []
self.series_list = []
legend_config = build_legend_config(legend)
self.instance.send(
self.id,
'parallelCoordinateChart',
{
'theme': theme.value,
'scaleTheme': theme_scale,
'license': license or conf.LICENSE_KEY,
'licenseInformation': license_information or conf.LICENSE_INFORMATION,
'htmlTextRendering': html_text_rendering,
'legendConfig': legend_config,
},
)
if title:
self.set_title(title)
apply_post_legend_config(self, legend)
[docs]
def set_axes(self, axes: list):
"""Set axes of the parallel coordinate chart as a list of strings.
Args:
axes (list): List of axis names or identifiers.
Returns:
The instance of the chart for fluent interface.
"""
self.axes = axes
self.instance.send(self.id, 'setAxes', {'axes': axes})
return self
[docs]
def get_axis(self, axis_key: str):
"""Retrieve a specific axis by its name or ID.
Args:
axis_key (str): The key or name of the axis.
Returns:
The corresponding axis object.
Raises:
ValueError: If the axis with the given key is not found.
"""
if axis_key in self.axes:
axis_name = axis_key
else:
raise ValueError(f"Axis with key '{axis_key}' not found.")
return ParallelCoordinateAxis(self, axis_name)
[docs]
def add_series(self):
"""Add a new data series to the chart.
Returns:
The created series instance.
"""
series = ParallelCoordinateSeries(self)
self.series_list.append(series)
return series
[docs]
def get_series(self) -> list[ParallelCoordinateSeries]:
"""Get all data series in the chart.
Returns:
A list of all series in the chart.
"""
return self.series_list
[docs]
def set_lut(
self,
axis_key: str,
interpolate: bool,
steps: list,
percentage_values: bool = False,
formatter_precision: int | None = None,
formatter_unit: str = '',
formatter_scale: float = 1.0,
formatter_type: str = 'standard',
formatter_operation: str = 'none',
):
"""Configure series coloring by a Value-Color Table (LUT) based on a specific axis.
Args:
axis_key (str): The key of the axis for which to apply LUT.
interpolate (bool): Whether to interpolate between LUT steps.
steps (list): List of LUT steps, each with a value and color.
percentage_values (bool): Whether values represent percentages or explicit values.
formatter_precision (int | None): Decimal places for legend display.
formatter_unit (str): Unit suffix (e.g., "mag", "ms").
formatter_scale (float): Multiply values by this factor.
formatter_type (str): 'standard', 'compact', 'engineering', 'scientific'.
formatter_operation (str): 'none', 'round', 'ceil', 'floor'.
Returns:
The instance of the chart for fluent interface.
"""
for step in steps:
step['color'] = convert_color_to_hex(step['color'])
lut_config = {
'interpolate': interpolate,
'steps': steps,
'percentageValues': percentage_values,
'formatter_precision': formatter_precision,
'formatter_unit': formatter_unit,
'formatter_scale': formatter_scale,
'formatter_type': formatter_type,
'formatter_operation': formatter_operation,
}
self.instance.send(self.id, 'setParallelAxisLUT', {'axisId': axis_key, 'lut': lut_config})
return self
[docs]
def set_spline(self, enabled: bool):
"""Enable or disable spline interpolation for the chart.
Args:
enabled (bool): True to enable spline interpolation, False to disable.
Returns:
The instance of the chart for fluent interface.
"""
self.instance.send(self.id, 'setSpline', {'enabled': enabled})
return self
[docs]
def set_series_stroke_thickness(self, thickness: int | float):
"""Set the thickness of series lines.
Args:
thickness (int | float): Thickness of the lines in pixels.
Returns:
The instance of the chart for fluent interface.
"""
self.instance.send(self.id, 'setSeriesStrokeThickness', {'thickness': thickness})
return self
[docs]
def set_highlight_on_hover(self, state: bool):
"""Enable or disable highlight on hover for series.
Args:
state (bool): True to enable highlight on hover, False to disable.
Returns:
The instance of the chart for fluent interface.
"""
self.instance.send(self.id, 'setSeriesHighlightOnHover', {'state': state})
return self
[docs]
def set_unselected_series_color(self, color: ColorInput | None) -> Self:
"""Set the color for unselected series.
Args:
Color: Color to apply to unselected series. Use 'transparent' or None to hide.
Returns:
The instance of the chart for fluent interface.
"""
color = convert_color_to_hex(color) if color is not None else None
self.instance.send(self.id, 'setUnselectedSeriesColor', {'color': color})
return self
[docs]
def set_user_interactions(self, interactions=...):
"""Configure user interactions from a set of preset options.
Args:
interactions (dict or None):
- `None`: disable all interactions
- `{}` or no argument: restore default interactions
- `dict`: configure specific interactions
Examples:
## Disable all interactions:
>>> chart.set_user_interactions(None)
## Restore default interactions:
>>> chart.set_user_interactions()
... chart.set_user_interactions({})
## Remove select range selector interactions
>>> chart.set_user_interactions(
... {
... 'rangeSelectors': {
... 'create': {
... 'doubleClickAxis': True,
... },
... 'dispose': {
... 'doubleClick': True,
... },
... },
... }
... )
"""
return super().set_user_interactions(interactions)
[docs]
def set_all_axes_tick_labels(
self,
major_size: int | float | None = None,
minor_size: int | float | None = None,
family: str | None = None,
style: str | None = None,
weight: str | None = None,
major_color: ColorInput | None = None,
minor_color: ColorInput | None = None,
major_rotation: float | None = None,
minor_rotation: float | None = None,
format_type: str = 'standard',
precision: int | None = None,
unit: str | None = None,
scale: float = 1.0,
):
"""Style tick labels for ALL parallel coordinate axes at once.
Args:
major_size: Font size for major tick labels in pixels.
minor_size: Font size for minor tick labels in pixels.
family: CSS font family for both major and minor tick labels.
style: CSS font style ('normal', 'italic').
weight: CSS font weight ('normal', 'bold').
major_color: Text color for major tick labels.
Accepts hex string, named color, RGB/RGBA tuple, or Color object.
minor_color: Text color for minor tick labels.
Accepts hex string, named color, RGB/RGBA tuple, or Color object.
major_rotation: Rotation angle in degrees for major tick labels.
minor_rotation: Rotation angle in degrees for minor tick labels.
format_type: Format style:
- 'standard': Normal number formatting (default)
- 'currency': Currency formatting with symbol
- 'percentage': Percentage formatting (value * 100 + %)
- 'thousands': Compact notation (K, M, B, T)
- 'integer': Rounded integer values
precision: Number of decimal places (None = auto).
unit: Unit to append (e.g., "kg", "ms", "items").
scale: Scale factor to multiply value (default: 1.0).
Returns:
The instance of the chart for fluent interface.
Examples:
Style all axes uniformly
>>> chart.set_all_axes_tick_labels(major_size=14, minor_size=10, weight='bold')
Rotated labels on all axes
>>> chart.set_all_axes_tick_labels(
... major_size=12,
... major_rotation=45,
... major_color='darkblue'
... )
Formatted with units on all axes
>>> chart.set_all_axes_tick_labels(
... major_size=12,
... precision=2,
... format_type='standard'
... )
"""
from lightningchart.utils import convert_color_to_hex
config = {}
if family is not None:
config['family'] = family
if style is not None:
config['style'] = style
if weight is not None:
config['weight'] = weight
if major_size is not None:
config['majorSize'] = major_size
if minor_size is not None:
config['minorSize'] = minor_size
if major_rotation is not None:
config['majorRotation'] = major_rotation
if minor_rotation is not None:
config['minorRotation'] = minor_rotation
if major_color is not None:
config['majorColor'] = convert_color_to_hex(major_color)
if minor_color is not None:
config['minorColor'] = convert_color_to_hex(minor_color)
if format_type is not None:
config['formatType'] = format_type
if precision is not None:
config['precision'] = precision
if unit is not None:
config['unit'] = unit
if scale is not None:
config['scale'] = scale
self.instance.send(self.id, 'setAllParallelAxesTickLabels', {'config': config})
return self
[docs]
class ParallelCoordinateAxis(GenericAxis):
def __init__(self, chart, axis_key):
"""Initialize a parallel coordinate axis.
Args:
chart (ParallelCoordinateChart): The parent chart.
axis_key (str): The identifier or name of the axis.
"""
self.chart = chart
self.axis_key = axis_key
self.instance = chart.instance
self.id = str(uuid.uuid4()).split('-')[0]
self.instance.send(
self.chart.id,
'getParallelAxisReference',
{'axisKey': axis_key, 'axisID': self.id},
)
[docs]
def add_range_selector(self):
"""Add a range selector to this axis.
Returns:
The created range selector object.
"""
selector_id = str(uuid.uuid4()).split('-')[0]
self.chart.instance.send(
self.chart.id,
'addRangeSelector',
{'axisId': self.axis_key, 'selectorId': selector_id},
)
return ParallelCoordinateAxisRangeSelector(self.chart, self.axis_key, selector_id)
[docs]
def set_palette_stroke(self, thickness: int | float, interpolate: bool, steps: list):
"""Set the stroke style of the axis with a palette.
Args:
thickness (int | float): Thickness of the stroke in pixels.
interpolate (bool): Whether to interpolate between palette steps.
steps (list): List of palette steps, each containing value and color.
Returns:
The instance of the axis for fluent interface.
"""
for step in steps:
step['color'] = convert_color_to_hex(step['color'])
self.chart.instance.send(
self.chart.id,
'setParallelAxisStrokeStyle',
{
'axisId': self.axis_key,
'thickness': thickness,
'lut': {'interpolate': interpolate, 'steps': steps},
},
)
return self
[docs]
def set_solid_stroke(self, thickness: int | float, color: ColorInput | None = None) -> Self:
"""Set a solid stroke style for the axis.
Args:
thickness (int | float): Thickness of the stroke in pixels.
color: Solid color for the stroke. Use 'transparent' or None to hide.
Returns:
The instance of the axis for fluent interface.
"""
color = convert_color_to_hex(color) if color is not None else None
self.chart.instance.send(
self.chart.id,
'setSolidStroke',
{'axisId': self.axis_key, 'thickness': thickness, 'color': color},
)
return self
[docs]
def set_tick_strategy(self, strategy: str, time_origin: int | float = None, utc: bool = False):
"""Set the tick strategy for the axis.
Args:
strategy (str): Tick strategy ("Empty", "Numeric", "DateTime", "Time").
time_origin (int | float, optional): Time origin for the strategy. Defaults to None.
utc (bool, optional): Whether to use UTC for DateTime strategy. Defaults to False.
Returns:
The instance of the axis for fluent interface.
"""
strategies = ('Empty', 'Numeric', 'DateTime', 'Time')
if strategy not in strategies:
raise ValueError(f"Expected strategy to be one of {strategies}, but got '{strategy}'.")
self.chart.instance.send(
self.chart.id,
'setParallelAxisTickStrategy',
{
'strategy': strategy,
'axisId': self.axis_key,
'timeOrigin': time_origin,
'utc': utc,
},
)
return self
[docs]
def set_stopped(self, stopped: bool):
"""Stop/resume axis so scroll strategy won't change its interval.
Args:
stopped (bool): True to stop, False to resume.
Returns:
The instance of the axis for fluent interface.
"""
self.chart.instance.send(
self.chart.id,
'setStopped',
{'axisId': self.axis_key, 'stopped': stopped},
)
return self
[docs]
def set_units(self, units: str | None, behavior: dict | None = None):
"""Set axis units (e.g., 'Hz', '°C') with optional behavior flags.
Args:
units: String (e.g., 'Hz') or None to clear.
behavior: Optional dict with keys:
- displayOnAxis (bool)
- displayInCursor (bool)
Returns:
The instance of the axis for fluent interface.
"""
payload = {'axisId': self.axis_key, 'units': units, 'behavior': behavior or {}}
self.chart.instance.send(self.chart.id, 'setUnits', payload)
return self
[docs]
def add_custom_tick(self):
"""Add custom tick to parallel coordinate axis.
Returns:
Reference to ParallelCoordinateCustomTick class.
"""
return ParallelCoordinateCustomTick(self.chart, self)
[docs]
def set_tick_labels(
self,
major_size: int | float | None = None,
minor_size: int | float | None = None,
family: str | None = None,
style: str | None = None,
weight: str | None = None,
major_color: ColorInput | None = None,
minor_color: ColorInput | None = None,
major_rotation: float | None = None,
minor_rotation: float | None = None,
format_type: str = 'standard',
precision: int | None = None,
unit: str | None = None,
scale: float = 1.0,
):
"""Style tick labels for this parallel coordinate axis.
Args:
major_size: Font size for major tick labels in pixels.
minor_size: Font size for minor tick labels in pixels.
family: CSS font family for both major and minor tick labels.
style: CSS font style ('normal', 'italic').
weight: CSS font weight ('normal', 'bold').
major_color: Text color for major tick labels.
Accepts hex string, named color, RGB/RGBA tuple, or Color object.
minor_color: Text color for minor tick labels.
Accepts hex string, named color, RGB/RGBA tuple, or Color object.
major_rotation: Rotation angle in degrees for major tick labels.
minor_rotation: Rotation angle in degrees for minor tick labels.
format_type: Format style:
- 'standard': Normal number formatting (default)
- 'currency': Currency formatting with symbol
- 'percentage': Percentage formatting (value * 100 + %)
- 'thousands': Compact notation (K, M, B, T)
- 'integer': Rounded integer values
precision: Number of decimal places (None = auto).
unit: Unit to append (e.g., "kg", "ms", "items").
scale: Scale factor to multiply value (default: 1.0).
Returns:
The instance of the axis for fluent interface.
Examples:
Basic font styling:
>>> axis.set_tick_labels(major_size=14, minor_size=10, weight='bold')
Rotated labels with color:
>>> axis.set_tick_labels(
... major_size=12,
... major_rotation=45,
... major_color='darkblue'
... )
Using various color formats:
>>> axis.set_tick_labels(major_color='#FF0000') # Hex string
>>> axis.set_tick_labels(major_color='red') # Named color
>>> axis.set_tick_labels(major_color=(255, 0, 0)) # RGB tuple
>>> axis.set_tick_labels(major_color=(255, 0, 0, 128)) # RGBA tuple
>>> axis.set_tick_labels(major_color=0xFF0000) # Integer
Formatted with units:
>>> axis.set_tick_labels(
... major_size=12,
... precision=2,
... unit='Hz',
... scale=1000
... )
Percentage formatting:
>>> axis.set_tick_labels(
... format_type='percentage',
... precision=1
... )
"""
from lightningchart.utils import convert_color_to_hex
config: dict[str, str | int | float] = {}
if family is not None:
config['family'] = family
if style is not None:
config['style'] = style
if weight is not None:
config['weight'] = weight
if major_size is not None:
config['majorSize'] = major_size
if minor_size is not None:
config['minorSize'] = minor_size
if major_rotation is not None:
config['majorRotation'] = major_rotation
if minor_rotation is not None:
config['minorRotation'] = minor_rotation
if major_color is not None:
config['majorColor'] = convert_color_to_hex(major_color)
if minor_color is not None:
config['minorColor'] = convert_color_to_hex(minor_color)
if format_type is not None:
config['formatType'] = format_type
if precision is not None:
config['precision'] = precision
if unit is not None:
config['unit'] = unit
if scale is not None:
config['scale'] = scale
self.chart.instance.send(self.chart.id, 'setParallelTickLabels', {'axisId': self.axis_key, 'config': config})
return self
[docs]
def get_custom_ticks(self):
"""Get information about all custom ticks on this axis.
Returns:
list[dict]: List of custom tick information dictionaries, each containing:
- id (str): Unique identifier for the tick
- value (float): Axis value where tick is positioned
- text (str): Label text displayed on the tick
- visible (bool): Whether tick is visible
- allocatesAxisSpace (bool): Whether tick reserves space on axis
- tickLength (float): Length of tick line in pixels
- gridStrokeLength (float): Length of grid line
- tickLabelPadding (float): Padding around label
- gridStrokeStyle (dict): Grid line style {'thickness': int, 'color': str}
- tickStrokeStyle (dict): Tick line style {'thickness': int, 'color': str}
- markerFont (dict): Font settings {'size': int, 'family': str, 'weight': str, 'style': str}
- markerColor (str): Marker text color (rgba string)
- textFont (dict): Text font settings
- textColor (str): Text color (rgba string)
- backgroundColor (str): Label background color (rgba string)
- backgroundStrokeStyle (dict): Background border style {'thickness': int, 'color': str}
- padding (float): Padding value
- tickLabelRotation (float): Label rotation in degrees
Notes:
Must be called in live mode and better after opening the chart window, chart.open(live=True).
Examples:
Get all custom ticks with their properties:
>>> ticks = axis.get_custom_ticks()
>>> for tick in ticks:
... print(f"Value: {tick['value']}, Text: {tick['text']}")
... print(f"Font: {tick['textFont']}")
... print(f"Color: {tick['textColor']}")
"""
return self.instance.get(self.id, 'getCustomTicks', {})
[docs]
class ParallelCoordinateAxisRangeSelector:
def __init__(self, chart, axis_key, selector_id):
"""Initialize a range selector for a parallel coordinate axis.
Args:
chart (ParallelCoordinateChart): The parent chart.
axis_key (str): The key or name of the axis.
selector_id (str): Unique identifier for the selector.
"""
self.chart = chart
self.axis_key = axis_key
self.selector_id = selector_id
[docs]
def set_interval(self, a: float, b: float, stop_axis_after: bool = False, animate: bool = False):
"""Set the range interval for the selector.
Args:
a (float): Start of the interval.
b (float): End of the interval.
stop_axis_after (bool, optional): Stop axis after the range. Defaults to False.
animate (bool, optional): Animate the range update. Defaults to False.
Returns:
The instance of the selector for fluent interface.
"""
self.chart.instance.send(
self.chart.id,
'setRangeSelectorInterval',
{
'selectorId': self.selector_id,
'axisId': self.axis_key,
'start': a,
'end': b,
'stop': stop_axis_after,
'animate': animate,
},
)
return self
[docs]
def dispose(self):
"""Remove the range selector permanently.
Returns:
The instance of the class for fluent interface.
"""
self.chart.instance.send(
self.chart.id,
'disposeRangeSelector',
{
'selectorId': self.selector_id,
},
)
return self
[docs]
class ParallelCoordinateChartDashboard(ParallelCoordinateChart):
"""Class for ParallelCoordinateChart contained in Dashboard."""
def __init__(
self,
instance: Instance,
dashboard_id: str,
column: int,
row: int,
colspan: int,
rowspan: int,
title: str = None,
legend: Optional[LegendOptions] = None,
):
Chart.__init__(self, instance)
self.axes = []
self.series_list = []
legend_config = build_legend_config(legend)
self.instance.send(
self.id,
'createParallelCoordinateChart',
{
'db': dashboard_id,
'column': column,
'row': row,
'colspan': colspan,
'rowspan': rowspan,
'legendConfig': legend_config,
},
)
if title:
self.instance.send(self.id, 'setTitle', {'title': title})
apply_post_legend_config(self, legend)
[docs]
class ParallelCoordinateChartContainer(ParallelCoordinateChart):
def __init__(self, instance, container, column, row, colspan, rowspan, title, legend):
Chart.__init__(self, instance)
self.axes = []
self.series_list = []
legend_config = build_legend_config(legend)
self.instance.send(
self.id,
'createParallelCoordinateChartContainer',
{
'containerId': container.id,
'column': column,
'row': row,
'colspan': colspan,
'rowspan': rowspan,
'legendConfig': legend_config,
},
)
if title:
self.instance.send(self.id, 'setTitle', {'title': title})
apply_post_legend_config(self, legend)