From 94bac81314ae8133a7c364ae59bc8ff5b6cef551 Mon Sep 17 00:00:00 2001 From: ze2j <46556066+ze2j@users.noreply.github.com> Date: Thu, 23 Apr 2026 17:32:40 +0100 Subject: [PATCH 01/23] Add and use XYLayout.map_point_to_screen() --- addons/tau-plot/plot/xy/bar/bar_hit_tester.gd | 4 +-- .../plot/xy/scatter/scatter_renderer.gd | 34 +++++-------------- addons/tau-plot/plot/xy/xy_layout.gd | 26 ++++++++++++++ 3 files changed, 36 insertions(+), 28 deletions(-) diff --git a/addons/tau-plot/plot/xy/bar/bar_hit_tester.gd b/addons/tau-plot/plot/xy/bar/bar_hit_tester.gd index d6799cb..e7d136a 100644 --- a/addons/tau-plot/plot/xy/bar/bar_hit_tester.gd +++ b/addons/tau-plot/plot/xy/bar/bar_hit_tester.gd @@ -80,14 +80,12 @@ class BarHitTester extends OverlayHitTester: if _layout.domain.config.x_axis.type == TauAxisConfig.Type.CATEGORICAL: return {} - # TODO: drop x_is_horizontal once XYLayout exposes a logical-x projector. - var x_is_horizontal: bool = _layout._x_is_horizontal var best_px := INF var best_val: float = 0.0 var found := false for record: BarHitRecord in _bar_renderer.get_hit_records(): - var anchor_along_x: float = record.anchor.x if x_is_horizontal else record.anchor.y + var anchor_along_x: float = _layout.map_screen_to_point(record.anchor).x if absf(p_along_x_px - anchor_along_x) < absf(p_along_x_px - best_px): best_px = anchor_along_x best_val = record.x_value diff --git a/addons/tau-plot/plot/xy/scatter/scatter_renderer.gd b/addons/tau-plot/plot/xy/scatter/scatter_renderer.gd index 815e73d..188eab2 100644 --- a/addons/tau-plot/plot/xy/scatter/scatter_renderer.gd +++ b/addons/tau-plot/plot/xy/scatter/scatter_renderer.gd @@ -695,18 +695,11 @@ class ScatterRenderer extends Control: # exactly on the pane boundary due to floating-point rounding in layout # mapping. This is purely cosmetic and does not affect layout or ticks. const TOLERANCE_PX := 0.5 - var screen_x: float - var screen_y: float - if _layout._x_is_horizontal: - screen_x = p_x - screen_y = p_y - else: - screen_x = p_y - screen_y = p_x - return (screen_x >= p_pane_rect.position.x - TOLERANCE_PX and - screen_x <= p_pane_rect.position.x + p_pane_rect.size.x + TOLERANCE_PX and - screen_y >= p_pane_rect.position.y - TOLERANCE_PX and - screen_y <= p_pane_rect.position.y + p_pane_rect.size.y + TOLERANCE_PX) + var screen := _layout.map_point_to_screen(p_x, p_y) + return (screen.x >= p_pane_rect.position.x - TOLERANCE_PX and + screen.x <= p_pane_rect.position.x + p_pane_rect.size.x + TOLERANCE_PX and + screen.y >= p_pane_rect.position.y - TOLERANCE_PX and + screen.y <= p_pane_rect.position.y + p_pane_rect.size.y + TOLERANCE_PX) #################################################################################################### # Per-instance custom data packing @@ -757,20 +750,11 @@ class ScatterRenderer extends Control: outline_color = _apply_alpha(_scatter_style.hovered_outline_color, alpha) # Transform: translate to center, scale by size_px. - # map_x_to_px returns screen-Y when x is vertical, and map_y_to_px returns - # screen-X when x is vertical. The callers pass the x-axis pixel as p_cx - # and the y-axis pixel as p_cy, so we must swap them for vertical x. - var screen_x: float - var screen_y: float - if _layout._x_is_horizontal: - screen_x = p_cx - screen_y = p_cy - else: - screen_x = p_cy - screen_y = p_cx + # The callers pass the x-axis pixel as p_cx and the y-axis pixel as p_cy + var screen := _layout.map_point_to_screen(p_cx, p_cy) var t := Transform2D() t = t.scaled(Vector2(size_px, size_px)) - t.origin = Vector2(screen_x, screen_y) + t.origin = screen p_entry.mm.set_instance_transform_2d(p_slot, t) # Color: fill color with alpha @@ -783,7 +767,7 @@ class ScatterRenderer extends Control: p_entry.mm.set_instance_custom_data(p_slot, _pack_custom_data(outline_color, shape, ow_norm)) # Record hover data for hit testing. - _hover_screen_positions.append(Vector2(screen_x, screen_y)) + _hover_screen_positions.append(screen) _hover_series_ids.append(series_id) _hover_sample_indices.append(p_sample_index) _hover_x_values.append(p_x_value) diff --git a/addons/tau-plot/plot/xy/xy_layout.gd b/addons/tau-plot/plot/xy/xy_layout.gd index f132764..3e69bf9 100644 --- a/addons/tau-plot/plot/xy/xy_layout.gd +++ b/addons/tau-plot/plot/xy/xy_layout.gd @@ -391,6 +391,32 @@ class XYLayout extends RefCounted: return map_y_to_px(p_pane_index, 0.0, p_y_axis_id) + ## Assembles a screen-space [Vector2] from two pixel coordinates that are + ## already expressed along the x-axis and y-axis directions respectively. + ## + ## [param p_x_axis_px] Pixel coordinate along the x-axis direction, as + ## returned by [method map_x_to_px] or + ## [method map_x_category_center_to_px]. + ## [param p_y_axis_px] Pixel coordinate along the y-axis direction, as + ## returned by [method map_y_to_px]. + ## + ## When the x axis is horizontal, the x-axis direction is screen-X and + ## the y-axis direction is screen-Y, so the values map straight through. + ## When the x axis is vertical, the two are swapped. + ## + ## Note: both input values already encode axis inversion and scale + ## (linear or logarithmic) because those are applied inside the + ## mapping functions. This helper only performs the orientation swap. + func map_point_to_screen(p_x_axis_px: float, p_y_axis_px: float) -> Vector2: + if _x_is_horizontal: + return Vector2(p_x_axis_px, p_y_axis_px) + return Vector2(p_y_axis_px, p_x_axis_px) + + func map_screen_to_point(p_screen_coords: Vector2) -> Vector2: + if _x_is_horizontal: + return Vector2(p_screen_coords.x, p_screen_coords.y) + return Vector2(p_screen_coords.y, p_screen_coords.x) + ################################################################################################ # Categorical label visibility ################################################################################################ From fa9db6d3bc47a2d4e5132bf44d7468bc52f21d03 Mon Sep 17 00:00:00 2001 From: ze2j <46556066+ze2j@users.noreply.github.com> Date: Thu, 23 Apr 2026 17:33:17 +0100 Subject: [PATCH 02/23] Line overlay minimal implementation --- .../plot/xy/hover/hover_controller.gd | 8 + addons/tau-plot/plot/xy/line/line_config.gd | 97 +++++++ .../tau-plot/plot/xy/line/line_config.gd.uid | 1 + addons/tau-plot/plot/xy/line/line_renderer.gd | 273 ++++++++++++++++++ .../plot/xy/line/line_renderer.gd.uid | 1 + addons/tau-plot/plot/xy/line/line_style.gd | 105 +++++++ .../tau-plot/plot/xy/line/line_style.gd.uid | 1 + .../tau-plot/plot/xy/line/line_validator.gd | 61 ++++ .../plot/xy/line/line_validator.gd.uid | 1 + .../plot/xy/line/line_visual_attributes.gd | 7 + .../xy/line/line_visual_attributes.gd.uid | 1 + .../plot/xy/line/line_visual_callbacks.gd | 7 + .../plot/xy/line/line_visual_callbacks.gd.uid | 1 + addons/tau-plot/plot/xy/pane_overlay_type.gd | 2 +- addons/tau-plot/plot/xy/xy_plot.gd | 140 ++++++++- addons/tau-plot/plot/xy/xy_plot_validator.gd | 11 + addons/tau-plot/plot/xy/xy_state.gd | 45 +++ 17 files changed, 758 insertions(+), 4 deletions(-) create mode 100644 addons/tau-plot/plot/xy/line/line_config.gd create mode 100644 addons/tau-plot/plot/xy/line/line_config.gd.uid create mode 100644 addons/tau-plot/plot/xy/line/line_renderer.gd create mode 100644 addons/tau-plot/plot/xy/line/line_renderer.gd.uid create mode 100644 addons/tau-plot/plot/xy/line/line_style.gd create mode 100644 addons/tau-plot/plot/xy/line/line_style.gd.uid create mode 100644 addons/tau-plot/plot/xy/line/line_validator.gd create mode 100644 addons/tau-plot/plot/xy/line/line_validator.gd.uid create mode 100644 addons/tau-plot/plot/xy/line/line_visual_attributes.gd create mode 100644 addons/tau-plot/plot/xy/line/line_visual_attributes.gd.uid create mode 100644 addons/tau-plot/plot/xy/line/line_visual_callbacks.gd create mode 100644 addons/tau-plot/plot/xy/line/line_visual_callbacks.gd.uid diff --git a/addons/tau-plot/plot/xy/hover/hover_controller.gd b/addons/tau-plot/plot/xy/hover/hover_controller.gd index d967542..f4f0772 100644 --- a/addons/tau-plot/plot/xy/hover/hover_controller.gd +++ b/addons/tau-plot/plot/xy/hover/hover_controller.gd @@ -9,6 +9,7 @@ const XYLayout := preload("res://addons/tau-plot/plot/xy/xy_layout.gd").XYLayout const PaneRenderer := preload("res://addons/tau-plot/plot/xy/pane_renderer.gd").PaneRenderer const BarRenderer := preload("res://addons/tau-plot/plot/xy/bar/bar_renderer.gd").BarRenderer const ScatterRenderer := preload("res://addons/tau-plot/plot/xy/scatter/scatter_renderer.gd").ScatterRenderer +const LineRenderer := preload("res://addons/tau-plot/plot/xy/line/line_renderer.gd").LineRenderer ## Handles input dispatch, hover mode resolution, hit aggregation across @@ -24,6 +25,7 @@ class HoverController extends RefCounted: var _pane_renderers: Array[PaneRenderer] = [] var _bar_renderers: Array[BarRenderer] = [] var _scatter_renderers: Array[ScatterRenderer] = [] + var _line_renderers: Array[LineRenderer] = [] var _resolved_xy_style: TauXYStyle = null # Hover state. @@ -58,6 +60,7 @@ class HoverController extends RefCounted: p_pane_renderers: Array[PaneRenderer], p_bar_renderers: Array[BarRenderer], p_scatter_renderers: Array[ScatterRenderer], + p_line_renderers: Array[LineRenderer], p_resolved_xy_style: TauXYStyle, p_formatter: HoverFormatter, p_hit_testers_per_pane: Array, # Array[Array[OverlayHitTester]] FIXME Godot 4.5 does not support nested typed collections. @@ -70,6 +73,7 @@ class HoverController extends RefCounted: _pane_renderers = p_pane_renderers _bar_renderers = p_bar_renderers _scatter_renderers = p_scatter_renderers + _line_renderers = p_line_renderers _resolved_xy_style = p_resolved_xy_style _formatter = p_formatter _hit_testers_per_pane = p_hit_testers_per_pane @@ -102,6 +106,7 @@ class HoverController extends RefCounted: _pane_renderers = [] _bar_renderers = [] _scatter_renderers = [] + _line_renderers = [] _resolved_xy_style = null _formatter = null _hover_config = null @@ -574,6 +579,9 @@ class HoverController extends RefCounted: var existing: SampleHit = scatter_hits_by_pane.get(hit.pane_index) if existing == null or hit.distance_px < existing.distance_px: scatter_hits_by_pane[hit.pane_index] = hit + elif hit.overlay_type == PaneOverlayType.LINE: + # TODO: categorize line hits once line hover is wired. + pass for pane_index: int in range(_bar_renderers.size()): var renderer: BarRenderer = _bar_renderers[pane_index] diff --git a/addons/tau-plot/plot/xy/line/line_config.gd b/addons/tau-plot/plot/xy/line/line_config.gd new file mode 100644 index 0000000..6cfb4c8 --- /dev/null +++ b/addons/tau-plot/plot/xy/line/line_config.gd @@ -0,0 +1,97 @@ +## Line-overlay specific rendering config. +class_name TauLineConfig extends TauPaneOverlayConfig + +const LineVisualCallbacks := preload("res://addons/tau-plot/plot/xy/line/line_visual_callbacks.gd").LineVisualCallbacks + +################################################################################################ +# WARNING: Any new member added to this class must be reflected in `is_equal_to()` +# and, if applicable, in `has_layout_affecting_change()`. +################################################################################################ + +## Theme-driven visual parameters for lines. +## Never null. Modify properties directly: line_config.style.line_width_px = 3.0. +## Properties set this way are automatically guarded from theme overwriting. +@export var style: TauLineStyle = TauLineStyle.new() + +enum LineMode +{ + INDEPENDENT, ## Each series is drawn independently. + STACKED ## Values at the same X are summed across series. +} +@export var mode: LineMode = LineMode.INDEPENDENT + +const StackedNormalization = preload("res://addons/tau-plot/plot/xy/stacked_normalization.gd").StackedNormalization +@export var stacked_normalization: StackedNormalization = StackedNormalization.NONE + +## Maximum pixel distance from the cursor to a sample position for the sample +## to be considered a hover hit. +## +## In NEAREST mode, this is a 2D Euclidean distance gate. Samples farther +## than this value from the cursor are excluded entirely. +## +## In X_ALIGNED mode, this is an x-axis-only pixel gate. Samples whose x +## screen position differs from the target x by more than this value are +## excluded. The same threshold sets [member SampleHit.contains_pointer] +## on included hits. +@export var hover_max_distance_px: int = 10 + + +#################################################################################################### +# Typed visual_callbacks accessor +#################################################################################################### + +## Typed accessor for line-specific visual callbacks. +## Shadows the base [member TauPaneOverlayConfig.visual_callbacks] with the concrete type. +var line_visual_callbacks: LineVisualCallbacks: + get: + return visual_callbacks as LineVisualCallbacks + set(value): + visual_callbacks = value + + +#################################################################################################### +# Helpers +#################################################################################################### + +func _init() -> void: + overlay_type = PaneOverlayType.LINE + + +func is_equal_to(p_other: TauPaneOverlayConfig) -> bool: + var other := p_other as TauLineConfig + if other == null: + return false + + if not super.is_equal_to(other): + return false + + if mode != other.mode: + return false + if stacked_normalization != other.stacked_normalization: + return false + if hover_max_distance_px != other.hover_max_distance_px: + return false + + return true + + +# Returns true if the change between this and p_other affects layout/domain. +# Returns false if the change only affects visual appearance. +# +# Only mode and stacked_normalization affect the domain: stacking changes the +# Y bounds. Hover distance is a pure hit-test parameter with no layout effect. +func has_layout_affecting_change(p_other: TauPaneOverlayConfig) -> bool: + var other := p_other as TauLineConfig + if other == null: + return false + + if not super.has_layout_affecting_change(other): + return false + + if mode != other.mode: + return true + + if mode == LineMode.STACKED and stacked_normalization != other.stacked_normalization: + return true + + return false diff --git a/addons/tau-plot/plot/xy/line/line_config.gd.uid b/addons/tau-plot/plot/xy/line/line_config.gd.uid new file mode 100644 index 0000000..bf62a0d --- /dev/null +++ b/addons/tau-plot/plot/xy/line/line_config.gd.uid @@ -0,0 +1 @@ +uid://cqwyag6rk1ekl diff --git a/addons/tau-plot/plot/xy/line/line_renderer.gd b/addons/tau-plot/plot/xy/line/line_renderer.gd new file mode 100644 index 0000000..482a347 --- /dev/null +++ b/addons/tau-plot/plot/xy/line/line_renderer.gd @@ -0,0 +1,273 @@ +# Dependencies +const Dataset := preload("res://addons/tau-plot/model/dataset.gd").Dataset +const XYLayout := preload("res://addons/tau-plot/plot/xy/xy_layout.gd").XYLayout +const SeriesAxisAssignment := preload("res://addons/tau-plot/plot/xy/series_axis_assignment.gd").SeriesAxisAssignment +const AxisId = preload("res://addons/tau-plot/plot/xy/xy_axes.gd").AxisId +const Axis = preload("res://addons/tau-plot/plot/xy/xy_axes.gd").Axis +const VisualAttributes = preload("res://addons/tau-plot/plot/xy/visual_attributes.gd").VisualAttributes +const LineVisualAttributes := preload("res://addons/tau-plot/plot/xy/line/line_visual_attributes.gd").LineVisualAttributes + + +# Draws line overlays from an XYLayout + Dataset. +# +# This renderer reads all samples through the Dataset public API (no direct +# buffer/series access). One contiguous run of valid samples produces one +# draw_polyline() call per series. +# +# Runtime behavior: +# - NaN and Inf X or Y values break the polyline (SKIP gap policy). +# - Logarithmic Y scales: y <= 0 breaks the polyline. +# - Logarithmic X scales: x <= 0 breaks the polyline. +# +# LineValidator is expected to enforce binding-level typing constraints. +class LineRenderer extends Control: + var _layout: XYLayout = null + var _dataset: Dataset = null + var _line_config: TauLineConfig = null + var _series_assignment: SeriesAxisAssignment = null + var _visual_attributes: Array[LineVisualAttributes] = [] + + # Pane index this renderer belongs to. Used for per-pane domain/layout queries. + var _pane_index: int = 0 + + # Line-specific series list: only series mapped as LINE are iterated. + # Must be provided at construction. Empty means this renderer has no + # series to draw. + var _line_series_ids: PackedInt64Array = PackedInt64Array() + + # Resolved style instances pushed by xy_plot. Treat as read-only. + var _line_style: TauLineStyle = null + var _xy_style: TauXYStyle = null + + + func _init(p_layout: XYLayout, + p_dataset: Dataset, + p_line_config: TauLineConfig, + p_xy_style: TauXYStyle, + p_series_assignment: SeriesAxisAssignment, + p_pane_index: int = 0, + p_visual_attributes: Array[LineVisualAttributes] = [], + p_line_series_ids: PackedInt64Array = PackedInt64Array()) -> void: + theme_type_variation = &"TauLine" + _layout = p_layout + _dataset = p_dataset + _line_config = p_line_config + _series_assignment = p_series_assignment + _pane_index = p_pane_index + _visual_attributes = p_visual_attributes + _line_series_ids = p_line_series_ids + _line_style = p_line_config.style + _xy_style = p_xy_style + + + func _ready() -> void: + mouse_filter = Control.MOUSE_FILTER_IGNORE + queue_redraw() + + + func _notification(what: int) -> void: + match what: + NOTIFICATION_RESIZED: + queue_redraw() + + + func get_config() -> TauLineConfig: + return _line_config + + + ## Receives the resolved TauLineStyle from xy_plot after cascade resolution. + func set_resolved_line_style(p_style: TauLineStyle) -> void: + _line_style = p_style + + + ## Receives the resolved TauXYStyle from xy_plot after cascade resolution. + func set_resolved_xy_style(p_style: TauXYStyle) -> void: + _xy_style = p_style + + + ## Creates a legend key Control for a line overlay. + func create_legend_key_control(_p_series_index: int) -> Control: + # TODO: implement create_legend_key_control for lines + return Control.new() + + + #################################################################################################### + # Private + #################################################################################################### + + func _draw() -> void: + if _line_style == null: + push_error("LineRenderer: resolved TauLineStyle is null.") + return + + var pane_rect := _layout.get_pane_rect(_pane_index) + if pane_rect.size.x <= 0.0 or pane_rect.size.y <= 0.0: + return + + var series_count := _get_line_series_count() + if series_count <= 0: + return + + var draw_order := _get_series_draw_order(series_count) + var width_px: float = max(_line_style.line_width_px, 0.0) + if width_px <= 0.0: + return + + for draw_rank in range(draw_order.size()): + var series_index: int = draw_order[draw_rank] + _draw_series_independent(series_index, width_px) + + + # Draws a single series as one or more polyline runs, respecting SKIP + # gap policy. Run emission follows these rules: + # - A valid sample is appended to the current run. + # - An invalid sample (NaN/Inf X or Y, or a value forbidden by the + # active axis scale) flushes the current run and starts a new one. + # - A run of fewer than two points is discarded (no polyline). + func _draw_series_independent(p_series_index: int, p_width_px: float) -> void: + var x_cfg := _get_x_axis_config() + if x_cfg != null and x_cfg.type == TauAxisConfig.Type.CATEGORICAL: + _draw_series_categorical(p_series_index, p_width_px) + else: + _draw_series_continuous(p_series_index, p_width_px) + + + func _draw_series_continuous(p_series_index: int, p_width_px: float) -> void: + var series_id := _get_line_series_id(p_series_index) + var global_series_index := _get_global_series_index(p_series_index) + var color := _resolve_series_color(global_series_index) + var y_axis_id := _get_y_axis_id_for_series(series_id) + + var run := PackedVector2Array() + + var is_shared_x := _dataset.get_mode() == Dataset.Mode.SHARED_X + var sample_count := _dataset.get_series_sample_count(series_id) + + for i in range(sample_count): + var x_value: float = float(_dataset.get_shared_x(i)) if is_shared_x else float(_dataset.get_series_x(series_id, i)) + if is_nan(x_value) or is_inf(x_value) or not _is_x_value_valid_for_scale(x_value): + run = _flush_run(run, color, p_width_px) + continue + + var y_value := _dataset.get_series_y(series_id, i) + if is_nan(y_value) or is_inf(y_value) or not _is_y_value_valid_for_scale(series_id, y_value): + run = _flush_run(run, color, p_width_px) + continue + + var x_px := _layout.map_x_to_px(_pane_index, x_value) + var y_px := _layout.map_y_to_px(_pane_index, y_value, y_axis_id) + run.append(_layout.map_point_to_screen(x_px, y_px)) + + if run.size() >= 2: + draw_polyline(run, color, p_width_px) + + + func _draw_series_categorical(p_series_index: int, p_width_px: float) -> void: + var series_id := _get_line_series_id(p_series_index) + var global_series_index := _get_global_series_index(p_series_index) + var color := _resolve_series_color(global_series_index) + var y_axis_id := _get_y_axis_id_for_series(series_id) + + var run := PackedVector2Array() + var sample_count := _dataset.get_series_sample_count(series_id) + + for cat_idx in range(sample_count): + var y_value := _dataset.get_series_y(series_id, cat_idx) + if is_nan(y_value) or is_inf(y_value) or not _is_y_value_valid_for_scale(series_id, y_value): + run = _flush_run(run, color, p_width_px) + continue + + var x_px := _layout.map_x_category_center_to_px(_pane_index, cat_idx) + var y_px := _layout.map_y_to_px(_pane_index, y_value, y_axis_id) + run.append(_layout.map_point_to_screen(x_px, y_px)) + + if run.size() >= 2: + draw_polyline(run, color, p_width_px) + + + func _flush_run(p_run: PackedVector2Array, p_color: Color, p_width_px: float) -> PackedVector2Array: + if p_run.size() >= 2: + draw_polyline(p_run, p_color, p_width_px) + return PackedVector2Array() + + + #################################################################################################### + # Series helpers + #################################################################################################### + + # Returns the number of series this renderer is responsible for. + func _get_line_series_count() -> int: + return _line_series_ids.size() + + + # Returns the dataset series_id for a given line-local series index. + func _get_line_series_id(p_line_index: int) -> int: + return _line_series_ids[p_line_index] + + + # Returns the dataset-global series index for a given pane-local series index. + func _get_global_series_index(p_local_index: int) -> int: + return _dataset.get_series_index_by_id(_line_series_ids[p_local_index]) + + + # Honors TauPaneOverlayConfig.z_order to decide which series is drawn on top. + func _get_series_draw_order(p_series_count: int) -> Array[int]: + var order: Array[int] = [] + for i in range(p_series_count): + order.append(i) + if _line_config.z_order == TauPaneOverlayConfig.ZOrder.REVERSE_SERIES_ORDER: + order.reverse() + return order + + + # Returns the shared x axis config. + func _get_x_axis_config() -> TauAxisConfig: + return _layout.domain.config.x_axis + + + #################################################################################################### + # Color resolution + #################################################################################################### + + # TODO: fully implement _resolve_series_color + func _resolve_series_color(p_global_series_index: int) -> Color: + var color := _xy_style.get_series_color(p_global_series_index) + color.a = clampf(_xy_style.series_alpha, 0.0, 1.0) + return color + + + #################################################################################################### + # Axis helpers + #################################################################################################### + + func _get_y_axis_id_for_series(p_series_id: int) -> AxisId: + var axis_id: int = _series_assignment.get_y_axis_id_for_series(p_series_id, _pane_index) + if axis_id != -1: + return axis_id as AxisId + # Fallback: should not happen if validation passed. + push_error("LineRenderer: series %d not assigned to any y-axis in pane %d" % [p_series_id, _pane_index]) + return Axis.get_orthogonal_axes(_layout.domain.config.x_axis_id)[0] + + + #################################################################################################### + # Axis-scale validity checks + #################################################################################################### + + func _is_x_value_valid_for_scale(p_x_value: float) -> bool: + var x_cfg := _layout.domain.config.x_axis + if x_cfg == null: + return true + if x_cfg.scale == TauAxisConfig.Scale.LOGARITHMIC and p_x_value <= 0.0: + return false + return true + + + func _is_y_value_valid_for_scale(p_series_id: int, p_y_value: float) -> bool: + var y_axis_id := _get_y_axis_id_for_series(p_series_id) + var pane_cfg: TauPaneConfig = _layout.domain.config.panes[_pane_index] + var y_cfg: TauAxisConfig = pane_cfg.get_y_axis_config(y_axis_id) + if y_cfg == null: + return true + if y_cfg.scale == TauAxisConfig.Scale.LOGARITHMIC and p_y_value <= 0.0: + return false + return true diff --git a/addons/tau-plot/plot/xy/line/line_renderer.gd.uid b/addons/tau-plot/plot/xy/line/line_renderer.gd.uid new file mode 100644 index 0000000..e2bbdb4 --- /dev/null +++ b/addons/tau-plot/plot/xy/line/line_renderer.gd.uid @@ -0,0 +1 @@ +uid://bjlgjhtxnkkv7 diff --git a/addons/tau-plot/plot/xy/line/line_style.gd b/addons/tau-plot/plot/xy/line/line_style.gd new file mode 100644 index 0000000..9d90fe3 --- /dev/null +++ b/addons/tau-plot/plot/xy/line/line_style.gd @@ -0,0 +1,105 @@ +## Contains theme-driven visual parameters for line overlays. +## +## Properties set on this resource take the highest priority, always winning +## over the theme and the built-in defaults. +## +## Properties left untouched fall back to the Godot theme. If the theme does +## not define them either, the built-in defaults apply. +## +## [b]Limitation:[/b] because "untouched" means "still equal to the built-in +## default", setting a property to exactly its default value has no visible +## effect. To force the default value to win over a theme, use an imperceptibly +## different value (e.g. 2.001 instead of 2.0). +class_name TauLineStyle extends Resource + +################################################################################################ +# WARNING: Any new member added to this class must be reflected in `is_equal_to()`, +# `apply_overrides_from()`, and, if applicable, in +# `has_layout_affecting_change()`. +################################################################################################ + +const DEFAULT_LINE_WIDTH_PX: float = 2.0 +@export var line_width_px: float = DEFAULT_LINE_WIDTH_PX + + +#################################################################################################### +# Cascade: theme loading (layer 2) +#################################################################################################### + +## Loads properties from the Godot theme attached to [param p_control]. +## +## For each property, the non-indexed theme key is fetched first (shared base +## for all panes), then the indexed key for [param p_pane_index] overwrites it +## if present. +## +## This method writes every property unconditionally because it is called on +## the resolved instance, not on the user-provided resource. +func load_from_theme(p_control: Control, p_pane_index: int) -> void: + if p_control == null: + push_error("TauLineStyle.load_from_theme(): control is null") + return + + # line_width_px + if p_control.has_theme_constant(&"line_width_px"): + line_width_px = max(float(p_control.get_theme_constant(&"line_width_px")), 0.0) + var indexed_width_key := StringName("line_width_px_%d" % p_pane_index) + if p_control.has_theme_constant(indexed_width_key): + line_width_px = max(float(p_control.get_theme_constant(indexed_width_key)), 0.0) + + +#################################################################################################### +# Cascade: user overrides (layer 3) +#################################################################################################### + +## Applies overridden properties from [param p_user_style] onto this resolved +## instance. A property is considered overridden when its value on the user +## resource differs from the matching DEFAULT_* constant. +func apply_overrides_from(p_user_style: TauLineStyle) -> void: + if p_user_style == null: + return + + if p_user_style.line_width_px != DEFAULT_LINE_WIDTH_PX: + line_width_px = p_user_style.line_width_px + + +#################################################################################################### +# Full cascade resolution +#################################################################################################### + +## Produces a fully resolved TauLineStyle by applying all three cascade layers: +## 1. Start from defaults (a fresh TauLineStyle instance). +## 2. Load theme values (non-indexed, then indexed for this pane). +## 3. Apply user overrides from [param p_user_style] (may be null). +## +## The returned instance is a new TauLineStyle owned by the caller. It is +## separate from [param p_user_style] which is never mutated. +static func resolve( + p_control: Control, + p_pane_index: int, + p_user_style: TauLineStyle +) -> TauLineStyle: + # Layer 1: defaults. + var resolved := TauLineStyle.new() + # Layer 2: theme values. + resolved.load_from_theme(p_control, p_pane_index) + # Layer 3: user overrides. + resolved.apply_overrides_from(p_user_style) + return resolved + + +#################################################################################################### +# Change detection +#################################################################################################### + +func is_equal_to(p_other: TauLineStyle) -> bool: + if p_other == null: + return false + if line_width_px != p_other.line_width_px: + return false + return true + + +# All TauLineStyle properties are visual-only. They control how lines are +# drawn within a fixed domain but do not affect domain, ticks, or pane rect. +func has_layout_affecting_change(p_other: TauLineStyle) -> bool: + return false diff --git a/addons/tau-plot/plot/xy/line/line_style.gd.uid b/addons/tau-plot/plot/xy/line/line_style.gd.uid new file mode 100644 index 0000000..151e788 --- /dev/null +++ b/addons/tau-plot/plot/xy/line/line_style.gd.uid @@ -0,0 +1 @@ +uid://hqhsstnn7m2q diff --git a/addons/tau-plot/plot/xy/line/line_validator.gd b/addons/tau-plot/plot/xy/line/line_validator.gd new file mode 100644 index 0000000..fc919d6 --- /dev/null +++ b/addons/tau-plot/plot/xy/line/line_validator.gd @@ -0,0 +1,61 @@ +# Dependencies +const Dataset := preload("res://addons/tau-plot/model/dataset.gd").Dataset +const PaneOverlayType = preload("res://addons/tau-plot/plot/xy/pane_overlay_type.gd").PaneOverlayType +const LineVisualAttributes = preload("res://addons/tau-plot/plot/xy/line/line_visual_attributes.gd").LineVisualAttributes +const LineVisualCallbacks = preload("res://addons/tau-plot/plot/xy/line/line_visual_callbacks.gd").LineVisualCallbacks +const ValidationResult = preload("res://addons/tau-plot/plot/validation_result.gd").ValidationResult + + +## Validates that the line overlay configuration for a single pane is +## internally consistent. +## +## This validator checks configuration only, not dataset values. The dataset +## is mutable after plot_xy() is called, so runtime data issues are handled +## elsewhere. +## +## All errors are accumulated into the provided [ValidationResult]. +class LineValidator extends RefCounted: + + static func validate(p_dataset: Dataset, p_domain_cfg: TauXYConfig, p_pane_index: int, p_line_overlay_bindings: Array[TauXYSeriesBinding], p_result: ValidationResult) -> void: + if p_dataset == null: + p_result.add_error("LineValidator: p_dataset is null") + return + if p_domain_cfg == null: + p_result.add_error("LineValidator: p_domain_cfg is null") + return + if p_pane_index < 0 or p_pane_index >= p_domain_cfg.panes.size(): + p_result.add_error("LineValidator: p_pane_index %d is out of range" % p_pane_index) + return + for binding in p_line_overlay_bindings: + if binding.pane_index != p_pane_index: + p_result.add_error("LineValidator: binding has pane_index %d, expected %d" % [binding.pane_index, p_pane_index]) + return + if binding.overlay_type != PaneOverlayType.LINE: + p_result.add_error("LineValidator: binding has overlay_type %d, expected LINE" % int(binding.overlay_type)) + return + + var pane_cfg := p_domain_cfg.panes[p_pane_index] + if pane_cfg == null: + p_result.add_error("LineValidator: pane %d: pane config is null" % p_pane_index) + return + + var line_config := pane_cfg.get_overlay_config(PaneOverlayType.LINE) as TauLineConfig + if line_config == null: + p_result.add_error("LineValidator: pane %d: no TauLineConfig found in pane overlays" % p_pane_index) + return + + _validate_line_visuals(p_pane_index, line_config, p_line_overlay_bindings, p_result) + + + #################################################################################################### + # Private + #################################################################################################### + + static func _validate_line_visuals(p_pane_index: int, p_line_config: TauPaneOverlayConfig, p_line_overlay_bindings: Array[TauXYSeriesBinding], p_result: ValidationResult) -> void: + if p_line_config.visual_callbacks != null and p_line_config.visual_callbacks is not LineVisualCallbacks: + p_result.add_error("LineValidator: pane %d: visual_callbacks is not a LineVisualCallbacks" % p_pane_index) + + for i in range(0, p_line_overlay_bindings.size()): + var binding: TauXYSeriesBinding = p_line_overlay_bindings[i] + if binding.visual_attributes != null and binding.visual_attributes is not LineVisualAttributes: + p_result.add_error("LineValidator: pane %d: series_id %d has visual_attributes that is not a LineVisualAttributes" % [p_pane_index, binding.series_id]) diff --git a/addons/tau-plot/plot/xy/line/line_validator.gd.uid b/addons/tau-plot/plot/xy/line/line_validator.gd.uid new file mode 100644 index 0000000..ee741ac --- /dev/null +++ b/addons/tau-plot/plot/xy/line/line_validator.gd.uid @@ -0,0 +1 @@ +uid://de3ehu51x06k1 diff --git a/addons/tau-plot/plot/xy/line/line_visual_attributes.gd b/addons/tau-plot/plot/xy/line/line_visual_attributes.gd new file mode 100644 index 0000000..83c9efa --- /dev/null +++ b/addons/tau-plot/plot/xy/line/line_visual_attributes.gd @@ -0,0 +1,7 @@ +# Dependencies +const VisualAttributes = preload("res://addons/tau-plot/plot/xy/visual_attributes.gd").VisualAttributes + +## Per-sample data-driven visual attribute buffers for LINE overlays. +class LineVisualAttributes extends VisualAttributes: + # TODO: add width_buffer. + pass diff --git a/addons/tau-plot/plot/xy/line/line_visual_attributes.gd.uid b/addons/tau-plot/plot/xy/line/line_visual_attributes.gd.uid new file mode 100644 index 0000000..d8b1ecc --- /dev/null +++ b/addons/tau-plot/plot/xy/line/line_visual_attributes.gd.uid @@ -0,0 +1 @@ +uid://dhgf2p6cr42ll diff --git a/addons/tau-plot/plot/xy/line/line_visual_callbacks.gd b/addons/tau-plot/plot/xy/line/line_visual_callbacks.gd new file mode 100644 index 0000000..992b214 --- /dev/null +++ b/addons/tau-plot/plot/xy/line/line_visual_callbacks.gd @@ -0,0 +1,7 @@ +# Dependencies +const VisualCallbacks = preload("res://addons/tau-plot/plot/xy/visual_callbacks.gd").VisualCallbacks + +## LINE overlay specific callbacks. +class LineVisualCallbacks extends VisualCallbacks: + # TODO: add width_callback. + pass diff --git a/addons/tau-plot/plot/xy/line/line_visual_callbacks.gd.uid b/addons/tau-plot/plot/xy/line/line_visual_callbacks.gd.uid new file mode 100644 index 0000000..e3929fa --- /dev/null +++ b/addons/tau-plot/plot/xy/line/line_visual_callbacks.gd.uid @@ -0,0 +1 @@ +uid://d4aien3nhx311 diff --git a/addons/tau-plot/plot/xy/pane_overlay_type.gd b/addons/tau-plot/plot/xy/pane_overlay_type.gd index 3503024..cd67fa3 100644 --- a/addons/tau-plot/plot/xy/pane_overlay_type.gd +++ b/addons/tau-plot/plot/xy/pane_overlay_type.gd @@ -2,5 +2,5 @@ enum PaneOverlayType { BAR = 0, SCATTER, - # Future: LINE, AREA, etc. + LINE, } diff --git a/addons/tau-plot/plot/xy/xy_plot.gd b/addons/tau-plot/plot/xy/xy_plot.gd index c0acb12..096d4d2 100644 --- a/addons/tau-plot/plot/xy/xy_plot.gd +++ b/addons/tau-plot/plot/xy/xy_plot.gd @@ -41,6 +41,9 @@ const ScatterRenderer := preload("res://addons/tau-plot/plot/xy/scatter/scatter_ const ScatterVisualAttributes = preload("res://addons/tau-plot/plot/xy/scatter/scatter_visual_attributes.gd").ScatterVisualAttributes const ScatterHitTester = preload("res://addons/tau-plot/plot/xy/scatter/scatter_hit_tester.gd").ScatterHitTester +const LineRenderer := preload("res://addons/tau-plot/plot/xy/line/line_renderer.gd").LineRenderer +const LineVisualAttributes := preload("res://addons/tau-plot/plot/xy/line/line_visual_attributes.gd").LineVisualAttributes + # External references (provided via setup) var _plot: PanelContainer = null @@ -60,6 +63,7 @@ var _xy_domain_overrides: XYDomainOverrides = null var _xy_layout: XYLayout = null var _bar_config_per_pane: Array[TauBarConfig] = [] # Elements may be null, one per pane var _scatter_config_per_pane: Array[TauScatterConfig] = [] # Elements may be null, one per pane +var _line_config_per_pane: Array[TauLineConfig] = [] # Elements may be null, one per pane var _series_bindings: Array[TauXYSeriesBinding] = [] var _series_assignment: SeriesAxisAssignment = null @@ -67,9 +71,9 @@ var _series_assignment: SeriesAxisAssignment = null var _pane_renderers: Array[PaneRenderer] = [] # Elements are never null, one per pane var _bar_renderers: Array[BarRenderer] = [] # Elements may be null, one per pane var _scatter_renderers: Array[ScatterRenderer] = [] # Elements may be null, one per pane +var _line_renderers: Array[LineRenderer] = [] # Elements may be null, one per pane -# The BoxContainer that holds all pane containers. Created as VBoxContainer -# (x horizontal) or HBoxContainer (x vertical) by _create_pane_stack(). +# The BoxContainer that holds all pane containers. var _pane_stack: BoxContainer = null # Per-pane containers (nodes inside _pane_stack) @@ -78,6 +82,7 @@ var _pane_containers: Array[Container] = [] # Elements are never null, one per # Per-pane series partitioning var _bar_series_ids_per_pane: Array[PackedInt64Array] = [] var _scatter_series_ids_per_pane: Array[PackedInt64Array] = [] +var _line_series_ids_per_pane: Array[PackedInt64Array] = [] # Per-pane resolved TauPaneStyle instances (produced by the three-layer cascade). # One entry per pane, always non-null after setup(). @@ -86,6 +91,7 @@ var _resolved_pane_styles: Array[TauPaneStyle] = [] # Per-pane resolved overlay style instances (produced by the three-layer cascade). var _resolved_bar_styles: Array[TauBarStyle] = [] var _resolved_scatter_styles: Array[TauScatterStyle] = [] +var _resolved_line_styles: Array[TauLineStyle] = [] # Plot-wide resolved TauXYStyle instance (produced by the three-layer cascade). # Distributed to all renderers (PaneRenderer, BarRenderer, ScatterRenderer). @@ -111,6 +117,7 @@ var _styles_dirty: bool = true var _xy_dirty_panes: Array[bool] = [] var _bars_dirty_panes: Array[bool] = [] var _scatter_dirty_panes: Array[bool] = [] +var _line_dirty_panes: Array[bool] = [] # Hover controller (null when setup() has not been called or hover is not wired) var _hover_controller: HoverController = null @@ -153,15 +160,21 @@ func setup( _bar_config_per_pane.fill(null) _scatter_config_per_pane.resize(pane_count) _scatter_config_per_pane.fill(null) + _line_config_per_pane.resize(pane_count) + _line_config_per_pane.fill(null) _bar_series_ids_per_pane.clear() _scatter_series_ids_per_pane.clear() + _line_series_ids_per_pane.clear() var bar_va_per_pane: Array = [] # Array of Array[BarVisualAttributes]. FIXME Godot 4.5 does not support nested typed collections. var scatter_va_per_pane: Array = [] # Array of Array[ScatterVisualAttributes]. FIXME Godot 4.5 does not support nested typed collections. + var line_va_per_pane: Array = [] # Array of Array[LineVisualAttributes]. FIXME Godot 4.5 does not support nested typed collections. for i in range(pane_count): _bar_series_ids_per_pane.append(PackedInt64Array()) _scatter_series_ids_per_pane.append(PackedInt64Array()) + _line_series_ids_per_pane.append(PackedInt64Array()) bar_va_per_pane.append([] as Array[BarVisualAttributes]) scatter_va_per_pane.append([] as Array[ScatterVisualAttributes]) + line_va_per_pane.append([] as Array[LineVisualAttributes]) # Extract series bindings _series_assignment = SeriesAxisAssignment.new(pane_count) @@ -199,6 +212,20 @@ func setup( # Type is guaranteed by validation (ScatterValidator._validate_scatter_visuals). scatter_va_per_pane[pane_index].append(binding.visual_attributes as ScatterVisualAttributes) + TauXYSeriesBinding.PaneOverlayType.LINE: + if sid not in _line_series_ids_per_pane[pane_index]: + _line_series_ids_per_pane[pane_index].append(sid) + + if _line_config_per_pane[pane_index] == null: + var pane_config: TauPaneConfig = p_xy_config.panes[pane_index] + var line_cfg := pane_config.get_overlay_config(TauXYSeriesBinding.PaneOverlayType.LINE) as TauLineConfig + if line_cfg != null: + _line_config_per_pane[pane_index] = line_cfg + + if binding.visual_attributes != null: + # Type is guaranteed by validation (LineValidator._validate_line_visuals). + line_va_per_pane[pane_index].append(binding.visual_attributes as LineVisualAttributes) + _: # Unknown overlay types are rejected by validation. pass @@ -217,6 +244,8 @@ func setup( _bar_config_per_pane[pane_index].style.changed.connect(_on_style_changed) if _scatter_config_per_pane[pane_index] != null and not _scatter_config_per_pane[pane_index].style.changed.is_connected(_on_style_changed): _scatter_config_per_pane[pane_index].style.changed.connect(_on_style_changed) + if _line_config_per_pane[pane_index] != null and not _line_config_per_pane[pane_index].style.changed.is_connected(_on_style_changed): + _line_config_per_pane[pane_index].style.changed.connect(_on_style_changed) var pane_config: TauPaneConfig = p_xy_config.panes[pane_index] if pane_config.style != null and not pane_config.style.changed.is_connected(_on_style_changed): pane_config.style.changed.connect(_on_style_changed) @@ -230,16 +259,20 @@ func setup( _pane_renderers.clear() _bar_renderers.clear() _scatter_renderers.clear() + _line_renderers.clear() _resolved_pane_styles.clear() _resolved_bar_styles.clear() _resolved_scatter_styles.clear() + _resolved_line_styles.clear() _pane_renderers.resize(pane_count) _bar_renderers.resize(pane_count) _scatter_renderers.resize(pane_count) + _line_renderers.resize(pane_count) _resolved_pane_styles.resize(pane_count) _resolved_bar_styles.resize(pane_count) _resolved_scatter_styles.resize(pane_count) + _resolved_line_styles.resize(pane_count) _pane_containers.resize(pane_count) # Resolve TauXYStyle cascade once against the TauPlot root so that theme @@ -310,6 +343,24 @@ func setup( scatter_renderer.set_resolved_scatter_style(resolved_scatter_style) scatter_renderer.set_resolved_xy_style(_resolved_xy_style) + # Create LineRenderer for this pane if it has line series + if not _line_series_ids_per_pane[pane_index].is_empty(): + var line_renderer := LineRenderer.new( + _xy_layout, _dataset, _line_config_per_pane[pane_index], p_xy_config.style, + _series_assignment, + pane_index, line_va_per_pane[pane_index], + _line_series_ids_per_pane[pane_index]) + pane_container.add_child(line_renderer) + line_renderer.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + _line_renderers[pane_index] = line_renderer + + # Resolve TauLineStyle cascade and push it to the renderer. + var line_user_style: TauLineStyle = _line_config_per_pane[pane_index].style + var resolved_line_style := TauLineStyle.resolve(line_renderer, pane_index, line_user_style) + _resolved_line_styles[pane_index] = resolved_line_style + line_renderer.set_resolved_line_style(resolved_line_style) + line_renderer.set_resolved_xy_style(_resolved_xy_style) + # Axis titles _axis_title_layout.build(p_xy_config, _series_assignment) @@ -365,7 +416,7 @@ func setup( _hover_controller.setup( _plot, _xy_layout, _domain_config, _pane_containers, _pane_renderers, - _bar_renderers, _scatter_renderers, + _bar_renderers, _scatter_renderers, _line_renderers, _resolved_xy_style, formatter, hit_testers_per_pane, p_hover_enabled, p_hover_config) @@ -389,11 +440,14 @@ func clear() -> void: _series_assignment = null _bar_config_per_pane.clear() _scatter_config_per_pane.clear() + _line_config_per_pane.clear() _bar_series_ids_per_pane.clear() _scatter_series_ids_per_pane.clear() + _line_series_ids_per_pane.clear() _resolved_pane_styles.clear() _resolved_bar_styles.clear() _resolved_scatter_styles.clear() + _resolved_line_styles.clear() _resolved_xy_style = null _resolved_legend_style = null _user_legend_style = null @@ -490,6 +544,30 @@ func refresh(p_plot_global_position: Vector2, p_legend_position: Position) -> vo _scatter_dirty_panes[pane_index] = true _state.save_scatter_config_for_pane(pane_index, pane_scatter_config) + # Step 3-line: Check if line config changed per pane + var has_any_line := false + for renderer in _line_renderers: + if renderer != null: + has_any_line = true + break + + if has_any_line: + for pane_index in range(pane_count): + var pane_line_config: TauLineConfig = _line_config_per_pane[pane_index] if pane_index < _line_config_per_pane.size() else null + if pane_line_config == null: + continue + var prev_line_config: TauLineConfig = _state.line_config_per_pane[pane_index] if pane_index < _state.line_config_per_pane.size() else null + if not pane_line_config.is_equal_to(prev_line_config): + if pane_line_config.has_layout_affecting_change(prev_line_config): + _domain_dirty = true + _ticks_dirty = true + _pane_rect_dirty = true + _set_all_pane_flags(_xy_dirty_panes, true) + _set_all_pane_flags(_line_dirty_panes, true) + else: + _line_dirty_panes[pane_index] = true + _state.save_line_config_for_pane(pane_index, pane_line_config) + # Step 3b: Check if styles changed (programmatic mutations via config.style.*) # XY style: three-layer change detection (theme dirty, ref change, content mutation). # - layout-affecting properties trigger ticks + pane rect recompute, @@ -524,6 +602,9 @@ func refresh(p_plot_global_position: Vector2, p_legend_position: Position) -> vo for renderer in _scatter_renderers: if renderer != null: renderer.set_resolved_xy_style(_resolved_xy_style) + for renderer in _line_renderers: + if renderer != null: + renderer.set_resolved_xy_style(_resolved_xy_style) # Legend keys read visual properties from renderer instances. # When the resolved TauXYStyle changes (colors, alpha), keys become stale. @@ -595,6 +676,34 @@ func refresh(p_plot_global_position: Vector2, p_legend_position: Position) -> vo _scatter_dirty_panes[pane_index] = true _legend_rebuild_needed = true + # Line style: three-layer change detection (theme dirty, ref change, content mutation). + # All TauLineStyle properties are visual-only, so only dirty the owning line pane. + if has_any_line: + for pane_index in range(pane_count): + var line_config: TauLineConfig = _line_config_per_pane[pane_index] if pane_index < _line_config_per_pane.size() else null + if line_config == null: + continue + var needs_line_re_resolve := _styles_dirty + + # Reference change: user assigned a different TauLineStyle resource. + if _state.has_line_style_ref_changed_for_pane(pane_index, line_config.style): + needs_line_re_resolve = true + _state.save_line_style_ref_for_pane(pane_index, line_config.style) + + # Content mutation: user changed a property on the existing TauLineStyle. + if not needs_line_re_resolve: + var prev_line_style: TauLineStyle = _state.line_style_per_pane[pane_index] if pane_index < _state.line_style_per_pane.size() else null + if not line_config.style.is_equal_to(prev_line_style): + needs_line_re_resolve = true + + if needs_line_re_resolve and _line_renderers[pane_index] != null: + var resolved_line := TauLineStyle.resolve(_line_renderers[pane_index], pane_index, line_config.style) + _resolved_line_styles[pane_index] = resolved_line + _line_renderers[pane_index].set_resolved_line_style(resolved_line) + _state.save_line_style_for_pane(pane_index, line_config.style) + _line_dirty_panes[pane_index] = true + _legend_rebuild_needed = true + # Step 3c: Check grid_line config changes, style reference changes, # and pane style mutations. All visual-only. for pane_index in range(pane_count): @@ -724,6 +833,9 @@ func refresh(p_plot_global_position: Vector2, p_legend_position: Position) -> vo if _scatter_dirty_panes[i] and _scatter_renderers[i] != null: _scatter_renderers[i].update_scatter() _scatter_dirty_panes[i] = false + if _line_dirty_panes[i] and _line_renderers[i] != null: + _line_renderers[i].queue_redraw() + _line_dirty_panes[i] = false _state.save_pane_view_rects(pane_view_rects) @@ -790,6 +902,11 @@ func _get_legend_key_factory(p_overlay_type: int, p_pane_index: int) -> Callable var r = _scatter_renderers[p_pane_index] if r != null: return r.create_legend_key_control + TauXYSeriesBinding.PaneOverlayType.LINE: + if p_pane_index >= 0 and p_pane_index < _line_renderers.size(): + var r = _line_renderers[p_pane_index] + if r != null: + return r.create_legend_key_control return Callable() @@ -820,9 +937,11 @@ func _init_pane_dirty_flags(p_pane_count: int) -> void: _xy_dirty_panes.resize(p_pane_count) _bars_dirty_panes.resize(p_pane_count) _scatter_dirty_panes.resize(p_pane_count) + _line_dirty_panes.resize(p_pane_count) _xy_dirty_panes.fill(true) _bars_dirty_panes.fill(true) _scatter_dirty_panes.fill(true) + _line_dirty_panes.fill(true) func _set_all_pane_flags(p_flags: Array[bool], p_value: bool) -> void: @@ -846,17 +965,20 @@ func _mark_domain_dependents_dirty() -> void: _set_all_pane_flags(_xy_dirty_panes, true) _set_all_pane_flags(_bars_dirty_panes, true) _set_all_pane_flags(_scatter_dirty_panes, true) + _set_all_pane_flags(_line_dirty_panes, true) func _mark_renderers_dirty() -> void: _set_all_pane_flags(_bars_dirty_panes, true) _set_all_pane_flags(_scatter_dirty_panes, true) + _set_all_pane_flags(_line_dirty_panes, true) func _mark_visual_dirty() -> void: _set_all_pane_flags(_xy_dirty_panes, true) _set_all_pane_flags(_bars_dirty_panes, true) _set_all_pane_flags(_scatter_dirty_panes, true) + _set_all_pane_flags(_line_dirty_panes, true) func _reset_dataset() -> void: @@ -879,6 +1001,10 @@ func _disconnect_style_signals() -> void: if scatter_cfg != null and scatter_cfg.style != null: if scatter_cfg.style.changed.is_connected(_on_style_changed): scatter_cfg.style.changed.disconnect(_on_style_changed) + for line_cfg in _line_config_per_pane: + if line_cfg != null and line_cfg.style != null: + if line_cfg.style.changed.is_connected(_on_style_changed): + line_cfg.style.changed.disconnect(_on_style_changed) if _domain_config != null: for pane_config in _domain_config.panes: if pane_config != null and pane_config.style != null: @@ -914,6 +1040,9 @@ func _has_any_data_renderer() -> bool: for renderer in _scatter_renderers: if renderer != null: return true + for renderer in _line_renderers: + if renderer != null: + return true return false @@ -933,6 +1062,11 @@ func _clear_pane_containers() -> void: renderer.queue_free() _scatter_renderers.clear() + for renderer in _line_renderers: + if renderer != null and is_instance_valid(renderer): + renderer.queue_free() + _line_renderers.clear() + if _pane_stack != null: for container in _pane_containers: if container != null and is_instance_valid(container): diff --git a/addons/tau-plot/plot/xy/xy_plot_validator.gd b/addons/tau-plot/plot/xy/xy_plot_validator.gd index ced18c0..4b6ab72 100644 --- a/addons/tau-plot/plot/xy/xy_plot_validator.gd +++ b/addons/tau-plot/plot/xy/xy_plot_validator.gd @@ -2,6 +2,7 @@ const Dataset := preload("res://addons/tau-plot/model/dataset.gd").Dataset const BarValidator := preload("res://addons/tau-plot/plot/xy/bar/bar_validator.gd").BarValidator const ScatterValidator := preload("res://addons/tau-plot/plot/xy/scatter/scatter_validator.gd").ScatterValidator +const LineValidator := preload("res://addons/tau-plot/plot/xy/line/line_validator.gd").LineValidator const Axis = preload("res://addons/tau-plot/plot/xy/xy_axes.gd").Axis const AxisId = preload("res://addons/tau-plot/plot/xy/xy_axes.gd").AxisId const ValidationResult = preload("res://addons/tau-plot/plot/validation_result.gd").ValidationResult @@ -263,6 +264,7 @@ class XYPlotValidator extends RefCounted: # Group series by overlay type and pane var bar_bindings_by_pane: Dictionary[int, Array] = {} # Array[TauXYSeriesBinding]. FIXME Godot 4.5 does not support nested typed collections. var scatter_bindings_by_pane: Dictionary[int, Array] = {} # Array[TauXYSeriesBinding]. FIXME Godot 4.5 does not support nested typed collections. + var line_bindings_by_pane: Dictionary[int, Array] = {} # Array[TauXYSeriesBinding]. FIXME Godot 4.5 does not support nested typed collections. for binding in p_series_bindings: match binding.overlay_type: @@ -274,6 +276,10 @@ class XYPlotValidator extends RefCounted: if not scatter_bindings_by_pane.has(binding.pane_index): scatter_bindings_by_pane[binding.pane_index] = [] scatter_bindings_by_pane[binding.pane_index].append(binding) + TauXYSeriesBinding.PaneOverlayType.LINE: + if not line_bindings_by_pane.has(binding.pane_index): + line_bindings_by_pane[binding.pane_index] = [] + line_bindings_by_pane[binding.pane_index].append(binding) _: p_result.add_error("XYPlotValidator: unsupported overlay_type %d" % int(binding.overlay_type)) @@ -287,6 +293,11 @@ class XYPlotValidator extends RefCounted: scatter_overlay_bindings.assign(scatter_bindings_by_pane[pane_index]) ScatterValidator.validate(p_dataset, p_xy_config, pane_index, scatter_overlay_bindings, p_result) + for pane_index in line_bindings_by_pane: + var line_overlay_bindings: Array[TauXYSeriesBinding] = [] + line_overlay_bindings.assign(line_bindings_by_pane[pane_index]) + LineValidator.validate(p_dataset, p_xy_config, pane_index, line_overlay_bindings, p_result) + #################################################################################################### # Warnings diff --git a/addons/tau-plot/plot/xy/xy_state.gd b/addons/tau-plot/plot/xy/xy_state.gd index b21efa1..b0f8790 100644 --- a/addons/tau-plot/plot/xy/xy_state.gd +++ b/addons/tau-plot/plot/xy/xy_state.gd @@ -78,6 +78,7 @@ class XYState extends RefCounted: # Per-pane config snapshots from last refresh var bar_config_per_pane: Array[TauBarConfig] = [] var scatter_config_per_pane: Array[TauScatterConfig] = [] + var line_config_per_pane: Array[TauLineConfig] = [] # Per-pane grid_line config snapshots var grid_line_config_per_pane: Array[TauGridLineConfig] = [] @@ -94,11 +95,13 @@ class XYState extends RefCounted: var xy_style_ref: TauXYStyle = null var bar_style_per_pane: Array[TauBarStyle] = [] var scatter_style_per_pane: Array[TauScatterStyle] = [] + var line_style_per_pane: Array[TauLineStyle] = [] # Per-pane overlay style resource references (for detecting when the user # assigns a different style resource to the config). var bar_style_ref_per_pane: Array = [] var scatter_style_ref_per_pane: Array = [] + var line_style_ref_per_pane: Array = [] # TauLegendStyle ref + content tracking var legend_style_ref: TauLegendStyle = null @@ -126,6 +129,11 @@ class XYState extends RefCounted: while scatter_config_per_pane.size() < p_pane_count: scatter_config_per_pane.append(null) + while line_config_per_pane.size() > p_pane_count: + line_config_per_pane.pop_back() + while line_config_per_pane.size() < p_pane_count: + line_config_per_pane.append(null) + while grid_line_config_per_pane.size() > p_pane_count: grid_line_config_per_pane.pop_back() while grid_line_config_per_pane.size() < p_pane_count: @@ -151,6 +159,11 @@ class XYState extends RefCounted: while scatter_style_per_pane.size() < p_pane_count: scatter_style_per_pane.append(null) + while line_style_per_pane.size() > p_pane_count: + line_style_per_pane.pop_back() + while line_style_per_pane.size() < p_pane_count: + line_style_per_pane.append(null) + while bar_style_ref_per_pane.size() > p_pane_count: bar_style_ref_per_pane.pop_back() while bar_style_ref_per_pane.size() < p_pane_count: @@ -161,6 +174,11 @@ class XYState extends RefCounted: while scatter_style_ref_per_pane.size() < p_pane_count: scatter_style_ref_per_pane.append(null) + while line_style_ref_per_pane.size() > p_pane_count: + line_style_ref_per_pane.pop_back() + while line_style_ref_per_pane.size() < p_pane_count: + line_style_ref_per_pane.append(null) + func reset() -> void: domain_x_min = INF @@ -176,6 +194,7 @@ class XYState extends RefCounted: bar_config_per_pane.clear() scatter_config_per_pane.clear() + line_config_per_pane.clear() grid_line_config_per_pane.clear() pane_style_ref_per_pane.clear() @@ -185,9 +204,11 @@ class XYState extends RefCounted: xy_style_ref = null bar_style_per_pane.clear() scatter_style_per_pane.clear() + line_style_per_pane.clear() bar_style_ref_per_pane.clear() scatter_style_ref_per_pane.clear() + line_style_ref_per_pane.clear() legend_style_ref = null legend_style_snapshot = null @@ -321,6 +342,12 @@ class XYState extends RefCounted: scatter_config_per_pane[p_pane_index] = p_scatter_config.duplicate() if p_scatter_config != null else null + func save_line_config_for_pane(p_pane_index: int, p_line_config: TauLineConfig) -> void: + if p_pane_index < 0 or p_pane_index >= line_config_per_pane.size(): + return + line_config_per_pane[p_pane_index] = p_line_config.duplicate() if p_line_config != null else null + + func save_grid_line_config_for_pane(p_pane_index: int, p_grid_line_config: TauGridLineConfig) -> void: if p_pane_index < 0 or p_pane_index >= grid_line_config_per_pane.size(): return @@ -406,6 +433,24 @@ class XYState extends RefCounted: return scatter_style_ref_per_pane[p_pane_index] != p_style_ref + func save_line_style_for_pane(p_pane_index: int, p_style: TauLineStyle) -> void: + if p_pane_index < 0 or p_pane_index >= line_style_per_pane.size(): + return + line_style_per_pane[p_pane_index] = p_style.duplicate() if p_style != null else null + + + func save_line_style_ref_for_pane(p_pane_index: int, p_style_ref: TauLineStyle) -> void: + if p_pane_index < 0 or p_pane_index >= line_style_ref_per_pane.size(): + return + line_style_ref_per_pane[p_pane_index] = p_style_ref + + + func has_line_style_ref_changed_for_pane(p_pane_index: int, p_style_ref: TauLineStyle) -> bool: + if p_pane_index < 0 or p_pane_index >= line_style_ref_per_pane.size(): + return true + return line_style_ref_per_pane[p_pane_index] != p_style_ref + + func save_pane_style_for_pane(p_pane_index: int, p_style: TauPaneStyle) -> void: if p_pane_index < 0 or p_pane_index >= pane_style_per_pane.size(): return From e884e4cf7ba510562097fb170eb7c54e88e12967 Mon Sep 17 00:00:00 2001 From: ze2j <46556066+ze2j@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:33:12 +0200 Subject: [PATCH 03/23] Add tests --- .../line/linear_scale/test_line_linear.gd | 172 +++++++ .../line/linear_scale/test_line_linear.gd.uid | 1 + .../line/linear_scale/test_line_linear.tscn | 55 +++ .../tests/line/log_scale/test_line_log.gd | 421 ++++++++++++++++++ .../tests/line/log_scale/test_line_log.gd.uid | 1 + .../tests/line/log_scale/test_line_log.tscn | 92 ++++ .../line/nan_or_inf/test_line_nan_inf_skip.gd | 206 +++++++++ .../nan_or_inf/test_line_nan_inf_skip.gd.uid | 1 + .../nan_or_inf/test_line_nan_inf_skip.tscn | 65 +++ 9 files changed, 1014 insertions(+) create mode 100644 addons/tau-plot/tests/line/linear_scale/test_line_linear.gd create mode 100644 addons/tau-plot/tests/line/linear_scale/test_line_linear.gd.uid create mode 100644 addons/tau-plot/tests/line/linear_scale/test_line_linear.tscn create mode 100644 addons/tau-plot/tests/line/log_scale/test_line_log.gd create mode 100644 addons/tau-plot/tests/line/log_scale/test_line_log.gd.uid create mode 100644 addons/tau-plot/tests/line/log_scale/test_line_log.tscn create mode 100644 addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_skip.gd create mode 100644 addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_skip.gd.uid create mode 100644 addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_skip.tscn diff --git a/addons/tau-plot/tests/line/linear_scale/test_line_linear.gd b/addons/tau-plot/tests/line/linear_scale/test_line_linear.gd new file mode 100644 index 0000000..3acfa70 --- /dev/null +++ b/addons/tau-plot/tests/line/linear_scale/test_line_linear.gd @@ -0,0 +1,172 @@ +@tool +extends Control + +func _ready() -> void: + _setup_test_1() + _setup_test_2() + _setup_test_3() + + +#################################################################################################### +# Test 1 +#################################################################################################### + +func _setup_test_1() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0]) + var y_a := PackedFloat64Array([5.0, 10.0, 8.0, 12.0]) + var y_b := PackedFloat64Array([25.0, 28.0, 23.0, 7.0]) + var y_c := PackedFloat64Array([30.0, 28.0, 39.0, 67.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b, y_c]) + + %TestPlot1.title = "SHARED_X + CONTINUOUS" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.INDEPENDENT + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] + + %TestPlot1.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 2 +#################################################################################################### + +func _setup_test_2() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x_a := PackedFloat64Array([10.0, 11.0, 12.0, 13.0]) + var x_b := PackedFloat64Array([9.0, 10.3, 12.7, 14.1]) + var x_c := PackedFloat64Array([9.2, 10.1, 10.5, 10.9]) + + var y_a := PackedFloat64Array([5.0, 10.0, 8.0, 12.0]) + var y_b := PackedFloat64Array([25.0, 28.0, 23.0, 7.0]) + var y_c := PackedFloat64Array([30.0, 28.0, 39.0, 67.0]) + + var dataset := TauPlot.Dataset.make_per_series_x_continuous(series_names, [x_a, x_b, x_c], [y_a, y_b, y_c]) + + %TestPlot2.title = "PER_SERIES_X + CONTINUOUS" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.INDEPENDENT + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] + + %TestPlot2.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 3 +#################################################################################################### + +func _setup_test_3() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedStringArray(["One", "Two", "Three", "Four"]) + var y_a := PackedFloat64Array([5.0, 10.0, 8.0, 12.0]) + var y_b := PackedFloat64Array([25.0, 28.0, 23.0, 7.0]) + var y_c := PackedFloat64Array([30.0, 28.0, 39.0, 67.0]) + + var dataset := TauPlot.Dataset.make_shared_x_categorical(series_names, x, [y_a, y_b, y_c]) + + %TestPlot3.title = "SHARED_X + CATEGORICAL" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CATEGORICAL + x_axis.scale = TauAxisConfig.Scale.LINEAR + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.INDEPENDENT + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] + + %TestPlot3.plot_xy(dataset, config, bindings) diff --git a/addons/tau-plot/tests/line/linear_scale/test_line_linear.gd.uid b/addons/tau-plot/tests/line/linear_scale/test_line_linear.gd.uid new file mode 100644 index 0000000..046a12b --- /dev/null +++ b/addons/tau-plot/tests/line/linear_scale/test_line_linear.gd.uid @@ -0,0 +1 @@ +uid://d0b7xei1hadvi diff --git a/addons/tau-plot/tests/line/linear_scale/test_line_linear.tscn b/addons/tau-plot/tests/line/linear_scale/test_line_linear.tscn new file mode 100644 index 0000000..02a14f9 --- /dev/null +++ b/addons/tau-plot/tests/line/linear_scale/test_line_linear.tscn @@ -0,0 +1,55 @@ +[gd_scene load_steps=3 format=3 uid="uid://bymhr4sv6e8pv"] + +[ext_resource type="Script" uid="uid://d0b7xei1hadvi" path="res://addons/tau-plot/tests/line/linear_scale/test_line_linear.gd" id="1_7re6l"] +[ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_67aov"] + +[node name="Bars" type="VBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_7re6l") + +[node name="Title" type="Label" parent="."] +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +text = "Linear scale feature" +horizontal_alignment = 1 + +[node name="GridContainer" type="GridContainer" parent="."] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/h_separation = 0 +theme_override_constants/v_separation = 0 +columns = 3 + +[node name="TestPlot1" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_67aov") +title = "SHARED_X + CONTINUOUS" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot2" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_67aov") +title = "PER_SERIES_X + CONTINUOUS" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot3" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_67aov") +title = "SHARED_X + CATEGORICAL" +metadata/_custom_type_script = "uid://dnhvsf2wip771" diff --git a/addons/tau-plot/tests/line/log_scale/test_line_log.gd b/addons/tau-plot/tests/line/log_scale/test_line_log.gd new file mode 100644 index 0000000..975b50b --- /dev/null +++ b/addons/tau-plot/tests/line/log_scale/test_line_log.gd @@ -0,0 +1,421 @@ +@tool +extends Control + +var _timer: Timer = null +var _t: float = 0.0 + +var _datasets: Array[TauPlot.Dataset] = [] +var _state: Array[Dictionary] = [] + +func _ready() -> void: + _create_timer() + _setup_all_tests() + _timer.start() + + +func _create_timer() -> void: + _timer = Timer.new() + _timer.one_shot = false + _timer.wait_time = 0.016 + _timer.timeout.connect(_on_tick) + add_child(_timer) + + +func _setup_all_tests() -> void: + _datasets.clear() + _state.clear() + + _setup_test_1() + _setup_test_2() + _setup_test_3() + _setup_test_4() + _setup_test_5() + _setup_test_6() + _setup_test_7() + + +func _on_tick() -> void: + _t += _timer.wait_time + + _step_test_1() + _step_test_2() + _step_test_3() + _step_test_4() + _step_test_5() + _step_test_6() + _step_test_7() + +#################################################################################################### +# Test 1 +#################################################################################################### + +func _setup_test_1() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x_a := PackedFloat64Array([0.1, 0.5, 1.0, 5.0, 10.0, 50.0, 100.0, 500.0, 1000.0]) + var x_b := PackedFloat64Array([0.1, 0.5, 1.0, 5.0, 10.0, 50.0, 100.0, 500.0, 1000.0].map(func (x) -> float: return 1.1*x)) + var x_c := PackedFloat64Array([0.1, 0.5, 1.0, 5.0, 10.0, 50.0, 100.0, 500.0, 1000.0].map(func (x) -> float: return 1.2*x)) + + var y_a := PackedFloat64Array() + var y_b := PackedFloat64Array() + var y_c := PackedFloat64Array() + y_a.resize(x_a.size()) + y_b.resize(x_b.size()) + y_c.resize(x_c.size()) + + for i in range(x_a.size()): + y_a[i] = 5.0 + 2.5 * sin(float(i) * 0.3) + for i in range(x_b.size()): + y_b[i] = 4.0 + 2.0 * cos(float(i) * 0.5) + for i in range(x_c.size()): + y_c[i] = 3.0 + 2.5 * sin(float(i) * 0.7) + + var dataset := TauPlot.Dataset.make_per_series_x_continuous(series_names, [x_a, x_b, x_c], [y_a, y_b, y_c]) + _datasets.append(dataset) + _state.append({}) + + %TestPlot1.title = "[LOG X, LINEAR Y] Independent - Animated Y values in [2.5, 7.5]" + + var x_axis := TauAxisConfig.new() + x_axis.title = "X (log scale)" + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LOGARITHMIC + + var y_axis := TauAxisConfig.new() + y_axis.title = "Y (linear)" + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.range_override_enabled = true + y_axis.min_override = 0.01 + y_axis.max_override = 8.00 + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.INDEPENDENT + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] + + %TestPlot1.plot_xy(dataset, config, bindings) + + +func _step_test_1() -> void: + var dataset := _datasets[0] + + dataset.begin_batch() + var ids := dataset.get_series_ids() + for s_i in range(ids.size()): + var series_id := ids[s_i] + var n := dataset.get_series_sample_count(series_id) + + var values := PackedFloat64Array() + values.resize(n) + var phase := float(s_i) * 0.7 + for i in range(values.size()): + values[i] = 5.0 + 2.5 * sin(_t + phase + float(i) * 0.5) + + dataset.set_series_y_slice(series_id, 0, values) + dataset.end_batch() + +#################################################################################################### +# Test 2 +#################################################################################################### + +func _setup_test_2() -> void: + var series_names := PackedStringArray(["Series A", "Series B"]) + var x_a := PackedFloat64Array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]) + var x_b := PackedFloat64Array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0].map(func (x) -> float: return 1.1*x)) + + var y_a := PackedFloat64Array() + var y_b := PackedFloat64Array() + y_a.resize(x_a.size()) + y_b.resize(x_b.size()) + + for i in range(x_a.size()): + y_a[i] = 0.1 * pow(10.0, float(i) * 0.4) + for i in range(x_b.size()): + y_b[i] = 0.05 * pow(10.0, float(i) * 0.4) + + var dataset := TauPlot.Dataset.make_per_series_x_continuous(series_names, [x_a, x_b], [y_a, y_b]) + _datasets.append(dataset) + _state.append({}) + + %TestPlot2.title = "[LINEAR X, LOG Y] Independent - Animated Y values (exponential growth)" + + var x_axis := TauAxisConfig.new() + x_axis.title = "X (linear)" + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + + var y_axis := TauAxisConfig.new() + y_axis.title = "Y (log scale)" + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LOGARITHMIC + y_axis.include_zero_in_domain = false + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.INDEPENDENT + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b] + + %TestPlot2.plot_xy(dataset, config, bindings) + + +func _step_test_2() -> void: + var dataset := _datasets[1] + + dataset.begin_batch() + var ids := dataset.get_series_ids() + for s_i in range(ids.size()): + var series_id := ids[s_i] + var n := dataset.get_series_sample_count(series_id) + + var values := PackedFloat64Array() + values.resize(n) + var base_multiplier := 0.1 if s_i == 0 else 0.05 + var phase := float(s_i) * 1.2 + + for i in range(n): + var osc := 1.0 + 0.5 * sin(_t + phase) + values[i] = base_multiplier * pow(10.0, float(i) * 0.4) * osc + + dataset.set_series_y_slice(series_id, 0, values) + dataset.end_batch() + +#################################################################################################### +# Test 3 +#################################################################################################### + +func _setup_test_3() -> void: + var series_names := PackedStringArray(["Series A", "Series B"]) + var x_a := PackedFloat64Array([0.05, 0.1, 0.5, 1.0, 5.0, 10.0, 50.0, 100.0, 500.0, 1000.0]) + var x_b := PackedFloat64Array([0.05, 0.1, 0.5, 1.0, 5.0, 10.0, 50.0, 100.0, 500.0, 1000.0].map(func (x) -> float: return 1.1*x)) + + var y_a := PackedFloat64Array() + var y_b := PackedFloat64Array() + y_a.resize(x_a.size()) + y_b.resize(x_b.size()) + + for i in range(x_a.size()): + y_a[i] = pow(x_a[i], 0.5) + for i in range(x_b.size()): + y_b[i] = pow(x_b[i], 0.25) + + var dataset := TauPlot.Dataset.make_per_series_x_continuous(series_names, [x_a, x_b], [y_a, y_b]) + _datasets.append(dataset) + _state.append({}) + + %TestPlot3.title = "[LOG X, LOG Y] Independent - Animated Y values (power laws => linear in log-log)" + + var x_axis := TauAxisConfig.new() + x_axis.title = "X (log scale)" + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LOGARITHMIC + + var y_axis := TauAxisConfig.new() + y_axis.title = "Y (log scale)" + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LOGARITHMIC + y_axis.include_zero_in_domain = false + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.INDEPENDENT + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b] + + %TestPlot3.plot_xy(dataset, config, bindings) + + +func _step_test_3() -> void: + var dataset := _datasets[2] + + # Animate by varying the power law exponent slightly + dataset.begin_batch() + var ids := dataset.get_series_ids() + for s_i in range(ids.size()): + var series_id := ids[s_i] + + var n := dataset.get_series_sample_count(series_id) + var values := PackedFloat64Array() + values.resize(n) + var base_exponent := 2.0 if s_i == 0 else 1.5 + var exponent := base_exponent + 0.3 * sin(_t + float(s_i)) + + for i in range(n): + var x_val := float(dataset.get_series_x(series_id, i)) + values[i] = pow(x_val, exponent) + + dataset.set_series_y_slice(series_id, 0, values) + dataset.end_batch() + +#################################################################################################### +# Test 4 +#################################################################################################### + +func _setup_test_4() -> void: + _datasets.append(null) + _state.append({}) + + +func _step_test_4() -> void: + pass + +#################################################################################################### +# Test 5 +#################################################################################################### + +func _setup_test_5() -> void: + _datasets.append(null) + _state.append({}) + + +func _step_test_5() -> void: + pass + +#################################################################################################### +# Test 6 +#################################################################################################### + +func _setup_test_6() -> void: + _datasets.append(null) + _state.append({}) + + +func _step_test_6() -> void: + pass + +#################################################################################################### +# Test 7 +#################################################################################################### + +func _setup_test_7() -> void: + var series_names := PackedStringArray(["Stream A", "Stream B"]) + # Start with a few initial points + var x := PackedFloat64Array([1.0, 2.0, 5.0]) + + var y_a := PackedFloat64Array([3.0, 5.0, 4.0]) + var y_b := PackedFloat64Array([2.0, 4.0, 3.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b], 64) + _datasets.append(dataset) + _state.append({ + "next_x": 10.0, + "time_since_append_s": 0.0, + "append_period_s": 0.5, + }) + + %TestPlot7.title = "[LOG X] Streaming - New samples every 0.5s" + + var x_axis := TauAxisConfig.new() + x_axis.title = "X (log scale, growing)" + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LOGARITHMIC + + var y_axis := TauAxisConfig.new() + y_axis.title = "Y (linear)" + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.INDEPENDENT + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b] + + %TestPlot7.plot_xy(dataset, config, bindings) + + +func _step_test_7() -> void: + var dataset := _datasets[6] + var st := _state[6] + + st["time_since_append_s"] = float(st["time_since_append_s"]) + _timer.wait_time + if float(st["time_since_append_s"]) < float(st["append_period_s"]): + return + st["time_since_append_s"] = 0.0 + + var next_x := float(st["next_x"]) + + # Append new sample at increasing X (logarithmic progression) + var ys := PackedFloat64Array([ + 4.0 + 2.0 * sin(_t), + 3.0 + 2.0 * cos(_t * 1.5) + ]) + + dataset.append_shared_sample(next_x, ys) + + # Next X grows exponentially (log-uniform spacing) + st["next_x"] = next_x * 1.5 diff --git a/addons/tau-plot/tests/line/log_scale/test_line_log.gd.uid b/addons/tau-plot/tests/line/log_scale/test_line_log.gd.uid new file mode 100644 index 0000000..6bb7d0e --- /dev/null +++ b/addons/tau-plot/tests/line/log_scale/test_line_log.gd.uid @@ -0,0 +1 @@ +uid://b5nqjwb6flndn diff --git a/addons/tau-plot/tests/line/log_scale/test_line_log.tscn b/addons/tau-plot/tests/line/log_scale/test_line_log.tscn new file mode 100644 index 0000000..30b6738 --- /dev/null +++ b/addons/tau-plot/tests/line/log_scale/test_line_log.tscn @@ -0,0 +1,92 @@ +[gd_scene load_steps=3 format=3 uid="uid://rct3vbvdtmly"] + +[ext_resource type="Script" uid="uid://b5nqjwb6flndn" path="res://addons/tau-plot/tests/line/log_scale/test_line_log.gd" id="1_k4moc"] +[ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_erpj8"] + +[node name="Bars" type="VBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_k4moc") + +[node name="Title" type="Label" parent="."] +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +text = "Logarithmic scale feature" +horizontal_alignment = 1 + +[node name="GridContainer" type="GridContainer" parent="."] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/h_separation = 0 +theme_override_constants/v_separation = 0 +columns = 3 + +[node name="TestPlot1" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_erpj8") +title = "[LOG X, LINEAR Y] Independent - Animated Y values in [2.5, 7.5]" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot2" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_erpj8") +title = "[LINEAR X, LOG Y] Independent - Animated Y values (exponential growth)" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot3" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_erpj8") +title = "[LOG X, LOG Y] Independent - Animated Y values (power laws => linear in log-log)" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot4" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_erpj8") +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot5" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_erpj8") +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot6" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_erpj8") +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot7" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_erpj8") +title = "[LOG X] Streaming - New samples every 0.5s" +metadata/_custom_type_script = "uid://dnhvsf2wip771" diff --git a/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_skip.gd b/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_skip.gd new file mode 100644 index 0000000..4258a41 --- /dev/null +++ b/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_skip.gd @@ -0,0 +1,206 @@ +@tool +extends Control + +func _ready() -> void: + _setup_test_1() + _setup_test_2() + _setup_test_3() + _setup_test_4() + +#################################################################################################### +# Test 1 +#################################################################################################### + +func _setup_test_1() -> void: + var series_names := PackedStringArray(["A", "B"]) + var x := PackedFloat64Array([0.0, NAN, 1.0, INF, 2.0, 2.5]) + var y_a := PackedFloat64Array([1.0, 2.0, 1.4, 2.2, 1.1, 1.8]) + var y_b := PackedFloat64Array([1.5, 1.0, 1.6, 2.4, 0.5, 1.2]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b]) + + %TestPlot1.title = "[SHARED_X] 2nd and 4th x values are invalid" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.tick_count_preferred = 8 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.include_zero_in_domain = true + + var line_config := TauLineConfig.new() + + var pane_config := TauPaneConfig.new() + pane_config.y_left_axis = y_axis + pane_config.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane_config] + config.style.series_alpha = 0.8 + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b] + + %TestPlot1.plot_xy(dataset, config, bindings) + +#################################################################################################### +# Test 2 +#################################################################################################### + +func _setup_test_2() -> void: + var series_names := PackedStringArray(["A", "B"]) + var x_a := PackedFloat64Array([0.0, NAN, 1.0, INF, 2.0, 2.5]) + var x_b := PackedFloat64Array([0.25, 0.75, 1.25, 1.75, 2.25]) + var y_a := PackedFloat64Array([1.0, 2.0, 1.4, 2.2, 1.1, 1.8]) + var y_b := PackedFloat64Array([1.5, 1.0, 1.6, 2.4, 0.5]) + + var dataset := TauPlot.Dataset.make_per_series_x_continuous(series_names, [x_a, x_b], [y_a, y_b]) + + %TestPlot2.title = "[PER_SERIES_X] 2nd and 4th x values are invalid for series A" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.tick_count_preferred = 8 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.include_zero_in_domain = true + + var line_config := TauLineConfig.new() + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + config.style.series_alpha = 0.8 + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b] + + %TestPlot2.plot_xy(dataset, config, bindings) + +#################################################################################################### +# Test 5 +#################################################################################################### + +func _setup_test_3() -> void: + var series_names := PackedStringArray(["A", "B"]) + var x := PackedFloat64Array([0.0, 0.5, 1.0, 1.5, 2.0, 2.5]) + var y_a := PackedFloat64Array([1.0, NAN, 1.4, INF, 1.1, 1.8]) + var y_b := PackedFloat64Array([1.5, 1.0, 1.6, 2.4, 0.5, 1.2]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b]) + + %TestPlot3.title = "[SHARED_X] 2nd and 4th y values are invalid for series A" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.tick_count_preferred = 8 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.include_zero_in_domain = true + + var line_config := TauLineConfig.new() + + var pane_config := TauPaneConfig.new() + pane_config.y_left_axis = y_axis + pane_config.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane_config] + config.style.series_alpha = 0.8 + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b] + + %TestPlot3.plot_xy(dataset, config, bindings) + +#################################################################################################### +# Test 4 +#################################################################################################### + +func _setup_test_4() -> void: + var series_names := PackedStringArray(["A", "B"]) + var x_a := PackedFloat64Array([0.0, 0.5, 1.0, 1.5, 2.0, 2.5]) + var x_b := PackedFloat64Array([0.25, 0.75, 1.25, 1.75, 2.25]) + var y_a := PackedFloat64Array([1.0, NAN, 1.4, INF, 1.1, 1.8]) + var y_b := PackedFloat64Array([1.5, 1.0, 1.6, 2.4, 0.5]) + + var dataset := TauPlot.Dataset.make_per_series_x_continuous(series_names, [x_a, x_b], [y_a, y_b]) + + %TestPlot4.title = "[PER_SERIES_X] 2nd and 4th y values are invalid for series A" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.tick_count_preferred = 8 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.include_zero_in_domain = true + + var line_config := TauLineConfig.new() + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + config.style.series_alpha = 0.8 + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b] + + %TestPlot4.plot_xy(dataset, config, bindings) diff --git a/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_skip.gd.uid b/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_skip.gd.uid new file mode 100644 index 0000000..92658b8 --- /dev/null +++ b/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_skip.gd.uid @@ -0,0 +1 @@ +uid://bokuk27l1c8du diff --git a/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_skip.tscn b/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_skip.tscn new file mode 100644 index 0000000..5759e00 --- /dev/null +++ b/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_skip.tscn @@ -0,0 +1,65 @@ +[gd_scene load_steps=3 format=3 uid="uid://cx0yem6x7nexc"] + +[ext_resource type="Script" uid="uid://bokuk27l1c8du" path="res://addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_skip.gd" id="1_e73yd"] +[ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_5c7d4"] + +[node name="Scatters" type="VBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_e73yd") + +[node name="Title" type="Label" parent="."] +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +text = "Test nan and inf values with GapPolicy.SKIP" +horizontal_alignment = 1 + +[node name="GridContainer" type="GridContainer" parent="."] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/h_separation = 0 +theme_override_constants/v_separation = 0 +columns = 2 + +[node name="TestPlot1" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_5c7d4") +title = "[SHARED_X] 2nd and 4th x values are invalid" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot2" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_5c7d4") +title = "[PER_SERIES_X] 2nd and 4th x values are invalid for series A" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot3" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_5c7d4") +title = "[SHARED_X] 2nd and 4th y values are invalid for series A" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot4" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_5c7d4") +title = "[PER_SERIES_X] 2nd and 4th y values are invalid for series A" +metadata/_custom_type_script = "uid://dnhvsf2wip771" From 1025c0cbaaf64b994a1cf0a7c02e0581d5d53da4 Mon Sep 17 00:00:00 2001 From: ze2j <46556066+ze2j@users.noreply.github.com> Date: Sat, 25 Apr 2026 11:42:37 +0200 Subject: [PATCH 04/23] Implement GapPolicy --- addons/tau-plot/plot/xy/line/line_config.gd | 21 + addons/tau-plot/plot/xy/line/line_renderer.gd | 28 +- ...t_line_log.gd => test_line_logarithmic.gd} | 0 ...og.gd.uid => test_line_logarithmic.gd.uid} | 0 ...ne_log.tscn => test_line_logarithmic.tscn} | 2 +- .../nan_or_inf/test_line_nan_inf_bridge.gd | 416 ++++++++++++++++++ .../test_line_nan_inf_bridge.gd.uid | 1 + .../nan_or_inf/test_line_nan_inf_bridge.tscn | 105 +++++ .../line/nan_or_inf/test_line_nan_inf_skip.gd | 248 ++++++++++- .../nan_or_inf/test_line_nan_inf_skip.tscn | 50 ++- 10 files changed, 837 insertions(+), 34 deletions(-) rename addons/tau-plot/tests/line/log_scale/{test_line_log.gd => test_line_logarithmic.gd} (100%) rename addons/tau-plot/tests/line/log_scale/{test_line_log.gd.uid => test_line_logarithmic.gd.uid} (100%) rename addons/tau-plot/tests/line/log_scale/{test_line_log.tscn => test_line_logarithmic.tscn} (97%) create mode 100644 addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_bridge.gd create mode 100644 addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_bridge.gd.uid create mode 100644 addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_bridge.tscn diff --git a/addons/tau-plot/plot/xy/line/line_config.gd b/addons/tau-plot/plot/xy/line/line_config.gd index 6cfb4c8..e09bcae 100644 --- a/addons/tau-plot/plot/xy/line/line_config.gd +++ b/addons/tau-plot/plot/xy/line/line_config.gd @@ -23,6 +23,25 @@ enum LineMode const StackedNormalization = preload("res://addons/tau-plot/plot/xy/stacked_normalization.gd").StackedNormalization @export var stacked_normalization: StackedNormalization = StackedNormalization.NONE +## Strategy applied when the curve encounters a sample with a NaN or infinite +## X or Y value (or a value forbidden by the active axis scale, such as a +## non-positive value on a logarithmic axis). +## +## SKIP breaks the polyline at the invalid sample. The runs on each side of +## the gap are drawn as independent contiguous polylines. +## +## BRIDGE drops the invalid sample from the sequence and connects the valid +## sample before it directly to the valid sample after it, so the polyline +## stays continuous across the gap. +## +## This property is visual-only and does not affect layout or domain. +enum GapPolicy +{ + SKIP, ## Break the polyline at invalid samples. + BRIDGE ## Drop invalid samples and connect the surrounding valid samples. +} +@export var gap_policy: GapPolicy = GapPolicy.SKIP + ## Maximum pixel distance from the cursor to a sample position for the sample ## to be considered a hover hit. ## @@ -69,6 +88,8 @@ func is_equal_to(p_other: TauPaneOverlayConfig) -> bool: return false if stacked_normalization != other.stacked_normalization: return false + if gap_policy != other.gap_policy: + return false if hover_max_distance_px != other.hover_max_distance_px: return false diff --git a/addons/tau-plot/plot/xy/line/line_renderer.gd b/addons/tau-plot/plot/xy/line/line_renderer.gd index 482a347..31f1a4d 100644 --- a/addons/tau-plot/plot/xy/line/line_renderer.gd +++ b/addons/tau-plot/plot/xy/line/line_renderer.gd @@ -15,9 +15,12 @@ const LineVisualAttributes := preload("res://addons/tau-plot/plot/xy/line/line_v # draw_polyline() call per series. # # Runtime behavior: -# - NaN and Inf X or Y values break the polyline (SKIP gap policy). -# - Logarithmic Y scales: y <= 0 breaks the polyline. -# - Logarithmic X scales: x <= 0 breaks the polyline. +# - NaN and Inf X or Y values are treated according to TauLineConfig.gap_policy. +# - Logarithmic Y scales: y <= 0 is treated as invalid. +# - Logarithmic X scales: x <= 0 is treated as invalid. +# - GapPolicy.SKIP breaks the polyline at every invalid sample. +# - GapPolicy.BRIDGE drops invalid samples and keeps the polyline contiguous, +# so the surrounding valid samples are connected directly. # # LineValidator is expected to enforce binding-level typing constraints. class LineRenderer extends Control: @@ -118,11 +121,13 @@ class LineRenderer extends Control: _draw_series_independent(series_index, width_px) - # Draws a single series as one or more polyline runs, respecting SKIP - # gap policy. Run emission follows these rules: + # Draws a single series as one or more polyline runs, respecting the + # active gap policy. Run emission follows these rules: # - A valid sample is appended to the current run. # - An invalid sample (NaN/Inf X or Y, or a value forbidden by the - # active axis scale) flushes the current run and starts a new one. + # active axis scale) is handled according to gap_policy: + # - SKIP flushes the current run and starts a new one. + # - BRIDGE drops the sample and keeps appending into the same run. # - A run of fewer than two points is discarded (no polyline). func _draw_series_independent(p_series_index: int, p_width_px: float) -> void: var x_cfg := _get_x_axis_config() @@ -137,6 +142,7 @@ class LineRenderer extends Control: var global_series_index := _get_global_series_index(p_series_index) var color := _resolve_series_color(global_series_index) var y_axis_id := _get_y_axis_id_for_series(series_id) + var bridge: bool = _line_config.gap_policy == TauLineConfig.GapPolicy.BRIDGE var run := PackedVector2Array() @@ -146,12 +152,14 @@ class LineRenderer extends Control: for i in range(sample_count): var x_value: float = float(_dataset.get_shared_x(i)) if is_shared_x else float(_dataset.get_series_x(series_id, i)) if is_nan(x_value) or is_inf(x_value) or not _is_x_value_valid_for_scale(x_value): - run = _flush_run(run, color, p_width_px) + if not bridge: + run = _flush_run(run, color, p_width_px) continue var y_value := _dataset.get_series_y(series_id, i) if is_nan(y_value) or is_inf(y_value) or not _is_y_value_valid_for_scale(series_id, y_value): - run = _flush_run(run, color, p_width_px) + if not bridge: + run = _flush_run(run, color, p_width_px) continue var x_px := _layout.map_x_to_px(_pane_index, x_value) @@ -167,6 +175,7 @@ class LineRenderer extends Control: var global_series_index := _get_global_series_index(p_series_index) var color := _resolve_series_color(global_series_index) var y_axis_id := _get_y_axis_id_for_series(series_id) + var bridge: bool = _line_config.gap_policy == TauLineConfig.GapPolicy.BRIDGE var run := PackedVector2Array() var sample_count := _dataset.get_series_sample_count(series_id) @@ -174,7 +183,8 @@ class LineRenderer extends Control: for cat_idx in range(sample_count): var y_value := _dataset.get_series_y(series_id, cat_idx) if is_nan(y_value) or is_inf(y_value) or not _is_y_value_valid_for_scale(series_id, y_value): - run = _flush_run(run, color, p_width_px) + if not bridge: + run = _flush_run(run, color, p_width_px) continue var x_px := _layout.map_x_category_center_to_px(_pane_index, cat_idx) diff --git a/addons/tau-plot/tests/line/log_scale/test_line_log.gd b/addons/tau-plot/tests/line/log_scale/test_line_logarithmic.gd similarity index 100% rename from addons/tau-plot/tests/line/log_scale/test_line_log.gd rename to addons/tau-plot/tests/line/log_scale/test_line_logarithmic.gd diff --git a/addons/tau-plot/tests/line/log_scale/test_line_log.gd.uid b/addons/tau-plot/tests/line/log_scale/test_line_logarithmic.gd.uid similarity index 100% rename from addons/tau-plot/tests/line/log_scale/test_line_log.gd.uid rename to addons/tau-plot/tests/line/log_scale/test_line_logarithmic.gd.uid diff --git a/addons/tau-plot/tests/line/log_scale/test_line_log.tscn b/addons/tau-plot/tests/line/log_scale/test_line_logarithmic.tscn similarity index 97% rename from addons/tau-plot/tests/line/log_scale/test_line_log.tscn rename to addons/tau-plot/tests/line/log_scale/test_line_logarithmic.tscn index 30b6738..280d768 100644 --- a/addons/tau-plot/tests/line/log_scale/test_line_log.tscn +++ b/addons/tau-plot/tests/line/log_scale/test_line_logarithmic.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=3 format=3 uid="uid://rct3vbvdtmly"] -[ext_resource type="Script" uid="uid://b5nqjwb6flndn" path="res://addons/tau-plot/tests/line/log_scale/test_line_log.gd" id="1_k4moc"] +[ext_resource type="Script" uid="uid://b5nqjwb6flndn" path="res://addons/tau-plot/tests/line/log_scale/test_line_logarithmic.gd" id="1_k4moc"] [ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_erpj8"] [node name="Bars" type="VBoxContainer"] diff --git a/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_bridge.gd b/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_bridge.gd new file mode 100644 index 0000000..ca687e2 --- /dev/null +++ b/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_bridge.gd @@ -0,0 +1,416 @@ +@tool +extends Control + +func _ready() -> void: + _setup_test_1() + _setup_test_2() + _setup_test_3() + _setup_test_4() + _setup_test_5() + _setup_test_6() + _setup_test_7() + _setup_test_8() + +#################################################################################################### +# Test 1 +#################################################################################################### + +func _setup_test_1() -> void: + var series_names := PackedStringArray(["A", "B"]) + var x := PackedFloat64Array([0.0, 0.5, NAN, INF, 2.0, 2.5, 3.0]) + var y_a := PackedFloat64Array([1.0, 2.0, 4.0, 4.5, 4.0, 2.0, 1.0]) + var y_b := PackedFloat64Array([2.0, 3.0, 5.0, 5.5, 5.0, 3.0, 2.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b]) + + %TestPlot1.title = "[SHARED_X] 3rd and 4th x values are invalid" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.tick_count_preferred = 8 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.include_zero_in_domain = true + + var line_config := TauLineConfig.new() + line_config.gap_policy = TauLineConfig.GapPolicy.BRIDGE + + var pane_config := TauPaneConfig.new() + pane_config.y_left_axis = y_axis + pane_config.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane_config] + config.style.series_alpha = 0.8 + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b] + + %TestPlot1.plot_xy(dataset, config, bindings) + +#################################################################################################### +# Test 2 +#################################################################################################### + +func _setup_test_2() -> void: + var series_names := PackedStringArray(["A", "B"]) + var x_a := PackedFloat64Array([0.0, 0.5, NAN, INF, 2.0, 2.5, 3.0]) + var x_b := PackedFloat64Array([0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0]) + var y_a := PackedFloat64Array([1.0, 2.0, 4.0, 4.5, 4.0, 2.0, 1.0]) + var y_b := PackedFloat64Array([2.0, 3.0, 5.0, 5.5, 5.0, 3.0, 2.0]) + + var dataset := TauPlot.Dataset.make_per_series_x_continuous(series_names, [x_a, x_b], [y_a, y_b]) + + %TestPlot2.title = "[PER_SERIES_X] 3rd and 4th x values are invalid for series A" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.tick_count_preferred = 8 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.include_zero_in_domain = true + + var line_config := TauLineConfig.new() + line_config.gap_policy = TauLineConfig.GapPolicy.BRIDGE + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + config.style.series_alpha = 0.8 + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b] + + %TestPlot2.plot_xy(dataset, config, bindings) + +#################################################################################################### +# Test 3 +#################################################################################################### + +func _setup_test_3() -> void: + var series_names := PackedStringArray(["A", "B"]) + var x := PackedFloat64Array([0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0]) + var y_a := PackedFloat64Array([1.0, 2.0, INF, NAN, 4.0, 2.0, 1.0]) + var y_b := PackedFloat64Array([2.0, 3.0, 5.0, 5.5, 5.0, 3.0, 2.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b]) + + %TestPlot3.title = "[SHARED_X] 3rd and 4th y values are invalid for series A" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.tick_count_preferred = 8 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.include_zero_in_domain = true + + var line_config := TauLineConfig.new() + line_config.gap_policy = TauLineConfig.GapPolicy.BRIDGE + + var pane_config := TauPaneConfig.new() + pane_config.y_left_axis = y_axis + pane_config.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane_config] + config.style.series_alpha = 0.8 + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b] + + %TestPlot3.plot_xy(dataset, config, bindings) + +#################################################################################################### +# Test 4 +#################################################################################################### + +func _setup_test_4() -> void: + var series_names := PackedStringArray(["A", "B"]) + var x_a := PackedFloat64Array([0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0]) + var x_b := PackedFloat64Array([0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0]) + var y_a := PackedFloat64Array([1.0, 2.0, INF, NAN, 4.0, 2.0, 1.0]) + var y_b := PackedFloat64Array([2.0, 3.0, 5.0, 5.5, 5.0, 3.0, 2.0]) + + var dataset := TauPlot.Dataset.make_per_series_x_continuous(series_names, [x_a, x_b], [y_a, y_b]) + + %TestPlot4.title = "[PER_SERIES_X] 3rd and 4th y values are invalid for series A" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.tick_count_preferred = 8 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.include_zero_in_domain = true + + var line_config := TauLineConfig.new() + line_config.gap_policy = TauLineConfig.GapPolicy.BRIDGE + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + config.style.series_alpha = 0.8 + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b] + + %TestPlot4.plot_xy(dataset, config, bindings) + +#################################################################################################### +# Test 5 +#################################################################################################### + +func _setup_test_5() -> void: + var series_names := PackedStringArray(["A", "B"]) + var x := PackedFloat64Array([pow(10, -3), pow(10, -2), -pow(10, -1), NAN, pow(10, 1), pow(10, 2), pow(10, 3)]) + var y_a := PackedFloat64Array([1.0, 2.0, 4.0, 4.5, 4.0, 2.0, 1.0]) + var y_b := PackedFloat64Array([2.0, 3.0, 5.0, 5.5, 5.0, 3.0, 2.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b]) + + %TestPlot5.title = "[LOG][SHARED_X] 3rd and 4th x values are invalid" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LOGARITHMIC + x_axis.tick_count_preferred = 8 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.include_zero_in_domain = true + + var line_config := TauLineConfig.new() + line_config.gap_policy = TauLineConfig.GapPolicy.BRIDGE + + var pane_config := TauPaneConfig.new() + pane_config.y_left_axis = y_axis + pane_config.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane_config] + config.style.series_alpha = 0.8 + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b] + + %TestPlot5.plot_xy(dataset, config, bindings) + +#################################################################################################### +# Test 6 +#################################################################################################### + +func _setup_test_6() -> void: + var series_names := PackedStringArray(["A", "B"]) + var x_a := PackedFloat64Array([pow(10, -3), pow(10, -2), -pow(10, -1), INF, pow(10, 1), pow(10, 2), pow(10, 3)]) + var x_b := PackedFloat64Array([pow(10, -3), pow(10, -2), pow(10, -1), pow(10, 0), pow(10, 1), pow(10, 2), pow(10, 3)]) + var y_a := PackedFloat64Array([1.0, 2.0, 4.0, 4.5, 4.0, 2.0, 1.0]) + var y_b := PackedFloat64Array([2.0, 3.0, 5.0, 5.5, 5.0, 3.0, 2.0]) + + var dataset := TauPlot.Dataset.make_per_series_x_continuous(series_names, [x_a, x_b], [y_a, y_b]) + + %TestPlot6.title = "[LOG][PER_SERIES_X] 3rd and 4th x values are invalid for series A" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LOGARITHMIC + x_axis.tick_count_preferred = 8 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.include_zero_in_domain = true + + var line_config := TauLineConfig.new() + line_config.gap_policy = TauLineConfig.GapPolicy.BRIDGE + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + config.style.series_alpha = 0.8 + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b] + + %TestPlot6.plot_xy(dataset, config, bindings) + +#################################################################################################### +# Test 7 +#################################################################################################### + +func _setup_test_7() -> void: + var series_names := PackedStringArray(["A", "B"]) + var x := PackedFloat64Array([0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0]) + var y_a := PackedFloat64Array([pow(10, 1), pow(10, 2), -pow(10, 4), INF, pow(10, 4), pow(10, 2), pow(10, 1)]) + var y_b := PackedFloat64Array([pow(10, 2), pow(10, 3), pow(10, 5), pow(10, 6), pow(10, 5), pow(10, 3), pow(10, 2)]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b]) + + %TestPlot7.title = "[LOG][SHARED_X] 3rd and 4th y values are invalid for series A" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.tick_count_preferred = 8 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LOGARITHMIC + y_axis.include_zero_in_domain = false + + var line_config := TauLineConfig.new() + line_config.gap_policy = TauLineConfig.GapPolicy.BRIDGE + + var pane_config := TauPaneConfig.new() + pane_config.y_left_axis = y_axis + pane_config.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane_config] + config.style.series_alpha = 0.8 + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b] + + %TestPlot7.plot_xy(dataset, config, bindings) + +#################################################################################################### +# Test 8 +#################################################################################################### + +func _setup_test_8() -> void: + var series_names := PackedStringArray(["A", "B"]) + var x_a := PackedFloat64Array([0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0]) + var x_b := PackedFloat64Array([0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0]) + var y_a := PackedFloat64Array([pow(10, 1), pow(10, 2), -pow(10, 4), INF, pow(10, 4), pow(10, 2), pow(10, 1)]) + var y_b := PackedFloat64Array([pow(10, 2), pow(10, 3), pow(10, 5), pow(10, 6), pow(10, 5), pow(10, 3), pow(10, 2)]) + + var dataset := TauPlot.Dataset.make_per_series_x_continuous(series_names, [x_a, x_b], [y_a, y_b]) + + %TestPlot8.title = "[LOG][PER_SERIES_X] 3rd and 4th y values are invalid for series A" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.tick_count_preferred = 8 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LOGARITHMIC + y_axis.include_zero_in_domain = false + + var line_config := TauLineConfig.new() + line_config.gap_policy = TauLineConfig.GapPolicy.BRIDGE + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + config.style.series_alpha = 0.8 + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b] + + %TestPlot8.plot_xy(dataset, config, bindings) diff --git a/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_bridge.gd.uid b/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_bridge.gd.uid new file mode 100644 index 0000000..e9e4af0 --- /dev/null +++ b/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_bridge.gd.uid @@ -0,0 +1 @@ +uid://cym0c8dl4swxu diff --git a/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_bridge.tscn b/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_bridge.tscn new file mode 100644 index 0000000..bc0c825 --- /dev/null +++ b/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_bridge.tscn @@ -0,0 +1,105 @@ +[gd_scene load_steps=3 format=3 uid="uid://d0icnobe65vpy"] + +[ext_resource type="Script" uid="uid://cym0c8dl4swxu" path="res://addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_bridge.gd" id="1_156gm"] +[ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_v7edd"] + +[node name="Scatters" type="VBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_156gm") + +[node name="Title" type="Label" parent="."] +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +text = "Test invalid values with GapPolicy.BRIDGE" +horizontal_alignment = 1 + +[node name="GridContainer" type="GridContainer" parent="."] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/h_separation = 0 +theme_override_constants/v_separation = 0 +columns = 2 + +[node name="TestPlot1" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_v7edd") +title = "[SHARED_X] 2nd and 4th x values are invalid" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot2" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_v7edd") +title = "[PER_SERIES_X] 2nd and 4th x values are invalid for series A" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot3" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_v7edd") +title = "[SHARED_X] 2nd and 4th y values are invalid for series A" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot4" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_v7edd") +title = "[PER_SERIES_X] 2nd and 4th y values are invalid for series A" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot5" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_v7edd") +title = "[LOG][SHARED_X] 2nd and 4th x values are negative" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot6" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_v7edd") +title = "[LOG][PER_SERIES_X] 2nd and 4th x values are negative for series A" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot7" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_v7edd") +title = "[LOG][SHARED_X] 2nd and 4th y values are negative for series A" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot8" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_v7edd") +title = "[LOG][PER_SERIES_X] 2nd and 4th y values are negative for series A" +metadata/_custom_type_script = "uid://dnhvsf2wip771" diff --git a/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_skip.gd b/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_skip.gd index 4258a41..1537c8a 100644 --- a/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_skip.gd +++ b/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_skip.gd @@ -6,6 +6,10 @@ func _ready() -> void: _setup_test_2() _setup_test_3() _setup_test_4() + _setup_test_5() + _setup_test_6() + _setup_test_7() + _setup_test_8() #################################################################################################### # Test 1 @@ -13,13 +17,13 @@ func _ready() -> void: func _setup_test_1() -> void: var series_names := PackedStringArray(["A", "B"]) - var x := PackedFloat64Array([0.0, NAN, 1.0, INF, 2.0, 2.5]) - var y_a := PackedFloat64Array([1.0, 2.0, 1.4, 2.2, 1.1, 1.8]) - var y_b := PackedFloat64Array([1.5, 1.0, 1.6, 2.4, 0.5, 1.2]) + var x := PackedFloat64Array([0.0, 0.5, NAN, INF, 2.0, 2.5, 3.0]) + var y_a := PackedFloat64Array([1.0, 2.0, 4.0, 4.5, 4.0, 2.0, 1.0]) + var y_b := PackedFloat64Array([2.0, 3.0, 5.0, 5.5, 5.0, 3.0, 2.0]) var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b]) - %TestPlot1.title = "[SHARED_X] 2nd and 4th x values are invalid" + %TestPlot1.title = "[SHARED_X] 3rd and 4th x values are invalid" var x_axis := TauAxisConfig.new() x_axis.type = TauAxisConfig.Type.CONTINUOUS @@ -32,6 +36,7 @@ func _setup_test_1() -> void: y_axis.include_zero_in_domain = true var line_config := TauLineConfig.new() + line_config.gap_policy = TauLineConfig.GapPolicy.SKIP var pane_config := TauPaneConfig.new() pane_config.y_left_axis = y_axis @@ -62,14 +67,14 @@ func _setup_test_1() -> void: func _setup_test_2() -> void: var series_names := PackedStringArray(["A", "B"]) - var x_a := PackedFloat64Array([0.0, NAN, 1.0, INF, 2.0, 2.5]) - var x_b := PackedFloat64Array([0.25, 0.75, 1.25, 1.75, 2.25]) - var y_a := PackedFloat64Array([1.0, 2.0, 1.4, 2.2, 1.1, 1.8]) - var y_b := PackedFloat64Array([1.5, 1.0, 1.6, 2.4, 0.5]) + var x_a := PackedFloat64Array([0.0, 0.5, NAN, INF, 2.0, 2.5, 3.0]) + var x_b := PackedFloat64Array([0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0]) + var y_a := PackedFloat64Array([1.0, 2.0, 4.0, 4.5, 4.0, 2.0, 1.0]) + var y_b := PackedFloat64Array([2.0, 3.0, 5.0, 5.5, 5.0, 3.0, 2.0]) var dataset := TauPlot.Dataset.make_per_series_x_continuous(series_names, [x_a, x_b], [y_a, y_b]) - %TestPlot2.title = "[PER_SERIES_X] 2nd and 4th x values are invalid for series A" + %TestPlot2.title = "[PER_SERIES_X] 3rd and 4th x values are invalid for series A" var x_axis := TauAxisConfig.new() x_axis.type = TauAxisConfig.Type.CONTINUOUS @@ -82,6 +87,7 @@ func _setup_test_2() -> void: y_axis.include_zero_in_domain = true var line_config := TauLineConfig.new() + line_config.gap_policy = TauLineConfig.GapPolicy.SKIP var pane := TauPaneConfig.new() pane.y_left_axis = y_axis @@ -107,18 +113,18 @@ func _setup_test_2() -> void: %TestPlot2.plot_xy(dataset, config, bindings) #################################################################################################### -# Test 5 +# Test 3 #################################################################################################### func _setup_test_3() -> void: var series_names := PackedStringArray(["A", "B"]) - var x := PackedFloat64Array([0.0, 0.5, 1.0, 1.5, 2.0, 2.5]) - var y_a := PackedFloat64Array([1.0, NAN, 1.4, INF, 1.1, 1.8]) - var y_b := PackedFloat64Array([1.5, 1.0, 1.6, 2.4, 0.5, 1.2]) + var x := PackedFloat64Array([0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0]) + var y_a := PackedFloat64Array([1.0, 2.0, INF, NAN, 4.0, 2.0, 1.0]) + var y_b := PackedFloat64Array([2.0, 3.0, 5.0, 5.5, 5.0, 3.0, 2.0]) var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b]) - %TestPlot3.title = "[SHARED_X] 2nd and 4th y values are invalid for series A" + %TestPlot3.title = "[SHARED_X] 3rd and 4th y values are invalid for series A" var x_axis := TauAxisConfig.new() x_axis.type = TauAxisConfig.Type.CONTINUOUS @@ -131,6 +137,7 @@ func _setup_test_3() -> void: y_axis.include_zero_in_domain = true var line_config := TauLineConfig.new() + line_config.gap_policy = TauLineConfig.GapPolicy.SKIP var pane_config := TauPaneConfig.new() pane_config.y_left_axis = y_axis @@ -161,14 +168,14 @@ func _setup_test_3() -> void: func _setup_test_4() -> void: var series_names := PackedStringArray(["A", "B"]) - var x_a := PackedFloat64Array([0.0, 0.5, 1.0, 1.5, 2.0, 2.5]) - var x_b := PackedFloat64Array([0.25, 0.75, 1.25, 1.75, 2.25]) - var y_a := PackedFloat64Array([1.0, NAN, 1.4, INF, 1.1, 1.8]) - var y_b := PackedFloat64Array([1.5, 1.0, 1.6, 2.4, 0.5]) + var x_a := PackedFloat64Array([0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0]) + var x_b := PackedFloat64Array([0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0]) + var y_a := PackedFloat64Array([1.0, 2.0, INF, NAN, 4.0, 2.0, 1.0]) + var y_b := PackedFloat64Array([2.0, 3.0, 5.0, 5.5, 5.0, 3.0, 2.0]) var dataset := TauPlot.Dataset.make_per_series_x_continuous(series_names, [x_a, x_b], [y_a, y_b]) - %TestPlot4.title = "[PER_SERIES_X] 2nd and 4th y values are invalid for series A" + %TestPlot4.title = "[PER_SERIES_X] 3rd and 4th y values are invalid for series A" var x_axis := TauAxisConfig.new() x_axis.type = TauAxisConfig.Type.CONTINUOUS @@ -181,6 +188,7 @@ func _setup_test_4() -> void: y_axis.include_zero_in_domain = true var line_config := TauLineConfig.new() + line_config.gap_policy = TauLineConfig.GapPolicy.SKIP var pane := TauPaneConfig.new() pane.y_left_axis = y_axis @@ -204,3 +212,205 @@ func _setup_test_4() -> void: var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b] %TestPlot4.plot_xy(dataset, config, bindings) + +#################################################################################################### +# Test 5 +#################################################################################################### + +func _setup_test_5() -> void: + var series_names := PackedStringArray(["A", "B"]) + var x := PackedFloat64Array([pow(10, -3), pow(10, -2), -pow(10, -1), NAN, pow(10, 1), pow(10, 2), pow(10, 3)]) + var y_a := PackedFloat64Array([1.0, 2.0, 4.0, 4.5, 4.0, 2.0, 1.0]) + var y_b := PackedFloat64Array([2.0, 3.0, 5.0, 5.5, 5.0, 3.0, 2.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b]) + + %TestPlot5.title = "[LOG][SHARED_X] 3rd and 4th x values are invalid" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LOGARITHMIC + x_axis.tick_count_preferred = 8 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.include_zero_in_domain = true + + var line_config := TauLineConfig.new() + line_config.gap_policy = TauLineConfig.GapPolicy.SKIP + + var pane_config := TauPaneConfig.new() + pane_config.y_left_axis = y_axis + pane_config.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane_config] + config.style.series_alpha = 0.8 + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b] + + %TestPlot5.plot_xy(dataset, config, bindings) + +#################################################################################################### +# Test 6 +#################################################################################################### + +func _setup_test_6() -> void: + var series_names := PackedStringArray(["A", "B"]) + var x_a := PackedFloat64Array([pow(10, -3), pow(10, -2), -pow(10, -1), INF, pow(10, 1), pow(10, 2), pow(10, 3)]) + var x_b := PackedFloat64Array([pow(10, -3), pow(10, -2), pow(10, -1), pow(10, 0), pow(10, 1), pow(10, 2), pow(10, 3)]) + var y_a := PackedFloat64Array([1.0, 2.0, 4.0, 4.5, 4.0, 2.0, 1.0]) + var y_b := PackedFloat64Array([2.0, 3.0, 5.0, 5.5, 5.0, 3.0, 2.0]) + + var dataset := TauPlot.Dataset.make_per_series_x_continuous(series_names, [x_a, x_b], [y_a, y_b]) + + %TestPlot6.title = "[LOG][PER_SERIES_X] 3rd and 4th x values are invalid for series A" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LOGARITHMIC + x_axis.tick_count_preferred = 8 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.include_zero_in_domain = true + + var line_config := TauLineConfig.new() + line_config.gap_policy = TauLineConfig.GapPolicy.SKIP + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + config.style.series_alpha = 0.8 + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b] + + %TestPlot6.plot_xy(dataset, config, bindings) + +#################################################################################################### +# Test 7 +#################################################################################################### + +func _setup_test_7() -> void: + var series_names := PackedStringArray(["A", "B"]) + var x := PackedFloat64Array([0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0]) + var y_a := PackedFloat64Array([pow(10, 1), pow(10, 2), -pow(10, 4), INF, pow(10, 4), pow(10, 2), pow(10, 1)]) + var y_b := PackedFloat64Array([pow(10, 2), pow(10, 3), pow(10, 5), pow(10, 6), pow(10, 5), pow(10, 3), pow(10, 2)]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b]) + + %TestPlot7.title = "[LOG][SHARED_X] 3rd and 4th y values are invalid for series A" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.tick_count_preferred = 8 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LOGARITHMIC + y_axis.include_zero_in_domain = false + + var line_config := TauLineConfig.new() + line_config.gap_policy = TauLineConfig.GapPolicy.SKIP + + var pane_config := TauPaneConfig.new() + pane_config.y_left_axis = y_axis + pane_config.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane_config] + config.style.series_alpha = 0.8 + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b] + + %TestPlot7.plot_xy(dataset, config, bindings) + +#################################################################################################### +# Test 8 +#################################################################################################### + +func _setup_test_8() -> void: + var series_names := PackedStringArray(["A", "B"]) + var x_a := PackedFloat64Array([0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0]) + var x_b := PackedFloat64Array([0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0]) + var y_a := PackedFloat64Array([pow(10, 1), pow(10, 2), -pow(10, 4), INF, pow(10, 4), pow(10, 2), pow(10, 1)]) + var y_b := PackedFloat64Array([pow(10, 2), pow(10, 3), pow(10, 5), pow(10, 6), pow(10, 5), pow(10, 3), pow(10, 2)]) + + var dataset := TauPlot.Dataset.make_per_series_x_continuous(series_names, [x_a, x_b], [y_a, y_b]) + + %TestPlot8.title = "[LOG][PER_SERIES_X] 3rd and 4th y values are invalid for series A" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.tick_count_preferred = 8 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LOGARITHMIC + y_axis.include_zero_in_domain = false + + var line_config := TauLineConfig.new() + line_config.gap_policy = TauLineConfig.GapPolicy.SKIP + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + config.style.series_alpha = 0.8 + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b] + + %TestPlot8.plot_xy(dataset, config, bindings) diff --git a/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_skip.tscn b/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_skip.tscn index 5759e00..9e7f315 100644 --- a/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_skip.tscn +++ b/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_skip.tscn @@ -14,7 +14,7 @@ script = ExtResource("1_e73yd") [node name="Title" type="Label" parent="."] layout_mode = 2 theme_override_font_sizes/font_size = 32 -text = "Test nan and inf values with GapPolicy.SKIP" +text = "Test invalid values with GapPolicy.SKIP" horizontal_alignment = 1 [node name="GridContainer" type="GridContainer" parent="."] @@ -31,7 +31,7 @@ size_flags_horizontal = 3 size_flags_vertical = 3 theme_type_variation = &"TauPlot" script = ExtResource("2_5c7d4") -title = "[SHARED_X] 2nd and 4th x values are invalid" +title = "[SHARED_X] 3rd and 4th x values are invalid" metadata/_custom_type_script = "uid://dnhvsf2wip771" [node name="TestPlot2" type="PanelContainer" parent="GridContainer"] @@ -41,7 +41,7 @@ size_flags_horizontal = 3 size_flags_vertical = 3 theme_type_variation = &"TauPlot" script = ExtResource("2_5c7d4") -title = "[PER_SERIES_X] 2nd and 4th x values are invalid for series A" +title = "[PER_SERIES_X] 3rd and 4th x values are invalid for series A" metadata/_custom_type_script = "uid://dnhvsf2wip771" [node name="TestPlot3" type="PanelContainer" parent="GridContainer"] @@ -51,7 +51,7 @@ size_flags_horizontal = 3 size_flags_vertical = 3 theme_type_variation = &"TauPlot" script = ExtResource("2_5c7d4") -title = "[SHARED_X] 2nd and 4th y values are invalid for series A" +title = "[SHARED_X] 3rd and 4th y values are invalid for series A" metadata/_custom_type_script = "uid://dnhvsf2wip771" [node name="TestPlot4" type="PanelContainer" parent="GridContainer"] @@ -61,5 +61,45 @@ size_flags_horizontal = 3 size_flags_vertical = 3 theme_type_variation = &"TauPlot" script = ExtResource("2_5c7d4") -title = "[PER_SERIES_X] 2nd and 4th y values are invalid for series A" +title = "[PER_SERIES_X] 3rd and 4th y values are invalid for series A" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot5" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_5c7d4") +title = "[LOG][SHARED_X] 3rd and 4th x values are invalid" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot6" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_5c7d4") +title = "[LOG][PER_SERIES_X] 3rd and 4th x values are invalid for series A" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot7" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_5c7d4") +title = "[LOG][SHARED_X] 3rd and 4th y values are invalid for series A" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot8" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_5c7d4") +title = "[LOG][PER_SERIES_X] 3rd and 4th y values are invalid for series A" metadata/_custom_type_script = "uid://dnhvsf2wip771" From 6a0481e443083f022cfeb614f0278633714c101a Mon Sep 17 00:00:00 2001 From: ze2j <46556066+ze2j@users.noreply.github.com> Date: Sun, 26 Apr 2026 13:56:56 +0200 Subject: [PATCH 05/23] Implement STEP_BEFORE, STEP_AFTER and STEP_MIDDLE --- addons/tau-plot/plot/xy/line/line_config.gd | 23 ++ addons/tau-plot/plot/xy/line/line_renderer.gd | 28 ++- .../test_line_interpolation_linear.gd | 228 ++++++++++++++++++ .../test_line_interpolation_linear.gd.uid | 1 + .../test_line_interpolation_linear.tscn | 157 ++++++++++++ .../test_line_interpolation_logarithmic.gd | 157 ++++++++++++ ...test_line_interpolation_logarithmic.gd.uid | 1 + .../test_line_interpolation_logarithmic.tscn | 113 +++++++++ .../line/linear_scale/test_line_linear.gd | 172 ------------- .../line/linear_scale/test_line_linear.gd.uid | 1 - .../line/linear_scale/test_line_linear.tscn | 55 ----- 11 files changed, 706 insertions(+), 230 deletions(-) create mode 100644 addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_linear.gd create mode 100644 addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_linear.gd.uid create mode 100644 addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_linear.tscn create mode 100644 addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_logarithmic.gd create mode 100644 addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_logarithmic.gd.uid create mode 100644 addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_logarithmic.tscn delete mode 100644 addons/tau-plot/tests/line/linear_scale/test_line_linear.gd delete mode 100644 addons/tau-plot/tests/line/linear_scale/test_line_linear.gd.uid delete mode 100644 addons/tau-plot/tests/line/linear_scale/test_line_linear.tscn diff --git a/addons/tau-plot/plot/xy/line/line_config.gd b/addons/tau-plot/plot/xy/line/line_config.gd index e09bcae..fc492d1 100644 --- a/addons/tau-plot/plot/xy/line/line_config.gd +++ b/addons/tau-plot/plot/xy/line/line_config.gd @@ -42,6 +42,27 @@ enum GapPolicy } @export var gap_policy: GapPolicy = GapPolicy.SKIP +## How consecutive samples are interpolated. +## +## LINEAR draws a straight segment between consecutive samples. +## +## The three step modes interpolate as a staircase between consecutive +## samples: only horizontal or vertical motion, with the modes differing by +## when the vertical jump happens. STEP_BEFORE jumps as early as possible (at +## the previous sample's X position), STEP_AFTER as late as possible (at the +## next sample's X position), and STEP_MIDDLE jumps at the pixel midpoint +## between the two X positions. +## +## This property is visual-only and does not affect layout or domain. +enum InterpolationMode +{ + LINEAR, ## Straight segment between consecutive samples. + STEP_BEFORE, ## Vertical jump first at the previous sample's X, then horizontal. + STEP_AFTER, ## Horizontal first, then vertical jump at the next sample's X. + STEP_MIDDLE ## Horizontal, vertical jump at the pixel midpoint, horizontal. +} +@export var interpolation_mode: InterpolationMode = InterpolationMode.LINEAR + ## Maximum pixel distance from the cursor to a sample position for the sample ## to be considered a hover hit. ## @@ -90,6 +111,8 @@ func is_equal_to(p_other: TauPaneOverlayConfig) -> bool: return false if gap_policy != other.gap_policy: return false + if interpolation_mode != other.interpolation_mode: + return false if hover_max_distance_px != other.hover_max_distance_px: return false diff --git a/addons/tau-plot/plot/xy/line/line_renderer.gd b/addons/tau-plot/plot/xy/line/line_renderer.gd index 31f1a4d..5cc2f13 100644 --- a/addons/tau-plot/plot/xy/line/line_renderer.gd +++ b/addons/tau-plot/plot/xy/line/line_renderer.gd @@ -21,6 +21,10 @@ const LineVisualAttributes := preload("res://addons/tau-plot/plot/xy/line/line_v # - GapPolicy.SKIP breaks the polyline at every invalid sample. # - GapPolicy.BRIDGE drops invalid samples and keeps the polyline contiguous, # so the surrounding valid samples are connected directly. +# - TauLineConfig.interpolation_mode controls the curve drawn between two +# consecutive valid samples. The step modes (STEP_BEFORE, STEP_AFTER, +# STEP_MIDDLE) are realized by inserting synthetic intermediate points in +# screen space into the polyline. LINEAR draws straight segments. # # LineValidator is expected to enforce binding-level typing constraints. class LineRenderer extends Control: @@ -143,6 +147,7 @@ class LineRenderer extends Control: var color := _resolve_series_color(global_series_index) var y_axis_id := _get_y_axis_id_for_series(series_id) var bridge: bool = _line_config.gap_policy == TauLineConfig.GapPolicy.BRIDGE + var interpolation: TauLineConfig.InterpolationMode = _line_config.interpolation_mode var run := PackedVector2Array() @@ -164,7 +169,7 @@ class LineRenderer extends Control: var x_px := _layout.map_x_to_px(_pane_index, x_value) var y_px := _layout.map_y_to_px(_pane_index, y_value, y_axis_id) - run.append(_layout.map_point_to_screen(x_px, y_px)) + _append_with_interpolation(run, _layout.map_point_to_screen(x_px, y_px), interpolation) if run.size() >= 2: draw_polyline(run, color, p_width_px) @@ -176,6 +181,7 @@ class LineRenderer extends Control: var color := _resolve_series_color(global_series_index) var y_axis_id := _get_y_axis_id_for_series(series_id) var bridge: bool = _line_config.gap_policy == TauLineConfig.GapPolicy.BRIDGE + var interpolation: TauLineConfig.InterpolationMode = _line_config.interpolation_mode var run := PackedVector2Array() var sample_count := _dataset.get_series_sample_count(series_id) @@ -189,7 +195,7 @@ class LineRenderer extends Control: var x_px := _layout.map_x_category_center_to_px(_pane_index, cat_idx) var y_px := _layout.map_y_to_px(_pane_index, y_value, y_axis_id) - run.append(_layout.map_point_to_screen(x_px, y_px)) + _append_with_interpolation(run, _layout.map_point_to_screen(x_px, y_px), interpolation) if run.size() >= 2: draw_polyline(run, color, p_width_px) @@ -201,6 +207,24 @@ class LineRenderer extends Control: return PackedVector2Array() + func _append_with_interpolation(p_run: PackedVector2Array, p_point: Vector2, p_mode: TauLineConfig.InterpolationMode) -> void: + if p_run.size() == 0 or p_mode == TauLineConfig.InterpolationMode.LINEAR: + p_run.append(p_point) + return + + var last_pt: Vector2 = p_run[p_run.size() - 1] + match p_mode: + TauLineConfig.InterpolationMode.STEP_BEFORE: + p_run.append(Vector2(last_pt.x, p_point.y)) + TauLineConfig.InterpolationMode.STEP_AFTER: + p_run.append(Vector2(p_point.x, last_pt.y)) + TauLineConfig.InterpolationMode.STEP_MIDDLE: + var mid_x: float = (last_pt.x + p_point.x) * 0.5 + p_run.append(Vector2(mid_x, last_pt.y)) + p_run.append(Vector2(mid_x, p_point.y)) + p_run.append(p_point) + + #################################################################################################### # Series helpers #################################################################################################### diff --git a/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_linear.gd b/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_linear.gd new file mode 100644 index 0000000..80466dc --- /dev/null +++ b/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_linear.gd @@ -0,0 +1,228 @@ +@tool +extends Control + +func _ready() -> void: + _setup_test_1() + _setup_test_2() + _setup_test_3() + _setup_test_4() + _setup_test_5() + _setup_test_6() + _setup_test_7() + _setup_test_8() + _setup_test_9() + _setup_test_10() + _setup_test_11() + _setup_test_12() + + +#################################################################################################### +# Helpers +#################################################################################################### + +func make_shared_x_continuous_plot(p_plot: TauPlot, p_title: String, p_interpolation: TauLineConfig.InterpolationMode) -> void: + var series_names := PackedStringArray(["Series A"]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0]) + var y_a := PackedFloat64Array([10.0, 20.0, 30.0, 40.0, 50.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a]) + + p_plot.title = p_title + p_plot.legend_enabled = false + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.tick_count_preferred = 10 + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.INDEPENDENT + line_config.interpolation_mode = p_interpolation + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a] + + p_plot.plot_xy(dataset, config, bindings) + + +func make_per_series_x_continuous_plot(p_plot: TauPlot, p_title: String, p_interpolation: TauLineConfig.InterpolationMode) -> void: + var series_names := PackedStringArray(["Series A"]) + var x_a := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0]) + var y_a := PackedFloat64Array([10.0, 20.0, 30.0, 40.0, 50.0]) + + var dataset := TauPlot.Dataset.make_per_series_x_continuous(series_names, [x_a], [y_a]) + + p_plot.title = p_title + p_plot.legend_enabled = false + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.tick_count_preferred = 10 + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.INDEPENDENT + line_config.interpolation_mode = p_interpolation + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a] + + p_plot.plot_xy(dataset, config, bindings) + + +func make_shared_x_categorical_plot(p_plot: TauPlot, p_title: String, p_interpolation: TauLineConfig.InterpolationMode) -> void: + var series_names := PackedStringArray(["Series A"]) + var x := PackedStringArray(["One", "Two", "Three", "Four", "Five"]) + var y_a := PackedFloat64Array([10.0, 20.0, 30.0, 40.0, 50.0]) + + var dataset := TauPlot.Dataset.make_shared_x_categorical(series_names, x, [y_a]) + + p_plot.title = p_title + p_plot.legend_enabled = false + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CATEGORICAL + x_axis.scale = TauAxisConfig.Scale.LINEAR + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.tick_count_preferred = 10 + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.INDEPENDENT + line_config.interpolation_mode = p_interpolation + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a] + + p_plot.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 1 +#################################################################################################### + +func _setup_test_1() -> void: + make_shared_x_continuous_plot(%TestPlot1, "[LINEAR] SHARED_X + CONTINUOUS", TauLineConfig.InterpolationMode.LINEAR) + +#################################################################################################### +# Test 2 +#################################################################################################### + +func _setup_test_2() -> void: + make_per_series_x_continuous_plot(%TestPlot2, "[LINEAR] PER_SERIES_X + CONTINUOUS", TauLineConfig.InterpolationMode.LINEAR) + +#################################################################################################### +# Test 3 +#################################################################################################### + +func _setup_test_3() -> void: + make_shared_x_categorical_plot(%TestPlot3, "[LINEAR] SHARED_X + CATEGORICAL", TauLineConfig.InterpolationMode.LINEAR) + +#################################################################################################### +# Test 4 +#################################################################################################### + +func _setup_test_4() -> void: + make_shared_x_continuous_plot(%TestPlot4, "[STEP_BEFORE] SHARED_X + CONTINUOUS", TauLineConfig.InterpolationMode.STEP_BEFORE) + +#################################################################################################### +# Test 5 +#################################################################################################### + +func _setup_test_5() -> void: + make_per_series_x_continuous_plot(%TestPlot5, "[STEP_BEFORE] PER_SERIES_X + CONTINUOUS", TauLineConfig.InterpolationMode.STEP_BEFORE) + +#################################################################################################### +# Test 6 +#################################################################################################### + +func _setup_test_6() -> void: + make_shared_x_categorical_plot(%TestPlot6, "[STEP_BEFORE] SHARED_X + CATEGORICAL", TauLineConfig.InterpolationMode.STEP_BEFORE) + +#################################################################################################### +# Test 7 +#################################################################################################### + +func _setup_test_7() -> void: + make_shared_x_continuous_plot(%TestPlot7, "[STEP_MIDDLE] SHARED_X + CONTINUOUS", TauLineConfig.InterpolationMode.STEP_MIDDLE) + +#################################################################################################### +# Test 8 +#################################################################################################### + +func _setup_test_8() -> void: + make_per_series_x_continuous_plot(%TestPlot8, "[STEP_MIDDLE] PER_SERIES_X + CONTINUOUS", TauLineConfig.InterpolationMode.STEP_MIDDLE) + +#################################################################################################### +# Test 9 +#################################################################################################### + +func _setup_test_9() -> void: + make_shared_x_categorical_plot(%TestPlot9, "[STEP_MIDDLE] SHARED_X + CATEGORICAL", TauLineConfig.InterpolationMode.STEP_MIDDLE) + +#################################################################################################### +# Test 10 +#################################################################################################### + +func _setup_test_10() -> void: + make_shared_x_continuous_plot(%TestPlot10, "[STEP_AFTER] SHARED_X + CONTINUOUS", TauLineConfig.InterpolationMode.STEP_AFTER) + +#################################################################################################### +# Test 11 +#################################################################################################### + +func _setup_test_11() -> void: + make_per_series_x_continuous_plot(%TestPlot11, "[STEP_AFTER] PER_SERIES_X + CONTINUOUS", TauLineConfig.InterpolationMode.STEP_AFTER) + +#################################################################################################### +# Test 12 +#################################################################################################### + +func _setup_test_12() -> void: + make_shared_x_categorical_plot(%TestPlot12, "[STEP_AFTER] SHARED_X + CATEGORICAL", TauLineConfig.InterpolationMode.STEP_AFTER) diff --git a/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_linear.gd.uid b/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_linear.gd.uid new file mode 100644 index 0000000..af94b4e --- /dev/null +++ b/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_linear.gd.uid @@ -0,0 +1 @@ +uid://dagnw8rnu7ils diff --git a/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_linear.tscn b/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_linear.tscn new file mode 100644 index 0000000..57f6107 --- /dev/null +++ b/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_linear.tscn @@ -0,0 +1,157 @@ +[gd_scene load_steps=3 format=3 uid="uid://dhokcl8lmbwud"] + +[ext_resource type="Script" uid="uid://dagnw8rnu7ils" path="res://addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_linear.gd" id="1_ahkji"] +[ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_nry5o"] + +[node name="Bars" type="VBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_ahkji") + +[node name="Title" type="Label" parent="."] +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +text = "Interpolation modes with linear scale" +horizontal_alignment = 1 + +[node name="GridContainer" type="GridContainer" parent="."] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/h_separation = 0 +theme_override_constants/v_separation = 0 +columns = 3 + +[node name="TestPlot1" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_nry5o") +title = "[LINEAR] SHARED_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot2" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_nry5o") +title = "[LINEAR] PER_SERIES_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot3" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_nry5o") +title = "[LINEAR] SHARED_X + CATEGORICAL" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot4" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_nry5o") +title = "[STEP_BEFORE] SHARED_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot5" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_nry5o") +title = "[STEP_BEFORE] PER_SERIES_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot6" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_nry5o") +title = "[STEP_BEFORE] SHARED_X + CATEGORICAL" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot7" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_nry5o") +title = "[STEP_MIDDLE] SHARED_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot8" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_nry5o") +title = "[STEP_MIDDLE] PER_SERIES_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot9" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_nry5o") +title = "[STEP_MIDDLE] SHARED_X + CATEGORICAL" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot10" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_nry5o") +title = "[STEP_AFTER] SHARED_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot11" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_nry5o") +title = "[STEP_AFTER] PER_SERIES_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot12" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_nry5o") +title = "[STEP_AFTER] SHARED_X + CATEGORICAL" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" diff --git a/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_logarithmic.gd b/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_logarithmic.gd new file mode 100644 index 0000000..564bc96 --- /dev/null +++ b/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_logarithmic.gd @@ -0,0 +1,157 @@ +@tool +extends Control + +func _ready() -> void: + _setup_test_1() + _setup_test_2() + _setup_test_3() + _setup_test_4() + _setup_test_5() + _setup_test_6() + _setup_test_7() + _setup_test_8() + + +#################################################################################################### +# Helpers +#################################################################################################### + +func make_shared_x_continuous_plot(p_plot: TauPlot, p_title: String, p_interpolation: TauLineConfig.InterpolationMode) -> void: + var series_names := PackedStringArray(["Series A"]) + var x := PackedFloat64Array([pow(10, -2), pow(10, -1), pow(10, 0), pow(10, 1), pow(10, 2)]) + var y_a := PackedFloat64Array([10.0, 20.0, 30.0, 40.0, 50.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a]) + + p_plot.title = p_title + p_plot.legend_enabled = false + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LOGARITHMIC + x_axis.include_zero_in_domain = false + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.tick_count_preferred = 10 + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.INDEPENDENT + line_config.interpolation_mode = p_interpolation + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a] + + p_plot.plot_xy(dataset, config, bindings) + + +func make_per_series_x_continuous_plot(p_plot: TauPlot, p_title: String, p_interpolation: TauLineConfig.InterpolationMode) -> void: + var series_names := PackedStringArray(["Series A"]) + var x_a := PackedFloat64Array([pow(10, -2), pow(10, -1), pow(10, 0), pow(10, 1), pow(10, 2)]) + var y_a := PackedFloat64Array([10.0, 20.0, 30.0, 40.0, 50.0]) + + var dataset := TauPlot.Dataset.make_per_series_x_continuous(series_names, [x_a], [y_a]) + + p_plot.title = p_title + p_plot.legend_enabled = false + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LOGARITHMIC + x_axis.include_zero_in_domain = false + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.tick_count_preferred = 10 + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.INDEPENDENT + line_config.interpolation_mode = p_interpolation + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a] + + p_plot.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 1 +#################################################################################################### + +func _setup_test_1() -> void: + make_shared_x_continuous_plot(%TestPlot1, "[LINEAR] SHARED_X + CONTINUOUS", TauLineConfig.InterpolationMode.LINEAR) + +#################################################################################################### +# Test 2 +#################################################################################################### + +func _setup_test_2() -> void: + make_per_series_x_continuous_plot(%TestPlot2, "[LINEAR] PER_SERIES_X + CONTINUOUS", TauLineConfig.InterpolationMode.LINEAR) + +#################################################################################################### +# Test 3 +#################################################################################################### + +func _setup_test_3() -> void: + make_shared_x_continuous_plot(%TestPlot3, "[STEP_BEFORE] SHARED_X + CONTINUOUS", TauLineConfig.InterpolationMode.STEP_BEFORE) + +#################################################################################################### +# Test 4 +#################################################################################################### + +func _setup_test_4() -> void: + make_per_series_x_continuous_plot(%TestPlot4, "[STEP_BEFORE] PER_SERIES_X + CONTINUOUS", TauLineConfig.InterpolationMode.STEP_BEFORE) + +#################################################################################################### +# Test 5 +#################################################################################################### + +func _setup_test_5() -> void: + make_shared_x_continuous_plot(%TestPlot5, "[STEP_MIDDLE] SHARED_X + CONTINUOUS", TauLineConfig.InterpolationMode.STEP_MIDDLE) + +#################################################################################################### +# Test 6 +#################################################################################################### + +func _setup_test_6() -> void: + make_per_series_x_continuous_plot(%TestPlot6, "[STEP_MIDDLE] PER_SERIES_X + CONTINUOUS", TauLineConfig.InterpolationMode.STEP_MIDDLE) + +#################################################################################################### +# Test 7 +#################################################################################################### + +func _setup_test_7() -> void: + make_shared_x_continuous_plot(%TestPlot7, "[STEP_AFTER] SHARED_X + CONTINUOUS", TauLineConfig.InterpolationMode.STEP_AFTER) + +#################################################################################################### +# Test 8 +#################################################################################################### + +func _setup_test_8() -> void: + make_per_series_x_continuous_plot(%TestPlot8, "[STEP_AFTER] PER_SERIES_X + CONTINUOUS", TauLineConfig.InterpolationMode.STEP_AFTER) diff --git a/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_logarithmic.gd.uid b/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_logarithmic.gd.uid new file mode 100644 index 0000000..609addc --- /dev/null +++ b/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_logarithmic.gd.uid @@ -0,0 +1 @@ +uid://bsivl5p06vdek diff --git a/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_logarithmic.tscn b/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_logarithmic.tscn new file mode 100644 index 0000000..cb19e48 --- /dev/null +++ b/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_logarithmic.tscn @@ -0,0 +1,113 @@ +[gd_scene load_steps=3 format=3 uid="uid://b7wpf2xypsmj2"] + +[ext_resource type="Script" uid="uid://bsivl5p06vdek" path="res://addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_logarithmic.gd" id="1_g1sf8"] +[ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_u5as0"] + +[node name="Bars" type="VBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_g1sf8") + +[node name="Title" type="Label" parent="."] +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +text = "Interpolation modes with logarithmic scale" +horizontal_alignment = 1 + +[node name="GridContainer" type="GridContainer" parent="."] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/h_separation = 0 +theme_override_constants/v_separation = 0 +columns = 2 + +[node name="TestPlot1" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_u5as0") +title = "[LINEAR] SHARED_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot2" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_u5as0") +title = "[LINEAR] PER_SERIES_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot3" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_u5as0") +title = "[STEP_BEFORE] SHARED_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot4" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_u5as0") +title = "[STEP_BEFORE] PER_SERIES_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot5" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_u5as0") +title = "[STEP_MIDDLE] SHARED_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot6" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_u5as0") +title = "[STEP_MIDDLE] PER_SERIES_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot7" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_u5as0") +title = "[STEP_AFTER] SHARED_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot8" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_u5as0") +title = "[STEP_AFTER] PER_SERIES_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" diff --git a/addons/tau-plot/tests/line/linear_scale/test_line_linear.gd b/addons/tau-plot/tests/line/linear_scale/test_line_linear.gd deleted file mode 100644 index 3acfa70..0000000 --- a/addons/tau-plot/tests/line/linear_scale/test_line_linear.gd +++ /dev/null @@ -1,172 +0,0 @@ -@tool -extends Control - -func _ready() -> void: - _setup_test_1() - _setup_test_2() - _setup_test_3() - - -#################################################################################################### -# Test 1 -#################################################################################################### - -func _setup_test_1() -> void: - var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) - var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0]) - var y_a := PackedFloat64Array([5.0, 10.0, 8.0, 12.0]) - var y_b := PackedFloat64Array([25.0, 28.0, 23.0, 7.0]) - var y_c := PackedFloat64Array([30.0, 28.0, 39.0, 67.0]) - - var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b, y_c]) - - %TestPlot1.title = "SHARED_X + CONTINUOUS" - - var x_axis := TauAxisConfig.new() - x_axis.type = TauAxisConfig.Type.CONTINUOUS - x_axis.scale = TauAxisConfig.Scale.LINEAR - - var y_axis := TauAxisConfig.new() - y_axis.type = TauAxisConfig.Type.CONTINUOUS - y_axis.scale = TauAxisConfig.Scale.LINEAR - - var line_config := TauLineConfig.new() - line_config.mode = TauLineConfig.LineMode.INDEPENDENT - - var pane := TauPaneConfig.new() - pane.y_left_axis = y_axis - pane.overlays = [line_config] - - var config := TauXYConfig.new() - config.x_axis = x_axis - config.panes = [pane] - - var sb_a := TauXYSeriesBinding.new() - sb_a.series_id = dataset.get_series_id_by_index(0) - sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE - sb_a.y_axis_id = TauPlot.AxisId.LEFT - - var sb_b := TauXYSeriesBinding.new() - sb_b.series_id = dataset.get_series_id_by_index(1) - sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE - sb_b.y_axis_id = TauPlot.AxisId.LEFT - - var sb_c := TauXYSeriesBinding.new() - sb_c.series_id = dataset.get_series_id_by_index(2) - sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE - sb_c.y_axis_id = TauPlot.AxisId.LEFT - - var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] - - %TestPlot1.plot_xy(dataset, config, bindings) - - -#################################################################################################### -# Test 2 -#################################################################################################### - -func _setup_test_2() -> void: - var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) - var x_a := PackedFloat64Array([10.0, 11.0, 12.0, 13.0]) - var x_b := PackedFloat64Array([9.0, 10.3, 12.7, 14.1]) - var x_c := PackedFloat64Array([9.2, 10.1, 10.5, 10.9]) - - var y_a := PackedFloat64Array([5.0, 10.0, 8.0, 12.0]) - var y_b := PackedFloat64Array([25.0, 28.0, 23.0, 7.0]) - var y_c := PackedFloat64Array([30.0, 28.0, 39.0, 67.0]) - - var dataset := TauPlot.Dataset.make_per_series_x_continuous(series_names, [x_a, x_b, x_c], [y_a, y_b, y_c]) - - %TestPlot2.title = "PER_SERIES_X + CONTINUOUS" - - var x_axis := TauAxisConfig.new() - x_axis.type = TauAxisConfig.Type.CONTINUOUS - x_axis.scale = TauAxisConfig.Scale.LINEAR - - var y_axis := TauAxisConfig.new() - y_axis.type = TauAxisConfig.Type.CONTINUOUS - y_axis.scale = TauAxisConfig.Scale.LINEAR - - var line_config := TauLineConfig.new() - line_config.mode = TauLineConfig.LineMode.INDEPENDENT - - var pane := TauPaneConfig.new() - pane.y_left_axis = y_axis - pane.overlays = [line_config] - - var config := TauXYConfig.new() - config.x_axis = x_axis - config.panes = [pane] - - var sb_a := TauXYSeriesBinding.new() - sb_a.series_id = dataset.get_series_id_by_index(0) - sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE - sb_a.y_axis_id = TauPlot.AxisId.LEFT - - var sb_b := TauXYSeriesBinding.new() - sb_b.series_id = dataset.get_series_id_by_index(1) - sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE - sb_b.y_axis_id = TauPlot.AxisId.LEFT - - var sb_c := TauXYSeriesBinding.new() - sb_c.series_id = dataset.get_series_id_by_index(2) - sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE - sb_c.y_axis_id = TauPlot.AxisId.LEFT - - var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] - - %TestPlot2.plot_xy(dataset, config, bindings) - - -#################################################################################################### -# Test 3 -#################################################################################################### - -func _setup_test_3() -> void: - var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) - var x := PackedStringArray(["One", "Two", "Three", "Four"]) - var y_a := PackedFloat64Array([5.0, 10.0, 8.0, 12.0]) - var y_b := PackedFloat64Array([25.0, 28.0, 23.0, 7.0]) - var y_c := PackedFloat64Array([30.0, 28.0, 39.0, 67.0]) - - var dataset := TauPlot.Dataset.make_shared_x_categorical(series_names, x, [y_a, y_b, y_c]) - - %TestPlot3.title = "SHARED_X + CATEGORICAL" - - var x_axis := TauAxisConfig.new() - x_axis.type = TauAxisConfig.Type.CATEGORICAL - x_axis.scale = TauAxisConfig.Scale.LINEAR - - var y_axis := TauAxisConfig.new() - y_axis.type = TauAxisConfig.Type.CONTINUOUS - y_axis.scale = TauAxisConfig.Scale.LINEAR - - var line_config := TauLineConfig.new() - line_config.mode = TauLineConfig.LineMode.INDEPENDENT - - var pane := TauPaneConfig.new() - pane.y_left_axis = y_axis - pane.overlays = [line_config] - - var config := TauXYConfig.new() - config.x_axis = x_axis - config.panes = [pane] - - var sb_a := TauXYSeriesBinding.new() - sb_a.series_id = dataset.get_series_id_by_index(0) - sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE - sb_a.y_axis_id = TauPlot.AxisId.LEFT - - var sb_b := TauXYSeriesBinding.new() - sb_b.series_id = dataset.get_series_id_by_index(1) - sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE - sb_b.y_axis_id = TauPlot.AxisId.LEFT - - var sb_c := TauXYSeriesBinding.new() - sb_c.series_id = dataset.get_series_id_by_index(2) - sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE - sb_c.y_axis_id = TauPlot.AxisId.LEFT - - var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] - - %TestPlot3.plot_xy(dataset, config, bindings) diff --git a/addons/tau-plot/tests/line/linear_scale/test_line_linear.gd.uid b/addons/tau-plot/tests/line/linear_scale/test_line_linear.gd.uid deleted file mode 100644 index 046a12b..0000000 --- a/addons/tau-plot/tests/line/linear_scale/test_line_linear.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://d0b7xei1hadvi diff --git a/addons/tau-plot/tests/line/linear_scale/test_line_linear.tscn b/addons/tau-plot/tests/line/linear_scale/test_line_linear.tscn deleted file mode 100644 index 02a14f9..0000000 --- a/addons/tau-plot/tests/line/linear_scale/test_line_linear.tscn +++ /dev/null @@ -1,55 +0,0 @@ -[gd_scene load_steps=3 format=3 uid="uid://bymhr4sv6e8pv"] - -[ext_resource type="Script" uid="uid://d0b7xei1hadvi" path="res://addons/tau-plot/tests/line/linear_scale/test_line_linear.gd" id="1_7re6l"] -[ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_67aov"] - -[node name="Bars" type="VBoxContainer"] -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -script = ExtResource("1_7re6l") - -[node name="Title" type="Label" parent="."] -layout_mode = 2 -theme_override_font_sizes/font_size = 32 -text = "Linear scale feature" -horizontal_alignment = 1 - -[node name="GridContainer" type="GridContainer" parent="."] -layout_mode = 2 -size_flags_vertical = 3 -theme_override_constants/h_separation = 0 -theme_override_constants/v_separation = 0 -columns = 3 - -[node name="TestPlot1" type="PanelContainer" parent="GridContainer"] -unique_name_in_owner = true -layout_mode = 2 -size_flags_horizontal = 3 -size_flags_vertical = 3 -theme_type_variation = &"TauPlot" -script = ExtResource("2_67aov") -title = "SHARED_X + CONTINUOUS" -metadata/_custom_type_script = "uid://dnhvsf2wip771" - -[node name="TestPlot2" type="PanelContainer" parent="GridContainer"] -unique_name_in_owner = true -layout_mode = 2 -size_flags_horizontal = 3 -size_flags_vertical = 3 -theme_type_variation = &"TauPlot" -script = ExtResource("2_67aov") -title = "PER_SERIES_X + CONTINUOUS" -metadata/_custom_type_script = "uid://dnhvsf2wip771" - -[node name="TestPlot3" type="PanelContainer" parent="GridContainer"] -unique_name_in_owner = true -layout_mode = 2 -size_flags_horizontal = 3 -size_flags_vertical = 3 -theme_type_variation = &"TauPlot" -script = ExtResource("2_67aov") -title = "SHARED_X + CATEGORICAL" -metadata/_custom_type_script = "uid://dnhvsf2wip771" From c2f923293c92a78cd157fc76c164584cbaecaaf5 Mon Sep 17 00:00:00 2001 From: ze2j <46556066+ze2j@users.noreply.github.com> Date: Sun, 26 Apr 2026 15:32:47 +0200 Subject: [PATCH 06/23] Implement SMOOTH_MONOTONE --- addons/tau-plot/plot/xy/line/line_config.gd | 14 +- addons/tau-plot/plot/xy/line/line_renderer.gd | 244 ++++++++++++++++-- .../test_line_interpolation_linear.gd | 34 ++- .../test_line_interpolation_linear.tscn | 33 +++ .../test_line_interpolation_logarithmic.gd | 23 +- .../test_line_interpolation_logarithmic.tscn | 22 ++ 6 files changed, 342 insertions(+), 28 deletions(-) diff --git a/addons/tau-plot/plot/xy/line/line_config.gd b/addons/tau-plot/plot/xy/line/line_config.gd index fc492d1..73aced2 100644 --- a/addons/tau-plot/plot/xy/line/line_config.gd +++ b/addons/tau-plot/plot/xy/line/line_config.gd @@ -53,13 +53,19 @@ enum GapPolicy ## next sample's X position), and STEP_MIDDLE jumps at the pixel midpoint ## between the two X positions. ## +## SMOOTH_MONOTONE draws a piecewise cubic Hermite curve through the samples, +## using Fritsch-Carlson tangent selection. The curve is C1 continuous, +## interpolates every sample exactly, and preserves local monotonicity, so +## no overshoot or local extrema are introduced between samples. +## ## This property is visual-only and does not affect layout or domain. enum InterpolationMode { - LINEAR, ## Straight segment between consecutive samples. - STEP_BEFORE, ## Vertical jump first at the previous sample's X, then horizontal. - STEP_AFTER, ## Horizontal first, then vertical jump at the next sample's X. - STEP_MIDDLE ## Horizontal, vertical jump at the pixel midpoint, horizontal. + LINEAR, ## Straight segment between consecutive samples. + STEP_BEFORE, ## Vertical jump first at the previous sample's X, then horizontal. + STEP_AFTER, ## Horizontal first, then vertical jump at the next sample's X. + STEP_MIDDLE, ## Horizontal, vertical jump at the pixel midpoint, horizontal. + SMOOTH_MONOTONE ## Fritsch-Carlson monotone piecewise cubic Hermite curve. } @export var interpolation_mode: InterpolationMode = InterpolationMode.LINEAR diff --git a/addons/tau-plot/plot/xy/line/line_renderer.gd b/addons/tau-plot/plot/xy/line/line_renderer.gd index 5cc2f13..361d74e 100644 --- a/addons/tau-plot/plot/xy/line/line_renderer.gd +++ b/addons/tau-plot/plot/xy/line/line_renderer.gd @@ -22,9 +22,13 @@ const LineVisualAttributes := preload("res://addons/tau-plot/plot/xy/line/line_v # - GapPolicy.BRIDGE drops invalid samples and keeps the polyline contiguous, # so the surrounding valid samples are connected directly. # - TauLineConfig.interpolation_mode controls the curve drawn between two -# consecutive valid samples. The step modes (STEP_BEFORE, STEP_AFTER, -# STEP_MIDDLE) are realized by inserting synthetic intermediate points in -# screen space into the polyline. LINEAR draws straight segments. +# consecutive valid samples. LINEAR draws straight segments. The step +# modes (STEP_BEFORE, STEP_AFTER, STEP_MIDDLE) insert synthetic +# intermediate points in screen space into the polyline. SMOOTH_MONOTONE +# replaces each segment with a fixed number of sub-samples from a +# Fritsch-Carlson piecewise cubic Hermite curve evaluated in screen space. +# Whichever interpolation is active, a contiguous run is still drawn with a +# single draw_polyline() call. # # LineValidator is expected to enforce binding-level typing constraints. class LineRenderer extends Control: @@ -46,6 +50,12 @@ class LineRenderer extends Control: var _line_style: TauLineStyle = null var _xy_style: TauXYStyle = null + # One-shot guard for the non-monotonic SMOOTH_MONOTONE fallback warning. + # Reset is intentionally absent: a single warning per renderer instance + # is enough to surface the misconfiguration without flooding the output + # on every redraw. + var _smooth_non_monotonic_warned: bool = false + func _init(p_layout: XYLayout, p_dataset: Dataset, @@ -158,21 +168,22 @@ class LineRenderer extends Control: var x_value: float = float(_dataset.get_shared_x(i)) if is_shared_x else float(_dataset.get_series_x(series_id, i)) if is_nan(x_value) or is_inf(x_value) or not _is_x_value_valid_for_scale(x_value): if not bridge: - run = _flush_run(run, color, p_width_px) + _finalize_run(run, color, p_width_px, interpolation) + run = PackedVector2Array() continue var y_value := _dataset.get_series_y(series_id, i) if is_nan(y_value) or is_inf(y_value) or not _is_y_value_valid_for_scale(series_id, y_value): if not bridge: - run = _flush_run(run, color, p_width_px) + _finalize_run(run, color, p_width_px, interpolation) + run = PackedVector2Array() continue var x_px := _layout.map_x_to_px(_pane_index, x_value) var y_px := _layout.map_y_to_px(_pane_index, y_value, y_axis_id) _append_with_interpolation(run, _layout.map_point_to_screen(x_px, y_px), interpolation) - if run.size() >= 2: - draw_polyline(run, color, p_width_px) + _finalize_run(run, color, p_width_px, interpolation) func _draw_series_categorical(p_series_index: int, p_width_px: float) -> void: @@ -190,25 +201,22 @@ class LineRenderer extends Control: var y_value := _dataset.get_series_y(series_id, cat_idx) if is_nan(y_value) or is_inf(y_value) or not _is_y_value_valid_for_scale(series_id, y_value): if not bridge: - run = _flush_run(run, color, p_width_px) + _finalize_run(run, color, p_width_px, interpolation) + run = PackedVector2Array() continue var x_px := _layout.map_x_category_center_to_px(_pane_index, cat_idx) var y_px := _layout.map_y_to_px(_pane_index, y_value, y_axis_id) _append_with_interpolation(run, _layout.map_point_to_screen(x_px, y_px), interpolation) - if run.size() >= 2: - draw_polyline(run, color, p_width_px) - - - func _flush_run(p_run: PackedVector2Array, p_color: Color, p_width_px: float) -> PackedVector2Array: - if p_run.size() >= 2: - draw_polyline(p_run, p_color, p_width_px) - return PackedVector2Array() + _finalize_run(run, color, p_width_px, interpolation) func _append_with_interpolation(p_run: PackedVector2Array, p_point: Vector2, p_mode: TauLineConfig.InterpolationMode) -> void: - if p_run.size() == 0 or p_mode == TauLineConfig.InterpolationMode.LINEAR: + # SMOOTH_MONOTONE buffers raw sample points untouched: cubic resampling + # requires the full neighborhood of every sample to compute tangents and + # is therefore deferred to _finalize_run(). + if p_run.size() == 0 or p_mode == TauLineConfig.InterpolationMode.LINEAR or p_mode == TauLineConfig.InterpolationMode.SMOOTH_MONOTONE: p_run.append(p_point) return @@ -225,6 +233,208 @@ class LineRenderer extends Control: p_run.append(p_point) + #################################################################################################### + # Smooth-monotone (Fritsch-Carlson) resampling + #################################################################################################### + + # Number of sub-segments inserted between two consecutive samples by + # SMOOTH_MONOTONE. The value balances visual smoothness on a typical + # screen against the per-segment cost paid by draw_polyline(). + const _SMOOTH_SUBDIVISIONS: int = 16 + + + # Draw the polyline for one buffered run. + # For LINEAR and the step modes the buffered run is already the final polyline. + # For SMOOTH_MONOTONE the run is first replaced by its Fritsch-Carlson piecewise cubic resampling. + # Runs of fewer than two points are silently dropped. + func _finalize_run(p_run: PackedVector2Array, p_color: Color, p_width_px: float, p_mode: TauLineConfig.InterpolationMode) -> void: + var polyline: PackedVector2Array = p_run + if p_mode == TauLineConfig.InterpolationMode.SMOOTH_MONOTONE and p_run.size() > 2: + polyline = _resample_smooth_monotone(p_run) + if polyline.size() >= 2: + draw_polyline(polyline, p_color, p_width_px) + + + # Builds the Fritsch-Carlson piecewise cubic Hermite curve through p_points + # and returns it sampled at _SMOOTH_SUBDIVISIONS sub-segments per input + # segment. Operates in screen space (the input is already in pixels), which + # keeps the curve visually smooth regardless of axis scale. + # + # The algorithm requires strictly monotonic X. The expected case is + # monotonically increasing screen X, but a user-inverted X axis produces + # monotonically decreasing screen X. Both directions are accepted: the + # input is processed internally on a strictly increasing X copy and the + # output is reversed back when needed. Consecutive points sharing the same + # screen X are dropped since the secant slope is undefined at h = 0. + # Inputs that are not monotonic in either direction fall back to the raw + # polyline for that run and emit a one-shot warning. + func _resample_smooth_monotone(p_points: PackedVector2Array) -> PackedVector2Array: + var direction := _detect_monotonic_x_direction(p_points) + if direction == 0: + if not _smooth_non_monotonic_warned: + push_warning("LineRenderer: SMOOTH_MONOTONE received samples whose screen X is not monotonic. Falling back to a straight polyline for the affected run. Use LINEAR interpolation if your data does not have a monotonic X parameter.") + _smooth_non_monotonic_warned = true + return p_points + + var ascending: bool = direction > 0 + + # Build strictly increasing X arrays, dropping flat-X duplicates. + var xs := PackedFloat32Array() + var ys := PackedFloat32Array() + var input_count := p_points.size() + if ascending: + xs.append(p_points[0].x) + ys.append(p_points[0].y) + for i in range(1, input_count): + if p_points[i].x > xs[xs.size() - 1]: + xs.append(p_points[i].x) + ys.append(p_points[i].y) + else: + xs.append(p_points[input_count - 1].x) + ys.append(p_points[input_count - 1].y) + for i in range(input_count - 2, -1, -1): + if p_points[i].x > xs[xs.size() - 1]: + xs.append(p_points[i].x) + ys.append(p_points[i].y) + + var n := xs.size() + if n < 2: + # All inputs collapsed to a single screen X. Nothing to draw. + return PackedVector2Array() + if n == 2: + # Two distinct X values produce a straight line through Hermite + # with both tangents equal to the secant slope. Short-circuit. + var trivial := PackedVector2Array() + trivial.append(Vector2(xs[0], ys[0])) + trivial.append(Vector2(xs[1], ys[1])) + if not ascending: + trivial.reverse() + return trivial + + var tangents := _fritsch_carlson_tangents(xs, ys) + + var out := PackedVector2Array() + # Pre-size the output for speed: n-1 segments times subdivisions plus + # the very first sample. + out.resize(1 + (n - 1) * _SMOOTH_SUBDIVISIONS) + out[0] = Vector2(xs[0], ys[0]) + + var write_index: int = 1 + var inv_subs: float = 1.0 / float(_SMOOTH_SUBDIVISIONS) + for k in range(n - 1): + var x0: float = xs[k] + var x1: float = xs[k + 1] + var y0: float = ys[k] + var y1: float = ys[k + 1] + var h: float = x1 - x0 + var m0: float = tangents[k] + var m1: float = tangents[k + 1] + + # Sub-points at t = 1/N, 2/N, ..., 1. The endpoint t=1 is the next + # sample, included here so the next segment begins at t=1/N. + for s in range(1, _SMOOTH_SUBDIVISIONS + 1): + var t: float = float(s) * inv_subs + var t2: float = t * t + var t3: float = t2 * t + var h00: float = 2.0 * t3 - 3.0 * t2 + 1.0 + var h10: float = t3 - 2.0 * t2 + t + var h01: float = -2.0 * t3 + 3.0 * t2 + var h11: float = t3 - t2 + var x: float = x0 + t * h + var y: float = h00 * y0 + h10 * h * m0 + h01 * y1 + h11 * h * m1 + out[write_index] = Vector2(x, y) + write_index += 1 + + if not ascending: + out.reverse() + return out + + + # Returns +1 if screen X is strictly monotonically increasing across the + # whole run, -1 if strictly decreasing, 0 if neither (some pair has equal + # X) or the run has fewer than 2 points. Equal consecutive X is allowed + # only as a single-point run (n < 2 case). + func _detect_monotonic_x_direction(p_points: PackedVector2Array) -> int: + var n := p_points.size() + if n < 2: + return 0 + var first_diff: float = p_points[1].x - p_points[0].x + # Find the first non-zero diff to set the direction. Equal-X pairs in + # the middle of an otherwise increasing run are tolerated and dropped + # by the caller, so they do not invalidate monotonicity here. + var direction: int = 0 + if first_diff > 0.0: + direction = 1 + elif first_diff < 0.0: + direction = -1 + for i in range(2, n): + var diff: float = p_points[i].x - p_points[i - 1].x + if diff > 0.0: + if direction == -1: + return 0 + direction = 1 + elif diff < 0.0: + if direction == 1: + return 0 + direction = -1 + return direction + + + # Fritsch-Carlson tangent computation. Returns one tangent per input point + # such that the resulting piecewise cubic Hermite curve is monotone where + # the data is monotone and never overshoots its data values. + # + # Reference: Fritsch, F. N. and Carlson, R. E. (1980), "Monotone Piecewise + # Cubic Interpolation", SIAM Journal on Numerical Analysis, 17 (2): 238-246. + # + # Precondition: xs is strictly increasing and xs.size() == ys.size() >= 2. + func _fritsch_carlson_tangents(p_xs: PackedFloat32Array, p_ys: PackedFloat32Array) -> PackedFloat32Array: + var n := p_xs.size() + var m := PackedFloat32Array() + m.resize(n) + + # Secant slopes between consecutive samples. + var d := PackedFloat32Array() + d.resize(n - 1) + for k in range(n - 1): + d[k] = (p_ys[k + 1] - p_ys[k]) / (p_xs[k + 1] - p_xs[k]) + + # Initial tangents: endpoint tangents copy the adjacent secant slope. + # Interior tangents are zero at extrema and the average of the two + # adjacent secants otherwise. + m[0] = d[0] + m[n - 1] = d[n - 2] + for k in range(1, n - 1): + if d[k - 1] * d[k] <= 0.0: + m[k] = 0.0 + else: + m[k] = 0.5 * (d[k - 1] + d[k]) + + # Fritsch-Carlson monotonicity correction. For each segment, project + # the (m[k], m[k+1]) pair onto the disk of radius 3 in the (alpha, + # beta) plane to guarantee no overshoot. + for k in range(n - 1): + if d[k] == 0.0: + m[k] = 0.0 + m[k + 1] = 0.0 + continue + var alpha: float = m[k] / d[k] + var beta: float = m[k + 1] / d[k] + if alpha < 0.0: + m[k] = 0.0 + alpha = 0.0 + if beta < 0.0: + m[k + 1] = 0.0 + beta = 0.0 + var sq: float = alpha * alpha + beta * beta + if sq > 9.0: + var tau: float = 3.0 / sqrt(sq) + m[k] = tau * alpha * d[k] + m[k + 1] = tau * beta * d[k] + + return m + + #################################################################################################### # Series helpers #################################################################################################### diff --git a/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_linear.gd b/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_linear.gd index 80466dc..463c87a 100644 --- a/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_linear.gd +++ b/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_linear.gd @@ -14,7 +14,9 @@ func _ready() -> void: _setup_test_10() _setup_test_11() _setup_test_12() - + _setup_test_13() + _setup_test_14() + _setup_test_15() #################################################################################################### # Helpers @@ -23,7 +25,7 @@ func _ready() -> void: func make_shared_x_continuous_plot(p_plot: TauPlot, p_title: String, p_interpolation: TauLineConfig.InterpolationMode) -> void: var series_names := PackedStringArray(["Series A"]) var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0]) - var y_a := PackedFloat64Array([10.0, 20.0, 30.0, 40.0, 50.0]) + var y_a := PackedFloat64Array([10.0, 20.0, 50.0, 65.0, 70.0]) var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a]) @@ -38,6 +40,7 @@ func make_shared_x_continuous_plot(p_plot: TauPlot, p_title: String, p_interpola y_axis.type = TauAxisConfig.Type.CONTINUOUS y_axis.scale = TauAxisConfig.Scale.LINEAR y_axis.tick_count_preferred = 10 + y_axis.overlap_strategy = TauAxisConfig.OverlapStrategy.NONE var line_config := TauLineConfig.new() line_config.mode = TauLineConfig.LineMode.INDEPENDENT @@ -64,7 +67,7 @@ func make_shared_x_continuous_plot(p_plot: TauPlot, p_title: String, p_interpola func make_per_series_x_continuous_plot(p_plot: TauPlot, p_title: String, p_interpolation: TauLineConfig.InterpolationMode) -> void: var series_names := PackedStringArray(["Series A"]) var x_a := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0]) - var y_a := PackedFloat64Array([10.0, 20.0, 30.0, 40.0, 50.0]) + var y_a := PackedFloat64Array([10.0, 20.0, 50.0, 65.0, 70.0]) var dataset := TauPlot.Dataset.make_per_series_x_continuous(series_names, [x_a], [y_a]) @@ -79,6 +82,7 @@ func make_per_series_x_continuous_plot(p_plot: TauPlot, p_title: String, p_inter y_axis.type = TauAxisConfig.Type.CONTINUOUS y_axis.scale = TauAxisConfig.Scale.LINEAR y_axis.tick_count_preferred = 10 + y_axis.overlap_strategy = TauAxisConfig.OverlapStrategy.NONE var line_config := TauLineConfig.new() line_config.mode = TauLineConfig.LineMode.INDEPENDENT @@ -105,7 +109,7 @@ func make_per_series_x_continuous_plot(p_plot: TauPlot, p_title: String, p_inter func make_shared_x_categorical_plot(p_plot: TauPlot, p_title: String, p_interpolation: TauLineConfig.InterpolationMode) -> void: var series_names := PackedStringArray(["Series A"]) var x := PackedStringArray(["One", "Two", "Three", "Four", "Five"]) - var y_a := PackedFloat64Array([10.0, 20.0, 30.0, 40.0, 50.0]) + var y_a := PackedFloat64Array([10.0, 20.0, 50.0, 65.0, 70.0]) var dataset := TauPlot.Dataset.make_shared_x_categorical(series_names, x, [y_a]) @@ -120,6 +124,7 @@ func make_shared_x_categorical_plot(p_plot: TauPlot, p_title: String, p_interpol y_axis.type = TauAxisConfig.Type.CONTINUOUS y_axis.scale = TauAxisConfig.Scale.LINEAR y_axis.tick_count_preferred = 10 + y_axis.overlap_strategy = TauAxisConfig.OverlapStrategy.NONE var line_config := TauLineConfig.new() line_config.mode = TauLineConfig.LineMode.INDEPENDENT @@ -226,3 +231,24 @@ func _setup_test_11() -> void: func _setup_test_12() -> void: make_shared_x_categorical_plot(%TestPlot12, "[STEP_AFTER] SHARED_X + CATEGORICAL", TauLineConfig.InterpolationMode.STEP_AFTER) + +#################################################################################################### +# Test 13 +#################################################################################################### + +func _setup_test_13() -> void: + make_shared_x_continuous_plot(%TestPlot13, "[SMOOTH_MONOTONE] SHARED_X + CONTINUOUS", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE) + +#################################################################################################### +# Test 14 +#################################################################################################### + +func _setup_test_14() -> void: + make_per_series_x_continuous_plot(%TestPlot14, "[SMOOTH_MONOTONE] PER_SERIES_X + CONTINUOUS", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE) + +#################################################################################################### +# Test 15 +#################################################################################################### + +func _setup_test_15() -> void: + make_shared_x_categorical_plot(%TestPlot15, "[SMOOTH_MONOTONE] SHARED_X + CATEGORICAL", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE) diff --git a/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_linear.tscn b/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_linear.tscn index 57f6107..8db02d7 100644 --- a/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_linear.tscn +++ b/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_linear.tscn @@ -155,3 +155,36 @@ script = ExtResource("2_nry5o") title = "[STEP_AFTER] SHARED_X + CATEGORICAL" legend_enabled = false metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot13" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_nry5o") +title = "[SMOOTH_MONOTONE] SHARED_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot14" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_nry5o") +title = "[SMOOTH_MONOTONE] PER_SERIES_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot15" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_nry5o") +title = "[SMOOTH_MONOTONE] SHARED_X + CATEGORICAL" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" diff --git a/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_logarithmic.gd b/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_logarithmic.gd index 564bc96..1a24559 100644 --- a/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_logarithmic.gd +++ b/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_logarithmic.gd @@ -10,7 +10,8 @@ func _ready() -> void: _setup_test_6() _setup_test_7() _setup_test_8() - + _setup_test_9() + _setup_test_10() #################################################################################################### # Helpers @@ -19,7 +20,7 @@ func _ready() -> void: func make_shared_x_continuous_plot(p_plot: TauPlot, p_title: String, p_interpolation: TauLineConfig.InterpolationMode) -> void: var series_names := PackedStringArray(["Series A"]) var x := PackedFloat64Array([pow(10, -2), pow(10, -1), pow(10, 0), pow(10, 1), pow(10, 2)]) - var y_a := PackedFloat64Array([10.0, 20.0, 30.0, 40.0, 50.0]) + var y_a := PackedFloat64Array([10.0, 20.0, 50.0, 65.0, 70.0]) var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a]) @@ -35,6 +36,7 @@ func make_shared_x_continuous_plot(p_plot: TauPlot, p_title: String, p_interpola y_axis.type = TauAxisConfig.Type.CONTINUOUS y_axis.scale = TauAxisConfig.Scale.LINEAR y_axis.tick_count_preferred = 10 + y_axis.overlap_strategy = TauAxisConfig.OverlapStrategy.NONE var line_config := TauLineConfig.new() line_config.mode = TauLineConfig.LineMode.INDEPENDENT @@ -61,7 +63,7 @@ func make_shared_x_continuous_plot(p_plot: TauPlot, p_title: String, p_interpola func make_per_series_x_continuous_plot(p_plot: TauPlot, p_title: String, p_interpolation: TauLineConfig.InterpolationMode) -> void: var series_names := PackedStringArray(["Series A"]) var x_a := PackedFloat64Array([pow(10, -2), pow(10, -1), pow(10, 0), pow(10, 1), pow(10, 2)]) - var y_a := PackedFloat64Array([10.0, 20.0, 30.0, 40.0, 50.0]) + var y_a := PackedFloat64Array([10.0, 20.0, 50.0, 65.0, 70.0]) var dataset := TauPlot.Dataset.make_per_series_x_continuous(series_names, [x_a], [y_a]) @@ -77,6 +79,7 @@ func make_per_series_x_continuous_plot(p_plot: TauPlot, p_title: String, p_inter y_axis.type = TauAxisConfig.Type.CONTINUOUS y_axis.scale = TauAxisConfig.Scale.LINEAR y_axis.tick_count_preferred = 10 + y_axis.overlap_strategy = TauAxisConfig.OverlapStrategy.NONE var line_config := TauLineConfig.new() line_config.mode = TauLineConfig.LineMode.INDEPENDENT @@ -155,3 +158,17 @@ func _setup_test_7() -> void: func _setup_test_8() -> void: make_per_series_x_continuous_plot(%TestPlot8, "[STEP_AFTER] PER_SERIES_X + CONTINUOUS", TauLineConfig.InterpolationMode.STEP_AFTER) + +#################################################################################################### +# Test 9 +#################################################################################################### + +func _setup_test_9() -> void: + make_shared_x_continuous_plot(%TestPlot9, "[SMOOTH_MONOTONE] SHARED_X + CONTINUOUS", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE) + +#################################################################################################### +# Test 10 +#################################################################################################### + +func _setup_test_10() -> void: + make_per_series_x_continuous_plot(%TestPlot10, "[SMOOTH_MONOTONE] PER_SERIES_X + CONTINUOUS", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE) diff --git a/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_logarithmic.tscn b/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_logarithmic.tscn index cb19e48..5842c19 100644 --- a/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_logarithmic.tscn +++ b/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_logarithmic.tscn @@ -111,3 +111,25 @@ script = ExtResource("2_u5as0") title = "[STEP_AFTER] PER_SERIES_X + CONTINUOUS" legend_enabled = false metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot9" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_u5as0") +title = "[STEP_AFTER] PER_SERIES_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot10" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_u5as0") +title = "[STEP_AFTER] PER_SERIES_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" From dde3dde1939a1165715be00d8df6b3cc80cc2421 Mon Sep 17 00:00:00 2001 From: ze2j <46556066+ze2j@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:51:53 +0200 Subject: [PATCH 07/23] Implement dash lines --- addons/tau-plot/plot/xy/line/line_renderer.gd | 97 ++++++++++++-- addons/tau-plot/plot/xy/line/line_style.gd | 18 ++- .../tests/line/dash/test_line_dash.gd | 122 ++++++++++++++++++ .../tests/line/dash/test_line_dash.gd.uid | 1 + .../tests/line/dash/test_line_dash.tscn | 115 +++++++++++++++++ 5 files changed, 338 insertions(+), 15 deletions(-) create mode 100644 addons/tau-plot/tests/line/dash/test_line_dash.gd create mode 100644 addons/tau-plot/tests/line/dash/test_line_dash.gd.uid create mode 100644 addons/tau-plot/tests/line/dash/test_line_dash.tscn diff --git a/addons/tau-plot/plot/xy/line/line_renderer.gd b/addons/tau-plot/plot/xy/line/line_renderer.gd index 361d74e..a6912f7 100644 --- a/addons/tau-plot/plot/xy/line/line_renderer.gd +++ b/addons/tau-plot/plot/xy/line/line_renderer.gd @@ -11,8 +11,8 @@ const LineVisualAttributes := preload("res://addons/tau-plot/plot/xy/line/line_v # Draws line overlays from an XYLayout + Dataset. # # This renderer reads all samples through the Dataset public API (no direct -# buffer/series access). One contiguous run of valid samples produces one -# draw_polyline() call per series. +# buffer/series access). Each contiguous run of valid samples is drawn with +# one Godot draw call. # # Runtime behavior: # - NaN and Inf X or Y values are treated according to TauLineConfig.gap_policy. @@ -27,11 +27,22 @@ const LineVisualAttributes := preload("res://addons/tau-plot/plot/xy/line/line_v # intermediate points in screen space into the polyline. SMOOTH_MONOTONE # replaces each segment with a fixed number of sub-samples from a # Fritsch-Carlson piecewise cubic Hermite curve evaluated in screen space. -# Whichever interpolation is active, a contiguous run is still drawn with a +# +# Rendering path selection: +# - Path 1, fast path: TauLineStyle.dash_px == 0. The run is drawn with a # single draw_polyline() call. +# - Path 2, dashed batched path: TauLineStyle.dash_px > 0. Dash phase is +# precomputed across the full run, the "on" intervals are collected into a +# flat segment array, and the run is drawn with a single draw_multiline() +# call. The dash phase is continuous across all segments of the polyline. # # LineValidator is expected to enforce binding-level typing constraints. class LineRenderer extends Control: + # Number of sub-segments inserted between two consecutive samples by + # SMOOTH_MONOTONE. The value balances visual smoothness on a typical + # screen against the per-segment cost paid by draw_polyline(). + const _SMOOTH_SUBDIVISIONS: int = 16 + var _layout: XYLayout = null var _dataset: Dataset = null var _line_config: TauLineConfig = null @@ -233,27 +244,85 @@ class LineRenderer extends Control: p_run.append(p_point) - #################################################################################################### - # Smooth-monotone (Fritsch-Carlson) resampling - #################################################################################################### - - # Number of sub-segments inserted between two consecutive samples by - # SMOOTH_MONOTONE. The value balances visual smoothness on a typical - # screen against the per-segment cost paid by draw_polyline(). - const _SMOOTH_SUBDIVISIONS: int = 16 - - # Draw the polyline for one buffered run. # For LINEAR and the step modes the buffered run is already the final polyline. # For SMOOTH_MONOTONE the run is first replaced by its Fritsch-Carlson piecewise cubic resampling. # Runs of fewer than two points are silently dropped. + # + # When TauLineStyle.dash_px is 0, the polyline is emitted via path 1 + # (draw_polyline). Otherwise it is emitted via path 2: dash phase is + # precomputed across the whole polyline and the resulting "on" intervals + # are flushed in a single draw_multiline() call. func _finalize_run(p_run: PackedVector2Array, p_color: Color, p_width_px: float, p_mode: TauLineConfig.InterpolationMode) -> void: var polyline: PackedVector2Array = p_run if p_mode == TauLineConfig.InterpolationMode.SMOOTH_MONOTONE and p_run.size() > 2: polyline = _resample_smooth_monotone(p_run) - if polyline.size() >= 2: + if polyline.size() < 2: + return + + var dash_px: int = max(_line_style.dash_px, 0) + if dash_px <= 0: draw_polyline(polyline, p_color, p_width_px) + else: + _draw_dashed_polyline(polyline, p_color, p_width_px, float(dash_px)) + + # Builds the flat segment array of "on" dash intervals along p_polyline and + # emits it with a single draw_multiline() call. The dash period is + # 2 * p_dash_px (one "on" length followed by one "off" length of equal + # size). Dash phase is tracked as a single scalar that advances along the + # polyline arc length, so the pattern is continuous across consecutive + # segments and does not reset at sample positions. + # + # Degenerate segments (zero length) are skipped: they cannot carry any + # dash and do not advance the phase. + func _draw_dashed_polyline(p_polyline: PackedVector2Array, p_color: Color, p_width_px: float, p_dash_px: float) -> void: + var period: float = p_dash_px * 2.0 + var n := p_polyline.size() + var phase: float = 0.0 + var segments := PackedVector2Array() + + for i in range(n - 1): + var seg_start: Vector2 = p_polyline[i] + var seg_end: Vector2 = p_polyline[i + 1] + var seg_vec: Vector2 = seg_end - seg_start + var seg_len: float = seg_vec.length() + if seg_len <= 0.0: + continue + + var seg_dir: Vector2 = seg_vec / seg_len + + # Position along the current segment, in pixels from seg_start. + # Phase 0..p_dash_px is "on", p_dash_px..period is "off". + var pos: float = 0.0 + while pos < seg_len: + var phase_in_period: float = phase + if phase_in_period < p_dash_px: + # Currently inside an "on" interval. + var remaining_on: float = p_dash_px - phase_in_period + var on_end: float = min(pos + remaining_on, seg_len) + segments.append(seg_start + seg_dir * pos) + segments.append(seg_start + seg_dir * on_end) + var consumed: float = on_end - pos + phase += consumed + pos = on_end + else: + # Currently inside an "off" interval. + var remaining_off: float = period - phase_in_period + var off_end: float = min(pos + remaining_off, seg_len) + var consumed_off: float = off_end - pos + phase += consumed_off + pos = off_end + + if phase >= period: + phase -= period + + if segments.size() >= 2: + draw_multiline(segments, p_color, p_width_px) + + #################################################################################################### + # Smooth-monotone (Fritsch-Carlson) resampling + #################################################################################################### # Builds the Fritsch-Carlson piecewise cubic Hermite curve through p_points # and returns it sampled at _SMOOTH_SUBDIVISIONS sub-segments per input diff --git a/addons/tau-plot/plot/xy/line/line_style.gd b/addons/tau-plot/plot/xy/line/line_style.gd index 9d90fe3..0b293bc 100644 --- a/addons/tau-plot/plot/xy/line/line_style.gd +++ b/addons/tau-plot/plot/xy/line/line_style.gd @@ -21,6 +21,12 @@ class_name TauLineStyle extends Resource const DEFAULT_LINE_WIDTH_PX: float = 2.0 @export var line_width_px: float = DEFAULT_LINE_WIDTH_PX +## Dash length in pixels. A value of 0 produces a solid line. Any positive +## value switches the line to dashed rendering with alternating on-off +## segments of that pixel length. +const DEFAULT_LINE_DASH_PX: int = 0 +@export var dash_px: int = DEFAULT_LINE_DASH_PX + #################################################################################################### # Cascade: theme loading (layer 2) @@ -39,13 +45,18 @@ func load_from_theme(p_control: Control, p_pane_index: int) -> void: push_error("TauLineStyle.load_from_theme(): control is null") return - # line_width_px if p_control.has_theme_constant(&"line_width_px"): line_width_px = max(float(p_control.get_theme_constant(&"line_width_px")), 0.0) var indexed_width_key := StringName("line_width_px_%d" % p_pane_index) if p_control.has_theme_constant(indexed_width_key): line_width_px = max(float(p_control.get_theme_constant(indexed_width_key)), 0.0) + if p_control.has_theme_constant(&"line_dash_px"): + dash_px = max(int(p_control.get_theme_constant(&"line_dash_px")), 0) + var indexed_dash_key := StringName("line_dash_px_%d" % p_pane_index) + if p_control.has_theme_constant(indexed_dash_key): + dash_px = max(int(p_control.get_theme_constant(indexed_dash_key)), 0) + #################################################################################################### # Cascade: user overrides (layer 3) @@ -61,6 +72,9 @@ func apply_overrides_from(p_user_style: TauLineStyle) -> void: if p_user_style.line_width_px != DEFAULT_LINE_WIDTH_PX: line_width_px = p_user_style.line_width_px + if p_user_style.dash_px != DEFAULT_LINE_DASH_PX: + dash_px = p_user_style.dash_px + #################################################################################################### # Full cascade resolution @@ -96,6 +110,8 @@ func is_equal_to(p_other: TauLineStyle) -> bool: return false if line_width_px != p_other.line_width_px: return false + if dash_px != p_other.dash_px: + return false return true diff --git a/addons/tau-plot/tests/line/dash/test_line_dash.gd b/addons/tau-plot/tests/line/dash/test_line_dash.gd new file mode 100644 index 0000000..d45021e --- /dev/null +++ b/addons/tau-plot/tests/line/dash/test_line_dash.gd @@ -0,0 +1,122 @@ +@tool +extends Control + +func _ready() -> void: + _setup_test_1() + _setup_test_2() + _setup_test_3() + _setup_test_4() + _setup_test_5() + _setup_test_6() + _setup_test_7() + _setup_test_8() + _setup_test_9() + +#################################################################################################### +# Helpers +#################################################################################################### + +func make_shared_x_continuous_plot(p_plot: TauPlot, p_title: String, p_interpolation: TauLineConfig.InterpolationMode, p_dash_px: int) -> void: + var series_names := PackedStringArray(["Series A"]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0]) + var y_a := PackedFloat64Array([10.0, 20.0, 50.0, 65.0, 70.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a]) + + p_plot.title = p_title + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.tick_count_preferred = 10 + y_axis.overlap_strategy = TauAxisConfig.OverlapStrategy.NONE + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.INDEPENDENT + line_config.interpolation_mode = p_interpolation + line_config.style.dash_px = p_dash_px + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a] + + p_plot.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 1 +#################################################################################################### + +func _setup_test_1() -> void: + make_shared_x_continuous_plot(%TestPlot1, "LINEAR + dash = 2", TauLineConfig.InterpolationMode.LINEAR, 2) + +#################################################################################################### +# Test 2 +#################################################################################################### + +func _setup_test_2() -> void: + make_shared_x_continuous_plot(%TestPlot2, "LINEAR + dash = 4", TauLineConfig.InterpolationMode.LINEAR, 4) + +#################################################################################################### +# Test 3 +#################################################################################################### + +func _setup_test_3() -> void: + make_shared_x_continuous_plot(%TestPlot3, "LINEAR + dash = 8", TauLineConfig.InterpolationMode.LINEAR, 8) + +#################################################################################################### +# Test 4 +#################################################################################################### + +func _setup_test_4() -> void: + make_shared_x_continuous_plot(%TestPlot4, "STEP_MIDDLE + dash = 2", TauLineConfig.InterpolationMode.STEP_MIDDLE, 2) + +#################################################################################################### +# Test 5 +#################################################################################################### + +func _setup_test_5() -> void: + make_shared_x_continuous_plot(%TestPlot5, "STEP_MIDDLE + dash = 4", TauLineConfig.InterpolationMode.STEP_MIDDLE, 4) + +#################################################################################################### +# Test 6 +#################################################################################################### + +func _setup_test_6() -> void: + make_shared_x_continuous_plot(%TestPlot6, "STEP_MIDDLE + dash = 8", TauLineConfig.InterpolationMode.STEP_MIDDLE, 8) + +#################################################################################################### +# Test 7 +#################################################################################################### + +func _setup_test_7() -> void: + make_shared_x_continuous_plot(%TestPlot7, "SMOOTH_MONOTONE + dash = 2", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE, 2) + +#################################################################################################### +# Test 8 +#################################################################################################### + +func _setup_test_8() -> void: + make_shared_x_continuous_plot(%TestPlot8, "SMOOTH_MONOTONE + dash = 4", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE, 4) + +#################################################################################################### +# Test 9 +#################################################################################################### + +func _setup_test_9() -> void: + make_shared_x_continuous_plot(%TestPlot9, "SMOOTH_MONOTONE + dash = 8", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE, 8) diff --git a/addons/tau-plot/tests/line/dash/test_line_dash.gd.uid b/addons/tau-plot/tests/line/dash/test_line_dash.gd.uid new file mode 100644 index 0000000..8828a78 --- /dev/null +++ b/addons/tau-plot/tests/line/dash/test_line_dash.gd.uid @@ -0,0 +1 @@ +uid://ccebd3cs615t7 diff --git a/addons/tau-plot/tests/line/dash/test_line_dash.tscn b/addons/tau-plot/tests/line/dash/test_line_dash.tscn new file mode 100644 index 0000000..d7b61ae --- /dev/null +++ b/addons/tau-plot/tests/line/dash/test_line_dash.tscn @@ -0,0 +1,115 @@ +[gd_scene load_steps=3 format=3 uid="uid://f5gbq1v64in0"] + +[ext_resource type="Script" uid="uid://ccebd3cs615t7" path="res://addons/tau-plot/tests/line/dash/test_line_dash.gd" id="1_4q4uh"] +[ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_mp1bp"] + +[node name="Bars" type="VBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_4q4uh") + +[node name="Title" type="Label" parent="."] +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +text = "Different dash lines" +horizontal_alignment = 1 + +[node name="GridContainer" type="GridContainer" parent="."] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/h_separation = 0 +theme_override_constants/v_separation = 0 +columns = 3 + +[node name="TestPlot1" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_mp1bp") +title = "LINEAR + dash = 2 for series A" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot2" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_mp1bp") +title = "LINEAR + dash = 4 for series A" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot3" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_mp1bp") +title = "LINEAR + dash = 8 for series A" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot4" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_mp1bp") +title = "STEP_MIDDLE + dash = 2 for series A" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot5" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_mp1bp") +title = "STEP_MIDDLE + dash = 4 for series A" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot6" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_mp1bp") +title = "STEP_MIDDLE + dash = 8 for series A" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot7" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_mp1bp") +title = "SMOOTH_MONOTONE + dash = 2 for series A" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot8" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_mp1bp") +title = "SMOOTH_MONOTONE + dash = 4 for series A" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot9" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_mp1bp") +title = "SMOOTH_MONOTONE + dash = 8 for series A" +metadata/_custom_type_script = "uid://dnhvsf2wip771" From 7f6e160ae2c0d92a9759ac3c2ea4facffcfba748 Mon Sep 17 00:00:00 2001 From: ze2j <46556066+ze2j@users.noreply.github.com> Date: Mon, 27 Apr 2026 18:17:32 +0200 Subject: [PATCH 08/23] Per-series dash length refactoring --- addons/tau-plot/plot/xy/line/line_renderer.gd | 45 +++++---- addons/tau-plot/plot/xy/line/line_style.gd | 95 +++++++++++++++---- .../tests/line/dash/test_line_dash.gd | 34 ++++--- .../test_line_interpolation_logarithmic.tscn | 4 +- 4 files changed, 128 insertions(+), 50 deletions(-) diff --git a/addons/tau-plot/plot/xy/line/line_renderer.gd b/addons/tau-plot/plot/xy/line/line_renderer.gd index a6912f7..6903bfb 100644 --- a/addons/tau-plot/plot/xy/line/line_renderer.gd +++ b/addons/tau-plot/plot/xy/line/line_renderer.gd @@ -28,13 +28,19 @@ const LineVisualAttributes := preload("res://addons/tau-plot/plot/xy/line/line_v # replaces each segment with a fixed number of sub-samples from a # Fritsch-Carlson piecewise cubic Hermite curve evaluated in screen space. # -# Rendering path selection: -# - Path 1, fast path: TauLineStyle.dash_px == 0. The run is drawn with a -# single draw_polyline() call. -# - Path 2, dashed batched path: TauLineStyle.dash_px > 0. Dash phase is -# precomputed across the full run, the "on" intervals are collected into a -# flat segment array, and the run is drawn with a single draw_multiline() -# call. The dash phase is continuous across all segments of the polyline. +# Rendering path selection (per series): +# - The active dash length for a series is resolved from +# TauLineStyle.dash_lengths_px through the helper +# TauLineStyle.get_series_dash_px(global_series_index). Path selection is +# therefore per series: two series in the same overlay can run on +# different paths in the same frame. +# - Path 1, fast path: resolved per-series dash length is 0. The run is +# drawn with a single draw_polyline() call. +# - Path 2, dashed batched path: resolved per-series dash length is positive. +# Dash phase is precomputed across the full run, the "on" intervals are +# collected into a flat segment array, and the run is drawn with a single +# draw_multiline() call. The dash phase is continuous across all segments +# of the polyline. # # LineValidator is expected to enforce binding-level typing constraints. class LineRenderer extends Control: @@ -166,6 +172,7 @@ class LineRenderer extends Control: var series_id := _get_line_series_id(p_series_index) var global_series_index := _get_global_series_index(p_series_index) var color := _resolve_series_color(global_series_index) + var dash_px: int = _line_style.get_series_dash_px(global_series_index) var y_axis_id := _get_y_axis_id_for_series(series_id) var bridge: bool = _line_config.gap_policy == TauLineConfig.GapPolicy.BRIDGE var interpolation: TauLineConfig.InterpolationMode = _line_config.interpolation_mode @@ -179,14 +186,14 @@ class LineRenderer extends Control: var x_value: float = float(_dataset.get_shared_x(i)) if is_shared_x else float(_dataset.get_series_x(series_id, i)) if is_nan(x_value) or is_inf(x_value) or not _is_x_value_valid_for_scale(x_value): if not bridge: - _finalize_run(run, color, p_width_px, interpolation) + _finalize_run(run, color, p_width_px, interpolation, dash_px) run = PackedVector2Array() continue var y_value := _dataset.get_series_y(series_id, i) if is_nan(y_value) or is_inf(y_value) or not _is_y_value_valid_for_scale(series_id, y_value): if not bridge: - _finalize_run(run, color, p_width_px, interpolation) + _finalize_run(run, color, p_width_px, interpolation, dash_px) run = PackedVector2Array() continue @@ -194,13 +201,14 @@ class LineRenderer extends Control: var y_px := _layout.map_y_to_px(_pane_index, y_value, y_axis_id) _append_with_interpolation(run, _layout.map_point_to_screen(x_px, y_px), interpolation) - _finalize_run(run, color, p_width_px, interpolation) + _finalize_run(run, color, p_width_px, interpolation, dash_px) func _draw_series_categorical(p_series_index: int, p_width_px: float) -> void: var series_id := _get_line_series_id(p_series_index) var global_series_index := _get_global_series_index(p_series_index) var color := _resolve_series_color(global_series_index) + var dash_px: int = _line_style.get_series_dash_px(global_series_index) var y_axis_id := _get_y_axis_id_for_series(series_id) var bridge: bool = _line_config.gap_policy == TauLineConfig.GapPolicy.BRIDGE var interpolation: TauLineConfig.InterpolationMode = _line_config.interpolation_mode @@ -212,7 +220,7 @@ class LineRenderer extends Control: var y_value := _dataset.get_series_y(series_id, cat_idx) if is_nan(y_value) or is_inf(y_value) or not _is_y_value_valid_for_scale(series_id, y_value): if not bridge: - _finalize_run(run, color, p_width_px, interpolation) + _finalize_run(run, color, p_width_px, interpolation, dash_px) run = PackedVector2Array() continue @@ -220,7 +228,7 @@ class LineRenderer extends Control: var y_px := _layout.map_y_to_px(_pane_index, y_value, y_axis_id) _append_with_interpolation(run, _layout.map_point_to_screen(x_px, y_px), interpolation) - _finalize_run(run, color, p_width_px, interpolation) + _finalize_run(run, color, p_width_px, interpolation, dash_px) func _append_with_interpolation(p_run: PackedVector2Array, p_point: Vector2, p_mode: TauLineConfig.InterpolationMode) -> void: @@ -249,18 +257,19 @@ class LineRenderer extends Control: # For SMOOTH_MONOTONE the run is first replaced by its Fritsch-Carlson piecewise cubic resampling. # Runs of fewer than two points are silently dropped. # - # When TauLineStyle.dash_px is 0, the polyline is emitted via path 1 - # (draw_polyline). Otherwise it is emitted via path 2: dash phase is - # precomputed across the whole polyline and the resulting "on" intervals - # are flushed in a single draw_multiline() call. - func _finalize_run(p_run: PackedVector2Array, p_color: Color, p_width_px: float, p_mode: TauLineConfig.InterpolationMode) -> void: + # When p_dash_px is 0, the polyline is emitted via path 1 (draw_polyline). + # Otherwise it is emitted via path 2: dash phase is precomputed across the + # whole polyline and the resulting "on" intervals are flushed in a single + # draw_multiline() call. p_dash_px is the resolved per-series dash length + # obtained from TauLineStyle.get_series_dash_px(). + func _finalize_run(p_run: PackedVector2Array, p_color: Color, p_width_px: float, p_mode: TauLineConfig.InterpolationMode, p_dash_px: int) -> void: var polyline: PackedVector2Array = p_run if p_mode == TauLineConfig.InterpolationMode.SMOOTH_MONOTONE and p_run.size() > 2: polyline = _resample_smooth_monotone(p_run) if polyline.size() < 2: return - var dash_px: int = max(_line_style.dash_px, 0) + var dash_px: int = max(p_dash_px, 0) if dash_px <= 0: draw_polyline(polyline, p_color, p_width_px) else: diff --git a/addons/tau-plot/plot/xy/line/line_style.gd b/addons/tau-plot/plot/xy/line/line_style.gd index 0b293bc..4bf68f7 100644 --- a/addons/tau-plot/plot/xy/line/line_style.gd +++ b/addons/tau-plot/plot/xy/line/line_style.gd @@ -21,11 +21,27 @@ class_name TauLineStyle extends Resource const DEFAULT_LINE_WIDTH_PX: float = 2.0 @export var line_width_px: float = DEFAULT_LINE_WIDTH_PX -## Dash length in pixels. A value of 0 produces a solid line. Any positive -## value switches the line to dashed rendering with alternating on-off -## segments of that pixel length. -const DEFAULT_LINE_DASH_PX: int = 0 -@export var dash_px: int = DEFAULT_LINE_DASH_PX +## Per-series dash length cycle, in pixels. Each entry sets the dash length +## for one series, with the array indexed cyclically by series index using +## modulo: series [code]i[/code] reads entry +## [code]i % dash_lengths_px.size()[/code]. An entry of [code]0[/code] +## produces a solid line for that series. Any positive entry switches that +## series to dashed rendering with alternating on-off segments of that pixel +## length. An empty array is treated as all series solid. +const DEFAULT_DASH_LENGTHS_PX: Array[int] = [0] +@export var dash_lengths_px: Array[int] = [0] + + +#################################################################################################### +# Helpers +#################################################################################################### + +## Returns the resolved dash length in pixels for the given series index. +func get_series_dash_px(p_series_index: int) -> int: + if dash_lengths_px.is_empty(): + return 0 + var entry: int = dash_lengths_px[p_series_index % dash_lengths_px.size()] + return max(entry, 0) #################################################################################################### @@ -34,9 +50,15 @@ const DEFAULT_LINE_DASH_PX: int = 0 ## Loads properties from the Godot theme attached to [param p_control]. ## -## For each property, the non-indexed theme key is fetched first (shared base -## for all panes), then the indexed key for [param p_pane_index] overwrites it -## if present. +## For scalar properties, the non-indexed theme key is fetched first (shared +## base for all panes), then the indexed key for [param p_pane_index] +## overwrites it if present. +## +## For [code]dash_lengths_px[/code], the same convention applies at series +## granularity: +## 1. [code]line_dash_px_N[/code] sets the dash length for series N across +## all panes. +## 2. [code]line_dash_px_N_P[/code] overrides series N in pane P only. ## ## This method writes every property unconditionally because it is called on ## the resolved instance, not on the user-provided resource. @@ -45,17 +67,39 @@ func load_from_theme(p_control: Control, p_pane_index: int) -> void: push_error("TauLineStyle.load_from_theme(): control is null") return + # line_width_px if p_control.has_theme_constant(&"line_width_px"): line_width_px = max(float(p_control.get_theme_constant(&"line_width_px")), 0.0) var indexed_width_key := StringName("line_width_px_%d" % p_pane_index) if p_control.has_theme_constant(indexed_width_key): line_width_px = max(float(p_control.get_theme_constant(indexed_width_key)), 0.0) - if p_control.has_theme_constant(&"line_dash_px"): - dash_px = max(int(p_control.get_theme_constant(&"line_dash_px")), 0) - var indexed_dash_key := StringName("line_dash_px_%d" % p_pane_index) - if p_control.has_theme_constant(indexed_dash_key): - dash_px = max(int(p_control.get_theme_constant(indexed_dash_key)), 0) + # dash_lengths_px: two-level indexed lookup. + # Level 1 (global): line_dash_px_N + var global_dashes: Array[int] = [] + var dash_index := 0 + while true: + var key := "line_dash_px_%d" % dash_index + if not p_control.has_theme_constant(key): + break + global_dashes.append(max(int(p_control.get_theme_constant(key)), 0)) + dash_index += 1 + + if not global_dashes.is_empty(): + dash_lengths_px = global_dashes + + # Level 2 (per-pane): line_dash_px_N_P overrides series N in pane P. + var pane_dash_index := 0 + while true: + var key := "line_dash_px_%d_%d" % [pane_dash_index, p_pane_index] + if not p_control.has_theme_constant(key): + break + # Grow the array if the per-pane theme defines more dash entries than + # the global theme (or the default). + if pane_dash_index >= dash_lengths_px.size(): + dash_lengths_px.resize(pane_dash_index + 1) + dash_lengths_px[pane_dash_index] = max(int(p_control.get_theme_constant(key)), 0) + pane_dash_index += 1 #################################################################################################### @@ -72,8 +116,9 @@ func apply_overrides_from(p_user_style: TauLineStyle) -> void: if p_user_style.line_width_px != DEFAULT_LINE_WIDTH_PX: line_width_px = p_user_style.line_width_px - if p_user_style.dash_px != DEFAULT_LINE_DASH_PX: - dash_px = p_user_style.dash_px + # dash_lengths_px: element-wise comparison against the default array. + if _is_dash_lengths_overridden(p_user_style.dash_lengths_px): + dash_lengths_px = p_user_style.dash_lengths_px.duplicate() #################################################################################################### @@ -110,8 +155,11 @@ func is_equal_to(p_other: TauLineStyle) -> bool: return false if line_width_px != p_other.line_width_px: return false - if dash_px != p_other.dash_px: + if dash_lengths_px.size() != p_other.dash_lengths_px.size(): return false + for i in range(dash_lengths_px.size()): + if dash_lengths_px[i] != p_other.dash_lengths_px[i]: + return false return true @@ -119,3 +167,18 @@ func is_equal_to(p_other: TauLineStyle) -> bool: # drawn within a fixed domain but do not affect domain, ticks, or pane rect. func has_layout_affecting_change(p_other: TauLineStyle) -> bool: return false + + +#################################################################################################### +# Private +#################################################################################################### + +## Returns true if [param p_dashes] differs from DEFAULT_DASH_LENGTHS_PX using +## a size + element loop (safest approach for typed arrays in GDScript). +static func _is_dash_lengths_overridden(p_dashes: Array[int]) -> bool: + if p_dashes.size() != DEFAULT_DASH_LENGTHS_PX.size(): + return true + for i in range(p_dashes.size()): + if p_dashes[i] != DEFAULT_DASH_LENGTHS_PX[i]: + return true + return false diff --git a/addons/tau-plot/tests/line/dash/test_line_dash.gd b/addons/tau-plot/tests/line/dash/test_line_dash.gd index d45021e..d986ae2 100644 --- a/addons/tau-plot/tests/line/dash/test_line_dash.gd +++ b/addons/tau-plot/tests/line/dash/test_line_dash.gd @@ -16,12 +16,13 @@ func _ready() -> void: # Helpers #################################################################################################### -func make_shared_x_continuous_plot(p_plot: TauPlot, p_title: String, p_interpolation: TauLineConfig.InterpolationMode, p_dash_px: int) -> void: - var series_names := PackedStringArray(["Series A"]) +func make_shared_x_continuous_plot(p_plot: TauPlot, p_title: String, p_interpolation: TauLineConfig.InterpolationMode, p_series_a_dash_px: int) -> void: + var series_names := PackedStringArray(["Series A", "Series B"]) var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0]) var y_a := PackedFloat64Array([10.0, 20.0, 50.0, 65.0, 70.0]) + var y_b := PackedFloat64Array([20.0, 12.0, 23.0, 7.0, 29.0]) - var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a]) + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b]) p_plot.title = p_title @@ -38,7 +39,7 @@ func make_shared_x_continuous_plot(p_plot: TauPlot, p_title: String, p_interpola var line_config := TauLineConfig.new() line_config.mode = TauLineConfig.LineMode.INDEPENDENT line_config.interpolation_mode = p_interpolation - line_config.style.dash_px = p_dash_px + line_config.style.dash_lengths_px = [p_series_a_dash_px, 0] var pane := TauPaneConfig.new() pane.y_left_axis = y_axis @@ -53,7 +54,12 @@ func make_shared_x_continuous_plot(p_plot: TauPlot, p_title: String, p_interpola sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE sb_a.y_axis_id = TauPlot.AxisId.LEFT - var bindings: Array[TauXYSeriesBinding] = [sb_a] + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b] p_plot.plot_xy(dataset, config, bindings) @@ -63,60 +69,60 @@ func make_shared_x_continuous_plot(p_plot: TauPlot, p_title: String, p_interpola #################################################################################################### func _setup_test_1() -> void: - make_shared_x_continuous_plot(%TestPlot1, "LINEAR + dash = 2", TauLineConfig.InterpolationMode.LINEAR, 2) + make_shared_x_continuous_plot(%TestPlot1, "LINEAR + dash = 2 for series A", TauLineConfig.InterpolationMode.LINEAR, 2) #################################################################################################### # Test 2 #################################################################################################### func _setup_test_2() -> void: - make_shared_x_continuous_plot(%TestPlot2, "LINEAR + dash = 4", TauLineConfig.InterpolationMode.LINEAR, 4) + make_shared_x_continuous_plot(%TestPlot2, "LINEAR + dash = 4 for series A", TauLineConfig.InterpolationMode.LINEAR, 4) #################################################################################################### # Test 3 #################################################################################################### func _setup_test_3() -> void: - make_shared_x_continuous_plot(%TestPlot3, "LINEAR + dash = 8", TauLineConfig.InterpolationMode.LINEAR, 8) + make_shared_x_continuous_plot(%TestPlot3, "LINEAR + dash = 8 for series A", TauLineConfig.InterpolationMode.LINEAR, 8) #################################################################################################### # Test 4 #################################################################################################### func _setup_test_4() -> void: - make_shared_x_continuous_plot(%TestPlot4, "STEP_MIDDLE + dash = 2", TauLineConfig.InterpolationMode.STEP_MIDDLE, 2) + make_shared_x_continuous_plot(%TestPlot4, "STEP_MIDDLE + dash = 2 for series A", TauLineConfig.InterpolationMode.STEP_MIDDLE, 2) #################################################################################################### # Test 5 #################################################################################################### func _setup_test_5() -> void: - make_shared_x_continuous_plot(%TestPlot5, "STEP_MIDDLE + dash = 4", TauLineConfig.InterpolationMode.STEP_MIDDLE, 4) + make_shared_x_continuous_plot(%TestPlot5, "STEP_MIDDLE + dash = 4 for series A", TauLineConfig.InterpolationMode.STEP_MIDDLE, 4) #################################################################################################### # Test 6 #################################################################################################### func _setup_test_6() -> void: - make_shared_x_continuous_plot(%TestPlot6, "STEP_MIDDLE + dash = 8", TauLineConfig.InterpolationMode.STEP_MIDDLE, 8) + make_shared_x_continuous_plot(%TestPlot6, "STEP_MIDDLE + dash = 8 for series A", TauLineConfig.InterpolationMode.STEP_MIDDLE, 8) #################################################################################################### # Test 7 #################################################################################################### func _setup_test_7() -> void: - make_shared_x_continuous_plot(%TestPlot7, "SMOOTH_MONOTONE + dash = 2", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE, 2) + make_shared_x_continuous_plot(%TestPlot7, "SMOOTH_MONOTONE + dash = 2 for series A", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE, 2) #################################################################################################### # Test 8 #################################################################################################### func _setup_test_8() -> void: - make_shared_x_continuous_plot(%TestPlot8, "SMOOTH_MONOTONE + dash = 4", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE, 4) + make_shared_x_continuous_plot(%TestPlot8, "SMOOTH_MONOTONE + dash = 4 for series A", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE, 4) #################################################################################################### # Test 9 #################################################################################################### func _setup_test_9() -> void: - make_shared_x_continuous_plot(%TestPlot9, "SMOOTH_MONOTONE + dash = 8", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE, 8) + make_shared_x_continuous_plot(%TestPlot9, "SMOOTH_MONOTONE + dash = 8 for series A", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE, 8) diff --git a/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_logarithmic.tscn b/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_logarithmic.tscn index 5842c19..7557394 100644 --- a/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_logarithmic.tscn +++ b/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_logarithmic.tscn @@ -119,7 +119,7 @@ size_flags_horizontal = 3 size_flags_vertical = 3 theme_type_variation = &"TauPlot" script = ExtResource("2_u5as0") -title = "[STEP_AFTER] PER_SERIES_X + CONTINUOUS" +title = "[SMOOTH_MONOTONE] SHARED_X + CONTINUOUS" legend_enabled = false metadata/_custom_type_script = "uid://dnhvsf2wip771" @@ -130,6 +130,6 @@ size_flags_horizontal = 3 size_flags_vertical = 3 theme_type_variation = &"TauPlot" script = ExtResource("2_u5as0") -title = "[STEP_AFTER] PER_SERIES_X + CONTINUOUS" +title = "[SMOOTH_MONOTONE] PER_SERIES_X + CONTINUOUS" legend_enabled = false metadata/_custom_type_script = "uid://dnhvsf2wip771" From 8cb7999b05f76a20e9f95fb6c6adaa58b3b3c919 Mon Sep 17 00:00:00 2001 From: ze2j <46556066+ze2j@users.noreply.github.com> Date: Mon, 27 Apr 2026 18:19:15 +0200 Subject: [PATCH 09/23] Per-series line width refactoring --- addons/tau-plot/plot/xy/line/line_config.gd | 2 +- addons/tau-plot/plot/xy/line/line_renderer.gd | 35 +++++--- addons/tau-plot/plot/xy/line/line_style.gd | 89 +++++++++++++++---- .../plot/xy/line/line_visual_attributes.gd | 1 - .../plot/xy/line/line_visual_callbacks.gd | 1 - 5 files changed, 92 insertions(+), 36 deletions(-) diff --git a/addons/tau-plot/plot/xy/line/line_config.gd b/addons/tau-plot/plot/xy/line/line_config.gd index 73aced2..2424ad4 100644 --- a/addons/tau-plot/plot/xy/line/line_config.gd +++ b/addons/tau-plot/plot/xy/line/line_config.gd @@ -9,7 +9,7 @@ const LineVisualCallbacks := preload("res://addons/tau-plot/plot/xy/line/line_vi ################################################################################################ ## Theme-driven visual parameters for lines. -## Never null. Modify properties directly: line_config.style.line_width_px = 3.0. +## Never null. Modify properties directly: line_config.style.line_widths_px = [3.0]. ## Properties set this way are automatically guarded from theme overwriting. @export var style: TauLineStyle = TauLineStyle.new() diff --git a/addons/tau-plot/plot/xy/line/line_renderer.gd b/addons/tau-plot/plot/xy/line/line_renderer.gd index 6903bfb..b2cc876 100644 --- a/addons/tau-plot/plot/xy/line/line_renderer.gd +++ b/addons/tau-plot/plot/xy/line/line_renderer.gd @@ -29,6 +29,10 @@ const LineVisualAttributes := preload("res://addons/tau-plot/plot/xy/line/line_v # Fritsch-Carlson piecewise cubic Hermite curve evaluated in screen space. # # Rendering path selection (per series): +# - The active line width for a series is resolved from +# TauLineStyle.line_widths_px through the helper +# TauLineStyle.get_series_width_px(global_series_index). A series whose +# resolved width is 0 is skipped entirely. # - The active dash length for a series is resolved from # TauLineStyle.dash_lengths_px through the helper # TauLineStyle.get_series_dash_px(global_series_index). Path selection is @@ -143,13 +147,10 @@ class LineRenderer extends Control: return var draw_order := _get_series_draw_order(series_count) - var width_px: float = max(_line_style.line_width_px, 0.0) - if width_px <= 0.0: - return for draw_rank in range(draw_order.size()): var series_index: int = draw_order[draw_rank] - _draw_series_independent(series_index, width_px) + _draw_series_independent(series_index) # Draws a single series as one or more polyline runs, respecting the @@ -160,18 +161,21 @@ class LineRenderer extends Control: # - SKIP flushes the current run and starts a new one. # - BRIDGE drops the sample and keeps appending into the same run. # - A run of fewer than two points is discarded (no polyline). - func _draw_series_independent(p_series_index: int, p_width_px: float) -> void: + func _draw_series_independent(p_series_index: int) -> void: var x_cfg := _get_x_axis_config() if x_cfg != null and x_cfg.type == TauAxisConfig.Type.CATEGORICAL: - _draw_series_categorical(p_series_index, p_width_px) + _draw_series_categorical(p_series_index) else: - _draw_series_continuous(p_series_index, p_width_px) + _draw_series_continuous(p_series_index) - func _draw_series_continuous(p_series_index: int, p_width_px: float) -> void: + func _draw_series_continuous(p_series_index: int) -> void: var series_id := _get_line_series_id(p_series_index) var global_series_index := _get_global_series_index(p_series_index) var color := _resolve_series_color(global_series_index) + var width_px: float = _line_style.get_series_width_px(global_series_index) + if width_px <= 0.0: + return var dash_px: int = _line_style.get_series_dash_px(global_series_index) var y_axis_id := _get_y_axis_id_for_series(series_id) var bridge: bool = _line_config.gap_policy == TauLineConfig.GapPolicy.BRIDGE @@ -186,14 +190,14 @@ class LineRenderer extends Control: var x_value: float = float(_dataset.get_shared_x(i)) if is_shared_x else float(_dataset.get_series_x(series_id, i)) if is_nan(x_value) or is_inf(x_value) or not _is_x_value_valid_for_scale(x_value): if not bridge: - _finalize_run(run, color, p_width_px, interpolation, dash_px) + _finalize_run(run, color, width_px, interpolation, dash_px) run = PackedVector2Array() continue var y_value := _dataset.get_series_y(series_id, i) if is_nan(y_value) or is_inf(y_value) or not _is_y_value_valid_for_scale(series_id, y_value): if not bridge: - _finalize_run(run, color, p_width_px, interpolation, dash_px) + _finalize_run(run, color, width_px, interpolation, dash_px) run = PackedVector2Array() continue @@ -201,13 +205,16 @@ class LineRenderer extends Control: var y_px := _layout.map_y_to_px(_pane_index, y_value, y_axis_id) _append_with_interpolation(run, _layout.map_point_to_screen(x_px, y_px), interpolation) - _finalize_run(run, color, p_width_px, interpolation, dash_px) + _finalize_run(run, color, width_px, interpolation, dash_px) - func _draw_series_categorical(p_series_index: int, p_width_px: float) -> void: + func _draw_series_categorical(p_series_index: int) -> void: var series_id := _get_line_series_id(p_series_index) var global_series_index := _get_global_series_index(p_series_index) var color := _resolve_series_color(global_series_index) + var width_px: float = _line_style.get_series_width_px(global_series_index) + if width_px <= 0.0: + return var dash_px: int = _line_style.get_series_dash_px(global_series_index) var y_axis_id := _get_y_axis_id_for_series(series_id) var bridge: bool = _line_config.gap_policy == TauLineConfig.GapPolicy.BRIDGE @@ -220,7 +227,7 @@ class LineRenderer extends Control: var y_value := _dataset.get_series_y(series_id, cat_idx) if is_nan(y_value) or is_inf(y_value) or not _is_y_value_valid_for_scale(series_id, y_value): if not bridge: - _finalize_run(run, color, p_width_px, interpolation, dash_px) + _finalize_run(run, color, width_px, interpolation, dash_px) run = PackedVector2Array() continue @@ -228,7 +235,7 @@ class LineRenderer extends Control: var y_px := _layout.map_y_to_px(_pane_index, y_value, y_axis_id) _append_with_interpolation(run, _layout.map_point_to_screen(x_px, y_px), interpolation) - _finalize_run(run, color, p_width_px, interpolation, dash_px) + _finalize_run(run, color, width_px, interpolation, dash_px) func _append_with_interpolation(p_run: PackedVector2Array, p_point: Vector2, p_mode: TauLineConfig.InterpolationMode) -> void: diff --git a/addons/tau-plot/plot/xy/line/line_style.gd b/addons/tau-plot/plot/xy/line/line_style.gd index 4bf68f7..2666f6a 100644 --- a/addons/tau-plot/plot/xy/line/line_style.gd +++ b/addons/tau-plot/plot/xy/line/line_style.gd @@ -18,8 +18,13 @@ class_name TauLineStyle extends Resource # `has_layout_affecting_change()`. ################################################################################################ -const DEFAULT_LINE_WIDTH_PX: float = 2.0 -@export var line_width_px: float = DEFAULT_LINE_WIDTH_PX +## Per-series cycle of line widths in pixels in the normal state. Each entry +## sets the line width for one series, with the array indexed cyclically by +## series index using modulo: series [code]i[/code] reads entry +## [code]i % line_widths_px.size()[/code]. An empty array is treated as all +## series rendered at [code]2.0[/code] pixels. +const DEFAULT_LINE_WIDTHS_PX: Array[float] = [2.0] +@export var line_widths_px: Array[float] = [2.0] ## Per-series dash length cycle, in pixels. Each entry sets the dash length ## for one series, with the array indexed cyclically by series index using @@ -36,6 +41,18 @@ const DEFAULT_DASH_LENGTHS_PX: Array[int] = [0] # Helpers #################################################################################################### +## Returns the resolved line width in pixels for the given series index. +## +## An empty [member line_widths_px] returns the default +## [constant DEFAULT_LINE_WIDTHS_PX] entry. The result is clamped to be +## non-negative. +func get_series_width_px(p_series_index: int) -> float: + if line_widths_px.is_empty(): + return DEFAULT_LINE_WIDTHS_PX[0] + var entry: float = line_widths_px[p_series_index % line_widths_px.size()] + return max(entry, 0.0) + + ## Returns the resolved dash length in pixels for the given series index. func get_series_dash_px(p_series_index: int) -> int: if dash_lengths_px.is_empty(): @@ -50,15 +67,14 @@ func get_series_dash_px(p_series_index: int) -> int: ## Loads properties from the Godot theme attached to [param p_control]. ## -## For scalar properties, the non-indexed theme key is fetched first (shared -## base for all panes), then the indexed key for [param p_pane_index] -## overwrites it if present. +## All properties on this resource are per-series arrays. For each, a +## two-level indexed lookup applies at series granularity: +## 1. [code]_N[/code] sets the value for series N across all panes. +## 2. [code]_N_P[/code] overrides series N in pane P only. ## -## For [code]dash_lengths_px[/code], the same convention applies at series -## granularity: -## 1. [code]line_dash_px_N[/code] sets the dash length for series N across -## all panes. -## 2. [code]line_dash_px_N_P[/code] overrides series N in pane P only. +## For [code]line_widths_px[/code] the keys are [code]line_width_px_N[/code] +## and [code]line_width_px_N_P[/code]. For [code]dash_lengths_px[/code] they +## are [code]line_dash_px_N[/code] and [code]line_dash_px_N_P[/code]. ## ## This method writes every property unconditionally because it is called on ## the resolved instance, not on the user-provided resource. @@ -67,12 +83,32 @@ func load_from_theme(p_control: Control, p_pane_index: int) -> void: push_error("TauLineStyle.load_from_theme(): control is null") return - # line_width_px - if p_control.has_theme_constant(&"line_width_px"): - line_width_px = max(float(p_control.get_theme_constant(&"line_width_px")), 0.0) - var indexed_width_key := StringName("line_width_px_%d" % p_pane_index) - if p_control.has_theme_constant(indexed_width_key): - line_width_px = max(float(p_control.get_theme_constant(indexed_width_key)), 0.0) + # line_widths_px: two-level indexed lookup. + # Level 1 (global): line_width_px_N + var global_widths: Array[float] = [] + var width_index := 0 + while true: + var key := "line_width_px_%d" % width_index + if not p_control.has_theme_constant(key): + break + global_widths.append(max(float(p_control.get_theme_constant(key)), 0.0)) + width_index += 1 + + if not global_widths.is_empty(): + line_widths_px = global_widths + + # Level 2 (per-pane): line_width_px_N_P overrides series N in pane P. + var pane_width_index := 0 + while true: + var key := "line_width_px_%d_%d" % [pane_width_index, p_pane_index] + if not p_control.has_theme_constant(key): + break + # Grow the array if the per-pane theme defines more width entries than + # the global theme (or the default). + if pane_width_index >= line_widths_px.size(): + line_widths_px.resize(pane_width_index + 1) + line_widths_px[pane_width_index] = max(float(p_control.get_theme_constant(key)), 0.0) + pane_width_index += 1 # dash_lengths_px: two-level indexed lookup. # Level 1 (global): line_dash_px_N @@ -113,8 +149,9 @@ func apply_overrides_from(p_user_style: TauLineStyle) -> void: if p_user_style == null: return - if p_user_style.line_width_px != DEFAULT_LINE_WIDTH_PX: - line_width_px = p_user_style.line_width_px + # line_widths_px: element-wise comparison against the default array. + if _is_line_widths_overridden(p_user_style.line_widths_px): + line_widths_px = p_user_style.line_widths_px.duplicate() # dash_lengths_px: element-wise comparison against the default array. if _is_dash_lengths_overridden(p_user_style.dash_lengths_px): @@ -153,8 +190,11 @@ static func resolve( func is_equal_to(p_other: TauLineStyle) -> bool: if p_other == null: return false - if line_width_px != p_other.line_width_px: + if line_widths_px.size() != p_other.line_widths_px.size(): return false + for i in range(line_widths_px.size()): + if line_widths_px[i] != p_other.line_widths_px[i]: + return false if dash_lengths_px.size() != p_other.dash_lengths_px.size(): return false for i in range(dash_lengths_px.size()): @@ -173,6 +213,17 @@ func has_layout_affecting_change(p_other: TauLineStyle) -> bool: # Private #################################################################################################### +## Returns true if [param p_widths] differs from DEFAULT_LINE_WIDTHS_PX using +## a size + element loop (safest approach for typed arrays in GDScript). +static func _is_line_widths_overridden(p_widths: Array[float]) -> bool: + if p_widths.size() != DEFAULT_LINE_WIDTHS_PX.size(): + return true + for i in range(p_widths.size()): + if p_widths[i] != DEFAULT_LINE_WIDTHS_PX[i]: + return true + return false + + ## Returns true if [param p_dashes] differs from DEFAULT_DASH_LENGTHS_PX using ## a size + element loop (safest approach for typed arrays in GDScript). static func _is_dash_lengths_overridden(p_dashes: Array[int]) -> bool: diff --git a/addons/tau-plot/plot/xy/line/line_visual_attributes.gd b/addons/tau-plot/plot/xy/line/line_visual_attributes.gd index 83c9efa..edcc860 100644 --- a/addons/tau-plot/plot/xy/line/line_visual_attributes.gd +++ b/addons/tau-plot/plot/xy/line/line_visual_attributes.gd @@ -3,5 +3,4 @@ const VisualAttributes = preload("res://addons/tau-plot/plot/xy/visual_attribute ## Per-sample data-driven visual attribute buffers for LINE overlays. class LineVisualAttributes extends VisualAttributes: - # TODO: add width_buffer. pass diff --git a/addons/tau-plot/plot/xy/line/line_visual_callbacks.gd b/addons/tau-plot/plot/xy/line/line_visual_callbacks.gd index 992b214..fe627af 100644 --- a/addons/tau-plot/plot/xy/line/line_visual_callbacks.gd +++ b/addons/tau-plot/plot/xy/line/line_visual_callbacks.gd @@ -3,5 +3,4 @@ const VisualCallbacks = preload("res://addons/tau-plot/plot/xy/visual_callbacks. ## LINE overlay specific callbacks. class LineVisualCallbacks extends VisualCallbacks: - # TODO: add width_callback. pass From f31087a8c1433f79db1d042e6218548dd8675734 Mon Sep 17 00:00:00 2001 From: ze2j <46556066+ze2j@users.noreply.github.com> Date: Wed, 29 Apr 2026 12:08:56 +0200 Subject: [PATCH 10/23] Per sample visual styling of LINE --- addons/tau-plot/plot/plot.gd | 2 + addons/tau-plot/plot/xy/line/line_renderer.gd | 193 ++++++++++++--- .../test_line_visual_attributes_dash_alpha.gd | 190 ++++++++++++++ ...t_line_visual_attributes_dash_alpha.gd.uid | 1 + ...est_line_visual_attributes_dash_alpha.tscn | 190 ++++++++++++++ .../test_line_visual_attributes_dash_color.gd | 190 ++++++++++++++ ...t_line_visual_attributes_dash_color.gd.uid | 1 + ...est_line_visual_attributes_dash_color.tscn | 190 ++++++++++++++ ...test_line_visual_attributes_solid_alpha.gd | 189 ++++++++++++++ ..._line_visual_attributes_solid_alpha.gd.uid | 1 + ...st_line_visual_attributes_solid_alpha.tscn | 190 ++++++++++++++ ...test_line_visual_attributes_solid_color.gd | 189 ++++++++++++++ ..._line_visual_attributes_solid_color.gd.uid | 1 + ...st_line_visual_attributes_solid_color.tscn | 190 ++++++++++++++ .../test_line_visual_callbacks_dash_alpha.gd | 233 ++++++++++++++++++ ...st_line_visual_callbacks_dash_alpha.gd.uid | 1 + ...test_line_visual_callbacks_dash_alpha.tscn | 79 ++++++ .../test_line_visual_callbacks_dash_color.gd | 231 +++++++++++++++++ ...st_line_visual_callbacks_dash_color.gd.uid | 1 + ...test_line_visual_callbacks_dash_color.tscn | 79 ++++++ .../test_line_visual_callbacks_solid_alpha.gd | 232 +++++++++++++++++ ...t_line_visual_callbacks_solid_alpha.gd.uid | 1 + ...est_line_visual_callbacks_solid_alpha.tscn | 79 ++++++ .../test_line_visual_callbacks_solid_color.gd | 230 +++++++++++++++++ ...t_line_visual_callbacks_solid_color.gd.uid | 1 + ...est_line_visual_callbacks_solid_color.tscn | 79 ++++++ 26 files changed, 2923 insertions(+), 40 deletions(-) create mode 100644 addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_alpha.gd create mode 100644 addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_alpha.gd.uid create mode 100644 addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_alpha.tscn create mode 100644 addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_color.gd create mode 100644 addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_color.gd.uid create mode 100644 addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_color.tscn create mode 100644 addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_alpha.gd create mode 100644 addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_alpha.gd.uid create mode 100644 addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_alpha.tscn create mode 100644 addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_color.gd create mode 100644 addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_color.gd.uid create mode 100644 addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_color.tscn create mode 100644 addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_alpha.gd create mode 100644 addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_alpha.gd.uid create mode 100644 addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_alpha.tscn create mode 100644 addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_color.gd create mode 100644 addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_color.gd.uid create mode 100644 addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_color.tscn create mode 100644 addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_alpha.gd create mode 100644 addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_alpha.gd.uid create mode 100644 addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_alpha.tscn create mode 100644 addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_color.gd create mode 100644 addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_color.gd.uid create mode 100644 addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_color.tscn diff --git a/addons/tau-plot/plot/plot.gd b/addons/tau-plot/plot/plot.gd index eca5133..8e0fe5c 100644 --- a/addons/tau-plot/plot/plot.gd +++ b/addons/tau-plot/plot/plot.gd @@ -11,10 +11,12 @@ const PaneOverlayType = preload("res://addons/tau-plot/plot/xy/pane_overlay_type const VisualAttributes = preload("res://addons/tau-plot/plot/xy/visual_attributes.gd").VisualAttributes const BarVisualAttributes := preload("res://addons/tau-plot/plot/xy/bar/bar_visual_attributes.gd").BarVisualAttributes const ScatterVisualAttributes = preload("res://addons/tau-plot/plot/xy/scatter/scatter_visual_attributes.gd").ScatterVisualAttributes +const LineVisualAttributes = preload("res://addons/tau-plot/plot/xy/line/line_visual_attributes.gd").LineVisualAttributes const VisualCallbacks = preload("res://addons/tau-plot/plot/xy/visual_callbacks.gd").VisualCallbacks const BarVisualCallbacks := preload("res://addons/tau-plot/plot/xy/bar/bar_visual_callbacks.gd").BarVisualCallbacks const ScatterVisualCallbacks = preload("res://addons/tau-plot/plot/xy/scatter/scatter_visual_callbacks.gd").ScatterVisualCallbacks +const LineVisualCallbacks = preload("res://addons/tau-plot/plot/xy/line/line_visual_callbacks.gd").LineVisualCallbacks const SampleHit = preload("res://addons/tau-plot/plot/xy/hover/sample_hit.gd").SampleHit diff --git a/addons/tau-plot/plot/xy/line/line_renderer.gd b/addons/tau-plot/plot/xy/line/line_renderer.gd index b2cc876..f0a1ed8 100644 --- a/addons/tau-plot/plot/xy/line/line_renderer.gd +++ b/addons/tau-plot/plot/xy/line/line_renderer.gd @@ -39,18 +39,31 @@ const LineVisualAttributes := preload("res://addons/tau-plot/plot/xy/line/line_v # therefore per series: two series in the same overlay can run on # different paths in the same frame. # - Path 1, fast path: resolved per-series dash length is 0. The run is -# drawn with a single draw_polyline() call. +# drawn with a single draw_polyline_colors() call. # - Path 2, dashed batched path: resolved per-series dash length is positive. # Dash phase is precomputed across the full run, the "on" intervals are # collected into a flat segment array, and the run is drawn with a single -# draw_multiline() call. The dash phase is continuous across all segments -# of the polyline. +# draw_multiline_colors() call. The dash phase is continuous across all +# segments of the polyline. +# +# Per-sample color and alpha resolution: +# - Color resolution order: LineVisualAttributes.color_buffer, then +# LineVisualCallbacks.color_callback, then the per-series color from +# TauXYStyle.series_colors. +# - Alpha resolution order: LineVisualAttributes.alpha_buffer, then +# LineVisualCallbacks.alpha_callback, then TauXYStyle.series_alpha. +# - The resolved alpha overwrites the alpha channel of the resolved color. +# - Each vertex of the polyline carries its own resolved color. Colors are +# linearly interpolated by Godot between consecutive vertices. +# - Synthetic step-mode intermediate vertices and SMOOTH_MONOTONE sub-samples +# are colored consistently with the underlying segment endpoints so the +# resulting interpolation matches the chosen interpolation mode. # # LineValidator is expected to enforce binding-level typing constraints. class LineRenderer extends Control: # Number of sub-segments inserted between two consecutive samples by # SMOOTH_MONOTONE. The value balances visual smoothness on a typical - # screen against the per-segment cost paid by draw_polyline(). + # screen against the per-segment cost paid by draw_polyline_colors(). const _SMOOTH_SUBDIVISIONS: int = 16 var _layout: XYLayout = null @@ -172,7 +185,6 @@ class LineRenderer extends Control: func _draw_series_continuous(p_series_index: int) -> void: var series_id := _get_line_series_id(p_series_index) var global_series_index := _get_global_series_index(p_series_index) - var color := _resolve_series_color(global_series_index) var width_px: float = _line_style.get_series_width_px(global_series_index) if width_px <= 0.0: return @@ -182,6 +194,7 @@ class LineRenderer extends Control: var interpolation: TauLineConfig.InterpolationMode = _line_config.interpolation_mode var run := PackedVector2Array() + var run_colors := PackedColorArray() var is_shared_x := _dataset.get_mode() == Dataset.Mode.SHARED_X var sample_count := _dataset.get_series_sample_count(series_id) @@ -190,28 +203,30 @@ class LineRenderer extends Control: var x_value: float = float(_dataset.get_shared_x(i)) if is_shared_x else float(_dataset.get_series_x(series_id, i)) if is_nan(x_value) or is_inf(x_value) or not _is_x_value_valid_for_scale(x_value): if not bridge: - _finalize_run(run, color, width_px, interpolation, dash_px) + _finalize_run(run, run_colors, width_px, interpolation, dash_px) run = PackedVector2Array() + run_colors = PackedColorArray() continue var y_value := _dataset.get_series_y(series_id, i) if is_nan(y_value) or is_inf(y_value) or not _is_y_value_valid_for_scale(series_id, y_value): if not bridge: - _finalize_run(run, color, width_px, interpolation, dash_px) + _finalize_run(run, run_colors, width_px, interpolation, dash_px) run = PackedVector2Array() + run_colors = PackedColorArray() continue var x_px := _layout.map_x_to_px(_pane_index, x_value) var y_px := _layout.map_y_to_px(_pane_index, y_value, y_axis_id) - _append_with_interpolation(run, _layout.map_point_to_screen(x_px, y_px), interpolation) + var sample_color := _resolve_sample_color(p_series_index, i, x_value, y_value) + _append_with_interpolation(run, run_colors, _layout.map_point_to_screen(x_px, y_px), sample_color, interpolation) - _finalize_run(run, color, width_px, interpolation, dash_px) + _finalize_run(run, run_colors, width_px, interpolation, dash_px) func _draw_series_categorical(p_series_index: int) -> void: var series_id := _get_line_series_id(p_series_index) var global_series_index := _get_global_series_index(p_series_index) - var color := _resolve_series_color(global_series_index) var width_px: float = _line_style.get_series_width_px(global_series_index) if width_px <= 0.0: return @@ -220,43 +235,58 @@ class LineRenderer extends Control: var bridge: bool = _line_config.gap_policy == TauLineConfig.GapPolicy.BRIDGE var interpolation: TauLineConfig.InterpolationMode = _line_config.interpolation_mode + var categories := _layout.domain.x_categories var run := PackedVector2Array() + var run_colors := PackedColorArray() var sample_count := _dataset.get_series_sample_count(series_id) for cat_idx in range(sample_count): var y_value := _dataset.get_series_y(series_id, cat_idx) if is_nan(y_value) or is_inf(y_value) or not _is_y_value_valid_for_scale(series_id, y_value): if not bridge: - _finalize_run(run, color, width_px, interpolation, dash_px) + _finalize_run(run, run_colors, width_px, interpolation, dash_px) run = PackedVector2Array() + run_colors = PackedColorArray() continue var x_px := _layout.map_x_category_center_to_px(_pane_index, cat_idx) var y_px := _layout.map_y_to_px(_pane_index, y_value, y_axis_id) - _append_with_interpolation(run, _layout.map_point_to_screen(x_px, y_px), interpolation) + var x_value: Variant = categories[cat_idx] + var sample_color := _resolve_sample_color(p_series_index, cat_idx, x_value, y_value) + _append_with_interpolation(run, run_colors, _layout.map_point_to_screen(x_px, y_px), sample_color, interpolation) - _finalize_run(run, color, width_px, interpolation, dash_px) + _finalize_run(run, run_colors, width_px, interpolation, dash_px) - func _append_with_interpolation(p_run: PackedVector2Array, p_point: Vector2, p_mode: TauLineConfig.InterpolationMode) -> void: + # Each appended sample (real or synthetic) gets the new sample's color. + # Combined with linear interpolation by draw_polyline_colors() between + # consecutive vertices, this places the color transition at the segment + # leading INTO the new sample, leaving the staircase tail solid. + func _append_with_interpolation(p_run: PackedVector2Array, p_run_colors: PackedColorArray, p_point: Vector2, p_color: Color, p_mode: TauLineConfig.InterpolationMode) -> void: # SMOOTH_MONOTONE buffers raw sample points untouched: cubic resampling # requires the full neighborhood of every sample to compute tangents and # is therefore deferred to _finalize_run(). if p_run.size() == 0 or p_mode == TauLineConfig.InterpolationMode.LINEAR or p_mode == TauLineConfig.InterpolationMode.SMOOTH_MONOTONE: p_run.append(p_point) + p_run_colors.append(p_color) return var last_pt: Vector2 = p_run[p_run.size() - 1] match p_mode: TauLineConfig.InterpolationMode.STEP_BEFORE: p_run.append(Vector2(last_pt.x, p_point.y)) + p_run_colors.append(p_color) TauLineConfig.InterpolationMode.STEP_AFTER: p_run.append(Vector2(p_point.x, last_pt.y)) + p_run_colors.append(p_color) TauLineConfig.InterpolationMode.STEP_MIDDLE: var mid_x: float = (last_pt.x + p_point.x) * 0.5 p_run.append(Vector2(mid_x, last_pt.y)) + p_run_colors.append(p_color) p_run.append(Vector2(mid_x, p_point.y)) + p_run_colors.append(p_color) p_run.append(p_point) + p_run_colors.append(p_color) # Draw the polyline for one buffered run. @@ -264,39 +294,50 @@ class LineRenderer extends Control: # For SMOOTH_MONOTONE the run is first replaced by its Fritsch-Carlson piecewise cubic resampling. # Runs of fewer than two points are silently dropped. # - # When p_dash_px is 0, the polyline is emitted via path 1 (draw_polyline). + # When p_dash_px is 0, the polyline is emitted via path 1 (draw_polyline_colors). # Otherwise it is emitted via path 2: dash phase is precomputed across the # whole polyline and the resulting "on" intervals are flushed in a single - # draw_multiline() call. p_dash_px is the resolved per-series dash length - # obtained from TauLineStyle.get_series_dash_px(). - func _finalize_run(p_run: PackedVector2Array, p_color: Color, p_width_px: float, p_mode: TauLineConfig.InterpolationMode, p_dash_px: int) -> void: + # draw_multiline_colors() call. p_dash_px is the resolved per-series dash + # length obtained from TauLineStyle.get_series_dash_px(). + func _finalize_run(p_run: PackedVector2Array, p_run_colors: PackedColorArray, p_width_px: float, p_mode: TauLineConfig.InterpolationMode, p_dash_px: int) -> void: var polyline: PackedVector2Array = p_run + var polyline_colors: PackedColorArray = p_run_colors if p_mode == TauLineConfig.InterpolationMode.SMOOTH_MONOTONE and p_run.size() > 2: - polyline = _resample_smooth_monotone(p_run) + var resampled := _resample_smooth_monotone(p_run, p_run_colors) + polyline = resampled[0] + polyline_colors = resampled[1] if polyline.size() < 2: return var dash_px: int = max(p_dash_px, 0) if dash_px <= 0: - draw_polyline(polyline, p_color, p_width_px) + draw_polyline_colors(polyline, polyline_colors, p_width_px) else: - _draw_dashed_polyline(polyline, p_color, p_width_px, float(dash_px)) + _draw_dashed_polyline(polyline, polyline_colors, p_width_px, float(dash_px)) # Builds the flat segment array of "on" dash intervals along p_polyline and - # emits it with a single draw_multiline() call. The dash period is + # emits it with a single draw_multiline_colors() call. The dash period is # 2 * p_dash_px (one "on" length followed by one "off" length of equal # size). Dash phase is tracked as a single scalar that advances along the # polyline arc length, so the pattern is continuous across consecutive # segments and does not reset at sample positions. # + # draw_multiline_colors takes one solid color per emitted segment (a pair + # of points). Each "on" interval gets the color of its midpoint along the + # enclosing polyline segment, computed by linear interpolation between the + # two flanking polyline-vertex colors. For typical dash sizes this is a + # good approximation of the per-vertex color gradient produced by the + # undashed path. + # # Degenerate segments (zero length) are skipped: they cannot carry any # dash and do not advance the phase. - func _draw_dashed_polyline(p_polyline: PackedVector2Array, p_color: Color, p_width_px: float, p_dash_px: float) -> void: + func _draw_dashed_polyline(p_polyline: PackedVector2Array, p_polyline_colors: PackedColorArray, p_width_px: float, p_dash_px: float) -> void: var period: float = p_dash_px * 2.0 var n := p_polyline.size() var phase: float = 0.0 var segments := PackedVector2Array() + var segment_colors := PackedColorArray() for i in range(n - 1): var seg_start: Vector2 = p_polyline[i] @@ -307,6 +348,8 @@ class LineRenderer extends Control: continue var seg_dir: Vector2 = seg_vec / seg_len + var col_start: Color = p_polyline_colors[i] + var col_end: Color = p_polyline_colors[i + 1] # Position along the current segment, in pixels from seg_start. # Phase 0..p_dash_px is "on", p_dash_px..period is "off". @@ -319,6 +362,8 @@ class LineRenderer extends Control: var on_end: float = min(pos + remaining_on, seg_len) segments.append(seg_start + seg_dir * pos) segments.append(seg_start + seg_dir * on_end) + var midpoint_t: float = ((pos + on_end) * 0.5) / seg_len + segment_colors.append(col_start.lerp(col_end, midpoint_t)) var consumed: float = on_end - pos phase += consumed pos = on_end @@ -334,7 +379,7 @@ class LineRenderer extends Control: phase -= period if segments.size() >= 2: - draw_multiline(segments, p_color, p_width_px) + draw_multiline_colors(segments, segment_colors, p_width_px) #################################################################################################### # Smooth-monotone (Fritsch-Carlson) resampling @@ -342,67 +387,90 @@ class LineRenderer extends Control: # Builds the Fritsch-Carlson piecewise cubic Hermite curve through p_points # and returns it sampled at _SMOOTH_SUBDIVISIONS sub-segments per input - # segment. Operates in screen space (the input is already in pixels), which - # keeps the curve visually smooth regardless of axis scale. + # segment, paired with a colors array sampled in lock-step. Operates in + # screen space (the input is already in pixels), which keeps the curve + # visually smooth regardless of axis scale. # # The algorithm requires strictly monotonic X. The expected case is # monotonically increasing screen X, but a user-inverted X axis produces # monotonically decreasing screen X. Both directions are accepted: the # input is processed internally on a strictly increasing X copy and the # output is reversed back when needed. Consecutive points sharing the same - # screen X are dropped since the secant slope is undefined at h = 0. + # screen X are dropped since the secant slope is undefined at h = 0. The + # matching color entries are dropped at the same indices. # Inputs that are not monotonic in either direction fall back to the raw # polyline for that run and emit a one-shot warning. - func _resample_smooth_monotone(p_points: PackedVector2Array) -> PackedVector2Array: + # + # Sub-sample colors are linearly interpolated between the two flanking + # kept-sample colors so that the visual color gradient matches what + # draw_polyline_colors would produce on a LINEAR polyline through the + # same kept samples. + # + # Returns [points: PackedVector2Array, colors: PackedColorArray]. + func _resample_smooth_monotone(p_points: PackedVector2Array, p_colors: PackedColorArray) -> Array: var direction := _detect_monotonic_x_direction(p_points) if direction == 0: if not _smooth_non_monotonic_warned: push_warning("LineRenderer: SMOOTH_MONOTONE received samples whose screen X is not monotonic. Falling back to a straight polyline for the affected run. Use LINEAR interpolation if your data does not have a monotonic X parameter.") _smooth_non_monotonic_warned = true - return p_points + return [p_points, p_colors] var ascending: bool = direction > 0 # Build strictly increasing X arrays, dropping flat-X duplicates. + # Colors are kept in lock-step with the kept points. var xs := PackedFloat32Array() var ys := PackedFloat32Array() + var cs := PackedColorArray() var input_count := p_points.size() if ascending: xs.append(p_points[0].x) ys.append(p_points[0].y) + cs.append(p_colors[0]) for i in range(1, input_count): if p_points[i].x > xs[xs.size() - 1]: xs.append(p_points[i].x) ys.append(p_points[i].y) + cs.append(p_colors[i]) else: xs.append(p_points[input_count - 1].x) ys.append(p_points[input_count - 1].y) + cs.append(p_colors[input_count - 1]) for i in range(input_count - 2, -1, -1): if p_points[i].x > xs[xs.size() - 1]: xs.append(p_points[i].x) ys.append(p_points[i].y) + cs.append(p_colors[i]) var n := xs.size() if n < 2: # All inputs collapsed to a single screen X. Nothing to draw. - return PackedVector2Array() + return [PackedVector2Array(), PackedColorArray()] if n == 2: # Two distinct X values produce a straight line through Hermite # with both tangents equal to the secant slope. Short-circuit. var trivial := PackedVector2Array() + var trivial_colors := PackedColorArray() trivial.append(Vector2(xs[0], ys[0])) trivial.append(Vector2(xs[1], ys[1])) + trivial_colors.append(cs[0]) + trivial_colors.append(cs[1]) if not ascending: trivial.reverse() - return trivial + trivial_colors.reverse() + return [trivial, trivial_colors] var tangents := _fritsch_carlson_tangents(xs, ys) var out := PackedVector2Array() - # Pre-size the output for speed: n-1 segments times subdivisions plus + var out_colors := PackedColorArray() + # Pre-size the outputs for speed: n-1 segments times subdivisions plus # the very first sample. - out.resize(1 + (n - 1) * _SMOOTH_SUBDIVISIONS) + var out_size: int = 1 + (n - 1) * _SMOOTH_SUBDIVISIONS + out.resize(out_size) + out_colors.resize(out_size) out[0] = Vector2(xs[0], ys[0]) + out_colors[0] = cs[0] var write_index: int = 1 var inv_subs: float = 1.0 / float(_SMOOTH_SUBDIVISIONS) @@ -414,6 +482,8 @@ class LineRenderer extends Control: var h: float = x1 - x0 var m0: float = tangents[k] var m1: float = tangents[k + 1] + var c0: Color = cs[k] + var c1: Color = cs[k + 1] # Sub-points at t = 1/N, 2/N, ..., 1. The endpoint t=1 is the next # sample, included here so the next segment begins at t=1/N. @@ -428,11 +498,13 @@ class LineRenderer extends Control: var x: float = x0 + t * h var y: float = h00 * y0 + h10 * h * m0 + h01 * y1 + h11 * h * m1 out[write_index] = Vector2(x, y) + out_colors[write_index] = c0.lerp(c1, t) write_index += 1 if not ascending: out.reverse() - return out + out_colors.reverse() + return [out, out_colors] # Returns +1 if screen X is strictly monotonically increasing across the @@ -555,14 +627,55 @@ class LineRenderer extends Control: #################################################################################################### - # Color resolution + # Per-sample color and alpha resolution #################################################################################################### - # TODO: fully implement _resolve_series_color - func _resolve_series_color(p_global_series_index: int) -> Color: - var color := _xy_style.get_series_color(p_global_series_index) - color.a = clampf(_xy_style.series_alpha, 0.0, 1.0) - return color + # Combined per-sample color resolution. The alpha resolved by + # _resolve_sample_alpha overwrites the alpha channel of the color resolved + # by _resolve_sample_color_only. + func _resolve_sample_color(p_series_index: int, p_sample_index: int, p_x_value: Variant, p_y_value: float) -> Color: + var base := _resolve_sample_color_only(p_series_index, p_sample_index, p_x_value, p_y_value) + var alpha := _resolve_sample_alpha(p_series_index, p_sample_index, p_x_value, p_y_value) + base.a = clampf(alpha, 0.0, 1.0) + return base + + + func _resolve_sample_color_only(p_series_index: int, p_sample_index: int, p_x_value: Variant, p_y_value: float) -> Color: + # Per-sample override from LineVisualAttributes.color_buffer. + if p_series_index >= 0 and p_series_index < _visual_attributes.size(): + var color_buffer: VisualAttributes.ColorBuffer = _visual_attributes[p_series_index].color_buffer + if color_buffer != null and p_sample_index >= 0 and p_sample_index < color_buffer.size(): + var c := color_buffer.get_value(p_sample_index) + if c != VisualAttributes.ColorBuffer.NO_COLOR: + return c + + var global_series_index := _get_global_series_index(p_series_index) + + # Per-sample override from LineVisualCallbacks.color_callback. + var vc := _line_config.line_visual_callbacks + if vc != null and vc.color_callback.is_valid(): + return vc.color_callback.call(global_series_index, p_sample_index, p_x_value, p_y_value) + + return _xy_style.get_series_color(global_series_index) + + + func _resolve_sample_alpha(p_series_index: int, p_sample_index: int, p_x_value: Variant, p_y_value: float) -> float: + # Per-sample override from LineVisualAttributes.alpha_buffer. + if p_series_index >= 0 and p_series_index < _visual_attributes.size(): + var alpha_buffer: VisualAttributes.AlphaBuffer = _visual_attributes[p_series_index].alpha_buffer + if alpha_buffer != null and p_sample_index >= 0 and p_sample_index < alpha_buffer.size(): + var a := alpha_buffer.get_value(p_sample_index) + if a >= 0.0: + return a + + # Per-sample override from LineVisualCallbacks.alpha_callback. + var vc := _line_config.line_visual_callbacks + if vc != null and vc.alpha_callback.is_valid(): + var a: float = vc.alpha_callback.call(_get_global_series_index(p_series_index), p_sample_index, p_x_value, p_y_value) + if a >= 0.0: + return a + + return _xy_style.series_alpha #################################################################################################### diff --git a/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_alpha.gd b/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_alpha.gd new file mode 100644 index 0000000..a043626 --- /dev/null +++ b/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_alpha.gd @@ -0,0 +1,190 @@ +@tool +extends Control + +func _ready() -> void: + _setup_test_1() + _setup_test_2() + _setup_test_3() + _setup_test_4() + _setup_test_5() + _setup_test_6() + _setup_test_7() + _setup_test_8() + _setup_test_9() + _setup_test_10() + _setup_test_11() + _setup_test_12() + _setup_test_13() + _setup_test_14() + _setup_test_15() + +#################################################################################################### +# Helpers +#################################################################################################### + +func make_shared_x_continuous_plot(p_plot: TauPlot, p_title: String, p_interpolation: TauLineConfig.InterpolationMode, p_num_alphas: int) -> void: + var series_names := PackedStringArray(["Series A"]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0]) + var y_a := PackedFloat64Array([10.0, 20.0, 50.0, 65.0, 70.0, 75.0, 60.0, 45.0, 25.0, 0.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a]) + + p_plot.title = p_title + p_plot.legend_enabled = false + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.tick_count_preferred = 10 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.tick_count_preferred = 10 + y_axis.overlap_strategy = TauAxisConfig.OverlapStrategy.NONE + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.INDEPENDENT + line_config.interpolation_mode = p_interpolation + line_config.style.dash_lengths_px = [8] + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + var alpha_buffer_a = TauPlot.VisualAttributes.AlphaBuffer.new(x.size()) + var alpha_values := PackedFloat32Array([ + 0.00, + 0.08, + 0.16, + 0.25, + 0.33, + 0.45, + 0.57, + 0.65, + 0.8, + 1.0, + ]) + alpha_values.resize(p_num_alphas) + alpha_buffer_a.append_values(alpha_values) + var visual_attributes_a := TauPlot.LineVisualAttributes.new() + visual_attributes_a.alpha_buffer = alpha_buffer_a + sb_a.visual_attributes = visual_attributes_a + + var bindings: Array[TauXYSeriesBinding] = [sb_a] + + p_plot.plot_xy(dataset, config, bindings) + +#################################################################################################### +# Test 1 +#################################################################################################### + +func _setup_test_1() -> void: + make_shared_x_continuous_plot(%TestPlot1, "[LINEAR] 10 alphas", TauLineConfig.InterpolationMode.LINEAR, 10) + +#################################################################################################### +# Test 2 +#################################################################################################### + +func _setup_test_2() -> void: + make_shared_x_continuous_plot(%TestPlot2, "[LINEAR] 5 alphas", TauLineConfig.InterpolationMode.LINEAR, 5) + +#################################################################################################### +# Test 3 +#################################################################################################### + +func _setup_test_3() -> void: + make_shared_x_continuous_plot(%TestPlot3, "[LINEAR] 1 alpha", TauLineConfig.InterpolationMode.LINEAR, 1) + +#################################################################################################### +# Test 4 +#################################################################################################### + +func _setup_test_4() -> void: + make_shared_x_continuous_plot(%TestPlot4, "[STEP_BEFORE] 10 alphas", TauLineConfig.InterpolationMode.STEP_BEFORE, 10) + +#################################################################################################### +# Test 5 +#################################################################################################### + +func _setup_test_5() -> void: + make_shared_x_continuous_plot(%TestPlot5, "[STEP_BEFORE] 5 alphas", TauLineConfig.InterpolationMode.STEP_BEFORE, 5) + + +#################################################################################################### +# Test 6 +#################################################################################################### + +func _setup_test_6() -> void: + make_shared_x_continuous_plot(%TestPlot6, "[STEP_BEFORE] 1 alpha", TauLineConfig.InterpolationMode.STEP_BEFORE, 1) + +#################################################################################################### +# Test 7 +#################################################################################################### + +func _setup_test_7() -> void: + make_shared_x_continuous_plot(%TestPlot7, "[STEP_MIDDLE] 10 alphas", TauLineConfig.InterpolationMode.STEP_MIDDLE, 10) + +#################################################################################################### +# Test 8 +#################################################################################################### + +func _setup_test_8() -> void: + make_shared_x_continuous_plot(%TestPlot8, "[STEP_MIDDLE] 5 alphas", TauLineConfig.InterpolationMode.STEP_MIDDLE, 5) + +#################################################################################################### +# Test 9 +#################################################################################################### + +func _setup_test_9() -> void: + make_shared_x_continuous_plot(%TestPlot9, "[STEP_MIDDLE] 1 alpha", TauLineConfig.InterpolationMode.STEP_MIDDLE, 1) + +#################################################################################################### +# Test 10 +#################################################################################################### + +func _setup_test_10() -> void: + make_shared_x_continuous_plot(%TestPlot10, "[STEP_AFTER] 10 alphas", TauLineConfig.InterpolationMode.STEP_AFTER, 10) + +#################################################################################################### +# Test 11 +#################################################################################################### + +func _setup_test_11() -> void: + make_shared_x_continuous_plot(%TestPlot11, "[STEP_AFTER] 5 alphas", TauLineConfig.InterpolationMode.STEP_AFTER, 5) + +#################################################################################################### +# Test 12 +#################################################################################################### + +func _setup_test_12() -> void: + make_shared_x_continuous_plot(%TestPlot12, "[STEP_AFTER] 1 alpha", TauLineConfig.InterpolationMode.STEP_AFTER, 1) + +#################################################################################################### +# Test 13 +#################################################################################################### + +func _setup_test_13() -> void: + make_shared_x_continuous_plot(%TestPlot13, "[SMOOTH_MONOTONE] 10 alphas", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE, 10) + +#################################################################################################### +# Test 14 +#################################################################################################### + +func _setup_test_14() -> void: + make_shared_x_continuous_plot(%TestPlot14, "[SMOOTH_MONOTONE] 5 alphas", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE, 5) + +#################################################################################################### +# Test 15 +#################################################################################################### + +func _setup_test_15() -> void: + make_shared_x_continuous_plot(%TestPlot15, "[SMOOTH_MONOTONE] 1 alpha", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE, 1) diff --git a/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_alpha.gd.uid b/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_alpha.gd.uid new file mode 100644 index 0000000..947d65c --- /dev/null +++ b/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_alpha.gd.uid @@ -0,0 +1 @@ +uid://vi4vdovvscly diff --git a/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_alpha.tscn b/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_alpha.tscn new file mode 100644 index 0000000..911e9f1 --- /dev/null +++ b/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_alpha.tscn @@ -0,0 +1,190 @@ +[gd_scene load_steps=3 format=3 uid="uid://by44kyvc18a1g"] + +[ext_resource type="Script" uid="uid://vi4vdovvscly" path="res://addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_alpha.gd" id="1_lv2ey"] +[ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_touct"] + +[node name="Bars" type="VBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_lv2ey") + +[node name="Title" type="Label" parent="."] +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +text = "Test LINE visual attributes (alpha only on dashed lines)" +horizontal_alignment = 1 + +[node name="GridContainer" type="GridContainer" parent="."] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/h_separation = 0 +theme_override_constants/v_separation = 0 +columns = 3 + +[node name="TestPlot1" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_touct") +title = "[LINEAR] 10 alphas" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot2" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_touct") +title = "[LINEAR] 5 alphas" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot3" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_touct") +title = "[LINEAR] 1 alpha" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot4" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_touct") +title = "[STEP_BEFORE] 10 alphas" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot5" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_touct") +title = "[STEP_BEFORE] 5 alphas" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot6" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_touct") +title = "[STEP_BEFORE] 1 alpha" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot7" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_touct") +title = "[STEP_MIDDLE] 10 alphas" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot8" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_touct") +title = "[STEP_MIDDLE] 5 alphas" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot9" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_touct") +title = "[STEP_MIDDLE] 1 alpha" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot10" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_touct") +title = "[STEP_AFTER] 10 alphas" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot11" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_touct") +title = "[STEP_AFTER] 5 alphas" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot12" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_touct") +title = "[STEP_AFTER] 1 alpha" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot13" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_touct") +title = "[SMOOTH_MONOTONE] 10 alphas" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot14" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_touct") +title = "[SMOOTH_MONOTONE] 5 alphas" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot15" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_touct") +title = "[SMOOTH_MONOTONE] 1 alpha" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" diff --git a/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_color.gd b/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_color.gd new file mode 100644 index 0000000..4910753 --- /dev/null +++ b/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_color.gd @@ -0,0 +1,190 @@ +@tool +extends Control + +func _ready() -> void: + _setup_test_1() + _setup_test_2() + _setup_test_3() + _setup_test_4() + _setup_test_5() + _setup_test_6() + _setup_test_7() + _setup_test_8() + _setup_test_9() + _setup_test_10() + _setup_test_11() + _setup_test_12() + _setup_test_13() + _setup_test_14() + _setup_test_15() + +#################################################################################################### +# Helpers +#################################################################################################### + +func make_shared_x_continuous_plot(p_plot: TauPlot, p_title: String, p_interpolation: TauLineConfig.InterpolationMode, p_num_colors: int) -> void: + var series_names := PackedStringArray(["Series A"]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0]) + var y_a := PackedFloat64Array([10.0, 20.0, 50.0, 65.0, 70.0, 75.0, 60.0, 45.0, 25.0, 0.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a]) + + p_plot.title = p_title + p_plot.legend_enabled = false + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.tick_count_preferred = 10 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.tick_count_preferred = 10 + y_axis.overlap_strategy = TauAxisConfig.OverlapStrategy.NONE + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.INDEPENDENT + line_config.interpolation_mode = p_interpolation + line_config.style.dash_lengths_px = [8] + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + var color_buffer_a = TauPlot.VisualAttributes.ColorBuffer.new(x.size()) + var color_values := PackedColorArray([ + Color(0.069, 0.391, 0.338, 1.0), + Color(0.069, 0.429, 0.508, 1.0), + Color(0.197, 0.328, 0.575, 1.0), + Color(0.358, 0.3, 0.475, 1.0), + Color(0.516, 0.211, 0.37, 1.0), + Color(0.507, 0.066, 0.284, 1.0), + Color(0.529, 0.232, 0.197, 1.0), + Color(0.49, 0.408, 0.116, 1.0), + Color(0.309, 0.473, 0.129, 1.0), + Color(0.01, 0.487, 0.249, 1.0), + ]) + color_values.resize(p_num_colors) + color_buffer_a.append_values(color_values) + var visual_attributes_a := TauPlot.LineVisualAttributes.new() + visual_attributes_a.color_buffer = color_buffer_a + sb_a.visual_attributes = visual_attributes_a + + var bindings: Array[TauXYSeriesBinding] = [sb_a] + + p_plot.plot_xy(dataset, config, bindings) + +#################################################################################################### +# Test 1 +#################################################################################################### + +func _setup_test_1() -> void: + make_shared_x_continuous_plot(%TestPlot1, "[LINEAR] 10 colors", TauLineConfig.InterpolationMode.LINEAR, 10) + +#################################################################################################### +# Test 2 +#################################################################################################### + +func _setup_test_2() -> void: + make_shared_x_continuous_plot(%TestPlot2, "[LINEAR] 5 colors", TauLineConfig.InterpolationMode.LINEAR, 5) + +#################################################################################################### +# Test 3 +#################################################################################################### + +func _setup_test_3() -> void: + make_shared_x_continuous_plot(%TestPlot3, "[LINEAR] 1 color", TauLineConfig.InterpolationMode.LINEAR, 1) + +#################################################################################################### +# Test 4 +#################################################################################################### + +func _setup_test_4() -> void: + make_shared_x_continuous_plot(%TestPlot4, "[STEP_BEFORE] 10 colors", TauLineConfig.InterpolationMode.STEP_BEFORE, 10) + +#################################################################################################### +# Test 5 +#################################################################################################### + +func _setup_test_5() -> void: + make_shared_x_continuous_plot(%TestPlot5, "[STEP_BEFORE] 5 colors", TauLineConfig.InterpolationMode.STEP_BEFORE, 5) + + +#################################################################################################### +# Test 6 +#################################################################################################### + +func _setup_test_6() -> void: + make_shared_x_continuous_plot(%TestPlot6, "[STEP_BEFORE] 1 color", TauLineConfig.InterpolationMode.STEP_BEFORE, 1) + +#################################################################################################### +# Test 7 +#################################################################################################### + +func _setup_test_7() -> void: + make_shared_x_continuous_plot(%TestPlot7, "[STEP_MIDDLE] 10 colors", TauLineConfig.InterpolationMode.STEP_MIDDLE, 10) + +#################################################################################################### +# Test 8 +#################################################################################################### + +func _setup_test_8() -> void: + make_shared_x_continuous_plot(%TestPlot8, "[STEP_MIDDLE] 5 colors", TauLineConfig.InterpolationMode.STEP_MIDDLE, 5) + +#################################################################################################### +# Test 9 +#################################################################################################### + +func _setup_test_9() -> void: + make_shared_x_continuous_plot(%TestPlot9, "[STEP_MIDDLE] 1 color", TauLineConfig.InterpolationMode.STEP_MIDDLE, 1) + +#################################################################################################### +# Test 10 +#################################################################################################### + +func _setup_test_10() -> void: + make_shared_x_continuous_plot(%TestPlot10, "[STEP_AFTER] 10 colors", TauLineConfig.InterpolationMode.STEP_AFTER, 10) + +#################################################################################################### +# Test 11 +#################################################################################################### + +func _setup_test_11() -> void: + make_shared_x_continuous_plot(%TestPlot11, "[STEP_AFTER] 5 colors", TauLineConfig.InterpolationMode.STEP_AFTER, 5) + +#################################################################################################### +# Test 12 +#################################################################################################### + +func _setup_test_12() -> void: + make_shared_x_continuous_plot(%TestPlot12, "[STEP_AFTER] 1 color", TauLineConfig.InterpolationMode.STEP_AFTER, 1) + +#################################################################################################### +# Test 13 +#################################################################################################### + +func _setup_test_13() -> void: + make_shared_x_continuous_plot(%TestPlot13, "[SMOOTH_MONOTONE] 10 colors", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE, 10) + +#################################################################################################### +# Test 14 +#################################################################################################### + +func _setup_test_14() -> void: + make_shared_x_continuous_plot(%TestPlot14, "[SMOOTH_MONOTONE] 5 colors", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE, 5) + +#################################################################################################### +# Test 15 +#################################################################################################### + +func _setup_test_15() -> void: + make_shared_x_continuous_plot(%TestPlot15, "[SMOOTH_MONOTONE] 1 color", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE, 1) diff --git a/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_color.gd.uid b/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_color.gd.uid new file mode 100644 index 0000000..a5c6807 --- /dev/null +++ b/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_color.gd.uid @@ -0,0 +1 @@ +uid://5dh41vpm1xd3 diff --git a/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_color.tscn b/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_color.tscn new file mode 100644 index 0000000..c77ef9a --- /dev/null +++ b/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_color.tscn @@ -0,0 +1,190 @@ +[gd_scene load_steps=3 format=3 uid="uid://bburlly6t4pae"] + +[ext_resource type="Script" uid="uid://5dh41vpm1xd3" path="res://addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_color.gd" id="1_yv5ay"] +[ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_le50r"] + +[node name="Bars" type="VBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_yv5ay") + +[node name="Title" type="Label" parent="."] +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +text = "Test LINE visual attributes (color only on dashed lines)" +horizontal_alignment = 1 + +[node name="GridContainer" type="GridContainer" parent="."] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/h_separation = 0 +theme_override_constants/v_separation = 0 +columns = 3 + +[node name="TestPlot1" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_le50r") +title = "[LINEAR] 10 colors" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot2" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_le50r") +title = "[LINEAR] 5 colors" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot3" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_le50r") +title = "[LINEAR] 1 color" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot4" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_le50r") +title = "[STEP_BEFORE] 10 colors" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot5" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_le50r") +title = "[STEP_BEFORE] 5 colors" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot6" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_le50r") +title = "[STEP_BEFORE] 1 color" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot7" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_le50r") +title = "[STEP_MIDDLE] 10 colors" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot8" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_le50r") +title = "[STEP_MIDDLE] 5 colors" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot9" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_le50r") +title = "[STEP_MIDDLE] 1 color" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot10" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_le50r") +title = "[STEP_AFTER] 10 colors" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot11" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_le50r") +title = "[STEP_AFTER] 5 colors" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot12" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_le50r") +title = "[STEP_AFTER] 1 color" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot13" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_le50r") +title = "[SMOOTH_MONOTONE] 10 colors" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot14" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_le50r") +title = "[SMOOTH_MONOTONE] 5 colors" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot15" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_le50r") +title = "[SMOOTH_MONOTONE] 1 color" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" diff --git a/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_alpha.gd b/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_alpha.gd new file mode 100644 index 0000000..f010578 --- /dev/null +++ b/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_alpha.gd @@ -0,0 +1,189 @@ +@tool +extends Control + +func _ready() -> void: + _setup_test_1() + _setup_test_2() + _setup_test_3() + _setup_test_4() + _setup_test_5() + _setup_test_6() + _setup_test_7() + _setup_test_8() + _setup_test_9() + _setup_test_10() + _setup_test_11() + _setup_test_12() + _setup_test_13() + _setup_test_14() + _setup_test_15() + +#################################################################################################### +# Helpers +#################################################################################################### + +func make_shared_x_continuous_plot(p_plot: TauPlot, p_title: String, p_interpolation: TauLineConfig.InterpolationMode, p_num_alphas: int) -> void: + var series_names := PackedStringArray(["Series A"]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0]) + var y_a := PackedFloat64Array([10.0, 20.0, 50.0, 65.0, 70.0, 75.0, 60.0, 45.0, 25.0, 0.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a]) + + p_plot.title = p_title + p_plot.legend_enabled = false + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.tick_count_preferred = 10 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.tick_count_preferred = 10 + y_axis.overlap_strategy = TauAxisConfig.OverlapStrategy.NONE + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.INDEPENDENT + line_config.interpolation_mode = p_interpolation + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + var alpha_buffer_a = TauPlot.VisualAttributes.AlphaBuffer.new(x.size()) + var alpha_values := PackedFloat32Array([ + 0.00, + 0.08, + 0.16, + 0.25, + 0.33, + 0.45, + 0.57, + 0.65, + 0.8, + 1.0, + ]) + alpha_values.resize(p_num_alphas) + alpha_buffer_a.append_values(alpha_values) + var visual_attributes_a := TauPlot.LineVisualAttributes.new() + visual_attributes_a.alpha_buffer = alpha_buffer_a + sb_a.visual_attributes = visual_attributes_a + + var bindings: Array[TauXYSeriesBinding] = [sb_a] + + p_plot.plot_xy(dataset, config, bindings) + +#################################################################################################### +# Test 1 +#################################################################################################### + +func _setup_test_1() -> void: + make_shared_x_continuous_plot(%TestPlot1, "[LINEAR] 10 alphas", TauLineConfig.InterpolationMode.LINEAR, 10) + +#################################################################################################### +# Test 2 +#################################################################################################### + +func _setup_test_2() -> void: + make_shared_x_continuous_plot(%TestPlot2, "[LINEAR] 5 alphas", TauLineConfig.InterpolationMode.LINEAR, 5) + +#################################################################################################### +# Test 3 +#################################################################################################### + +func _setup_test_3() -> void: + make_shared_x_continuous_plot(%TestPlot3, "[LINEAR] 1 alpha", TauLineConfig.InterpolationMode.LINEAR, 1) + +#################################################################################################### +# Test 4 +#################################################################################################### + +func _setup_test_4() -> void: + make_shared_x_continuous_plot(%TestPlot4, "[STEP_BEFORE] 10 alphas", TauLineConfig.InterpolationMode.STEP_BEFORE, 10) + +#################################################################################################### +# Test 5 +#################################################################################################### + +func _setup_test_5() -> void: + make_shared_x_continuous_plot(%TestPlot5, "[STEP_BEFORE] 5 alphas", TauLineConfig.InterpolationMode.STEP_BEFORE, 5) + + +#################################################################################################### +# Test 6 +#################################################################################################### + +func _setup_test_6() -> void: + make_shared_x_continuous_plot(%TestPlot6, "[STEP_BEFORE] 1 alpha", TauLineConfig.InterpolationMode.STEP_BEFORE, 1) + +#################################################################################################### +# Test 7 +#################################################################################################### + +func _setup_test_7() -> void: + make_shared_x_continuous_plot(%TestPlot7, "[STEP_MIDDLE] 10 alphas", TauLineConfig.InterpolationMode.STEP_MIDDLE, 10) + +#################################################################################################### +# Test 8 +#################################################################################################### + +func _setup_test_8() -> void: + make_shared_x_continuous_plot(%TestPlot8, "[STEP_MIDDLE] 5 alphas", TauLineConfig.InterpolationMode.STEP_MIDDLE, 5) + +#################################################################################################### +# Test 9 +#################################################################################################### + +func _setup_test_9() -> void: + make_shared_x_continuous_plot(%TestPlot9, "[STEP_MIDDLE] 1 alpha", TauLineConfig.InterpolationMode.STEP_MIDDLE, 1) + +#################################################################################################### +# Test 10 +#################################################################################################### + +func _setup_test_10() -> void: + make_shared_x_continuous_plot(%TestPlot10, "[STEP_AFTER] 10 alphas", TauLineConfig.InterpolationMode.STEP_AFTER, 10) + +#################################################################################################### +# Test 11 +#################################################################################################### + +func _setup_test_11() -> void: + make_shared_x_continuous_plot(%TestPlot11, "[STEP_AFTER] 5 alphas", TauLineConfig.InterpolationMode.STEP_AFTER, 5) + +#################################################################################################### +# Test 12 +#################################################################################################### + +func _setup_test_12() -> void: + make_shared_x_continuous_plot(%TestPlot12, "[STEP_AFTER] 1 alpha", TauLineConfig.InterpolationMode.STEP_AFTER, 1) + +#################################################################################################### +# Test 13 +#################################################################################################### + +func _setup_test_13() -> void: + make_shared_x_continuous_plot(%TestPlot13, "[SMOOTH_MONOTONE] 10 alphas", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE, 10) + +#################################################################################################### +# Test 14 +#################################################################################################### + +func _setup_test_14() -> void: + make_shared_x_continuous_plot(%TestPlot14, "[SMOOTH_MONOTONE] 5 alphas", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE, 5) + +#################################################################################################### +# Test 15 +#################################################################################################### + +func _setup_test_15() -> void: + make_shared_x_continuous_plot(%TestPlot15, "[SMOOTH_MONOTONE] 1 alpha", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE, 1) diff --git a/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_alpha.gd.uid b/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_alpha.gd.uid new file mode 100644 index 0000000..4e7b51b --- /dev/null +++ b/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_alpha.gd.uid @@ -0,0 +1 @@ +uid://vro08bntv7uo diff --git a/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_alpha.tscn b/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_alpha.tscn new file mode 100644 index 0000000..45a5473 --- /dev/null +++ b/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_alpha.tscn @@ -0,0 +1,190 @@ +[gd_scene load_steps=3 format=3 uid="uid://bx0xwt3nrm2tb"] + +[ext_resource type="Script" uid="uid://vro08bntv7uo" path="res://addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_alpha.gd" id="1_u76at"] +[ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_hy82u"] + +[node name="Bars" type="VBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_u76at") + +[node name="Title" type="Label" parent="."] +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +text = "Test LINE visual attributes (alpha only on solid lines)" +horizontal_alignment = 1 + +[node name="GridContainer" type="GridContainer" parent="."] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/h_separation = 0 +theme_override_constants/v_separation = 0 +columns = 3 + +[node name="TestPlot1" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_hy82u") +title = "[LINEAR] 10 alphas" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot2" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_hy82u") +title = "[LINEAR] 5 alphas" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot3" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_hy82u") +title = "[LINEAR] 1 alpha" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot4" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_hy82u") +title = "[STEP_BEFORE] 10 alphas" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot5" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_hy82u") +title = "[STEP_BEFORE] 5 alphas" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot6" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_hy82u") +title = "[STEP_BEFORE] 1 alpha" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot7" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_hy82u") +title = "[STEP_MIDDLE] 10 alphas" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot8" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_hy82u") +title = "[STEP_MIDDLE] 5 alphas" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot9" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_hy82u") +title = "[STEP_MIDDLE] 1 alpha" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot10" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_hy82u") +title = "[STEP_AFTER] 10 alphas" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot11" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_hy82u") +title = "[STEP_AFTER] 5 alphas" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot12" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_hy82u") +title = "[STEP_AFTER] 1 alpha" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot13" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_hy82u") +title = "[SMOOTH_MONOTONE] 10 alphas" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot14" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_hy82u") +title = "[SMOOTH_MONOTONE] 5 alphas" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot15" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_hy82u") +title = "[SMOOTH_MONOTONE] 1 alpha" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" diff --git a/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_color.gd b/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_color.gd new file mode 100644 index 0000000..d170609 --- /dev/null +++ b/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_color.gd @@ -0,0 +1,189 @@ +@tool +extends Control + +func _ready() -> void: + _setup_test_1() + _setup_test_2() + _setup_test_3() + _setup_test_4() + _setup_test_5() + _setup_test_6() + _setup_test_7() + _setup_test_8() + _setup_test_9() + _setup_test_10() + _setup_test_11() + _setup_test_12() + _setup_test_13() + _setup_test_14() + _setup_test_15() + +#################################################################################################### +# Helpers +#################################################################################################### + +func make_shared_x_continuous_plot(p_plot: TauPlot, p_title: String, p_interpolation: TauLineConfig.InterpolationMode, p_num_colors: int) -> void: + var series_names := PackedStringArray(["Series A"]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0]) + var y_a := PackedFloat64Array([10.0, 20.0, 50.0, 65.0, 70.0, 75.0, 60.0, 45.0, 25.0, 0.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a]) + + p_plot.title = p_title + p_plot.legend_enabled = false + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.tick_count_preferred = 10 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.tick_count_preferred = 10 + y_axis.overlap_strategy = TauAxisConfig.OverlapStrategy.NONE + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.INDEPENDENT + line_config.interpolation_mode = p_interpolation + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + var color_buffer_a = TauPlot.VisualAttributes.ColorBuffer.new(x.size()) + var color_values := PackedColorArray([ + Color(0.069, 0.391, 0.338, 1.0), + Color(0.069, 0.429, 0.508, 1.0), + Color(0.197, 0.328, 0.575, 1.0), + Color(0.358, 0.3, 0.475, 1.0), + Color(0.516, 0.211, 0.37, 1.0), + Color(0.507, 0.066, 0.284, 1.0), + Color(0.529, 0.232, 0.197, 1.0), + Color(0.49, 0.408, 0.116, 1.0), + Color(0.309, 0.473, 0.129, 1.0), + Color(0.01, 0.487, 0.249, 1.0), + ]) + color_values.resize(p_num_colors) + color_buffer_a.append_values(color_values) + var visual_attributes_a := TauPlot.LineVisualAttributes.new() + visual_attributes_a.color_buffer = color_buffer_a + sb_a.visual_attributes = visual_attributes_a + + var bindings: Array[TauXYSeriesBinding] = [sb_a] + + p_plot.plot_xy(dataset, config, bindings) + +#################################################################################################### +# Test 1 +#################################################################################################### + +func _setup_test_1() -> void: + make_shared_x_continuous_plot(%TestPlot1, "[LINEAR] 10 colors", TauLineConfig.InterpolationMode.LINEAR, 10) + +#################################################################################################### +# Test 2 +#################################################################################################### + +func _setup_test_2() -> void: + make_shared_x_continuous_plot(%TestPlot2, "[LINEAR] 5 colors", TauLineConfig.InterpolationMode.LINEAR, 5) + +#################################################################################################### +# Test 3 +#################################################################################################### + +func _setup_test_3() -> void: + make_shared_x_continuous_plot(%TestPlot3, "[LINEAR] 1 color", TauLineConfig.InterpolationMode.LINEAR, 1) + +#################################################################################################### +# Test 4 +#################################################################################################### + +func _setup_test_4() -> void: + make_shared_x_continuous_plot(%TestPlot4, "[STEP_BEFORE] 10 colors", TauLineConfig.InterpolationMode.STEP_BEFORE, 10) + +#################################################################################################### +# Test 5 +#################################################################################################### + +func _setup_test_5() -> void: + make_shared_x_continuous_plot(%TestPlot5, "[STEP_BEFORE] 5 colors", TauLineConfig.InterpolationMode.STEP_BEFORE, 5) + + +#################################################################################################### +# Test 6 +#################################################################################################### + +func _setup_test_6() -> void: + make_shared_x_continuous_plot(%TestPlot6, "[STEP_BEFORE] 1 color", TauLineConfig.InterpolationMode.STEP_BEFORE, 1) + +#################################################################################################### +# Test 7 +#################################################################################################### + +func _setup_test_7() -> void: + make_shared_x_continuous_plot(%TestPlot7, "[STEP_MIDDLE] 10 colors", TauLineConfig.InterpolationMode.STEP_MIDDLE, 10) + +#################################################################################################### +# Test 8 +#################################################################################################### + +func _setup_test_8() -> void: + make_shared_x_continuous_plot(%TestPlot8, "[STEP_MIDDLE] 5 colors", TauLineConfig.InterpolationMode.STEP_MIDDLE, 5) + +#################################################################################################### +# Test 9 +#################################################################################################### + +func _setup_test_9() -> void: + make_shared_x_continuous_plot(%TestPlot9, "[STEP_MIDDLE] 1 color", TauLineConfig.InterpolationMode.STEP_MIDDLE, 1) + +#################################################################################################### +# Test 10 +#################################################################################################### + +func _setup_test_10() -> void: + make_shared_x_continuous_plot(%TestPlot10, "[STEP_AFTER] 10 colors", TauLineConfig.InterpolationMode.STEP_AFTER, 10) + +#################################################################################################### +# Test 11 +#################################################################################################### + +func _setup_test_11() -> void: + make_shared_x_continuous_plot(%TestPlot11, "[STEP_AFTER] 5 colors", TauLineConfig.InterpolationMode.STEP_AFTER, 5) + +#################################################################################################### +# Test 12 +#################################################################################################### + +func _setup_test_12() -> void: + make_shared_x_continuous_plot(%TestPlot12, "[STEP_AFTER] 1 color", TauLineConfig.InterpolationMode.STEP_AFTER, 1) + +#################################################################################################### +# Test 13 +#################################################################################################### + +func _setup_test_13() -> void: + make_shared_x_continuous_plot(%TestPlot13, "[SMOOTH_MONOTONE] 10 colors", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE, 10) + +#################################################################################################### +# Test 14 +#################################################################################################### + +func _setup_test_14() -> void: + make_shared_x_continuous_plot(%TestPlot14, "[SMOOTH_MONOTONE] 5 colors", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE, 5) + +#################################################################################################### +# Test 15 +#################################################################################################### + +func _setup_test_15() -> void: + make_shared_x_continuous_plot(%TestPlot15, "[SMOOTH_MONOTONE] 1 color", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE, 1) diff --git a/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_color.gd.uid b/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_color.gd.uid new file mode 100644 index 0000000..1104420 --- /dev/null +++ b/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_color.gd.uid @@ -0,0 +1 @@ +uid://c78fi08wv7vqd diff --git a/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_color.tscn b/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_color.tscn new file mode 100644 index 0000000..1d39731 --- /dev/null +++ b/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_color.tscn @@ -0,0 +1,190 @@ +[gd_scene load_steps=3 format=3 uid="uid://djun6fo6ivusk"] + +[ext_resource type="Script" uid="uid://c78fi08wv7vqd" path="res://addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_color.gd" id="1_7vjol"] +[ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_lc0kd"] + +[node name="Bars" type="VBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_7vjol") + +[node name="Title" type="Label" parent="."] +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +text = "Test LINE visual attributes (color only on solid lines)" +horizontal_alignment = 1 + +[node name="GridContainer" type="GridContainer" parent="."] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/h_separation = 0 +theme_override_constants/v_separation = 0 +columns = 3 + +[node name="TestPlot1" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_lc0kd") +title = "[LINEAR] 10 colors" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot2" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_lc0kd") +title = "[LINEAR] 5 colors" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot3" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_lc0kd") +title = "[LINEAR] 1 color" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot4" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_lc0kd") +title = "[STEP_BEFORE] 10 colors" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot5" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_lc0kd") +title = "[STEP_BEFORE] 5 colors" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot6" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_lc0kd") +title = "[STEP_BEFORE] 1 color" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot7" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_lc0kd") +title = "[STEP_MIDDLE] 10 colors" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot8" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_lc0kd") +title = "[STEP_MIDDLE] 5 colors" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot9" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_lc0kd") +title = "[STEP_MIDDLE] 1 color" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot10" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_lc0kd") +title = "[STEP_AFTER] 10 colors" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot11" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_lc0kd") +title = "[STEP_AFTER] 5 colors" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot12" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_lc0kd") +title = "[STEP_AFTER] 1 color" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot13" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_lc0kd") +title = "[SMOOTH_MONOTONE] 10 colors" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot14" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_lc0kd") +title = "[SMOOTH_MONOTONE] 5 colors" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot15" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_lc0kd") +title = "[SMOOTH_MONOTONE] 1 color" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" diff --git a/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_alpha.gd b/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_alpha.gd new file mode 100644 index 0000000..71c983d --- /dev/null +++ b/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_alpha.gd @@ -0,0 +1,233 @@ +@tool +extends Control + +class Spectrum extends RefCounted: + + const ATTACK_HZ: float = 18.0 + const RELEASE_HZ: float = 6.0 + const ENERGY_DECAY_HZ: float = 10.0 + const HIT_RATE: float = 5.5 + const HIT_STRENGTH: float = 1.2 + const BREATH: float = 0.08 + + var values: Array[float] = [] + var energy: Array[float] = [] + var phase: Array[float] = [] + + func _init(p_band_count: int = 16) -> void: + values.resize(p_band_count) + energy.resize(p_band_count) + phase.resize(p_band_count) + + for i in p_band_count: + values[i] = randf() + energy[i] = 0.0 + phase[i] = randf() * TAU + + func step(delta: float) -> void: + for i in values.size(): + _step_band(i, delta) + + func _step_band(i: int, delta: float) -> void: + # 1) transient impulses (Poisson-ish) + if randf() < HIT_RATE * delta: + var impulse := pow(randf(), 3.0) * HIT_STRENGTH + energy[i] += impulse + + # 2) excitation decay + energy[i] *= exp(-ENERGY_DECAY_HZ * delta) + + # 3) slow breathing motion + phase[i] += (2.0 + 3.0 * randf()) * delta + var slow_noise := sin(phase[i]) * BREATH + + var target := energy[i] + slow_noise + + # 4) fast attack, slow release + if target > values[i]: + values[i] += (target - values[i]) * (1.0 - exp(-ATTACK_HZ * delta)) + else: + values[i] += (target - values[i]) * (1.0 - exp(-RELEASE_HZ * delta)) + +var _t: float = 0.0 + +var _datasets: Array[TauPlot.Dataset] = [] +var _state: Array[Dictionary] = [] + +var _spectrum: Spectrum = null +var _spectrum_2: Spectrum = null + +var _gradient: Gradient = null + +func _ready() -> void: + _t = 0.0 + _datasets.clear() + _state.clear() + _spectrum = Spectrum.new(10) + + _gradient = Gradient.new() + _gradient.offsets = PackedFloat32Array([ + 0.0, + 0.25, + 0.5, + 0.75, + 1.0 + ]) + _gradient.colors = PackedColorArray([ + Color("bd0034ff"), + Color("cc4219ff"), + Color("c97010ff"), + Color("b4aa00ff"), + Color("439800ff"), + ]) + + _setup_test_1() + _setup_test_2() + _setup_test_3() + _setup_test_4() + _setup_test_5() + + +func _process(delta: float) -> void: + _t += delta + + _spectrum.step(delta) + + _step_test_1(delta) + _step_test_2(delta) + _step_test_3(delta) + _step_test_4(delta) + _step_test_5(delta) + +#################################################################################################### +# Helpers +#################################################################################################### + +func make_shared_x_continuous_plot(p_plot: TauPlot, p_title: String, p_interpolation: TauLineConfig.InterpolationMode) -> void: + var series_names := PackedStringArray(["Series A"]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0]) + var y_a := PackedFloat64Array([10.0, 20.0, 50.0, 65.0, 70.0, 75.0, 60.0, 45.0, 25.0, 0.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a]) + _datasets.append(dataset) + + p_plot.title = p_title + p_plot.legend_enabled = false + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.tick_count_preferred = 10 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.tick_count_preferred = 10 + y_axis.overlap_strategy = TauAxisConfig.OverlapStrategy.NONE + y_axis.range_override_enabled = true + y_axis.min_override = -Spectrum.BREATH + y_axis.max_override = 1.0 + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.INDEPENDENT + line_config.interpolation_mode = p_interpolation + line_config.style.dash_lengths_px = [8] + var visual_callbacks := TauPlot.LineVisualCallbacks.new() + visual_callbacks.alpha_callback = Callable(func(series_index: int, sample_index: int, x_value: Variant, y_value: float) -> float: + const k := 1.6 + var y := clampf(y_value, 0.0, 1.0) + return (k + 1.0) * y / (1.0 + k * y) + ) + line_config.visual_callbacks = visual_callbacks + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a] + + p_plot.plot_xy(dataset, config, bindings) + +#################################################################################################### +# Test 1 +#################################################################################################### + +func _setup_test_1() -> void: + make_shared_x_continuous_plot(%TestPlot1, "[LINEAR]", TauLineConfig.InterpolationMode.LINEAR) + + +func _step_test_1(delta: float) -> void: + var dataset := _datasets[0] + dataset.begin_batch() + for b in range(dataset.get_shared_capacity()): + dataset.set_series_y(dataset.get_series_id_by_index(0), b, _spectrum.values[b]) + dataset.end_batch() + +#################################################################################################### +# Test 2 +#################################################################################################### + +func _setup_test_2() -> void: + make_shared_x_continuous_plot(%TestPlot2, "[STEP_BEFORE]", TauLineConfig.InterpolationMode.STEP_BEFORE) + + +func _step_test_2(delta: float) -> void: + var dataset := _datasets[1] + dataset.begin_batch() + for b in range(dataset.get_shared_capacity()): + dataset.set_series_y(dataset.get_series_id_by_index(0), b, _spectrum.values[b]) + dataset.end_batch() + +#################################################################################################### +# Test 3 +#################################################################################################### + +func _setup_test_3() -> void: + make_shared_x_continuous_plot(%TestPlot3, "[STEP_MIDDLE]", TauLineConfig.InterpolationMode.STEP_MIDDLE) + + +func _step_test_3(delta: float) -> void: + var dataset := _datasets[2] + dataset.begin_batch() + for b in range(dataset.get_shared_capacity()): + dataset.set_series_y(dataset.get_series_id_by_index(0), b, _spectrum.values[b]) + dataset.end_batch() + +#################################################################################################### +# Test 4 +#################################################################################################### + +func _setup_test_4() -> void: + make_shared_x_continuous_plot(%TestPlot4, "[STEP_AFTER]", TauLineConfig.InterpolationMode.STEP_AFTER) + + +func _step_test_4(delta: float) -> void: + var dataset := _datasets[3] + dataset.begin_batch() + for b in range(dataset.get_shared_capacity()): + dataset.set_series_y(dataset.get_series_id_by_index(0), b, _spectrum.values[b]) + dataset.end_batch() + +#################################################################################################### +# Test 5 +#################################################################################################### + +func _setup_test_5() -> void: + make_shared_x_continuous_plot(%TestPlot5, "[SMOOTH_MONOTONE]", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE) + + +func _step_test_5(delta: float) -> void: + var dataset := _datasets[4] + dataset.begin_batch() + for b in range(dataset.get_shared_capacity()): + dataset.set_series_y(dataset.get_series_id_by_index(0), b, _spectrum.values[b]) + dataset.end_batch() diff --git a/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_alpha.gd.uid b/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_alpha.gd.uid new file mode 100644 index 0000000..e96042a --- /dev/null +++ b/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_alpha.gd.uid @@ -0,0 +1 @@ +uid://d1ehkjmoa0dhx diff --git a/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_alpha.tscn b/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_alpha.tscn new file mode 100644 index 0000000..7dc3e02 --- /dev/null +++ b/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_alpha.tscn @@ -0,0 +1,79 @@ +[gd_scene load_steps=3 format=3 uid="uid://bnf1v6jsjtk6l"] + +[ext_resource type="Script" uid="uid://d1ehkjmoa0dhx" path="res://addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_alpha.gd" id="1_w8xoc"] +[ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_wvy6q"] + +[node name="Bars" type="VBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_w8xoc") + +[node name="Title" type="Label" parent="."] +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +text = "Test LINE visual callbacks (alpha only on dashed lines)" +horizontal_alignment = 1 + +[node name="GridContainer" type="GridContainer" parent="."] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/h_separation = 0 +theme_override_constants/v_separation = 0 + +[node name="TestPlot1" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_wvy6q") +title = "[LINEAR]" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot2" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_wvy6q") +title = "[STEP_BEFORE]" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot3" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_wvy6q") +title = "[STEP_MIDDLE]" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot4" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_wvy6q") +title = "[STEP_AFTER]" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot5" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_wvy6q") +title = "[SMOOTH_MONOTONE]" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" diff --git a/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_color.gd b/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_color.gd new file mode 100644 index 0000000..21af4aa --- /dev/null +++ b/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_color.gd @@ -0,0 +1,231 @@ +@tool +extends Control + +class Spectrum extends RefCounted: + + const ATTACK_HZ: float = 18.0 + const RELEASE_HZ: float = 6.0 + const ENERGY_DECAY_HZ: float = 10.0 + const HIT_RATE: float = 5.5 + const HIT_STRENGTH: float = 1.2 + const BREATH: float = 0.08 + + var values: Array[float] = [] + var energy: Array[float] = [] + var phase: Array[float] = [] + + func _init(p_band_count: int = 16) -> void: + values.resize(p_band_count) + energy.resize(p_band_count) + phase.resize(p_band_count) + + for i in p_band_count: + values[i] = randf() + energy[i] = 0.0 + phase[i] = randf() * TAU + + func step(delta: float) -> void: + for i in values.size(): + _step_band(i, delta) + + func _step_band(i: int, delta: float) -> void: + # 1) transient impulses (Poisson-ish) + if randf() < HIT_RATE * delta: + var impulse := pow(randf(), 3.0) * HIT_STRENGTH + energy[i] += impulse + + # 2) excitation decay + energy[i] *= exp(-ENERGY_DECAY_HZ * delta) + + # 3) slow breathing motion + phase[i] += (2.0 + 3.0 * randf()) * delta + var slow_noise := sin(phase[i]) * BREATH + + var target := energy[i] + slow_noise + + # 4) fast attack, slow release + if target > values[i]: + values[i] += (target - values[i]) * (1.0 - exp(-ATTACK_HZ * delta)) + else: + values[i] += (target - values[i]) * (1.0 - exp(-RELEASE_HZ * delta)) + +var _t: float = 0.0 + +var _datasets: Array[TauPlot.Dataset] = [] +var _state: Array[Dictionary] = [] + +var _spectrum: Spectrum = null +var _spectrum_2: Spectrum = null + +var _gradient: Gradient = null + +func _ready() -> void: + _t = 0.0 + _datasets.clear() + _state.clear() + _spectrum = Spectrum.new(10) + + _gradient = Gradient.new() + _gradient.offsets = PackedFloat32Array([ + 0.0, + 0.25, + 0.5, + 0.75, + 1.0 + ]) + _gradient.colors = PackedColorArray([ + Color("bd0034ff"), + Color("cc4219ff"), + Color("c97010ff"), + Color("b4aa00ff"), + Color("439800ff"), + ]) + + _setup_test_1() + _setup_test_2() + _setup_test_3() + _setup_test_4() + _setup_test_5() + + +func _process(delta: float) -> void: + _t += delta + + _spectrum.step(delta) + + _step_test_1(delta) + _step_test_2(delta) + _step_test_3(delta) + _step_test_4(delta) + _step_test_5(delta) + +#################################################################################################### +# Helpers +#################################################################################################### + +func make_shared_x_continuous_plot(p_plot: TauPlot, p_title: String, p_interpolation: TauLineConfig.InterpolationMode) -> void: + var series_names := PackedStringArray(["Series A"]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0]) + var y_a := PackedFloat64Array([10.0, 20.0, 50.0, 65.0, 70.0, 75.0, 60.0, 45.0, 25.0, 0.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a]) + _datasets.append(dataset) + + p_plot.title = p_title + p_plot.legend_enabled = false + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.tick_count_preferred = 10 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.tick_count_preferred = 10 + y_axis.overlap_strategy = TauAxisConfig.OverlapStrategy.NONE + y_axis.range_override_enabled = true + y_axis.min_override = -Spectrum.BREATH + y_axis.max_override = 1.0 + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.INDEPENDENT + line_config.interpolation_mode = p_interpolation + line_config.style.dash_lengths_px = [8] + var visual_callbacks := TauPlot.LineVisualCallbacks.new() + visual_callbacks.color_callback = Callable(func(series_index: int, sample_index: int, x_value: Variant, y_value: float) -> Color: + return _gradient.sample(clampf(1.0 - y_value, 0.0, 1.0)) + ) + line_config.visual_callbacks = visual_callbacks + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a] + + p_plot.plot_xy(dataset, config, bindings) + +#################################################################################################### +# Test 1 +#################################################################################################### + +func _setup_test_1() -> void: + make_shared_x_continuous_plot(%TestPlot1, "[LINEAR]", TauLineConfig.InterpolationMode.LINEAR) + + +func _step_test_1(delta: float) -> void: + var dataset := _datasets[0] + dataset.begin_batch() + for b in range(dataset.get_shared_capacity()): + dataset.set_series_y(dataset.get_series_id_by_index(0), b, _spectrum.values[b]) + dataset.end_batch() + +#################################################################################################### +# Test 2 +#################################################################################################### + +func _setup_test_2() -> void: + make_shared_x_continuous_plot(%TestPlot2, "[STEP_BEFORE]", TauLineConfig.InterpolationMode.STEP_BEFORE) + + +func _step_test_2(delta: float) -> void: + var dataset := _datasets[1] + dataset.begin_batch() + for b in range(dataset.get_shared_capacity()): + dataset.set_series_y(dataset.get_series_id_by_index(0), b, _spectrum.values[b]) + dataset.end_batch() + +#################################################################################################### +# Test 3 +#################################################################################################### + +func _setup_test_3() -> void: + make_shared_x_continuous_plot(%TestPlot3, "[STEP_MIDDLE]", TauLineConfig.InterpolationMode.STEP_MIDDLE) + + +func _step_test_3(delta: float) -> void: + var dataset := _datasets[2] + dataset.begin_batch() + for b in range(dataset.get_shared_capacity()): + dataset.set_series_y(dataset.get_series_id_by_index(0), b, _spectrum.values[b]) + dataset.end_batch() + +#################################################################################################### +# Test 4 +#################################################################################################### + +func _setup_test_4() -> void: + make_shared_x_continuous_plot(%TestPlot4, "[STEP_AFTER]", TauLineConfig.InterpolationMode.STEP_AFTER) + + +func _step_test_4(delta: float) -> void: + var dataset := _datasets[3] + dataset.begin_batch() + for b in range(dataset.get_shared_capacity()): + dataset.set_series_y(dataset.get_series_id_by_index(0), b, _spectrum.values[b]) + dataset.end_batch() + +#################################################################################################### +# Test 5 +#################################################################################################### + +func _setup_test_5() -> void: + make_shared_x_continuous_plot(%TestPlot5, "[SMOOTH_MONOTONE]", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE) + + +func _step_test_5(delta: float) -> void: + var dataset := _datasets[4] + dataset.begin_batch() + for b in range(dataset.get_shared_capacity()): + dataset.set_series_y(dataset.get_series_id_by_index(0), b, _spectrum.values[b]) + dataset.end_batch() diff --git a/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_color.gd.uid b/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_color.gd.uid new file mode 100644 index 0000000..948d88c --- /dev/null +++ b/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_color.gd.uid @@ -0,0 +1 @@ +uid://tgw2e12ovgd diff --git a/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_color.tscn b/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_color.tscn new file mode 100644 index 0000000..41f03e1 --- /dev/null +++ b/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_color.tscn @@ -0,0 +1,79 @@ +[gd_scene load_steps=3 format=3 uid="uid://6t7qku0by8cw"] + +[ext_resource type="Script" uid="uid://tgw2e12ovgd" path="res://addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_color.gd" id="1_mx55d"] +[ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_0c0l8"] + +[node name="Bars" type="VBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_mx55d") + +[node name="Title" type="Label" parent="."] +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +text = "Test LINE visual callbacks (color only on dashed lines)" +horizontal_alignment = 1 + +[node name="GridContainer" type="GridContainer" parent="."] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/h_separation = 0 +theme_override_constants/v_separation = 0 + +[node name="TestPlot1" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_0c0l8") +title = "[LINEAR]" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot2" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_0c0l8") +title = "[STEP_BEFORE]" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot3" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_0c0l8") +title = "[STEP_MIDDLE]" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot4" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_0c0l8") +title = "[STEP_AFTER]" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot5" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_0c0l8") +title = "[SMOOTH_MONOTONE]" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" diff --git a/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_alpha.gd b/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_alpha.gd new file mode 100644 index 0000000..9e722f2 --- /dev/null +++ b/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_alpha.gd @@ -0,0 +1,232 @@ +@tool +extends Control + +class Spectrum extends RefCounted: + + const ATTACK_HZ: float = 18.0 + const RELEASE_HZ: float = 6.0 + const ENERGY_DECAY_HZ: float = 10.0 + const HIT_RATE: float = 5.5 + const HIT_STRENGTH: float = 1.2 + const BREATH: float = 0.08 + + var values: Array[float] = [] + var energy: Array[float] = [] + var phase: Array[float] = [] + + func _init(p_band_count: int = 16) -> void: + values.resize(p_band_count) + energy.resize(p_band_count) + phase.resize(p_band_count) + + for i in p_band_count: + values[i] = randf() + energy[i] = 0.0 + phase[i] = randf() * TAU + + func step(delta: float) -> void: + for i in values.size(): + _step_band(i, delta) + + func _step_band(i: int, delta: float) -> void: + # 1) transient impulses (Poisson-ish) + if randf() < HIT_RATE * delta: + var impulse := pow(randf(), 3.0) * HIT_STRENGTH + energy[i] += impulse + + # 2) excitation decay + energy[i] *= exp(-ENERGY_DECAY_HZ * delta) + + # 3) slow breathing motion + phase[i] += (2.0 + 3.0 * randf()) * delta + var slow_noise := sin(phase[i]) * BREATH + + var target := energy[i] + slow_noise + + # 4) fast attack, slow release + if target > values[i]: + values[i] += (target - values[i]) * (1.0 - exp(-ATTACK_HZ * delta)) + else: + values[i] += (target - values[i]) * (1.0 - exp(-RELEASE_HZ * delta)) + +var _t: float = 0.0 + +var _datasets: Array[TauPlot.Dataset] = [] +var _state: Array[Dictionary] = [] + +var _spectrum: Spectrum = null +var _spectrum_2: Spectrum = null + +var _gradient: Gradient = null + +func _ready() -> void: + _t = 0.0 + _datasets.clear() + _state.clear() + _spectrum = Spectrum.new(10) + + _gradient = Gradient.new() + _gradient.offsets = PackedFloat32Array([ + 0.0, + 0.25, + 0.5, + 0.75, + 1.0 + ]) + _gradient.colors = PackedColorArray([ + Color("bd0034ff"), + Color("cc4219ff"), + Color("c97010ff"), + Color("b4aa00ff"), + Color("439800ff"), + ]) + + _setup_test_1() + _setup_test_2() + _setup_test_3() + _setup_test_4() + _setup_test_5() + + +func _process(delta: float) -> void: + _t += delta + + _spectrum.step(delta) + + _step_test_1(delta) + _step_test_2(delta) + _step_test_3(delta) + _step_test_4(delta) + _step_test_5(delta) + +#################################################################################################### +# Helpers +#################################################################################################### + +func make_shared_x_continuous_plot(p_plot: TauPlot, p_title: String, p_interpolation: TauLineConfig.InterpolationMode) -> void: + var series_names := PackedStringArray(["Series A"]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0]) + var y_a := PackedFloat64Array([10.0, 20.0, 50.0, 65.0, 70.0, 75.0, 60.0, 45.0, 25.0, 0.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a]) + _datasets.append(dataset) + + p_plot.title = p_title + p_plot.legend_enabled = false + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.tick_count_preferred = 10 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.tick_count_preferred = 10 + y_axis.overlap_strategy = TauAxisConfig.OverlapStrategy.NONE + y_axis.range_override_enabled = true + y_axis.min_override = -Spectrum.BREATH + y_axis.max_override = 1.0 + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.INDEPENDENT + line_config.interpolation_mode = p_interpolation + var visual_callbacks := TauPlot.LineVisualCallbacks.new() + visual_callbacks.alpha_callback = Callable(func(series_index: int, sample_index: int, x_value: Variant, y_value: float) -> float: + const k := 1.6 + var y := clampf(y_value, 0.0, 1.0) + return (k + 1.0) * y / (1.0 + k * y) + ) + line_config.visual_callbacks = visual_callbacks + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a] + + p_plot.plot_xy(dataset, config, bindings) + +#################################################################################################### +# Test 1 +#################################################################################################### + +func _setup_test_1() -> void: + make_shared_x_continuous_plot(%TestPlot1, "[LINEAR]", TauLineConfig.InterpolationMode.LINEAR) + + +func _step_test_1(delta: float) -> void: + var dataset := _datasets[0] + dataset.begin_batch() + for b in range(dataset.get_shared_capacity()): + dataset.set_series_y(dataset.get_series_id_by_index(0), b, _spectrum.values[b]) + dataset.end_batch() + +#################################################################################################### +# Test 2 +#################################################################################################### + +func _setup_test_2() -> void: + make_shared_x_continuous_plot(%TestPlot2, "[STEP_BEFORE]", TauLineConfig.InterpolationMode.STEP_BEFORE) + + +func _step_test_2(delta: float) -> void: + var dataset := _datasets[1] + dataset.begin_batch() + for b in range(dataset.get_shared_capacity()): + dataset.set_series_y(dataset.get_series_id_by_index(0), b, _spectrum.values[b]) + dataset.end_batch() + +#################################################################################################### +# Test 3 +#################################################################################################### + +func _setup_test_3() -> void: + make_shared_x_continuous_plot(%TestPlot3, "[STEP_MIDDLE]", TauLineConfig.InterpolationMode.STEP_MIDDLE) + + +func _step_test_3(delta: float) -> void: + var dataset := _datasets[2] + dataset.begin_batch() + for b in range(dataset.get_shared_capacity()): + dataset.set_series_y(dataset.get_series_id_by_index(0), b, _spectrum.values[b]) + dataset.end_batch() + +#################################################################################################### +# Test 4 +#################################################################################################### + +func _setup_test_4() -> void: + make_shared_x_continuous_plot(%TestPlot4, "[STEP_AFTER]", TauLineConfig.InterpolationMode.STEP_AFTER) + + +func _step_test_4(delta: float) -> void: + var dataset := _datasets[3] + dataset.begin_batch() + for b in range(dataset.get_shared_capacity()): + dataset.set_series_y(dataset.get_series_id_by_index(0), b, _spectrum.values[b]) + dataset.end_batch() + +#################################################################################################### +# Test 5 +#################################################################################################### + +func _setup_test_5() -> void: + make_shared_x_continuous_plot(%TestPlot5, "[SMOOTH_MONOTONE]", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE) + + +func _step_test_5(delta: float) -> void: + var dataset := _datasets[4] + dataset.begin_batch() + for b in range(dataset.get_shared_capacity()): + dataset.set_series_y(dataset.get_series_id_by_index(0), b, _spectrum.values[b]) + dataset.end_batch() diff --git a/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_alpha.gd.uid b/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_alpha.gd.uid new file mode 100644 index 0000000..546db18 --- /dev/null +++ b/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_alpha.gd.uid @@ -0,0 +1 @@ +uid://byunb5b3q5f0n diff --git a/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_alpha.tscn b/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_alpha.tscn new file mode 100644 index 0000000..4363503 --- /dev/null +++ b/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_alpha.tscn @@ -0,0 +1,79 @@ +[gd_scene load_steps=3 format=3 uid="uid://8juhu4rtvj3p"] + +[ext_resource type="Script" uid="uid://byunb5b3q5f0n" path="res://addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_alpha.gd" id="1_8ct1l"] +[ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_ronnj"] + +[node name="Bars" type="VBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_8ct1l") + +[node name="Title" type="Label" parent="."] +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +text = "Test LINE visual callbacks (alpha only on solid lines)" +horizontal_alignment = 1 + +[node name="GridContainer" type="GridContainer" parent="."] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/h_separation = 0 +theme_override_constants/v_separation = 0 + +[node name="TestPlot1" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_ronnj") +title = "[LINEAR]" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot2" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_ronnj") +title = "[STEP_BEFORE]" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot3" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_ronnj") +title = "[STEP_MIDDLE]" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot4" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_ronnj") +title = "[STEP_AFTER]" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot5" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_ronnj") +title = "[SMOOTH_MONOTONE]" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" diff --git a/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_color.gd b/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_color.gd new file mode 100644 index 0000000..e748d01 --- /dev/null +++ b/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_color.gd @@ -0,0 +1,230 @@ +@tool +extends Control + +class Spectrum extends RefCounted: + + const ATTACK_HZ: float = 18.0 + const RELEASE_HZ: float = 6.0 + const ENERGY_DECAY_HZ: float = 10.0 + const HIT_RATE: float = 5.5 + const HIT_STRENGTH: float = 1.2 + const BREATH: float = 0.08 + + var values: Array[float] = [] + var energy: Array[float] = [] + var phase: Array[float] = [] + + func _init(p_band_count: int = 16) -> void: + values.resize(p_band_count) + energy.resize(p_band_count) + phase.resize(p_band_count) + + for i in p_band_count: + values[i] = randf() + energy[i] = 0.0 + phase[i] = randf() * TAU + + func step(delta: float) -> void: + for i in values.size(): + _step_band(i, delta) + + func _step_band(i: int, delta: float) -> void: + # 1) transient impulses (Poisson-ish) + if randf() < HIT_RATE * delta: + var impulse := pow(randf(), 3.0) * HIT_STRENGTH + energy[i] += impulse + + # 2) excitation decay + energy[i] *= exp(-ENERGY_DECAY_HZ * delta) + + # 3) slow breathing motion + phase[i] += (2.0 + 3.0 * randf()) * delta + var slow_noise := sin(phase[i]) * BREATH + + var target := energy[i] + slow_noise + + # 4) fast attack, slow release + if target > values[i]: + values[i] += (target - values[i]) * (1.0 - exp(-ATTACK_HZ * delta)) + else: + values[i] += (target - values[i]) * (1.0 - exp(-RELEASE_HZ * delta)) + +var _t: float = 0.0 + +var _datasets: Array[TauPlot.Dataset] = [] +var _state: Array[Dictionary] = [] + +var _spectrum: Spectrum = null +var _spectrum_2: Spectrum = null + +var _gradient: Gradient = null + +func _ready() -> void: + _t = 0.0 + _datasets.clear() + _state.clear() + _spectrum = Spectrum.new(10) + + _gradient = Gradient.new() + _gradient.offsets = PackedFloat32Array([ + 0.0, + 0.25, + 0.5, + 0.75, + 1.0 + ]) + _gradient.colors = PackedColorArray([ + Color("bd0034ff"), + Color("cc4219ff"), + Color("c97010ff"), + Color("b4aa00ff"), + Color("439800ff"), + ]) + + _setup_test_1() + _setup_test_2() + _setup_test_3() + _setup_test_4() + _setup_test_5() + + +func _process(delta: float) -> void: + _t += delta + + _spectrum.step(delta) + + _step_test_1(delta) + _step_test_2(delta) + _step_test_3(delta) + _step_test_4(delta) + _step_test_5(delta) + +#################################################################################################### +# Helpers +#################################################################################################### + +func make_shared_x_continuous_plot(p_plot: TauPlot, p_title: String, p_interpolation: TauLineConfig.InterpolationMode) -> void: + var series_names := PackedStringArray(["Series A"]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0]) + var y_a := PackedFloat64Array([10.0, 20.0, 50.0, 65.0, 70.0, 75.0, 60.0, 45.0, 25.0, 0.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a]) + _datasets.append(dataset) + + p_plot.title = p_title + p_plot.legend_enabled = false + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.tick_count_preferred = 10 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.tick_count_preferred = 10 + y_axis.overlap_strategy = TauAxisConfig.OverlapStrategy.NONE + y_axis.range_override_enabled = true + y_axis.min_override = -Spectrum.BREATH + y_axis.max_override = 1.0 + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.INDEPENDENT + line_config.interpolation_mode = p_interpolation + var visual_callbacks := TauPlot.LineVisualCallbacks.new() + visual_callbacks.color_callback = Callable(func(series_index: int, sample_index: int, x_value: Variant, y_value: float) -> Color: + return _gradient.sample(clampf(1.0 - y_value, 0.0, 1.0)) + ) + line_config.visual_callbacks = visual_callbacks + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a] + + p_plot.plot_xy(dataset, config, bindings) + +#################################################################################################### +# Test 1 +#################################################################################################### + +func _setup_test_1() -> void: + make_shared_x_continuous_plot(%TestPlot1, "[LINEAR]", TauLineConfig.InterpolationMode.LINEAR) + + +func _step_test_1(delta: float) -> void: + var dataset := _datasets[0] + dataset.begin_batch() + for b in range(dataset.get_shared_capacity()): + dataset.set_series_y(dataset.get_series_id_by_index(0), b, _spectrum.values[b]) + dataset.end_batch() + +#################################################################################################### +# Test 2 +#################################################################################################### + +func _setup_test_2() -> void: + make_shared_x_continuous_plot(%TestPlot2, "[STEP_BEFORE]", TauLineConfig.InterpolationMode.STEP_BEFORE) + + +func _step_test_2(delta: float) -> void: + var dataset := _datasets[1] + dataset.begin_batch() + for b in range(dataset.get_shared_capacity()): + dataset.set_series_y(dataset.get_series_id_by_index(0), b, _spectrum.values[b]) + dataset.end_batch() + +#################################################################################################### +# Test 3 +#################################################################################################### + +func _setup_test_3() -> void: + make_shared_x_continuous_plot(%TestPlot3, "[STEP_MIDDLE]", TauLineConfig.InterpolationMode.STEP_MIDDLE) + + +func _step_test_3(delta: float) -> void: + var dataset := _datasets[2] + dataset.begin_batch() + for b in range(dataset.get_shared_capacity()): + dataset.set_series_y(dataset.get_series_id_by_index(0), b, _spectrum.values[b]) + dataset.end_batch() + +#################################################################################################### +# Test 4 +#################################################################################################### + +func _setup_test_4() -> void: + make_shared_x_continuous_plot(%TestPlot4, "[STEP_AFTER]", TauLineConfig.InterpolationMode.STEP_AFTER) + + +func _step_test_4(delta: float) -> void: + var dataset := _datasets[3] + dataset.begin_batch() + for b in range(dataset.get_shared_capacity()): + dataset.set_series_y(dataset.get_series_id_by_index(0), b, _spectrum.values[b]) + dataset.end_batch() + +#################################################################################################### +# Test 5 +#################################################################################################### + +func _setup_test_5() -> void: + make_shared_x_continuous_plot(%TestPlot5, "[SMOOTH_MONOTONE]", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE) + + +func _step_test_5(delta: float) -> void: + var dataset := _datasets[4] + dataset.begin_batch() + for b in range(dataset.get_shared_capacity()): + dataset.set_series_y(dataset.get_series_id_by_index(0), b, _spectrum.values[b]) + dataset.end_batch() diff --git a/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_color.gd.uid b/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_color.gd.uid new file mode 100644 index 0000000..1c0aa55 --- /dev/null +++ b/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_color.gd.uid @@ -0,0 +1 @@ +uid://bnuv0vxxw14ea diff --git a/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_color.tscn b/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_color.tscn new file mode 100644 index 0000000..2a93ff8 --- /dev/null +++ b/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_color.tscn @@ -0,0 +1,79 @@ +[gd_scene load_steps=3 format=3 uid="uid://7nurv5el8l02"] + +[ext_resource type="Script" uid="uid://bnuv0vxxw14ea" path="res://addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_color.gd" id="1_lev7j"] +[ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_xbeyi"] + +[node name="Bars" type="VBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_lev7j") + +[node name="Title" type="Label" parent="."] +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +text = "Test LINE visual callbacks (color only on solid lines)" +horizontal_alignment = 1 + +[node name="GridContainer" type="GridContainer" parent="."] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/h_separation = 0 +theme_override_constants/v_separation = 0 + +[node name="TestPlot1" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_xbeyi") +title = "[LINEAR]" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot2" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_xbeyi") +title = "[STEP_BEFORE]" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot3" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_xbeyi") +title = "[STEP_MIDDLE]" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot4" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_xbeyi") +title = "[STEP_AFTER]" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot5" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_xbeyi") +title = "[SMOOTH_MONOTONE]" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" From e5433b05451253c7e3d7a4d321fe9236f89ff2b0 Mon Sep 17 00:00:00 2001 From: ze2j <46556066+ze2j@users.noreply.github.com> Date: Fri, 1 May 2026 12:25:19 +0200 Subject: [PATCH 11/23] Hover lines initial version --- .../plot/xy/hover/hover_controller.gd | 69 ++-- .../tau-plot/plot/xy/line/line_hit_record.gd | 18 + .../plot/xy/line/line_hit_record.gd.uid | 1 + .../tau-plot/plot/xy/line/line_hit_tester.gd | 165 ++++++++ .../plot/xy/line/line_hit_tester.gd.uid | 1 + addons/tau-plot/plot/xy/line/line_renderer.gd | 363 ++++++++++++++++-- addons/tau-plot/plot/xy/line/line_style.gd | 79 +++- addons/tau-plot/plot/xy/xy_plot.gd | 8 + 8 files changed, 644 insertions(+), 60 deletions(-) create mode 100644 addons/tau-plot/plot/xy/line/line_hit_record.gd create mode 100644 addons/tau-plot/plot/xy/line/line_hit_record.gd.uid create mode 100644 addons/tau-plot/plot/xy/line/line_hit_tester.gd create mode 100644 addons/tau-plot/plot/xy/line/line_hit_tester.gd.uid diff --git a/addons/tau-plot/plot/xy/hover/hover_controller.gd b/addons/tau-plot/plot/xy/hover/hover_controller.gd index f4f0772..c1e6986 100644 --- a/addons/tau-plot/plot/xy/hover/hover_controller.gd +++ b/addons/tau-plot/plot/xy/hover/hover_controller.gd @@ -139,7 +139,7 @@ class HoverController extends RefCounted: _current_pane = -1 _hide_transient_tooltip() _hide_all_crosshairs() - _clear_highlight_state_on_renderers() + _clear_renderers_hover_state() # Also hide pinned tooltip since screen positions are stale. if not _pinned_hits.is_empty(): _pinned_hits.clear() @@ -193,7 +193,7 @@ class HoverController extends RefCounted: return if p_event is InputEventMouseMotion: - _process_motion(p_pane_index, p_local_pos) + _process_mouse_motion(p_pane_index, p_local_pos) elif p_event is InputEventMouseButton: var mb := p_event as InputEventMouseButton @@ -210,7 +210,7 @@ class HoverController extends RefCounted: ## Processes mouse motion: runs hit testing and emits hover signals. - func _process_motion(p_pane_index: int, p_local_pos: Vector2) -> void: + func _process_mouse_motion(p_pane_index: int, p_local_pos: Vector2) -> void: # Convert pane-local position to plot-local for tooltip positioning. _last_mouse_pos = _pane_to_plot_local(p_pane_index, p_local_pos) @@ -228,7 +228,7 @@ class HoverController extends RefCounted: _current_pane = -1 _hide_transient_tooltip() _hide_all_crosshairs() - _clear_highlight_state_on_renderers() + _clear_renderers_hover_state() _plot.sample_hover_exited.emit() return @@ -236,7 +236,7 @@ class HoverController extends RefCounted: _current_pane = p_pane_index _show_transient_tooltip(hits, p_pane_index) _show_crosshairs(hits, p_pane_index, p_local_pos) - _push_highlight_state_to_renderers(hits) + _update_renderers_hover_state(hits) _plot.sample_hovered.emit(hits) @@ -282,15 +282,14 @@ class HoverController extends RefCounted: for hit_tester: OverlayHitTester in hit_testers: if not hit_tester.is_hoverable(): continue + var preferred: int = hit_tester.get_preferred_hover_mode() if agreed_mode == -1: agreed_mode = preferred elif agreed_mode != preferred: return HoverMode.NEAREST - if agreed_mode == -1: - return HoverMode.NEAREST - return agreed_mode as HoverMode + return agreed_mode as HoverMode if agreed_mode != -1 else HoverMode.NEAREST #################################################################### @@ -534,32 +533,41 @@ class HoverController extends RefCounted: return _resolve_mode(p_pane_index) == HoverMode.X_ALIGNED - ## Pushes highlight state to all bar and scatter renderers based on the - ## current set of hits. Each renderer receives set_hover_state with the - ## hit that belongs to it (matched by pane index and overlay type). If a - ## renderer has no hit, it still receives p_active = true so that the - ## color callback dims its samples, but p_series_id = -1 so no sample - ## gets hovered-state style properties. + ## Update the renderers hover state based on the current set of hits. + ## Each renderer receives set_hover_state with the hit that belongs to it + ## (matched by pane index and overlay type). If a renderer has no hit, it + ## still receives p_active = true so that the color callback dims its samples, + ## but p_series_id = -1 so no sample gets hovered-state style properties. ## ## For GROUPED bars in X_ALIGNED mode, the entire group at the hovered ## sample index is highlighted together via set_hover_state_group. - func _push_highlight_state_to_renderers(p_hits: Array[SampleHit]) -> void: + ## + ## For line overlays in X_ALIGNED mode, the closest hit to the pointer + ## (smallest distance_px) is selected as the visually emphasized sample + ## in each pane: hovering "the line at column X" picks the single line + ## whose curve passes nearest to the cursor for the thicker emphasis, + ## while the tooltip still lists all line series at that X. + func _update_renderers_hover_state(p_hits: Array[SampleHit]) -> void: if not _is_highlight_enabled(): - _clear_highlight_state_on_renderers() + _clear_renderers_hover_state() return var highlight_cb: Callable = _get_hover_highlight_callback() # Build a lookup from pane_index to the best hit for that pane, per - # overlay type. Selection priority: + # overlay type. Selection priority for bars and scatter: # 1. Hits where contains_pointer is true (cursor inside the visual # element). Among those, pick the one with the smallest distance_px. # 2. If no hit contains the pointer, no sample is highlighted for that # overlay (series_id = -1). The tooltip still shows all hits, but # the visual highlight is suppressed because the cursor is not # physically inside any element. + # Lines are not bounded by a visual element along the y axis in + # X_ALIGNED mode, so they pick the smallest distance_px regardless of + # contains_pointer: the closest curve to the cursor wins the emphasis. var bar_hits_by_pane: Dictionary[int, SampleHit] = {} var scatter_hits_by_pane: Dictionary[int, SampleHit] = {} + var line_hits_by_pane: Dictionary[int, SampleHit] = {} # Also track the sample_index for group highlighting (any bar hit, # even without contains_pointer, tells us the hovered category). @@ -580,8 +588,9 @@ class HoverController extends RefCounted: if existing == null or hit.distance_px < existing.distance_px: scatter_hits_by_pane[hit.pane_index] = hit elif hit.overlay_type == PaneOverlayType.LINE: - # TODO: categorize line hits once line hover is wired. - pass + var existing: SampleHit = line_hits_by_pane.get(hit.pane_index) + if existing == null or hit.distance_px < existing.distance_px: + line_hits_by_pane[hit.pane_index] = hit for pane_index: int in range(_bar_renderers.size()): var renderer: BarRenderer = _bar_renderers[pane_index] @@ -612,10 +621,20 @@ class HoverController extends RefCounted: else: renderer.set_hover_state(true, -1, -1, highlight_cb) + for pane_index: int in range(_line_renderers.size()): + var renderer: LineRenderer = _line_renderers[pane_index] + if renderer == null: + continue # Pane has no line overlay. + var hit: SampleHit = line_hits_by_pane.get(pane_index) + if hit != null: + renderer.set_hover_state(true, hit.series_id, hit.sample_index, highlight_cb) + else: + renderer.set_hover_state(true, -1, -1, highlight_cb) + - ## Clears highlight state on all bar and scatter renderers, returning - ## them to normal (non-highlighted) drawing. - func _clear_highlight_state_on_renderers() -> void: + ## Clears hover state on all bar, scatter, and line renderers, + ## returning them to normal (non-highlighted) drawing. + func _clear_renderers_hover_state() -> void: for pane_index: int in range(_bar_renderers.size()): var renderer: BarRenderer = _bar_renderers[pane_index] if renderer == null: @@ -628,6 +647,12 @@ class HoverController extends RefCounted: continue # Pane has no scatter overlay. renderer.set_hover_state(false, -1, -1, Callable()) + for pane_index: int in range(_line_renderers.size()): + var renderer: LineRenderer = _line_renderers[pane_index] + if renderer == null: + continue # Pane has no line overlay. + renderer.set_hover_state(false, -1, -1, Callable()) + #################################################################### # Private: coordinate conversion diff --git a/addons/tau-plot/plot/xy/line/line_hit_record.gd b/addons/tau-plot/plot/xy/line/line_hit_record.gd new file mode 100644 index 0000000..52f610b --- /dev/null +++ b/addons/tau-plot/plot/xy/line/line_hit_record.gd @@ -0,0 +1,18 @@ +## Snapshot of one real sample drawn onto a line polyline, in pane-local +## screen coordinates. One record per dataset sample, sub-samples +## are not recorded. +class LineHitRecord extends RefCounted: + ## Dataset series id. + var series_id: int + + ## Sample index within the series. + var sample_index: int + + ## Float for continuous x, String for categorical. + var x_value: Variant + + ## Plotted y value. + var y_value: float + + ## Real sample position in pane-local screen coordinates. + var screen_position: Vector2 diff --git a/addons/tau-plot/plot/xy/line/line_hit_record.gd.uid b/addons/tau-plot/plot/xy/line/line_hit_record.gd.uid new file mode 100644 index 0000000..2a8a16a --- /dev/null +++ b/addons/tau-plot/plot/xy/line/line_hit_record.gd.uid @@ -0,0 +1 @@ +uid://ba1k0sngdojnn diff --git a/addons/tau-plot/plot/xy/line/line_hit_tester.gd b/addons/tau-plot/plot/xy/line/line_hit_tester.gd new file mode 100644 index 0000000..df8be79 --- /dev/null +++ b/addons/tau-plot/plot/xy/line/line_hit_tester.gd @@ -0,0 +1,165 @@ +const Dataset := preload("res://addons/tau-plot/model/dataset.gd").Dataset +const XYLayout := preload("res://addons/tau-plot/plot/xy/xy_layout.gd").XYLayout +const SampleHit = preload("res://addons/tau-plot/plot/xy/hover/sample_hit.gd").SampleHit +const HoverMode = preload("res://addons/tau-plot/plot/xy/hover/hover_config.gd").HoverMode +const OverlayHitTester = preload("res://addons/tau-plot/plot/xy/hover/overlay_hit_tester.gd").OverlayHitTester +const PaneOverlayType = preload("res://addons/tau-plot/plot/xy/pane_overlay_type.gd").PaneOverlayType +const LineRenderer := preload("res://addons/tau-plot/plot/xy/line/line_renderer.gd").LineRenderer +const LineHitRecord := preload("res://addons/tau-plot/plot/xy/line/line_hit_record.gd").LineHitRecord + + +## Hit tester for the line overlay. Reads the renderer's LineHitRecord cache +## so the hit geometry cannot drift from the painted geometry. +## +## In NEAREST mode, performs a brute-force linear scan over all records and +## returns the closest sample within hover_max_distance_px. +## In X_ALIGNED mode, collects all samples whose x position (categorical +## index or continuous value) matches the target. +## +## Line overlays prefer X_ALIGNED in TauHoverConfig.HoverMode.AUTO because +## line charts are typically read along the x axis. +class LineHitTester extends OverlayHitTester: + var _pane_index: int + var _line_config: TauLineConfig + var _line_renderer: LineRenderer + var _dataset: Dataset + var _layout: XYLayout + + + func _init( + p_pane_index: int, + p_line_config: TauLineConfig, + p_line_renderer: LineRenderer, + p_dataset: Dataset, + p_layout: XYLayout) -> void: + _pane_index = p_pane_index + _line_config = p_line_config + _line_renderer = p_line_renderer + _dataset = p_dataset + _layout = p_layout + + + func is_hoverable() -> bool: + return _line_config.hoverable + + + func get_preferred_hover_mode() -> HoverMode: + return HoverMode.X_ALIGNED + + + #################################################################### + # NEAREST mode + #################################################################### + + ## Brute-force linear scan. Returns the closest record within + ## hover_max_distance_px, or null. + func hit_test_nearest(p_local_pos: Vector2) -> SampleHit: + var max_dist: float = float(_line_config.hover_max_distance_px) + var max_dist_sq: float = max_dist * max_dist + var best_record: LineHitRecord = null + var best_dist_sq: float = INF + + for record: LineHitRecord in _line_renderer.get_hit_records(): + var dx: float = p_local_pos.x - record.screen_position.x + var dy: float = p_local_pos.y - record.screen_position.y + var dist_sq: float = dx * dx + dy * dy + if dist_sq < best_dist_sq and dist_sq <= max_dist_sq: + best_dist_sq = dist_sq + best_record = record + + if best_record == null: + return null + return _build_hit(best_record, sqrt(best_dist_sq), true) + + + #################################################################### + # X_ALIGNED mode + #################################################################### + + func collect_hits_at_category(p_category_index: int, p_x_value: String, p_local_pos: Vector2) -> Array[SampleHit]: + var hits: Array[SampleHit] = [] + var max_dist: float = float(_line_config.hover_max_distance_px) + + for record: LineHitRecord in _line_renderer.get_hit_records(): + if record.sample_index != p_category_index: + continue + var dx: float = p_local_pos.x - record.screen_position.x + var dy: float = p_local_pos.y - record.screen_position.y + var dist: float = sqrt(dx * dx + dy * dy) + # Categorical hits override the cached x_value with the resolved + # category label so the hit reads consistently with bars and + # scatter at the same X. + var hit := _build_hit(record, dist, dist <= max_dist) + hit.x_value = p_x_value + hits.append(hit) + + return hits + + + func collect_hits_at_continuous_x(p_x_value: float, p_local_pos: Vector2) -> Array[SampleHit]: + var max_dist_x: float = float(_line_config.hover_max_distance_px) + var x_is_horizontal: bool = _layout._x_is_horizontal + var target_x_px: float = _layout.map_x_to_px(_pane_index, p_x_value) + var hits: Array[SampleHit] = [] + + for record: LineHitRecord in _line_renderer.get_hit_records(): + var sample_x_px: float = record.screen_position.x if x_is_horizontal else record.screen_position.y + var x_dist: float = absf(sample_x_px - target_x_px) + if x_dist > max_dist_x: + continue + + # Reject records whose x value drifts numerically from the target. + # The pixel gate above is necessary but not sufficient for sparse + # datasets where two distinct samples can map to nearby pixels. + if record.x_value is float: + if not OverlayHitTester.x_values_match(record.x_value, p_x_value): + continue + + var dx: float = p_local_pos.x - record.screen_position.x + var dy: float = p_local_pos.y - record.screen_position.y + var dist: float = sqrt(dx * dx + dy * dy) + hits.append(_build_hit(record, dist, dist <= max_dist_x)) + + return hits + + + ## Returns the nearest x pixel position and data value across all + ## records, or an empty dictionary when no records exist. + func find_nearest_x(p_along_x_px: float) -> Dictionary: + var x_is_horizontal: bool = _layout._x_is_horizontal + var best_px: float = INF + var best_val: float = 0.0 + var found: bool = false + + for record: LineHitRecord in _line_renderer.get_hit_records(): + var x_px: float = record.screen_position.x if x_is_horizontal else record.screen_position.y + if absf(p_along_x_px - x_px) < absf(p_along_x_px - best_px): + best_px = x_px + if record.x_value is float: + best_val = record.x_value + else: + best_val = float(record.x_value) + found = true + + if not found: + return {} + return { "x_px": best_px, "x_value": best_val } + + + #################################################################### + # Private + #################################################################### + + func _build_hit(p_record: LineHitRecord, p_distance: float, p_contains: bool) -> SampleHit: + var hit := SampleHit.new() + hit.series_id = p_record.series_id + hit.series_name = _dataset.get_series_name(p_record.series_id) + hit.sample_index = p_record.sample_index + hit.x_value = p_record.x_value + hit.y_value = p_record.y_value + hit.screen_position = p_record.screen_position + hit.pane_index = _pane_index + hit.overlay_type = PaneOverlayType.LINE + hit.distance_px = p_distance + hit.contains_pointer = p_contains + return hit diff --git a/addons/tau-plot/plot/xy/line/line_hit_tester.gd.uid b/addons/tau-plot/plot/xy/line/line_hit_tester.gd.uid new file mode 100644 index 0000000..d41c899 --- /dev/null +++ b/addons/tau-plot/plot/xy/line/line_hit_tester.gd.uid @@ -0,0 +1 @@ +uid://dh43for0yf2ab diff --git a/addons/tau-plot/plot/xy/line/line_renderer.gd b/addons/tau-plot/plot/xy/line/line_renderer.gd index f0a1ed8..1bf0708 100644 --- a/addons/tau-plot/plot/xy/line/line_renderer.gd +++ b/addons/tau-plot/plot/xy/line/line_renderer.gd @@ -6,13 +6,15 @@ const AxisId = preload("res://addons/tau-plot/plot/xy/xy_axes.gd").AxisId const Axis = preload("res://addons/tau-plot/plot/xy/xy_axes.gd").Axis const VisualAttributes = preload("res://addons/tau-plot/plot/xy/visual_attributes.gd").VisualAttributes const LineVisualAttributes := preload("res://addons/tau-plot/plot/xy/line/line_visual_attributes.gd").LineVisualAttributes +const LineHitRecord := preload("res://addons/tau-plot/plot/xy/line/line_hit_record.gd").LineHitRecord # Draws line overlays from an XYLayout + Dataset. # # This renderer reads all samples through the Dataset public API (no direct # buffer/series access). Each contiguous run of valid samples is drawn with -# one Godot draw call. +# one Godot draw call when no hover emphasis applies, or up to three +# draw calls when the hovered sample lies inside the run. # # Runtime behavior: # - NaN and Inf X or Y values are treated according to TauLineConfig.gap_policy. @@ -38,14 +40,28 @@ const LineVisualAttributes := preload("res://addons/tau-plot/plot/xy/line/line_v # TauLineStyle.get_series_dash_px(global_series_index). Path selection is # therefore per series: two series in the same overlay can run on # different paths in the same frame. -# - Path 1, fast path: resolved per-series dash length is 0. The run is +# - Fast path: resolved per-series dash length is 0. The run is # drawn with a single draw_polyline_colors() call. -# - Path 2, dashed batched path: resolved per-series dash length is positive. +# - Dashed batched path: resolved per-series dash length is positive. # Dash phase is precomputed across the full run, the "on" intervals are # collected into a flat segment array, and the run is drawn with a single # draw_multiline_colors() call. The dash phase is continuous across all # segments of the polyline. # +# Hover behavior: +# - When TauLineConfig.hoverable is true and set_hover_state() has flagged a +# sample as hovered, the per-sample color is routed through +# TauHoverConfig.hover_highlight_callback (or a built-in dim/brighten +# default) so non-hovered samples are de-emphasized. +# - When the hovered sample lies inside a drawn run, the polyline is split +# into up to three contiguous parts at the hovered sample's adjacent real +# neighbors. The middle part is drawn at the per-series resolved hovered +# width from TauLineStyle.hovered_line_widths_px, clamped to be at least +# the per-series base width. The outer two parts keep the base width. +# Each part is one draw call. For dashed lines, each part inherits the +# cumulative arc-length offset from the polyline start, so the dash +# pattern stays continuous through the slices. +# # Per-sample color and alpha resolution: # - Color resolution order: LineVisualAttributes.color_buffer, then # LineVisualCallbacks.color_callback, then the per-series color from @@ -53,6 +69,8 @@ const LineVisualAttributes := preload("res://addons/tau-plot/plot/xy/line/line_v # - Alpha resolution order: LineVisualAttributes.alpha_buffer, then # LineVisualCallbacks.alpha_callback, then TauXYStyle.series_alpha. # - The resolved alpha overwrites the alpha channel of the resolved color. +# - When the highlight feature is active, the resulting color is then +# routed through TauHoverConfig.hover_highlight_callback. # - Each vertex of the polyline carries its own resolved color. Colors are # linearly interpolated by Godot between consecutive vertices. # - Synthetic step-mode intermediate vertices and SMOOTH_MONOTONE sub-samples @@ -90,6 +108,20 @@ class LineRenderer extends Control: # on every redraw. var _smooth_non_monotonic_warned: bool = false + # Per-frame cache of every real sample drawn onto a polyline this frame. + # Rebuilt every _draw() so the cache never drifts from what is on screen. + var _hit_records: Array[LineHitRecord] = [] + + # Hover highlight state. When _highlight_active is true, the per-sample + # color is routed through the hover color callback (or a built-in + # dim/brighten default). When the hovered sample lies within a drawn + # run, the two segments adjacent to it are drawn at the per-series + # resolved hovered width clamped to be at least the per-series base width. + var _highlight_active: bool = false + var _hovered_series_id: int = -1 + var _hovered_sample_index: int = -1 + var _hover_highlight_callback: Callable = Callable() + func _init(p_layout: XYLayout, p_dataset: Dataset, @@ -136,6 +168,23 @@ class LineRenderer extends Control: _xy_style = p_style + ## Updates the hover highlight state. A change triggers a redraw so the + ## line is repainted with the new emphasis slice and dimming pattern. + func set_hover_state(p_active: bool, p_series_id: int, p_sample_index: int, p_color_callback: Callable) -> void: + var changed := p_active != _highlight_active or p_series_id != _hovered_series_id or p_sample_index != _hovered_sample_index + _highlight_active = p_active + _hovered_series_id = p_series_id + _hovered_sample_index = p_sample_index + _hover_highlight_callback = p_color_callback + if changed: + queue_redraw() + + + ## Returns the per-frame hit records cache. Treat as read-only. + func get_hit_records() -> Array[LineHitRecord]: + return _hit_records + + ## Creates a legend key Control for a line overlay. func create_legend_key_control(_p_series_index: int) -> Control: # TODO: implement create_legend_key_control for lines @@ -147,6 +196,8 @@ class LineRenderer extends Control: #################################################################################################### func _draw() -> void: + _hit_records.clear() + if _line_style == null: push_error("LineRenderer: resolved TauLineStyle is null.") return @@ -189,12 +240,20 @@ class LineRenderer extends Control: if width_px <= 0.0: return var dash_px: int = _line_style.get_series_dash_px(global_series_index) + var hover_width_px: float = max(_line_style.get_series_hovered_width_px(global_series_index), width_px) var y_axis_id := _get_y_axis_id_for_series(series_id) var bridge: bool = _line_config.gap_policy == TauLineConfig.GapPolicy.BRIDGE var interpolation: TauLineConfig.InterpolationMode = _line_config.interpolation_mode var run := PackedVector2Array() var run_colors := PackedColorArray() + # Parallel arrays describing the real samples appended to the current + # run. real_polyline_indices[k] is the index into `run` where the k-th + # real sample of this run landed before any post-processing + # (SMOOTH_MONOTONE resampling). The dataset index is needed so the + # finalizer can locate the hovered sample within the current run. + var real_polyline_indices := PackedInt32Array() + var real_dataset_indices := PackedInt32Array() var is_shared_x := _dataset.get_mode() == Dataset.Mode.SHARED_X var sample_count := _dataset.get_series_sample_count(series_id) @@ -203,25 +262,42 @@ class LineRenderer extends Control: var x_value: float = float(_dataset.get_shared_x(i)) if is_shared_x else float(_dataset.get_series_x(series_id, i)) if is_nan(x_value) or is_inf(x_value) or not _is_x_value_valid_for_scale(x_value): if not bridge: - _finalize_run(run, run_colors, width_px, interpolation, dash_px) + _finalize_run(run, run_colors, real_polyline_indices, real_dataset_indices, series_id, width_px, hover_width_px, interpolation, dash_px) run = PackedVector2Array() run_colors = PackedColorArray() + real_polyline_indices = PackedInt32Array() + real_dataset_indices = PackedInt32Array() continue var y_value := _dataset.get_series_y(series_id, i) if is_nan(y_value) or is_inf(y_value) or not _is_y_value_valid_for_scale(series_id, y_value): if not bridge: - _finalize_run(run, run_colors, width_px, interpolation, dash_px) + _finalize_run(run, run_colors, real_polyline_indices, real_dataset_indices, series_id, width_px, hover_width_px, interpolation, dash_px) run = PackedVector2Array() run_colors = PackedColorArray() + real_polyline_indices = PackedInt32Array() + real_dataset_indices = PackedInt32Array() continue var x_px := _layout.map_x_to_px(_pane_index, x_value) var y_px := _layout.map_y_to_px(_pane_index, y_value, y_axis_id) + var screen_pos := _layout.map_point_to_screen(x_px, y_px) var sample_color := _resolve_sample_color(p_series_index, i, x_value, y_value) - _append_with_interpolation(run, run_colors, _layout.map_point_to_screen(x_px, y_px), sample_color, interpolation) + _append_with_interpolation(run, run_colors, screen_pos, sample_color, interpolation) + # The real sample is always the last vertex appended by + # _append_with_interpolation, regardless of the interpolation mode. + real_polyline_indices.append(run.size() - 1) + real_dataset_indices.append(i) - _finalize_run(run, run_colors, width_px, interpolation, dash_px) + var record := LineHitRecord.new() + record.series_id = series_id + record.sample_index = i + record.x_value = x_value + record.y_value = y_value + record.screen_position = screen_pos + _hit_records.append(record) + + _finalize_run(run, run_colors, real_polyline_indices, real_dataset_indices, series_id, width_px, hover_width_px, interpolation, dash_px) func _draw_series_categorical(p_series_index: int) -> void: @@ -231,6 +307,7 @@ class LineRenderer extends Control: if width_px <= 0.0: return var dash_px: int = _line_style.get_series_dash_px(global_series_index) + var hover_width_px: float = max(_line_style.get_series_hovered_width_px(global_series_index), width_px) var y_axis_id := _get_y_axis_id_for_series(series_id) var bridge: bool = _line_config.gap_policy == TauLineConfig.GapPolicy.BRIDGE var interpolation: TauLineConfig.InterpolationMode = _line_config.interpolation_mode @@ -238,24 +315,39 @@ class LineRenderer extends Control: var categories := _layout.domain.x_categories var run := PackedVector2Array() var run_colors := PackedColorArray() + var real_polyline_indices := PackedInt32Array() + var real_dataset_indices := PackedInt32Array() var sample_count := _dataset.get_series_sample_count(series_id) for cat_idx in range(sample_count): var y_value := _dataset.get_series_y(series_id, cat_idx) if is_nan(y_value) or is_inf(y_value) or not _is_y_value_valid_for_scale(series_id, y_value): if not bridge: - _finalize_run(run, run_colors, width_px, interpolation, dash_px) + _finalize_run(run, run_colors, real_polyline_indices, real_dataset_indices, series_id, width_px, hover_width_px, interpolation, dash_px) run = PackedVector2Array() run_colors = PackedColorArray() + real_polyline_indices = PackedInt32Array() + real_dataset_indices = PackedInt32Array() continue var x_px := _layout.map_x_category_center_to_px(_pane_index, cat_idx) var y_px := _layout.map_y_to_px(_pane_index, y_value, y_axis_id) + var screen_pos := _layout.map_point_to_screen(x_px, y_px) var x_value: Variant = categories[cat_idx] var sample_color := _resolve_sample_color(p_series_index, cat_idx, x_value, y_value) - _append_with_interpolation(run, run_colors, _layout.map_point_to_screen(x_px, y_px), sample_color, interpolation) + _append_with_interpolation(run, run_colors, screen_pos, sample_color, interpolation) + real_polyline_indices.append(run.size() - 1) + real_dataset_indices.append(cat_idx) + + var record := LineHitRecord.new() + record.series_id = series_id + record.sample_index = cat_idx + record.x_value = x_value + record.y_value = y_value + record.screen_position = screen_pos + _hit_records.append(record) - _finalize_run(run, run_colors, width_px, interpolation, dash_px) + _finalize_run(run, run_colors, real_polyline_indices, real_dataset_indices, series_id, width_px, hover_width_px, interpolation, dash_px) # Each appended sample (real or synthetic) gets the new sample's color. @@ -294,36 +386,184 @@ class LineRenderer extends Control: # For SMOOTH_MONOTONE the run is first replaced by its Fritsch-Carlson piecewise cubic resampling. # Runs of fewer than two points are silently dropped. # - # When p_dash_px is 0, the polyline is emitted via path 1 (draw_polyline_colors). - # Otherwise it is emitted via path 2: dash phase is precomputed across the - # whole polyline and the resulting "on" intervals are flushed in a single - # draw_multiline_colors() call. p_dash_px is the resolved per-series dash - # length obtained from TauLineStyle.get_series_dash_px(). - func _finalize_run(p_run: PackedVector2Array, p_run_colors: PackedColorArray, p_width_px: float, p_mode: TauLineConfig.InterpolationMode, p_dash_px: int) -> void: + # Path selection per series (no hover): + # - p_dash_px == 0: one draw_polyline_colors call. + # - p_dash_px > 0: one draw_multiline_colors call after dash precomputation. + # + # When the hovered sample belongs to this run, the polyline is split into + # up to three contiguous parts and each part is drawn with its own draw + # call: + # - Part A: vertices [0 .. slice_start], drawn at p_width_px. + # - Part B: vertices [slice_start .. slice_end], drawn at p_hover_width_px. + # - Part C: vertices [slice_end .. last], drawn at p_width_px. + # slice_start is the polyline index of the hovered sample's previous real + # neighbor, or the hovered sample itself when it has no previous neighbor + # in this run. slice_end is the index of the next real neighbor, or the + # hovered sample itself when it has none. For dashed lines, the dash phase + # is carried across parts using each part's cumulative arc-length offset + # from the polyline start, so the dash pattern stays continuous through + # the slices. + # + # p_real_polyline_indices and p_real_dataset_indices are parallel arrays + # whose length equals the number of real samples appended to this run. + # real_polyline_indices[k] is the index in p_run where the k-th real + # sample landed. real_dataset_indices[k] is the dataset sample index for + # that real sample. + func _finalize_run(p_run: PackedVector2Array, p_run_colors: PackedColorArray, p_real_polyline_indices: PackedInt32Array, p_real_dataset_indices: PackedInt32Array, p_series_id: int, p_width_px: float, p_hover_width_px: float, p_mode: TauLineConfig.InterpolationMode, p_dash_px: int) -> void: var polyline: PackedVector2Array = p_run var polyline_colors: PackedColorArray = p_run_colors + var real_polyline_indices: PackedInt32Array = p_real_polyline_indices if p_mode == TauLineConfig.InterpolationMode.SMOOTH_MONOTONE and p_run.size() > 2: var resampled := _resample_smooth_monotone(p_run, p_run_colors) polyline = resampled[0] polyline_colors = resampled[1] + # In SMOOTH_MONOTONE the input run holds only real samples, so + # every entry in p_real_polyline_indices is its own input index + # and the resample's input-to-output map is the new mapping. + real_polyline_indices = resampled[2] if polyline.size() < 2: return var dash_px: int = max(p_dash_px, 0) - if dash_px <= 0: - draw_polyline_colors(polyline, polyline_colors, p_width_px) + var slice_bounds := _resolve_hover_slice_bounds(real_polyline_indices, p_real_dataset_indices, p_series_id) + + if slice_bounds.is_empty(): + # No hover emphasis: draw the entire polyline in a single call. + _draw_polyline_segment(polyline, polyline_colors, 0, polyline.size() - 1, p_width_px, dash_px, 0.0) + return + + var slice_start: int = slice_bounds[0] + var slice_end: int = slice_bounds[1] + var last: int = polyline.size() - 1 + + # Precompute cumulative arc length so each part inherits a starting + # phase that keeps the dash pattern continuous across the slices. + # When dash_px is 0 the offsets are still computed but ignored by the + # solid path. + var arc_at_slice_start: float = _arc_length_to_index(polyline, slice_start) + var arc_at_slice_end: float = arc_at_slice_start + _arc_length_between(polyline, slice_start, slice_end) + + # Part A: from the polyline start up to and including slice_start. + _draw_polyline_segment(polyline, polyline_colors, 0, slice_start, p_width_px, dash_px, 0.0) + # Part B: the two adjacent portions, emphasized. + _draw_polyline_segment(polyline, polyline_colors, slice_start, slice_end, p_hover_width_px, dash_px, arc_at_slice_start) + # Part C: from slice_end to the polyline end. + _draw_polyline_segment(polyline, polyline_colors, slice_end, last, p_width_px, dash_px, arc_at_slice_end) + + + # Resolves the polyline-vertex bounds of the hover-emphasized slice for + # the current run, or an empty array when no emphasis applies. + # + # Returns [slice_start, slice_end] when the hovered sample belongs to the + # current run and has at least one real neighbor that produces a non-zero + # slice. slice_start is the polyline index of the previous real neighbor + # (or the hovered sample itself when it has no previous neighbor). + # slice_end is the polyline index of the next real neighbor (or the + # hovered sample itself when it has none). + # + # When the hovered sample has been deduplicated by SMOOTH_MONOTONE + # resampling (consecutive real samples sharing the same screen X), it + # shares its polyline index with the surviving neighbor it was deduped + # against, so the slice still covers the right neighborhood. + func _resolve_hover_slice_bounds(p_real_polyline_indices: PackedInt32Array, p_real_dataset_indices: PackedInt32Array, p_series_id: int) -> PackedInt32Array: + if not _highlight_active or _hovered_series_id != p_series_id or _hovered_sample_index < 0: + return PackedInt32Array() + var real_count: int = p_real_dataset_indices.size() + if real_count <= 1: + return PackedInt32Array() + + var hover_pos_in_run: int = -1 + for k in range(real_count): + if p_real_dataset_indices[k] == _hovered_sample_index: + hover_pos_in_run = k + break + if hover_pos_in_run < 0: + return PackedInt32Array() + + var hovered_idx: int = p_real_polyline_indices[hover_pos_in_run] + var slice_start: int = hovered_idx + if hover_pos_in_run > 0: + slice_start = p_real_polyline_indices[hover_pos_in_run - 1] + var slice_end: int = hovered_idx + if hover_pos_in_run < real_count - 1: + slice_end = p_real_polyline_indices[hover_pos_in_run + 1] + + if slice_end <= slice_start: + return PackedInt32Array() + + var bounds := PackedInt32Array() + bounds.append(slice_start) + bounds.append(slice_end) + return bounds + + + # Draws a contiguous polyline part bounded by the given vertex indices, + # inclusive on both ends. Dispatches to draw_polyline_colors or _draw_dashed_polyline + # based on p_dash_px. p_phase_offset seeds the dash phase tracker so + # callers can chain multiple parts with continuous phase across slice + # boundaries. + # + # Parts of length less than 2 are skipped: a single-vertex slice cannot + # produce any drawable segment. + func _draw_polyline_segment(p_polyline: PackedVector2Array, p_polyline_colors: PackedColorArray, p_first: int, p_last: int, p_width_px: float, p_dash_px: int, p_phase_offset: float) -> void: + if p_last <= p_first: + return + # A non-fragmented full run is the common case: avoid the slice copy. + if p_first == 0 and p_last == p_polyline.size() - 1: + if p_dash_px <= 0: + draw_polyline_colors(p_polyline, p_polyline_colors, p_width_px) + else: + _draw_dashed_polyline(p_polyline, p_polyline_colors, p_width_px, float(p_dash_px), p_phase_offset) + return + + var sub_polyline := PackedVector2Array() + var sub_colors := PackedColorArray() + var sub_size: int = p_last - p_first + 1 + sub_polyline.resize(sub_size) + sub_colors.resize(sub_size) + for j in range(sub_size): + sub_polyline[j] = p_polyline[p_first + j] + sub_colors[j] = p_polyline_colors[p_first + j] + + if p_dash_px <= 0: + draw_polyline_colors(sub_polyline, sub_colors, p_width_px) else: - _draw_dashed_polyline(polyline, polyline_colors, p_width_px, float(dash_px)) + _draw_dashed_polyline(sub_polyline, sub_colors, p_width_px, float(p_dash_px), p_phase_offset) + + + func _arc_length_to_index(p_polyline: PackedVector2Array, p_index: int) -> float: + if p_index <= 0: + return 0.0 + var total: float = 0.0 + for i in range(p_index): + total += p_polyline[i].distance_to(p_polyline[i + 1]) + return total + + + func _arc_length_between(p_polyline: PackedVector2Array, p_first: int, p_last: int) -> float: + if p_last <= p_first: + return 0.0 + var total: float = 0.0 + for i in range(p_first, p_last): + total += p_polyline[i].distance_to(p_polyline[i + 1]) + return total # Builds the flat segment array of "on" dash intervals along p_polyline and - # emits it with a single draw_multiline_colors() call. The dash period is + # draws it with a single draw_multiline_colors() call. The dash period is # 2 * p_dash_px (one "on" length followed by one "off" length of equal # size). Dash phase is tracked as a single scalar that advances along the # polyline arc length, so the pattern is continuous across consecutive # segments and does not reset at sample positions. # - # draw_multiline_colors takes one solid color per emitted segment (a pair + # p_phase_offset seeds the phase tracker at the start of the polyline, + # expressed in pixels of arc length. It lets a caller draw a contiguous + # polyline as multiple back-to-back parts (each with its own draw call) + # and keep the dash pattern continuous across the part boundaries: each + # subsequent part passes the cumulative arc length from the original + # polyline start as its phase offset. + # + # draw_multiline_colors takes one solid color per drawn segment (a pair # of points). Each "on" interval gets the color of its midpoint along the # enclosing polyline segment, computed by linear interpolation between the # two flanking polyline-vertex colors. For typical dash sizes this is a @@ -332,10 +572,10 @@ class LineRenderer extends Control: # # Degenerate segments (zero length) are skipped: they cannot carry any # dash and do not advance the phase. - func _draw_dashed_polyline(p_polyline: PackedVector2Array, p_polyline_colors: PackedColorArray, p_width_px: float, p_dash_px: float) -> void: + func _draw_dashed_polyline(p_polyline: PackedVector2Array, p_polyline_colors: PackedColorArray, p_width_px: float, p_dash_px: float, p_phase_offset: float = 0.0) -> void: var period: float = p_dash_px * 2.0 var n := p_polyline.size() - var phase: float = 0.0 + var phase: float = fposmod(p_phase_offset, period) var segments := PackedVector2Array() var segment_colors := PackedColorArray() @@ -399,53 +639,71 @@ class LineRenderer extends Control: # screen X are dropped since the secant slope is undefined at h = 0. The # matching color entries are dropped at the same indices. # Inputs that are not monotonic in either direction fall back to the raw - # polyline for that run and emit a one-shot warning. + # polyline for that run and push a one-shot warning. # # Sub-sample colors are linearly interpolated between the two flanking # kept-sample colors so that the visual color gradient matches what # draw_polyline_colors would produce on a LINEAR polyline through the # same kept samples. # - # Returns [points: PackedVector2Array, colors: PackedColorArray]. + # Returns [points: PackedVector2Array, colors: PackedColorArray, input_to_output: PackedInt32Array]. + # input_to_output[i] is the index in the returned points array where input point i lands. + # Dropped inputs (consecutive same-screen-X duplicates) inherit the output index of the + # neighbor they were deduplicated against. func _resample_smooth_monotone(p_points: PackedVector2Array, p_colors: PackedColorArray) -> Array: + var input_count := p_points.size() var direction := _detect_monotonic_x_direction(p_points) if direction == 0: if not _smooth_non_monotonic_warned: push_warning("LineRenderer: SMOOTH_MONOTONE received samples whose screen X is not monotonic. Falling back to a straight polyline for the affected run. Use LINEAR interpolation if your data does not have a monotonic X parameter.") _smooth_non_monotonic_warned = true - return [p_points, p_colors] + var identity := PackedInt32Array() + identity.resize(input_count) + for i in range(input_count): + identity[i] = i + return [p_points, p_colors, identity] var ascending: bool = direction > 0 # Build strictly increasing X arrays, dropping flat-X duplicates. - # Colors are kept in lock-step with the kept points. + # Colors are kept in lock-step with the kept points. kept_of[i] is the + # kept-array index for original input i, used later to remap real + # samples to their output-polyline position. Dropped inputs share + # their surviving neighbor's kept index. var xs := PackedFloat32Array() var ys := PackedFloat32Array() var cs := PackedColorArray() - var input_count := p_points.size() + var kept_of := PackedInt32Array() + kept_of.resize(input_count) if ascending: xs.append(p_points[0].x) ys.append(p_points[0].y) cs.append(p_colors[0]) + kept_of[0] = 0 for i in range(1, input_count): if p_points[i].x > xs[xs.size() - 1]: xs.append(p_points[i].x) ys.append(p_points[i].y) cs.append(p_colors[i]) + kept_of[i] = xs.size() - 1 else: xs.append(p_points[input_count - 1].x) ys.append(p_points[input_count - 1].y) cs.append(p_colors[input_count - 1]) + kept_of[input_count - 1] = 0 for i in range(input_count - 2, -1, -1): if p_points[i].x > xs[xs.size() - 1]: xs.append(p_points[i].x) ys.append(p_points[i].y) cs.append(p_colors[i]) + kept_of[i] = xs.size() - 1 var n := xs.size() if n < 2: # All inputs collapsed to a single screen X. Nothing to draw. - return [PackedVector2Array(), PackedColorArray()] + var empty_map := PackedInt32Array() + empty_map.resize(input_count) + return [PackedVector2Array(), PackedColorArray(), empty_map] if n == 2: # Two distinct X values produce a straight line through Hermite # with both tangents equal to the secant slope. Short-circuit. @@ -458,7 +716,15 @@ class LineRenderer extends Control: if not ascending: trivial.reverse() trivial_colors.reverse() - return [trivial, trivial_colors] + var trivial_map := PackedInt32Array() + trivial_map.resize(input_count) + for i in range(input_count): + var kept_idx: int = kept_of[i] + if ascending: + trivial_map[i] = kept_idx + else: + trivial_map[i] = 1 - kept_idx + return [trivial, trivial_colors, trivial_map] var tangents := _fritsch_carlson_tangents(xs, ys) @@ -504,7 +770,20 @@ class LineRenderer extends Control: if not ascending: out.reverse() out_colors.reverse() - return [out, out_colors] + + # Build the input-to-output index map. In the ascending output, kept + # input k sits at out[k * SUBS]. When the output is reversed for a + # descending input, that position becomes (out_size - 1 - k * SUBS). + var input_to_output := PackedInt32Array() + input_to_output.resize(input_count) + for i in range(input_count): + var kept_idx: int = kept_of[i] + var out_idx: int = kept_idx * _SMOOTH_SUBDIVISIONS + if not ascending: + out_idx = out_size - 1 - out_idx + input_to_output[i] = out_idx + + return [out, out_colors, input_to_output] # Returns +1 if screen X is strictly monotonically increasing across the @@ -630,14 +909,28 @@ class LineRenderer extends Control: # Per-sample color and alpha resolution #################################################################################################### - # Combined per-sample color resolution. The alpha resolved by - # _resolve_sample_alpha overwrites the alpha channel of the color resolved - # by _resolve_sample_color_only. + # Combined per-sample color resolution. Alpha overwrites the resolved + # color's alpha channel, then the result is routed through the + # hover-highlight callback when active. func _resolve_sample_color(p_series_index: int, p_sample_index: int, p_x_value: Variant, p_y_value: float) -> Color: var base := _resolve_sample_color_only(p_series_index, p_sample_index, p_x_value, p_y_value) var alpha := _resolve_sample_alpha(p_series_index, p_sample_index, p_x_value, p_y_value) base.a = clampf(alpha, 0.0, 1.0) - return base + return _apply_hover_color(base, _get_line_series_id(p_series_index), p_sample_index) + + + # Applies the hover-highlight callback (or the built-in dim/brighten + # default) to a resolved per-sample color. Returns the color unchanged + # when the highlight feature is off. + func _apply_hover_color(p_color: Color, p_series_id: int, p_sample_index: int) -> Color: + if not _highlight_active: + return p_color + var is_hovered: bool = (p_series_id == _hovered_series_id) and (p_sample_index == _hovered_sample_index) + if _hover_highlight_callback.is_valid(): + return _hover_highlight_callback.call(p_color, is_hovered) + if is_hovered: + return p_color.lightened(0.15) + return Color(p_color, 0.5) func _resolve_sample_color_only(p_series_index: int, p_sample_index: int, p_x_value: Variant, p_y_value: float) -> Color: diff --git a/addons/tau-plot/plot/xy/line/line_style.gd b/addons/tau-plot/plot/xy/line/line_style.gd index 2666f6a..4b2b486 100644 --- a/addons/tau-plot/plot/xy/line/line_style.gd +++ b/addons/tau-plot/plot/xy/line/line_style.gd @@ -26,6 +26,20 @@ class_name TauLineStyle extends Resource const DEFAULT_LINE_WIDTHS_PX: Array[float] = [2.0] @export var line_widths_px: Array[float] = [2.0] +## Per-series cycle of line widths in pixels for the two segments adjacent to +## the hovered sample. Each entry sets the hovered width for one series, with +## the array indexed cyclically by series index using modulo: series +## [code]i[/code] reads entry [code]i % hovered_line_widths_px.size()[/code]. +## An empty array means no hover emphasis: the segments adjacent to the +## hovered sample are drawn at the resolved [member line_widths_px] value for +## that series. +## +## At draw time, the resolved per-series hovered width is clamped to be at +## least the resolved per-series base width from [member line_widths_px], so +## a thicker series never becomes thinner on hover. +const DEFAULT_HOVERED_LINE_WIDTHS_PX: Array[float] = [3.0] +@export var hovered_line_widths_px: Array[float] = [3.0] + ## Per-series dash length cycle, in pixels. Each entry sets the dash length ## for one series, with the array indexed cyclically by series index using ## modulo: series [code]i[/code] reads entry @@ -53,6 +67,19 @@ func get_series_width_px(p_series_index: int) -> float: return max(entry, 0.0) +## Returns the resolved hovered line width in pixels for the given series +## index. An empty [member hovered_line_widths_px] returns [code]0.0[/code] +## as a "no hover emphasis" sentinel. The renderer clamps the result against +## the per-series base width from [member line_widths_px], so the empty-array +## case naturally falls back to the base width and never produces a thinner +## line on hover. +func get_series_hovered_width_px(p_series_index: int) -> float: + if hovered_line_widths_px.is_empty(): + return 0.0 + var entry: float = hovered_line_widths_px[p_series_index % hovered_line_widths_px.size()] + return max(entry, 0.0) + + ## Returns the resolved dash length in pixels for the given series index. func get_series_dash_px(p_series_index: int) -> int: if dash_lengths_px.is_empty(): @@ -72,9 +99,10 @@ func get_series_dash_px(p_series_index: int) -> int: ## 1. [code]_N[/code] sets the value for series N across all panes. ## 2. [code]_N_P[/code] overrides series N in pane P only. ## -## For [code]line_widths_px[/code] the keys are [code]line_width_px_N[/code] -## and [code]line_width_px_N_P[/code]. For [code]dash_lengths_px[/code] they -## are [code]line_dash_px_N[/code] and [code]line_dash_px_N_P[/code]. +## Theme key prefixes: +## - [member line_widths_px]: [code]line_width_px[/code] +## - [member hovered_line_widths_px]: [code]line_hovered_width_px[/code] +## - [member dash_lengths_px]: [code]line_dash_px[/code] ## ## This method writes every property unconditionally because it is called on ## the resolved instance, not on the user-provided resource. @@ -110,6 +138,31 @@ func load_from_theme(p_control: Control, p_pane_index: int) -> void: line_widths_px[pane_width_index] = max(float(p_control.get_theme_constant(key)), 0.0) pane_width_index += 1 + # hovered_line_widths_px: two-level indexed lookup. + # Level 1 (global): line_hovered_width_px_N + var global_hovered: Array[float] = [] + var hovered_index := 0 + while true: + var key := "line_hovered_width_px_%d" % hovered_index + if not p_control.has_theme_constant(key): + break + global_hovered.append(max(float(p_control.get_theme_constant(key)), 0.0)) + hovered_index += 1 + + if not global_hovered.is_empty(): + hovered_line_widths_px = global_hovered + + # Level 2 (per-pane): line_hovered_width_px_N_P overrides series N in pane P. + var pane_hovered_index := 0 + while true: + var key := "line_hovered_width_px_%d_%d" % [pane_hovered_index, p_pane_index] + if not p_control.has_theme_constant(key): + break + if pane_hovered_index >= hovered_line_widths_px.size(): + hovered_line_widths_px.resize(pane_hovered_index + 1) + hovered_line_widths_px[pane_hovered_index] = max(float(p_control.get_theme_constant(key)), 0.0) + pane_hovered_index += 1 + # dash_lengths_px: two-level indexed lookup. # Level 1 (global): line_dash_px_N var global_dashes: Array[int] = [] @@ -153,6 +206,10 @@ func apply_overrides_from(p_user_style: TauLineStyle) -> void: if _is_line_widths_overridden(p_user_style.line_widths_px): line_widths_px = p_user_style.line_widths_px.duplicate() + # hovered_line_widths_px: element-wise comparison against the default array. + if _is_hovered_line_widths_overridden(p_user_style.hovered_line_widths_px): + hovered_line_widths_px = p_user_style.hovered_line_widths_px.duplicate() + # dash_lengths_px: element-wise comparison against the default array. if _is_dash_lengths_overridden(p_user_style.dash_lengths_px): dash_lengths_px = p_user_style.dash_lengths_px.duplicate() @@ -195,6 +252,11 @@ func is_equal_to(p_other: TauLineStyle) -> bool: for i in range(line_widths_px.size()): if line_widths_px[i] != p_other.line_widths_px[i]: return false + if hovered_line_widths_px.size() != p_other.hovered_line_widths_px.size(): + return false + for i in range(hovered_line_widths_px.size()): + if hovered_line_widths_px[i] != p_other.hovered_line_widths_px[i]: + return false if dash_lengths_px.size() != p_other.dash_lengths_px.size(): return false for i in range(dash_lengths_px.size()): @@ -224,6 +286,17 @@ static func _is_line_widths_overridden(p_widths: Array[float]) -> bool: return false +## Returns true if [param p_widths] differs from DEFAULT_HOVERED_LINE_WIDTHS_PX +## using a size + element loop (safest approach for typed arrays in GDScript). +static func _is_hovered_line_widths_overridden(p_widths: Array[float]) -> bool: + if p_widths.size() != DEFAULT_HOVERED_LINE_WIDTHS_PX.size(): + return true + for i in range(p_widths.size()): + if p_widths[i] != DEFAULT_HOVERED_LINE_WIDTHS_PX[i]: + return true + return false + + ## Returns true if [param p_dashes] differs from DEFAULT_DASH_LENGTHS_PX using ## a size + element loop (safest approach for typed arrays in GDScript). static func _is_dash_lengths_overridden(p_dashes: Array[int]) -> bool: diff --git a/addons/tau-plot/plot/xy/xy_plot.gd b/addons/tau-plot/plot/xy/xy_plot.gd index 096d4d2..e617b9e 100644 --- a/addons/tau-plot/plot/xy/xy_plot.gd +++ b/addons/tau-plot/plot/xy/xy_plot.gd @@ -43,6 +43,7 @@ const ScatterHitTester = preload("res://addons/tau-plot/plot/xy/scatter/scatter_ const LineRenderer := preload("res://addons/tau-plot/plot/xy/line/line_renderer.gd").LineRenderer const LineVisualAttributes := preload("res://addons/tau-plot/plot/xy/line/line_visual_attributes.gd").LineVisualAttributes +const LineHitTester = preload("res://addons/tau-plot/plot/xy/line/line_hit_tester.gd").LineHitTester # External references (provided via setup) @@ -410,6 +411,13 @@ func setup( _scatter_renderers[pane_index], _dataset, _xy_layout)) + if _line_renderers[pane_index] != null: + testers.append(LineHitTester.new( + pane_index, + _line_config_per_pane[pane_index], + _line_renderers[pane_index], + _dataset, _xy_layout)) + hit_testers_per_pane.append(testers) _hover_controller = HoverController.new() From 56ca21885c80b954502963e6e6aec0abecac32e4 Mon Sep 17 00:00:00 2001 From: ze2j <46556066+ze2j@users.noreply.github.com> Date: Fri, 1 May 2026 12:26:03 +0200 Subject: [PATCH 12/23] cleanup --- addons/tau-plot/plot/xy/bar/bar_hit_tester.gd | 2 +- addons/tau-plot/plot/xy/bar/bar_renderer.gd | 10 +++++----- addons/tau-plot/plot/xy/scatter/scatter_hit_tester.gd | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/addons/tau-plot/plot/xy/bar/bar_hit_tester.gd b/addons/tau-plot/plot/xy/bar/bar_hit_tester.gd index e7d136a..d4be55b 100644 --- a/addons/tau-plot/plot/xy/bar/bar_hit_tester.gd +++ b/addons/tau-plot/plot/xy/bar/bar_hit_tester.gd @@ -35,7 +35,7 @@ class BarHitTester extends OverlayHitTester: return _bar_config.hoverable - func get_preferred_hover_mode() -> int: + func get_preferred_hover_mode() -> HoverMode: return HoverMode.X_ALIGNED diff --git a/addons/tau-plot/plot/xy/bar/bar_renderer.gd b/addons/tau-plot/plot/xy/bar/bar_renderer.gd index 6be3632..c0c88ea 100644 --- a/addons/tau-plot/plot/xy/bar/bar_renderer.gd +++ b/addons/tau-plot/plot/xy/bar/bar_renderer.gd @@ -145,6 +145,11 @@ class BarRenderer extends Control: queue_redraw() + ## Returns the per-frame hit records cache. Treat as read-only. + func get_hit_records() -> Array[BarHitRecord]: + return _hit_records + + ## Creates a legend key Control for a bar overlay: a filled square with alpha. ## Reads fill color and alpha from resolved styles on this renderer instance. ## Does not set custom_minimum_size, so the legend applies its default key_size_px. @@ -536,11 +541,6 @@ class BarRenderer extends Control: _hit_records.append(record) - ## Returns the per-frame hit records cache. Treat as read-only. - func get_hit_records() -> Array[BarHitRecord]: - return _hit_records - - func _draw_grouped_bars(p_pane_rect: Rect2, p_series_count: int) -> void: if p_series_count <= 0: return diff --git a/addons/tau-plot/plot/xy/scatter/scatter_hit_tester.gd b/addons/tau-plot/plot/xy/scatter/scatter_hit_tester.gd index ef2d573..1eda034 100644 --- a/addons/tau-plot/plot/xy/scatter/scatter_hit_tester.gd +++ b/addons/tau-plot/plot/xy/scatter/scatter_hit_tester.gd @@ -43,7 +43,7 @@ class ScatterHitTester extends OverlayHitTester: ## Scatter points are best selected individually, so NEAREST is preferred. - func get_preferred_hover_mode() -> int: + func get_preferred_hover_mode() -> HoverMode: return HoverMode.NEAREST From 223f8ddf11e8de3cfecca29a98e38fcad13a2cfb Mon Sep 17 00:00:00 2001 From: ze2j <46556066+ze2j@users.noreply.github.com> Date: Sat, 2 May 2026 17:53:19 +0200 Subject: [PATCH 13/23] Fix stacking order (breaking change) --- addons/tau-plot/plot/xy/xy_plot.gd | 22 ++ .../stacking_order/test_bar_stacking_order.gd | 349 ++++++++++++++++++ .../test_bar_stacking_order.gd.uid | 1 + .../test_bar_stacking_order.tscn | 85 +++++ 4 files changed, 457 insertions(+) create mode 100644 addons/tau-plot/tests/bar/stacking_order/test_bar_stacking_order.gd create mode 100644 addons/tau-plot/tests/bar/stacking_order/test_bar_stacking_order.gd.uid create mode 100644 addons/tau-plot/tests/bar/stacking_order/test_bar_stacking_order.tscn diff --git a/addons/tau-plot/plot/xy/xy_plot.gd b/addons/tau-plot/plot/xy/xy_plot.gd index e617b9e..9438de2 100644 --- a/addons/tau-plot/plot/xy/xy_plot.gd +++ b/addons/tau-plot/plot/xy/xy_plot.gd @@ -231,6 +231,13 @@ func setup( # Unknown overlay types are rejected by validation. pass + # Bindings populate the per-pane series id arrays in binding-iteration + # order, which is unrelated to dataset order. Sorting here makes the + # stacking order predictable (layer 0 = first declared series). + for pane_index in range(pane_count): + _sort_series_ids_by_dataset_index(_bar_series_ids_per_pane[pane_index]) + _sort_series_ids_by_dataset_index(_line_series_ids_per_pane[pane_index]) + # Domain + layout creation _xy_domain_overrides = XYDomainOverrides.new() _xy_domain_overrides.init_panes(pane_count) @@ -941,6 +948,21 @@ func _attach_legend_outside(p_legend: Control, p_position: Position) -> void: hbox.move_child(p_legend, hbox.get_child_count() - 1) +func _sort_series_ids_by_dataset_index(p_ids: PackedInt64Array) -> void: + # Insertion sort. PackedInt64Array exposes no sort_custom, and the per-pane + # series count is small enough that anything more elaborate is overkill. + var count := p_ids.size() + for sorted_count in range(count): + # Pick the next unsorted element and find where it belongs in the already-sorted [0, sorted_count) part. + var current_sid := p_ids[sorted_count] + var current_dataset_index := _dataset.get_series_index_by_id(current_sid) + var insert_at := sorted_count + while insert_at > 0 and _dataset.get_series_index_by_id(p_ids[insert_at - 1]) > current_dataset_index: + p_ids[insert_at] = p_ids[insert_at - 1] + insert_at -= 1 + p_ids[insert_at] = current_sid + + func _init_pane_dirty_flags(p_pane_count: int) -> void: _xy_dirty_panes.resize(p_pane_count) _bars_dirty_panes.resize(p_pane_count) diff --git a/addons/tau-plot/tests/bar/stacking_order/test_bar_stacking_order.gd b/addons/tau-plot/tests/bar/stacking_order/test_bar_stacking_order.gd new file mode 100644 index 0000000..19828ca --- /dev/null +++ b/addons/tau-plot/tests/bar/stacking_order/test_bar_stacking_order.gd @@ -0,0 +1,349 @@ +@tool +extends Control + +func _ready() -> void: + _setup_test_1() + _setup_test_2() + _setup_test_3() + _setup_test_4() + _setup_test_5() + _setup_test_6() + + +#################################################################################################### +# Test 1 +#################################################################################################### + +func _setup_test_1() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0]) + var y_a := PackedFloat64Array([5.0, 10.0, 15.0, 20.0]) + var y_b := PackedFloat64Array([25.0, 30.0, 35.0, 40.0]) + var y_c := PackedFloat64Array([30.0, 40.0, 50.0, 60.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b, y_c]) + + %TestPlot1.title = "[CONTINUOUS] with stacked_normalization = NONE" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.domain_padding_mode = TauAxisConfig.DomainPaddingMode.DATA_UNITS + x_axis.domain_padding_min = 0.5 + x_axis.domain_padding_max = 0.5 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + + var bar_config := TauBarConfig.new() + bar_config.mode = TauBarConfig.BarMode.STACKED + bar_config.stacked_normalization = TauBarConfig.StackedNormalization.NONE + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [bar_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_c, sb_a, sb_b] + + %TestPlot1.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 2 +#################################################################################################### + +func _setup_test_2() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0]) + var y_a := PackedFloat64Array([5.0, 10.0, 15.0, 20.0]) + var y_b := PackedFloat64Array([25.0, 30.0, 35.0, 40.0]) + var y_c := PackedFloat64Array([30.0, 40.0, 50.0, 60.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b, y_c]) + + %TestPlot2.title = "[CONTINUOUS] with stacked_normalization = FRACTION" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.domain_padding_mode = TauAxisConfig.DomainPaddingMode.DATA_UNITS + x_axis.domain_padding_min = 0.5 + x_axis.domain_padding_max = 0.5 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + + var bar_config := TauBarConfig.new() + bar_config.mode = TauBarConfig.BarMode.STACKED + bar_config.stacked_normalization = TauBarConfig.StackedNormalization.FRACTION + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [bar_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_b, sb_a, sb_c] + + %TestPlot2.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 3 +#################################################################################################### + +func _setup_test_3() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0]) + var y_a := PackedFloat64Array([5.0, 10.0, 15.0, 20.0]) + var y_b := PackedFloat64Array([25.0, 30.0, 35.0, 40.0]) + var y_c := PackedFloat64Array([30.0, 40.0, 50.0, 60.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b, y_c]) + + %TestPlot3.title = "[CONTINUOUS] with stacked_normalization = PERCENT" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.domain_padding_mode = TauAxisConfig.DomainPaddingMode.DATA_UNITS + x_axis.domain_padding_min = 0.5 + x_axis.domain_padding_max = 0.5 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.format_tick_label = func(label: String) -> String: + return label + "%" + + var bar_config := TauBarConfig.new() + bar_config.mode = TauBarConfig.BarMode.STACKED + bar_config.stacked_normalization = TauBarConfig.StackedNormalization.PERCENT + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [bar_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_c, sb_b] + + %TestPlot3.plot_xy(dataset, config, bindings) + +#################################################################################################### +# Test 4 +#################################################################################################### + +func _setup_test_4() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedStringArray(["One", "Two", "Three", "Four"]) + var y_a := PackedFloat64Array([5.0, 10.0, 15.0, 20.0]) + var y_b := PackedFloat64Array([25.0, 30.0, 35.0, 40.0]) + var y_c := PackedFloat64Array([30.0, 40.0, 50.0, 60.0]) + + var dataset := TauPlot.Dataset.make_shared_x_categorical(series_names, x, [y_a, y_b, y_c]) + + %TestPlot4.title = "[CATEGORICAL] with stacked_normalization = NONE" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CATEGORICAL + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + + var bar_config := TauBarConfig.new() + bar_config.mode = TauBarConfig.BarMode.STACKED + bar_config.stacked_normalization = TauBarConfig.StackedNormalization.NONE + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [bar_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_b, sb_a, sb_c] + + %TestPlot4.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 5 +#################################################################################################### + +func _setup_test_5() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedStringArray(["One", "Two", "Three", "Four"]) + var y_a := PackedFloat64Array([5.0, 10.0, 15.0, 20.0]) + var y_b := PackedFloat64Array([25.0, 30.0, 35.0, 40.0]) + var y_c := PackedFloat64Array([30.0, 40.0, 50.0, 60.0]) + + var dataset := TauPlot.Dataset.make_shared_x_categorical(series_names, x, [y_a, y_b, y_c]) + + %TestPlot5.title = "[CATEGORICAL] with stacked_normalization = FRACTION" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CATEGORICAL + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + + var bar_config := TauBarConfig.new() + bar_config.mode = TauBarConfig.BarMode.STACKED + bar_config.stacked_normalization = TauBarConfig.StackedNormalization.FRACTION + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [bar_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_c, sb_a, sb_b] + + %TestPlot5.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 6 +#################################################################################################### + +func _setup_test_6() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedStringArray(["One", "Two", "Three", "Four"]) + var y_a := PackedFloat64Array([5.0, 10.0, 15.0, 20.0]) + var y_b := PackedFloat64Array([25.0, 30.0, 35.0, 40.0]) + var y_c := PackedFloat64Array([30.0, 40.0, 50.0, 60.0]) + + var dataset := TauPlot.Dataset.make_shared_x_categorical(series_names, x, [y_a, y_b, y_c]) + + %TestPlot6.title = "[CATEGORICAL] with stacked_normalization = PERCENT" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CATEGORICAL + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.format_tick_label = func(label: String) -> String: + return label + "%" + + var bar_config := TauBarConfig.new() + bar_config.mode = TauBarConfig.BarMode.STACKED + bar_config.stacked_normalization = TauBarConfig.StackedNormalization.PERCENT + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [bar_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_b, sb_c, sb_a] + + %TestPlot6.plot_xy(dataset, config, bindings) diff --git a/addons/tau-plot/tests/bar/stacking_order/test_bar_stacking_order.gd.uid b/addons/tau-plot/tests/bar/stacking_order/test_bar_stacking_order.gd.uid new file mode 100644 index 0000000..66bf9da --- /dev/null +++ b/addons/tau-plot/tests/bar/stacking_order/test_bar_stacking_order.gd.uid @@ -0,0 +1 @@ +uid://dc8xbkxehai5e diff --git a/addons/tau-plot/tests/bar/stacking_order/test_bar_stacking_order.tscn b/addons/tau-plot/tests/bar/stacking_order/test_bar_stacking_order.tscn new file mode 100644 index 0000000..f6539b7 --- /dev/null +++ b/addons/tau-plot/tests/bar/stacking_order/test_bar_stacking_order.tscn @@ -0,0 +1,85 @@ +[gd_scene load_steps=3 format=3 uid="uid://dtphakr4ucelg"] + +[ext_resource type="Script" uid="uid://dc8xbkxehai5e" path="res://addons/tau-plot/tests/bar/stacking_order/test_bar_stacking_order.gd" id="1_6dmnp"] +[ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_p3gyn"] + +[node name="Bars" type="VBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_6dmnp") + +[node name="Title" type="Label" parent="."] +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +text = "Test stacking order which is dataset order, not binding order." +horizontal_alignment = 1 + +[node name="GridContainer" type="GridContainer" parent="."] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/h_separation = 0 +theme_override_constants/v_separation = 0 +columns = 3 + +[node name="TestPlot1" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_p3gyn") +title = "[CONTINUOUS] with stacked_normalization = NONE" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot2" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_p3gyn") +title = "[CONTINUOUS] with stacked_normalization = FRACTION" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot3" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_p3gyn") +title = "[CONTINUOUS] with stacked_normalization = PERCENT" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot4" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_p3gyn") +title = "[CATEGORICAL] with stacked_normalization = NONE" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot5" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_p3gyn") +title = "[CATEGORICAL] with stacked_normalization = FRACTION" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot6" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_p3gyn") +title = "[CATEGORICAL] with stacked_normalization = PERCENT" +metadata/_custom_type_script = "uid://dnhvsf2wip771" From 7c5807b221d9202d7532b28c7053b59460f38b7e Mon Sep 17 00:00:00 2001 From: ze2j <46556066+ze2j@users.noreply.github.com> Date: Sun, 3 May 2026 10:22:33 +0200 Subject: [PATCH 14/23] Callbacks receive the y raw value (breaking change) --- addons/tau-plot/plot/xy/bar/bar_renderer.gd | 35 ++++++++++++--------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/addons/tau-plot/plot/xy/bar/bar_renderer.gd b/addons/tau-plot/plot/xy/bar/bar_renderer.gd index c0c88ea..5dabab5 100644 --- a/addons/tau-plot/plot/xy/bar/bar_renderer.gd +++ b/addons/tau-plot/plot/xy/bar/bar_renderer.gd @@ -469,14 +469,15 @@ class BarRenderer extends Control: ## Draws a single bar, orientation-aware, using a StyleBox. ## Records a BarHitRecord for every bar that survives clipping. + ## [param p_y_value] is stored in the hit record. + ## [param p_y_value_for_callbacks] is passed to color, alpha, and style_box callbacks. func _draw_bar(p_pane_rect: Rect2, p_x_screen: float, p_y_from_screen: float, p_y_to_screen: float, p_thickness_px: float, p_color: Color, p_series_index: int, p_sample_index: int, - p_x_value: Variant, p_y_value: float) -> void: + p_x_value: Variant, p_y_value: float, + p_y_value_for_callbacks: float) -> void: var x_is_horizontal: bool = _layout._x_is_horizontal - # TODO: replace the x_is_horizontal branches below with XYLayout.map_point_to_screen() once it exists. - # Build the clipped screen rect. var rect: Rect2 if x_is_horizontal: @@ -512,7 +513,7 @@ class BarRenderer extends Control: return rect = Rect2(Vector2(clipped_left, clipped_top), Vector2(w, h)) - var style_box := _get_style_box(p_series_index, p_sample_index, p_x_value, p_y_value) + var style_box := _get_style_box(p_series_index, p_sample_index, p_x_value, p_y_value_for_callbacks) var final_color := _apply_hover_color(p_color, _get_bar_series_id(p_series_index), p_sample_index) _set_style_box_color(style_box, final_color) @@ -592,7 +593,7 @@ class BarRenderer extends Control: var base_color := _get_bar_color(series_index, category_index, x_value, y_value) var alpha_override := _get_bar_alpha(series_index, category_index, x_value, y_value) var color := _apply_alpha_override(base_color, alpha_override) - _draw_bar(p_pane_rect, center_px, zero_px, y_px, bar_width_px, color, series_index, category_index, x_value, y_value) + _draw_bar(p_pane_rect, center_px, zero_px, y_px, bar_width_px, color, series_index, category_index, x_value, y_value, y_value) func _draw_grouped_bars_continuous(p_pane_rect: Rect2, p_series_count: int) -> void: @@ -651,7 +652,7 @@ class BarRenderer extends Control: var base_color := _get_bar_color(series_index, i, x_value, y_value) var alpha_override := _get_bar_alpha(series_index, i, x_value, y_value) var color := _apply_alpha_override(base_color, alpha_override) - _draw_bar(p_pane_rect, center_px, zero_px, y_px, bar_width_px, color, series_index, i, x_value, y_value) + _draw_bar(p_pane_rect, center_px, zero_px, y_px, bar_width_px, color, series_index, i, x_value, y_value, y_value) func _draw_stacked_bars(p_pane_rect: Rect2, p_series_count: int) -> void: @@ -744,11 +745,13 @@ class BarRenderer extends Control: var x_value: Variant = categories[category_index] var y_value: float = segment["y1"] - var base_color := _get_bar_color(series_index, category_index, x_value, y_value) - var alpha_override := _get_bar_alpha(series_index, category_index, x_value, y_value) + # Callbacks see the raw dataset value, not the stacked top. + var y_raw: float = _dataset.get_series_y(_get_bar_series_id(series_index), category_index) + var base_color := _get_bar_color(series_index, category_index, x_value, y_raw) + var alpha_override := _get_bar_alpha(series_index, category_index, x_value, y_raw) var color := _apply_alpha_override(base_color, alpha_override) - _draw_bar(p_pane_rect, group_center_px, y0_px, y1_px, bar_width_px, color, series_index, category_index, x_value, y_value) + _draw_bar(p_pane_rect, group_center_px, y0_px, y1_px, bar_width_px, color, series_index, category_index, x_value, y_value, y_raw) func _draw_stacked_bars_continuous_shared_x(p_pane_rect: Rect2, p_series_count: int) -> void: @@ -846,11 +849,13 @@ class BarRenderer extends Control: var y1_px := _layout.map_y_to_px(_pane_index, segment["y1"], stacked_axis_id) var y_value: float = segment["y1"] - var base_color := _get_bar_color(series_index, i, x_value, y_value) - var alpha_override := _get_bar_alpha(series_index, i, x_value, y_value) + # Callbacks see the raw dataset value, not the stacked top. + var y_raw: float = _dataset.get_series_y(_get_bar_series_id(series_index), i) + var base_color := _get_bar_color(series_index, i, x_value, y_raw) + var alpha_override := _get_bar_alpha(series_index, i, x_value, y_raw) var color := _apply_alpha_override(base_color, alpha_override) - _draw_bar(p_pane_rect, group_center_px, y0_px, y1_px, bar_width_px, color, series_index, i, x_value, y_value) + _draw_bar(p_pane_rect, group_center_px, y0_px, y1_px, bar_width_px, color, series_index, i, x_value, y_value, y_raw) func _draw_independent_bars(p_pane_rect: Rect2, p_series_count: int) -> void: @@ -903,7 +908,7 @@ class BarRenderer extends Control: var alpha_override := _get_bar_alpha(series_index, category_index, x_value, y_value) var color := _apply_alpha_override(base_color, alpha_override) - _draw_bar(p_pane_rect, center_px, zero_px, y_px, bar_width_px, color, series_index, category_index, x_value, y_value) + _draw_bar(p_pane_rect, center_px, zero_px, y_px, bar_width_px, color, series_index, category_index, x_value, y_value, y_value) func _draw_independent_bars_continuous(p_pane_rect: Rect2, p_series_count: int) -> void: @@ -960,7 +965,7 @@ class BarRenderer extends Control: var alpha_override := _get_bar_alpha(series_index, i, x_value, y_value) var color := _apply_alpha_override(base_color, alpha_override) - _draw_bar(p_pane_rect, center_px, zero_px, y_px, bar_width_px, color, series_index, i, x_value, y_value) + _draw_bar(p_pane_rect, center_px, zero_px, y_px, bar_width_px, color, series_index, i, x_value, y_value, y_value) func _draw_independent_bars_continuous_per_series_x(p_pane_rect: Rect2, p_series_count: int) -> void: @@ -1004,7 +1009,7 @@ class BarRenderer extends Control: var alpha_override := _get_bar_alpha(series_index, i, x_value, y_value) var color := _apply_alpha_override(base_color, alpha_override) - _draw_bar(p_pane_rect, center_px, zero_px, y_px, bar_width_px, color, series_index, i, x_value, y_value) + _draw_bar(p_pane_rect, center_px, zero_px, y_px, bar_width_px, color, series_index, i, x_value, y_value, y_value) func _get_series_draw_order(p_series_count: int) -> Array[int]: From 2e6dec8773e86e482049289d555b60c4b7d95b3c Mon Sep 17 00:00:00 2001 From: ze2j <46556066+ze2j@users.noreply.github.com> Date: Sun, 3 May 2026 12:38:03 +0200 Subject: [PATCH 15/23] Hit carry both plotted and raw values (breaking change) --- addons/tau-plot/plot/xy/bar/bar_hit_record.gd | 11 ++-- addons/tau-plot/plot/xy/bar/bar_hit_tester.gd | 3 +- addons/tau-plot/plot/xy/bar/bar_renderer.gd | 54 ++++++++++--------- .../tau-plot/plot/xy/hover/hover_formatter.gd | 2 +- addons/tau-plot/plot/xy/hover/sample_hit.gd | 10 +++- .../tau-plot/plot/xy/line/line_hit_record.gd | 10 +++- .../tau-plot/plot/xy/line/line_hit_tester.gd | 3 +- addons/tau-plot/plot/xy/line/line_renderer.gd | 6 ++- .../plot/xy/scatter/scatter_hit_tester.gd | 9 ++-- 9 files changed, 69 insertions(+), 39 deletions(-) diff --git a/addons/tau-plot/plot/xy/bar/bar_hit_record.gd b/addons/tau-plot/plot/xy/bar/bar_hit_record.gd index 5f09859..e53c485 100644 --- a/addons/tau-plot/plot/xy/bar/bar_hit_record.gd +++ b/addons/tau-plot/plot/xy/bar/bar_hit_record.gd @@ -9,9 +9,14 @@ class BarHitRecord extends RefCounted: ## Float for continuous x, String for categorical. var x_value: Variant - ## Plotted y value. For STACKED with normalization, this is the scaled - ## value matching the y-axis labels, not the raw dataset value. - var y_value: float + ## Y position the bar's top is drawn at, in axis units. + ## Differs from y_raw_value when STACKED is on (cumulative top) or when + ## FRACTION/PERCENT normalization is on. + var y_plotted_value: float + + ## Original dataset value, before any stacking, normalization, or + ## accumulation. Equal to y_plotted_value when STACKED is off. + var y_raw_value: float ## Painted rectangle, clipped to the pane. var rect: Rect2 diff --git a/addons/tau-plot/plot/xy/bar/bar_hit_tester.gd b/addons/tau-plot/plot/xy/bar/bar_hit_tester.gd index d4be55b..59eed8f 100644 --- a/addons/tau-plot/plot/xy/bar/bar_hit_tester.gd +++ b/addons/tau-plot/plot/xy/bar/bar_hit_tester.gd @@ -106,7 +106,8 @@ class BarHitTester extends OverlayHitTester: hit.series_name = _dataset.get_series_name(p_record.series_id) hit.sample_index = p_record.sample_index hit.x_value = p_record.x_value - hit.y_value = p_record.y_value + hit.y_plotted_value = p_record.y_plotted_value + hit.y_raw_value = p_record.y_raw_value hit.screen_position = p_record.anchor hit.pane_index = _pane_index hit.overlay_type = PaneOverlayType.BAR diff --git a/addons/tau-plot/plot/xy/bar/bar_renderer.gd b/addons/tau-plot/plot/xy/bar/bar_renderer.gd index 5dabab5..b2b6928 100644 --- a/addons/tau-plot/plot/xy/bar/bar_renderer.gd +++ b/addons/tau-plot/plot/xy/bar/bar_renderer.gd @@ -469,27 +469,37 @@ class BarRenderer extends Control: ## Draws a single bar, orientation-aware, using a StyleBox. ## Records a BarHitRecord for every bar that survives clipping. - ## [param p_y_value] is stored in the hit record. - ## [param p_y_value_for_callbacks] is passed to color, alpha, and style_box callbacks. - func _draw_bar(p_pane_rect: Rect2, p_x_screen: float, p_y_from_screen: float, - p_y_to_screen: float, p_thickness_px: float, p_color: Color, - p_series_index: int, p_sample_index: int, - p_x_value: Variant, p_y_value: float, - p_y_value_for_callbacks: float) -> void: + ## p_pane_rect: pane bounds used to clip the bar rect. + ## p_x_axis_px: bar center along the x-axis direction, in pixels. + ## p_y_axis_from_px: baseline end of the bar along the y-axis direction, in pixels. + ## p_y_axis_to_px: tip end of the bar along the y-axis direction, in pixels. + ## p_thickness_px: bar thickness across the x-axis direction, in pixels. + ## p_color: fill color before hover remap. + ## p_series_index: index into the bar series list, not the dataset series id. + ## p_sample_index: sample index / category index. + ## p_x_value: float for continuous x, String for categorical. + ## p_y_plotted_value: cumulative top in STACKED mode, scaled when FRACTION/PERCENT is on. + ## p_y_raw_value: original dataset value. Equal to p_y_plotted_value when STACKED is off. + func _draw_bar( + p_pane_rect: Rect2, p_x_axis_px: float, p_y_axis_from_px: float, p_y_axis_to_px: float, + p_thickness_px: float, p_color: Color, + p_series_index: int, p_sample_index: int, + p_x_value: Variant, p_y_plotted_value: float, p_y_raw_value: float + ) -> void: var x_is_horizontal: bool = _layout._x_is_horizontal # Build the clipped screen rect. var rect: Rect2 if x_is_horizontal: - var left := p_x_screen - p_thickness_px * 0.5 + var left := p_x_axis_px - p_thickness_px * 0.5 var right := left + p_thickness_px var clipped_left := max(left, p_pane_rect.position.x) var clipped_right := min(right, p_pane_rect.position.x + p_pane_rect.size.x) var w: float = clipped_right - clipped_left if w <= 0.0: return - var top := min(p_y_from_screen, p_y_to_screen) - var bottom := max(p_y_from_screen, p_y_to_screen) + var top := min(p_y_axis_from_px, p_y_axis_to_px) + var bottom := max(p_y_axis_from_px, p_y_axis_to_px) var clipped_top := max(top, p_pane_rect.position.y) var clipped_bottom := min(bottom, p_pane_rect.position.y + p_pane_rect.size.y) var h: float = clipped_bottom - clipped_top @@ -497,15 +507,15 @@ class BarRenderer extends Control: return rect = Rect2(Vector2(clipped_left, clipped_top), Vector2(w, h)) else: - var top := p_x_screen - p_thickness_px * 0.5 + var top := p_x_axis_px - p_thickness_px * 0.5 var bottom := top + p_thickness_px var clipped_top := max(top, p_pane_rect.position.y) var clipped_bottom := min(bottom, p_pane_rect.position.y + p_pane_rect.size.y) var h: float = clipped_bottom - clipped_top if h <= 0.0: return - var left := min(p_y_from_screen, p_y_to_screen) - var right := max(p_y_from_screen, p_y_to_screen) + var left := min(p_y_axis_from_px, p_y_axis_to_px) + var right := max(p_y_axis_from_px, p_y_axis_to_px) var clipped_left := max(left, p_pane_rect.position.x) var clipped_right := min(right, p_pane_rect.position.x + p_pane_rect.size.x) var w: float = clipped_right - clipped_left @@ -513,30 +523,26 @@ class BarRenderer extends Control: return rect = Rect2(Vector2(clipped_left, clipped_top), Vector2(w, h)) - var style_box := _get_style_box(p_series_index, p_sample_index, p_x_value, p_y_value_for_callbacks) + var style_box := _get_style_box(p_series_index, p_sample_index, p_x_value, p_y_raw_value) var final_color := _apply_hover_color(p_color, _get_bar_series_id(p_series_index), p_sample_index) _set_style_box_color(style_box, final_color) if style_box is StyleBoxFlat: - var tip_at_min: bool = (p_y_to_screen < p_y_from_screen) + var tip_at_min: bool = (p_y_axis_to_px < p_y_axis_from_px) _remap_corners_and_borders(style_box as StyleBoxFlat, _derived_source_ref as StyleBoxFlat, x_is_horizontal, tip_at_min) draw_style_box(style_box, rect) - # Tip-center in screen coords, un-clipped so the anchor stays on the data point - # even when the bar is partly outside the pane. - # TODO: use XYLayout.map_point_to_screen() once it exists. - var anchor: Vector2 - if x_is_horizontal: - anchor = Vector2(p_x_screen, p_y_to_screen) - else: - anchor = Vector2(p_y_to_screen, p_x_screen) + # Tip center in screen coords, un-clipped so the anchor stays on the data + # point even when the bar is partly outside the pane. + var anchor := _layout.map_point_to_screen(p_x_axis_px, p_y_axis_to_px) var record := BarHitRecord.new() record.series_id = _get_bar_series_id(p_series_index) record.sample_index = p_sample_index record.x_value = p_x_value - record.y_value = p_y_value + record.y_plotted_value = p_y_plotted_value + record.y_raw_value = p_y_raw_value record.rect = rect record.anchor = anchor _hit_records.append(record) diff --git a/addons/tau-plot/plot/xy/hover/hover_formatter.gd b/addons/tau-plot/plot/xy/hover/hover_formatter.gd index 3953868..1e8d86e 100644 --- a/addons/tau-plot/plot/xy/hover/hover_formatter.gd +++ b/addons/tau-plot/plot/xy/hover/hover_formatter.gd @@ -98,7 +98,7 @@ class HoverFormatter extends RefCounted: ## Formats the y value of a hit, using axis format_tick_label if available. func _format_hit_y_value(p_hit: SampleHit) -> String: var span := _get_y_domain_span(p_hit) - var raw_str := _format_value(p_hit.y_value, span, _precision_digits) + var raw_str := _format_value(p_hit.y_raw_value, span, _precision_digits) return _apply_y_format_callback(raw_str, p_hit) diff --git a/addons/tau-plot/plot/xy/hover/sample_hit.gd b/addons/tau-plot/plot/xy/hover/sample_hit.gd index 2722a9e..e104fcc 100644 --- a/addons/tau-plot/plot/xy/hover/sample_hit.gd +++ b/addons/tau-plot/plot/xy/hover/sample_hit.gd @@ -16,8 +16,14 @@ class SampleHit extends RefCounted: ## X value: float for continuous axes, String for categorical. var x_value: Variant - ## Y value. - var y_value: float + ## Y position the sample is drawn at, in axis units. + ## Differs from y_raw_value when STACKED is on (cumulative top) or when + ## FRACTION/PERCENT normalization is on. + var y_plotted_value: float + + ## Original dataset value, before any stacking, normalization, or + ## accumulation. Equal to y_plotted_value when STACKED is off. + var y_raw_value: float ## Screen position of the data point in plot-local coordinates. ## For bars this is the top-center of the bar (or the relevant edge diff --git a/addons/tau-plot/plot/xy/line/line_hit_record.gd b/addons/tau-plot/plot/xy/line/line_hit_record.gd index 52f610b..a0d38d1 100644 --- a/addons/tau-plot/plot/xy/line/line_hit_record.gd +++ b/addons/tau-plot/plot/xy/line/line_hit_record.gd @@ -11,8 +11,14 @@ class LineHitRecord extends RefCounted: ## Float for continuous x, String for categorical. var x_value: Variant - ## Plotted y value. - var y_value: float + ## Y position the polyline vertex is drawn at, in axis units. + ## Differs from y_raw_value when STACKED is on (cumulative top) or when + ## FRACTION/PERCENT normalization is on. + var y_plotted_value: float + + ## Original dataset value, before any stacking, normalization, or + ## accumulation. Equal to y_plotted_value when STACKED is off. + var y_raw_value: float ## Real sample position in pane-local screen coordinates. var screen_position: Vector2 diff --git a/addons/tau-plot/plot/xy/line/line_hit_tester.gd b/addons/tau-plot/plot/xy/line/line_hit_tester.gd index df8be79..d5c7679 100644 --- a/addons/tau-plot/plot/xy/line/line_hit_tester.gd +++ b/addons/tau-plot/plot/xy/line/line_hit_tester.gd @@ -156,7 +156,8 @@ class LineHitTester extends OverlayHitTester: hit.series_name = _dataset.get_series_name(p_record.series_id) hit.sample_index = p_record.sample_index hit.x_value = p_record.x_value - hit.y_value = p_record.y_value + hit.y_plotted_value = p_record.y_plotted_value + hit.y_raw_value = p_record.y_raw_value hit.screen_position = p_record.screen_position hit.pane_index = _pane_index hit.overlay_type = PaneOverlayType.LINE diff --git a/addons/tau-plot/plot/xy/line/line_renderer.gd b/addons/tau-plot/plot/xy/line/line_renderer.gd index 1bf0708..c595cf1 100644 --- a/addons/tau-plot/plot/xy/line/line_renderer.gd +++ b/addons/tau-plot/plot/xy/line/line_renderer.gd @@ -293,7 +293,8 @@ class LineRenderer extends Control: record.series_id = series_id record.sample_index = i record.x_value = x_value - record.y_value = y_value + record.y_plotted_value = y_value + record.y_raw_value = y_value record.screen_position = screen_pos _hit_records.append(record) @@ -343,7 +344,8 @@ class LineRenderer extends Control: record.series_id = series_id record.sample_index = cat_idx record.x_value = x_value - record.y_value = y_value + record.y_plotted_value = y_value + record.y_raw_value = y_value record.screen_position = screen_pos _hit_records.append(record) diff --git a/addons/tau-plot/plot/xy/scatter/scatter_hit_tester.gd b/addons/tau-plot/plot/xy/scatter/scatter_hit_tester.gd index 1eda034..a2f43a8 100644 --- a/addons/tau-plot/plot/xy/scatter/scatter_hit_tester.gd +++ b/addons/tau-plot/plot/xy/scatter/scatter_hit_tester.gd @@ -104,7 +104,8 @@ class ScatterHitTester extends OverlayHitTester: hit.series_name = _dataset.get_series_name(hit.series_id) hit.sample_index = sample_idx hit.x_value = p_x_value - hit.y_value = _scatter_renderer.get_hover_y_value(i) + hit.y_plotted_value = _scatter_renderer.get_hover_y_value(i) + hit.y_raw_value = hit.y_plotted_value hit.screen_position = screen_pos hit.pane_index = _pane_index hit.overlay_type = PaneOverlayType.SCATTER @@ -153,7 +154,8 @@ class ScatterHitTester extends OverlayHitTester: hit.series_name = _dataset.get_series_name(hit.series_id) hit.sample_index = _scatter_renderer.get_hover_sample_index(i) hit.x_value = _scatter_renderer.get_hover_x_value(i) - hit.y_value = _scatter_renderer.get_hover_y_value(i) + hit.y_plotted_value = _scatter_renderer.get_hover_y_value(i) + hit.y_raw_value = hit.y_plotted_value hit.screen_position = screen_pos hit.pane_index = _pane_index hit.overlay_type = PaneOverlayType.SCATTER @@ -213,7 +215,8 @@ class ScatterHitTester extends OverlayHitTester: hit.series_name = _dataset.get_series_name(hit.series_id) hit.sample_index = _scatter_renderer.get_hover_sample_index(p_cache_index) hit.x_value = _scatter_renderer.get_hover_x_value(p_cache_index) - hit.y_value = _scatter_renderer.get_hover_y_value(p_cache_index) + hit.y_plotted_value = _scatter_renderer.get_hover_y_value(p_cache_index) + hit.y_raw_value = hit.y_plotted_value hit.screen_position = _scatter_renderer.get_hover_screen_position(p_cache_index) hit.pane_index = _pane_index hit.overlay_type = PaneOverlayType.SCATTER From 3b59c744ae84253abb67052fba822253b856aab1 Mon Sep 17 00:00:00 2001 From: ze2j <46556066+ze2j@users.noreply.github.com> Date: Sun, 3 May 2026 12:59:49 +0200 Subject: [PATCH 16/23] Tooltip no longer use axis-tick formatters (breaking change) --- .../tau-plot/plot/xy/hover/hover_formatter.gd | 46 +++---------------- addons/tau-plot/plot/xy/xy_plot.gd | 2 +- 2 files changed, 7 insertions(+), 41 deletions(-) diff --git a/addons/tau-plot/plot/xy/hover/hover_formatter.gd b/addons/tau-plot/plot/xy/hover/hover_formatter.gd index 1e8d86e..eaa329b 100644 --- a/addons/tau-plot/plot/xy/hover/hover_formatter.gd +++ b/addons/tau-plot/plot/xy/hover/hover_formatter.gd @@ -6,22 +6,18 @@ const SeriesAxisAssignment := preload("res://addons/tau-plot/plot/xy/series_axis ## Produces human-readable strings from raw numeric values for tooltip display. ## ## Adapts decimal places to the visible domain span and a configurable -## precision digit count. Honors user-provided format_tick_label callbacks -## on axis configs when they are set. +## precision digit count. class HoverFormatter extends RefCounted: var _domain: XYDomain - var _domain_config: TauXYConfig var _series_assignment: SeriesAxisAssignment var _precision_digits: int func _init( p_domain: XYDomain, - p_domain_config: TauXYConfig, p_series_assignment: SeriesAxisAssignment, p_precision_digits: int) -> void: _domain = p_domain - _domain_config = p_domain_config _series_assignment = p_series_assignment _precision_digits = p_precision_digits @@ -85,21 +81,14 @@ class HoverFormatter extends RefCounted: return "\n".join(lines) - ## Formats the x value of a hit, using axis format_tick_label if available. func _format_hit_x_value(p_hit: SampleHit) -> String: if p_hit.x_value is String: - return _apply_x_format_callback(p_hit.x_value as String) + return p_hit.x_value as String + return _format_continuous_x_value(p_hit.x_value as float) - # Continuous x: format with domain-aware precision for tooltip display. - var raw_str := _format_continuous_x_value(p_hit.x_value as float) - return _apply_x_format_callback(raw_str) - - ## Formats the y value of a hit, using axis format_tick_label if available. func _format_hit_y_value(p_hit: SampleHit) -> String: - var span := _get_y_domain_span(p_hit) - var raw_str := _format_value(p_hit.y_raw_value, span, _precision_digits) - return _apply_y_format_callback(raw_str, p_hit) + return _format_value(p_hit.y_raw_value, _get_y_domain_span(p_hit), _precision_digits) ## Formats a continuous x value for tooltip display. @@ -110,8 +99,7 @@ class HoverFormatter extends RefCounted: ## places is derived from the x domain span and the configured precision ## digits. func _format_continuous_x_value(p_value: float) -> String: - var span := _get_x_domain_span() - return _format_value(p_value, span, _precision_digits) + return _format_value(p_value, _get_x_domain_span(), _precision_digits) ## Formats a float with precision derived from the domain span. @@ -120,12 +108,8 @@ class HoverFormatter extends RefCounted: ## represents roughly 1/(10^p_precision_digits) of the span. When values ## are extremely small or extremely large, scientific notation is used. static func _format_value(p_value: float, p_domain_span: float, p_precision_digits: int) -> String: - # The span must be strictly positive (which is guaranteed by XYDomain). - if p_domain_span <= 0.0: - push_error("HoverFormatter: domain span must be > 0, got %f" % p_domain_span) - return String.num(p_value, 3) - # Compute decimals so precision is ~span / 10^p_precision_digits. + # The domain span is strictly positive (guaranteed by XYDomain). var decimals := maxi(0, -int(floor(log(p_domain_span) / log(10.0))) + p_precision_digits) # If we would need more than 12 decimal places, switch to scientific @@ -153,21 +137,3 @@ class HoverFormatter extends RefCounted: var pane_domain := _domain.get_pane_domain(pane_idx) var y_domain := pane_domain.get_y_axis_domain(y_axis_id) return y_domain.max_val - y_domain.min_val - - - ## Applies the x axis format_tick_label callback if set. - func _apply_x_format_callback(p_text: String) -> String: - var x_cfg := _domain_config.x_axis - if x_cfg == null or not x_cfg.format_tick_label.is_valid(): - return p_text - return x_cfg.format_tick_label.call(p_text) - - - ## Applies the y axis format_tick_label callback if set for the hit's pane and axis. - func _apply_y_format_callback(p_text: String, p_hit: SampleHit) -> String: - var y_axis_id: int = _series_assignment.get_y_axis_id_for_series(p_hit.series_id, p_hit.pane_index) - var pane_config: TauPaneConfig = _domain_config.panes[p_hit.pane_index] - var y_cfg := pane_config.get_y_axis_config(y_axis_id) - if y_cfg == null or not y_cfg.format_tick_label.is_valid(): - return p_text - return y_cfg.format_tick_label.call(p_text) diff --git a/addons/tau-plot/plot/xy/xy_plot.gd b/addons/tau-plot/plot/xy/xy_plot.gd index 9438de2..3df5348 100644 --- a/addons/tau-plot/plot/xy/xy_plot.gd +++ b/addons/tau-plot/plot/xy/xy_plot.gd @@ -396,7 +396,7 @@ func setup( # Hover setup var tooltip_precision_digits := p_hover_config.tooltip_precision_digits if p_hover_config != null else 3 - var formatter := HoverFormatter.new(_xy_domain, _domain_config, _series_assignment, tooltip_precision_digits) + var formatter := HoverFormatter.new(_xy_domain, _series_assignment, tooltip_precision_digits) # Create per-pane hit testers. var hit_testers_per_pane: Array = [] From 39191bdbbbb0dc48c1a9241f50c3f8a279a35c84 Mon Sep 17 00:00:00 2001 From: ze2j <46556066+ze2j@users.noreply.github.com> Date: Sun, 3 May 2026 14:51:16 +0200 Subject: [PATCH 17/23] Add StackedNegativePolicy (breaking change) --- addons/tau-plot/plot/xy/bar/bar_config.gd | 25 ++- addons/tau-plot/plot/xy/bar/bar_renderer.gd | 151 +++++++++--------- addons/tau-plot/plot/xy/bar/bar_validator.gd | 23 +-- addons/tau-plot/plot/xy/line/line_config.gd | 20 ++- .../plot/xy/stacked_negative_policy.gd | 12 ++ .../plot/xy/stacked_negative_policy.gd.uid | 1 + .../tau-plot/plot/xy/stacked_pinned_range.gd | 31 ++++ .../plot/xy/stacked_pinned_range.gd.uid | 1 + addons/tau-plot/plot/xy/xy_plot.gd | 13 +- 9 files changed, 179 insertions(+), 98 deletions(-) create mode 100644 addons/tau-plot/plot/xy/stacked_negative_policy.gd create mode 100644 addons/tau-plot/plot/xy/stacked_negative_policy.gd.uid create mode 100644 addons/tau-plot/plot/xy/stacked_pinned_range.gd create mode 100644 addons/tau-plot/plot/xy/stacked_pinned_range.gd.uid diff --git a/addons/tau-plot/plot/xy/bar/bar_config.gd b/addons/tau-plot/plot/xy/bar/bar_config.gd index bf867e6..1795851 100644 --- a/addons/tau-plot/plot/xy/bar/bar_config.gd +++ b/addons/tau-plot/plot/xy/bar/bar_config.gd @@ -24,6 +24,17 @@ enum BarMode const StackedNormalization = preload("res://addons/tau-plot/plot/xy/stacked_normalization.gd").StackedNormalization @export var stacked_normalization: StackedNormalization = StackedNormalization.NONE +const StackedNegativePolicy = preload("res://addons/tau-plot/plot/xy/stacked_negative_policy.gd").StackedNegativePolicy + +## How negative values are handled in STACKED mode: +## - SKIP_NEGATIVES (default) drops negative samples entirely from the stack. +## - DIVERGING splits each X into an upper stack of positive values and +## a lower stack of negative values, both anchored at zero. +## - SIGNED_SUM is not a valid choice for bars: bar geometry cannot represent a +## downward dip without overlapping rectangles. Setting it produces a validation +## error. +@export var stacked_negative_policy: StackedNegativePolicy = StackedNegativePolicy.SKIP_NEGATIVES + enum BarWidthPolicy { @@ -150,6 +161,8 @@ func is_equal_to(p_other: TauPaneOverlayConfig) -> bool: return false if stacked_normalization != other.stacked_normalization: return false + if stacked_negative_policy != other.stacked_negative_policy: + return false if bar_width_policy != other.bar_width_policy: return false @@ -180,10 +193,11 @@ func is_equal_to(p_other: TauPaneOverlayConfig) -> bool: # Returns true if the change between this and p_other affects layout/domain. # Returns false if the change only affects visual appearance. # -# Only mode and stacked_normalization affect the domain (stacking changes Y -# bounds via _apply_bar_domain_overrides_y). All width, gap, and spacing -# properties are visual-only: they control how bars are drawn within a fixed -# domain but do not feed into domain or tick computation. +# mode, stacked_normalization, and stacked_negative_policy affect the domain: +# stacking changes Y bounds, normalization pins the range, and the negative +# policy decides whether the lower half-axis exists. All width, gap, and +# spacing properties are visual-only: they control how bars are drawn within +# a fixed domain but do not feed into domain or tick computation. func has_layout_affecting_change(p_other: TauPaneOverlayConfig) -> bool: var other := p_other as TauBarConfig if other == null: @@ -198,4 +212,7 @@ func has_layout_affecting_change(p_other: TauPaneOverlayConfig) -> bool: if mode == BarMode.STACKED and stacked_normalization != other.stacked_normalization: return true + if mode == BarMode.STACKED and stacked_negative_policy != other.stacked_negative_policy: + return true + return false diff --git a/addons/tau-plot/plot/xy/bar/bar_renderer.gd b/addons/tau-plot/plot/xy/bar/bar_renderer.gd index b2b6928..ac0cd50 100644 --- a/addons/tau-plot/plot/xy/bar/bar_renderer.gd +++ b/addons/tau-plot/plot/xy/bar/bar_renderer.gd @@ -16,7 +16,8 @@ const BarHitRecord := preload("res://addons/tau-plot/plot/xy/bar/bar_hit_record. # - NaN and Inf are always silently skipped # - Logarithmic Y scales: y <= 0 are skipped # - Logarithmic X scales: x <= 0 are skipped -# - STACKED mode: negative values are skipped (would produce misleading visualization) +# - STACKED mode: negative values follow TauBarConfig.stacked_negative_policy +# (DIVERGING by default: a separate downward stack from zero). # BarValidator is expected to enforce: # - dataset shape constraints for the chosen mode, # - length consistency in SHARED_X mode, @@ -661,6 +662,76 @@ class BarRenderer extends Control: _draw_bar(p_pane_rect, center_px, zero_px, y_px, bar_width_px, color, series_index, i, x_value, y_value, y_value) + # Builds one X column's worth of stacked bar segments in dataset order, + # applying the active normalization and negative policy. The returned list + # drives the second-pass painter, which looks up segments by series_index. + func _compute_stacked_segments_at(p_series_count: int, p_sample_index: int) -> Array: + var normalization := _bar_config.stacked_normalization + var policy := _bar_config.stacked_negative_policy + + # DIVERGING needs positive and absolute-negative totals tracked + # separately so each half-stack normalizes against its own total. + var pos_total := 0.0 + var neg_total_abs := 0.0 + if normalization != TauBarConfig.StackedNormalization.NONE: + for series_index in range(p_series_count): + var series_id := _get_bar_series_id(series_index) + var y_value := _dataset.get_series_y(series_id, p_sample_index) + if is_nan(y_value) or is_inf(y_value): + continue + if y_value >= 0.0: + pos_total += y_value + else: + neg_total_abs += -y_value + + var pos_scale := 1.0 + var neg_scale := 1.0 + match normalization: + TauBarConfig.StackedNormalization.FRACTION: + pos_scale = 1.0 / pos_total if pos_total > 0.0 else 0.0 + neg_scale = 1.0 / neg_total_abs if neg_total_abs > 0.0 else 0.0 + + TauBarConfig.StackedNormalization.PERCENT: + pos_scale = 100.0 / pos_total if pos_total > 0.0 else 0.0 + neg_scale = 100.0 / neg_total_abs if neg_total_abs > 0.0 else 0.0 + + var segments: Array = [] + var accum_pos := 0.0 + var accum_neg := 0.0 + + for series_index in range(p_series_count): + var series_id := _get_bar_series_id(series_index) + var y_raw := _dataset.get_series_y(series_id, p_sample_index) + if is_nan(y_raw) or is_inf(y_raw): + continue + + match policy: + TauBarConfig.StackedNegativePolicy.SKIP_NEGATIVES: + if y_raw < 0.0: + continue + var y := y_raw * pos_scale + var y0 := accum_pos + var y1 := accum_pos + y + segments.append({"series_index": series_index, "y0": y0, "y1": y1}) + accum_pos = y1 + + TauBarConfig.StackedNegativePolicy.DIVERGING: + if y_raw >= 0.0: + var y := y_raw * pos_scale + var y0 := accum_pos + var y1 := accum_pos + y + segments.append({"series_index": series_index, "y0": y0, "y1": y1}) + accum_pos = y1 + else: + var y := y_raw * neg_scale + var y0 := accum_neg + var y1 := accum_neg + y + segments.append({"series_index": series_index, "y0": y0, "y1": y1}) + accum_neg = y1 + + return segments + + func _draw_stacked_bars(p_pane_rect: Rect2, p_series_count: int) -> void: # Validator ensures: STACKED requires SHARED_X and SHARED y-axis if p_series_count <= 0: @@ -677,6 +748,7 @@ class BarRenderer extends Control: push_error("Unexpected x-axis type %d" % int(x_config.type)) + # FIXME: the second pass here duplicates _draw_stacked_bars_continuous_shared_x almost line-for-line. func _draw_stacked_bars_categorical(p_pane_rect: Rect2, p_series_count: int) -> void: var stacked_axis_id := _get_y_axis_id_for_series(_get_bar_series_id(0)) var categories := _layout.domain.x_categories @@ -690,44 +762,8 @@ class BarRenderer extends Control: # First pass: compute all segments for all categories (in dataset order for stacking) var all_segments: Array = [] # Array of arrays, one per category. FIXME Godot 4.5 does not support nested typed collections. all_segments.resize(n) - for category_index in range(n): - var total := 0.0 - if _bar_config.stacked_normalization != TauBarConfig.StackedNormalization.NONE: - for series_index in range(p_series_count): - var series_id := _get_bar_series_id(series_index) - var y_value := _dataset.get_series_y(series_id, category_index) - if is_nan(y_value) or is_inf(y_value): - continue - if y_value < 0.0: - continue # STACKED: skip negative values for total calculation - total += y_value - - var scale := 1.0 - if _bar_config.stacked_normalization == TauBarConfig.StackedNormalization.FRACTION: - scale = 1.0 / total if total > 0.0 else 0.0 - elif _bar_config.stacked_normalization == TauBarConfig.StackedNormalization.PERCENT: - scale = 100.0 / total if total > 0.0 else 0.0 - - # Compute segments for this category in dataset order - var segments: Array = [] - var accum := 0.0 - for series_index in range(p_series_count): - var series_id := _get_bar_series_id(series_index) - var y_raw := _dataset.get_series_y(series_id, category_index) - if is_nan(y_raw) or is_inf(y_raw): - continue - if y_raw < 0.0: - continue # STACKED: skip negative values - - var y := y_raw * scale - var y0 := accum - var y1 := accum + y - - segments.append({"series_index": series_index, "y0": y0, "y1": y1}) - accum = y1 - - all_segments[category_index] = segments + all_segments[category_index] = _compute_stacked_segments_at(p_series_count, category_index) # Second pass: paint in z_order (series first, then categories) var draw_order := _get_series_draw_order(p_series_count) @@ -760,6 +796,7 @@ class BarRenderer extends Control: _draw_bar(p_pane_rect, group_center_px, y0_px, y1_px, bar_width_px, color, series_index, category_index, x_value, y_value, y_raw) + # FIXME: the second pass here duplicates _draw_stacked_bars_categorical almost line-for-line. func _draw_stacked_bars_continuous_shared_x(p_pane_rect: Rect2, p_series_count: int) -> void: var stacked_axis_id := _get_y_axis_id_for_series(_get_bar_series_id(0)) var n := _dataset.get_shared_sample_count() @@ -771,7 +808,6 @@ class BarRenderer extends Control: # First pass: compute all segments for all X positions (in dataset order for stacking) var all_segments: Array = [] # Array of arrays, one per X position. FIXME Godot 4.5 does not support nested typed collections. all_segments.resize(n) - for i in range(n): var x_value := float(_dataset.get_shared_x(i)) if is_nan(x_value) or is_inf(x_value): @@ -781,42 +817,7 @@ class BarRenderer extends Control: all_segments[i] = [] continue - var total := 0.0 - if _bar_config.stacked_normalization != TauBarConfig.StackedNormalization.NONE: - for series_index in range(p_series_count): - var series_id := _get_bar_series_id(series_index) - var y_value := _dataset.get_series_y(series_id, i) - if is_nan(y_value) or is_inf(y_value): - continue - if y_value < 0.0: - continue # STACKED: skip negative values for total calculation - total += y_value - - var scale := 1.0 - if _bar_config.stacked_normalization == TauBarConfig.StackedNormalization.FRACTION: - scale = 1.0 / total if total > 0.0 else 0.0 - elif _bar_config.stacked_normalization == TauBarConfig.StackedNormalization.PERCENT: - scale = 100.0 / total if total > 0.0 else 0.0 - - # Compute segments for this X position in dataset order - var segments: Array = [] - var accum := 0.0 - for series_index in range(p_series_count): - var series_id := _get_bar_series_id(series_index) - var y_raw := _dataset.get_series_y(series_id, i) - if is_nan(y_raw) or is_inf(y_raw): - continue - if y_raw < 0.0: - continue # STACKED: skip negative values - - var y := y_raw * scale - var y0 := accum - var y1 := accum + y - - segments.append({"series_index": series_index, "y0": y0, "y1": y1}) - accum = y1 - - all_segments[i] = segments + all_segments[i] = _compute_stacked_segments_at(p_series_count, i) # Second pass: paint in z_order (series first, then X positions) var draw_order := _get_series_draw_order(p_series_count) diff --git a/addons/tau-plot/plot/xy/bar/bar_validator.gd b/addons/tau-plot/plot/xy/bar/bar_validator.gd index a829f55..ada08b7 100644 --- a/addons/tau-plot/plot/xy/bar/bar_validator.gd +++ b/addons/tau-plot/plot/xy/bar/bar_validator.gd @@ -32,7 +32,7 @@ class BarValidator extends RefCounted: p_result.add_error("BarValidator: binding has pane_index %d, expected %d" % [binding.pane_index, p_pane_index]) return if binding.overlay_type != PaneOverlayType.BAR: - p_result.add_error("BarValidator: binding has overlay_type %d, expected BAR" % int(binding.overlay_type)) + p_result.add_error("BarValidator: binding has overlay_type %d, expected BAR" % binding.overlay_type) return var pane_cfg := p_domain_cfg.panes[p_pane_index] @@ -48,7 +48,7 @@ class BarValidator extends RefCounted: _validate_bar_visuals(p_pane_index, bar_config, p_bar_overlay_bindings, p_result) var is_shared_x := (p_dataset.get_mode() == Dataset.Mode.SHARED_X) - _validate_bar_mode_constraints(p_pane_index, bar_config.mode, pane_cfg, p_bar_overlay_bindings, is_shared_x, p_result) + _validate_bar_mode_constraints(p_pane_index, bar_config, pane_cfg, p_bar_overlay_bindings, is_shared_x, p_result) var x_cfg := p_domain_cfg.x_axis if x_cfg != null: @@ -69,8 +69,8 @@ class BarValidator extends RefCounted: p_result.add_error("BarValidator: pane %d: series_id %d has visual_attributes that is not a BarVisualAttributes" % [p_pane_index, binding.series_id]) - static func _validate_bar_mode_constraints(p_pane_index: int, p_bar_mode: TauBarConfig.BarMode, p_pane_cfg: TauPaneConfig, p_bar_overlay_bindings: Array[TauXYSeriesBinding], p_is_shared_x: bool, p_result: ValidationResult) -> void: - match p_bar_mode: + static func _validate_bar_mode_constraints(p_pane_index: int, p_bar_config: TauBarConfig, p_pane_cfg: TauPaneConfig, p_bar_overlay_bindings: Array[TauXYSeriesBinding], p_is_shared_x: bool, p_result: ValidationResult) -> void: + match p_bar_config.mode: TauBarConfig.BarMode.GROUPED: if not p_is_shared_x: p_result.add_error("BarValidator: pane %d: GROUPED mode requires SHARED_X dataset mode" % p_pane_index) @@ -79,6 +79,11 @@ class BarValidator extends RefCounted: if not p_is_shared_x: p_result.add_error("BarValidator: pane %d: STACKED mode requires SHARED_X dataset mode" % p_pane_index) + # SIGNED_SUM is geometrically incompatible with bar STACKED: + # negative contributions would require subtracting from the cumulative, which produces overlapping rectangles. + if p_bar_config.stacked_negative_policy == TauBarConfig.StackedNegativePolicy.SIGNED_SUM: + p_result.add_error("BarValidator: pane %d: STACKED mode does not support SIGNED_SUM negative policy. Use DIVERGING or SKIP_NEGATIVES." % p_pane_index) + if not p_bar_overlay_bindings.is_empty(): # All stacked bar series must share the same y axis. var first_y_axis_id := p_bar_overlay_bindings[0].y_axis_id @@ -95,7 +100,7 @@ class BarValidator extends RefCounted: pass # No mode-specific constraints _: - p_result.add_error("BarValidator: pane %d: unsupported bar mode %d" % [p_pane_index, int(p_bar_mode)]) + p_result.add_error("BarValidator: pane %d: unsupported bar mode %d" % [p_pane_index, p_bar_config.mode]) static func _validate_bar_width_config(p_pane_index: int, p_x_axis_cfg: TauAxisConfig, p_bar_config: TauBarConfig, p_result: ValidationResult) -> void: @@ -112,17 +117,17 @@ class BarValidator extends RefCounted: TauBarConfig.BarWidthPolicy.THEME, TauBarConfig.BarWidthPolicy.CATEGORY_WIDTH_FRACTION: pass _: - p_result.add_error("BarValidator: pane %d: bar_width_policy %d is not allowed for CATEGORICAL x-axis" % [p_pane_index, int(p_policy)]) + p_result.add_error("BarValidator: pane %d: bar_width_policy %d is not allowed for CATEGORICAL x-axis" % [p_pane_index, p_policy]) TauAxisConfig.Type.CONTINUOUS: match p_policy: TauBarConfig.BarWidthPolicy.THEME, TauBarConfig.BarWidthPolicy.DATA_UNITS, TauBarConfig.BarWidthPolicy.NEIGHBOR_SPACING_FRACTION: pass _: - p_result.add_error("BarValidator: pane %d: bar_width_policy %d is not allowed for CONTINUOUS x-axis" % [p_pane_index, int(p_policy)]) + p_result.add_error("BarValidator: pane %d: bar_width_policy %d is not allowed for CONTINUOUS x-axis" % [p_pane_index, p_policy]) _: - p_result.add_error("BarValidator: pane %d: unexpected x-axis type %d" % [p_pane_index, int(p_x_axis_type)]) + p_result.add_error("BarValidator: pane %d: unexpected x-axis type %d" % [p_pane_index, p_x_axis_type]) static func _validate_bar_width_policy_params(p_pane_index: int, p_x_axis_scale: TauAxisConfig.Scale, p_policy: TauBarConfig.BarWidthPolicy, p_bar_config: TauBarConfig, p_result: ValidationResult) -> void: @@ -163,4 +168,4 @@ class BarValidator extends RefCounted: p_result.add_error("BarValidator: pane %d: neighbor_gap_fraction must be >= 0 (got %f)" % [p_pane_index, p_bar_config.neighbor_gap_fraction]) _: - p_result.add_error("BarValidator: pane %d: unsupported bar_width_policy %d" % [p_pane_index, int(p_policy)]) + p_result.add_error("BarValidator: pane %d: unsupported bar_width_policy %d" % [p_pane_index, p_policy]) diff --git a/addons/tau-plot/plot/xy/line/line_config.gd b/addons/tau-plot/plot/xy/line/line_config.gd index 2424ad4..e9e362b 100644 --- a/addons/tau-plot/plot/xy/line/line_config.gd +++ b/addons/tau-plot/plot/xy/line/line_config.gd @@ -23,6 +23,15 @@ enum LineMode const StackedNormalization = preload("res://addons/tau-plot/plot/xy/stacked_normalization.gd").StackedNormalization @export var stacked_normalization: StackedNormalization = StackedNormalization.NONE +const StackedNegativePolicy = preload("res://addons/tau-plot/plot/xy/stacked_negative_policy.gd").StackedNegativePolicy + +## How negative values are handled in STACKED mode. +## SIGNED_SUM (default) folds negative values into the cumulative as a downward +## dip, the streamgraph behavior. DIVERGING splits each X into an upper stack +## of positive values and a lower stack of negative values, both anchored at +## zero. SKIP_NEGATIVES drops negative samples entirely from the stack. +@export var stacked_negative_policy: StackedNegativePolicy = StackedNegativePolicy.SIGNED_SUM + ## Strategy applied when the curve encounters a sample with a NaN or infinite ## X or Y value (or a value forbidden by the active axis scale, such as a ## non-positive value on a logarithmic axis). @@ -115,6 +124,8 @@ func is_equal_to(p_other: TauPaneOverlayConfig) -> bool: return false if stacked_normalization != other.stacked_normalization: return false + if stacked_negative_policy != other.stacked_negative_policy: + return false if gap_policy != other.gap_policy: return false if interpolation_mode != other.interpolation_mode: @@ -128,8 +139,10 @@ func is_equal_to(p_other: TauPaneOverlayConfig) -> bool: # Returns true if the change between this and p_other affects layout/domain. # Returns false if the change only affects visual appearance. # -# Only mode and stacked_normalization affect the domain: stacking changes the -# Y bounds. Hover distance is a pure hit-test parameter with no layout effect. +# mode, stacked_normalization, and stacked_negative_policy affect the domain: +# stacking changes Y bounds, normalization pins the range, and the negative +# policy decides whether the lower half-axis exists. Hover distance is a pure +# hit-test parameter with no layout effect. func has_layout_affecting_change(p_other: TauPaneOverlayConfig) -> bool: var other := p_other as TauLineConfig if other == null: @@ -144,4 +157,7 @@ func has_layout_affecting_change(p_other: TauPaneOverlayConfig) -> bool: if mode == LineMode.STACKED and stacked_normalization != other.stacked_normalization: return true + if mode == LineMode.STACKED and stacked_negative_policy != other.stacked_negative_policy: + return true + return false diff --git a/addons/tau-plot/plot/xy/stacked_negative_policy.gd b/addons/tau-plot/plot/xy/stacked_negative_policy.gd new file mode 100644 index 0000000..e81be3a --- /dev/null +++ b/addons/tau-plot/plot/xy/stacked_negative_policy.gd @@ -0,0 +1,12 @@ +enum StackedNegativePolicy +{ + DIVERGING, ## Positive values stack upward from zero, negative values stack + ## downward from zero. Two independent cumulatives per X. + + SIGNED_SUM, ## Negative values are summed signed into the cumulative. + ## A negative contribution causes the cumulative to dip below + ## the previous layer. + + SKIP_NEGATIVES ## Negative values are dropped from the cumulative. The sample + ## is treated as if its value were zero for stacking purposes. +} diff --git a/addons/tau-plot/plot/xy/stacked_negative_policy.gd.uid b/addons/tau-plot/plot/xy/stacked_negative_policy.gd.uid new file mode 100644 index 0000000..2c351bf --- /dev/null +++ b/addons/tau-plot/plot/xy/stacked_negative_policy.gd.uid @@ -0,0 +1 @@ +uid://bbq86jgmtfkyu diff --git a/addons/tau-plot/plot/xy/stacked_pinned_range.gd b/addons/tau-plot/plot/xy/stacked_pinned_range.gd new file mode 100644 index 0000000..5c7cffc --- /dev/null +++ b/addons/tau-plot/plot/xy/stacked_pinned_range.gd @@ -0,0 +1,31 @@ +const StackedNormalization = preload("res://addons/tau-plot/plot/xy/stacked_normalization.gd").StackedNormalization +const StackedNegativePolicy = preload("res://addons/tau-plot/plot/xy/stacked_negative_policy.gd").StackedNegativePolicy + + +## Computes the pinned Y axis range that a stacked overlay imposes on its +## target axis, given the active normalization and negative policy. +## +## NONE normalization does not pin the range: the axis is computed from the +## stacked data itself by the domain scanner. Returns Vector2.ZERO and the +## caller is expected to ignore the result when normalization is NONE. +## +## FRACTION and PERCENT pin the range so that every per-X stack fits exactly: +## SKIP_NEGATIVES yields [0, 1] / [0, 100], the diverging and signed_sum +## policies need both half-axes and yield [-1, 1] / [-100, 100]. +class StackedPinnedRange extends RefCounted: + + ## Returns the (min, max) pinned range. Caller decides whether to apply it + ## based on normalization (NONE means no pinning). + static func compute(p_normalization: StackedNormalization, p_policy: StackedNegativePolicy) -> Vector2: + match p_normalization: + StackedNormalization.FRACTION: + if p_policy == StackedNegativePolicy.SKIP_NEGATIVES: + return Vector2(0.0, 1.0) + return Vector2(-1.0, 1.0) + + StackedNormalization.PERCENT: + if p_policy == StackedNegativePolicy.SKIP_NEGATIVES: + return Vector2(0.0, 100.0) + return Vector2(-100.0, 100.0) + + return Vector2.ZERO diff --git a/addons/tau-plot/plot/xy/stacked_pinned_range.gd.uid b/addons/tau-plot/plot/xy/stacked_pinned_range.gd.uid new file mode 100644 index 0000000..8a3faa2 --- /dev/null +++ b/addons/tau-plot/plot/xy/stacked_pinned_range.gd.uid @@ -0,0 +1 @@ +uid://dtqwlm0r8gpw7 diff --git a/addons/tau-plot/plot/xy/xy_plot.gd b/addons/tau-plot/plot/xy/xy_plot.gd index 3df5348..8e99409 100644 --- a/addons/tau-plot/plot/xy/xy_plot.gd +++ b/addons/tau-plot/plot/xy/xy_plot.gd @@ -23,6 +23,7 @@ const XYState := preload("res://addons/tau-plot/plot/xy/xy_state.gd").XYState const XYDomain := preload("res://addons/tau-plot/plot/xy/xy_domain.gd").XYDomain const XYDomainOverrides := preload("res://addons/tau-plot/plot/xy/xy_domain_overrides.gd").XYDomainOverrides const YDomainOverride := preload("res://addons/tau-plot/plot/xy/xy_domain_overrides.gd").YDomainOverride +const StackedPinnedRange := preload("res://addons/tau-plot/plot/xy/stacked_pinned_range.gd").StackedPinnedRange const XYLayout := preload("res://addons/tau-plot/plot/xy/xy_layout.gd").XYLayout const XYAxisTitleLayout := preload("res://addons/tau-plot/plot/xy/xy_axis_title_layout.gd").XYAxisTitleLayout const VisualAttributes = preload("res://addons/tau-plot/plot/xy/visual_attributes.gd").VisualAttributes @@ -1184,15 +1185,11 @@ func _apply_bar_domain_overrides_y() -> void: TauBarConfig.StackedNormalization.NONE: y_domain_override.stack_y_values = true - TauBarConfig.StackedNormalization.FRACTION: + TauBarConfig.StackedNormalization.FRACTION, TauBarConfig.StackedNormalization.PERCENT: + var pinned := StackedPinnedRange.compute(pane_bar_config.stacked_normalization, pane_bar_config.stacked_negative_policy) y_domain_override.force_y_range = true - y_domain_override.force_y_min = 0.0 - y_domain_override.force_y_max = 1.0 - - TauBarConfig.StackedNormalization.PERCENT: - y_domain_override.force_y_range = true - y_domain_override.force_y_min = 0.0 - y_domain_override.force_y_max = 100.0 + y_domain_override.force_y_min = pinned.x + y_domain_override.force_y_max = pinned.y _: push_error("_apply_bar_domain_overrides_y(): unexpected stacked normalization") From 6f9576e4c0a8278fed35718b25ba97e43c267805 Mon Sep 17 00:00:00 2001 From: ze2j <46556066+ze2j@users.noreply.github.com> Date: Tue, 5 May 2026 23:02:56 +0200 Subject: [PATCH 18/23] Per-axis domain override storage --- .../plot/xy/dataset_change_analyzer.gd | 6 +-- addons/tau-plot/plot/xy/xy_domain.gd | 30 ++++++------ .../tau-plot/plot/xy/xy_domain_overrides.gd | 48 ++++++++++++++----- addons/tau-plot/plot/xy/xy_plot.gd | 9 ++-- 4 files changed, 57 insertions(+), 36 deletions(-) diff --git a/addons/tau-plot/plot/xy/dataset_change_analyzer.gd b/addons/tau-plot/plot/xy/dataset_change_analyzer.gd index ba8441b..9093652 100644 --- a/addons/tau-plot/plot/xy/dataset_change_analyzer.gd +++ b/addons/tau-plot/plot/xy/dataset_change_analyzer.gd @@ -113,9 +113,9 @@ class DatasetChangeAnalyzer extends RefCounted: if axis_domain.config != null and axis_domain.config.range_override_enabled: continue - # Bar stacking override (FRACTION/PERCENT) pins the range on the target axis. - var pane_override: YDomainOverride = p_domain_overrides.y_domain_overrides[pane_idx] - if pane_override.force_y_range and pane_override.target_y_axis_id == y_axis_id: + # A stacking override (FRACTION/PERCENT) on this axis pins the range. + var pane_override: YDomainOverride = p_domain_overrides.get_override(pane_idx, y_axis_id) + if pane_override != null and pane_override.force_y_range: continue # include_zero could shift bounds even if the new values are in range. diff --git a/addons/tau-plot/plot/xy/xy_domain.gd b/addons/tau-plot/plot/xy/xy_domain.gd index 3e6d82e..c1f6197 100644 --- a/addons/tau-plot/plot/xy/xy_domain.gd +++ b/addons/tau-plot/plot/xy/xy_domain.gd @@ -249,8 +249,8 @@ class XYDomain extends RefCounted: var y_range_forced := _is_y_range_forced(p_pane_idx, y_axis_id) if y_range_forced: - y_axis_domain.min_val = _get_forced_y_min(p_pane_idx) - y_axis_domain.max_val = _get_forced_y_max(p_pane_idx) + y_axis_domain.min_val = _get_forced_y_min(p_pane_idx, y_axis_id) + y_axis_domain.max_val = _get_forced_y_max(p_pane_idx, y_axis_id) else: var final_range := _finalize_y_axis_domain(y_axis_domain) y_axis_domain.min_val = final_range.x @@ -356,35 +356,37 @@ class XYDomain extends RefCounted: func _is_y_range_forced(p_pane_index: int, p_y_axis_id: AxisId) -> bool: if overrides == null: return false - if p_pane_index < 0 or p_pane_index >= overrides.y_domain_overrides.size(): + var ydo := overrides.get_override(p_pane_index, p_y_axis_id) + if ydo == null: return false - var ydo := overrides.y_domain_overrides[p_pane_index] - return ydo.force_y_range and ydo.target_y_axis_id == p_y_axis_id + return ydo.force_y_range - func _get_forced_y_min(p_pane_index: int) -> float: + func _get_forced_y_min(p_pane_index: int, p_y_axis_id: AxisId) -> float: if overrides == null: return 0.0 - if p_pane_index < 0 or p_pane_index >= overrides.y_domain_overrides.size(): + var ydo := overrides.get_override(p_pane_index, p_y_axis_id) + if ydo == null: return 0.0 - return overrides.y_domain_overrides[p_pane_index].force_y_min + return ydo.force_y_min - func _get_forced_y_max(p_pane_index: int) -> float: + func _get_forced_y_max(p_pane_index: int, p_y_axis_id: AxisId) -> float: if overrides == null: return 1.0 - if p_pane_index < 0 or p_pane_index >= overrides.y_domain_overrides.size(): + var ydo := overrides.get_override(p_pane_index, p_y_axis_id) + if ydo == null: return 1.0 - return overrides.y_domain_overrides[p_pane_index].force_y_max + return ydo.force_y_max func _must_stack_y_values(p_pane_index: int, p_y_axis_id: AxisId) -> bool: if overrides == null: return false - if p_pane_index < 0 or p_pane_index >= overrides.y_domain_overrides.size(): + var ydo := overrides.get_override(p_pane_index, p_y_axis_id) + if ydo == null: return false - var ydo := overrides.y_domain_overrides[p_pane_index] - return ydo.stack_y_values and ydo.target_y_axis_id == p_y_axis_id + return ydo.stack_y_values func _scan_series_y_range(p_dataset: Dataset, p_y_axis_series_ids: PackedInt64Array, p_pane_idx: int, p_y_axis_id: AxisId, p_is_log: bool) -> Vector2: diff --git a/addons/tau-plot/plot/xy/xy_domain_overrides.gd b/addons/tau-plot/plot/xy/xy_domain_overrides.gd index 9242ca3..ea73630 100644 --- a/addons/tau-plot/plot/xy/xy_domain_overrides.gd +++ b/addons/tau-plot/plot/xy/xy_domain_overrides.gd @@ -1,10 +1,8 @@ -class YDomainOverride extends RefCounted: - const AxisId = preload("res://addons/tau-plot/plot/xy/xy_axes.gd").AxisId +const AxisId = preload("res://addons/tau-plot/plot/xy/xy_axes.gd").AxisId + - # The y-axis that the override targets. Only that axis is affected by - # force_y_range and stack_y_values. A value of -1 means "no axis targeted" - # (override is inactive regardless of the flags above). - var target_y_axis_id: int = -1 +# Stacking and forced-range override for one (pane, y-axis) pair. +class YDomainOverride extends RefCounted: var force_y_range: bool = false var force_y_min: float = 0.0 @@ -14,23 +12,47 @@ class YDomainOverride extends RefCounted: func reset() -> void: - target_y_axis_id = -1 force_y_range = false force_y_min = 0.0 force_y_max = 1.0 stack_y_values = false -# XY plot domain overrides driven by renderers. -# Y axis overrides are per-pane. +# Per-pane and per-axis Y domain overrides driven by renderers. class XYDomainOverrides extends RefCounted: - var y_domain_overrides: Array[YDomainOverride] = [] # Per-pane Y axis overrides. + var y_domain_overrides: Array = [] # FIXME Real type is Array[Dictionary[AxisId, YDomainOverride]. Godot 4.5 does not support nested typed collections. - # Initializes per-pane override storage. Existing entries are preserved up to - # p_pane_count. Excess entries are trimmed and missing entries are appended. + + # Resizes per-pane storage. Existing entries below p_pane_count are kept. func init_panes(p_pane_count: int) -> void: while y_domain_overrides.size() > p_pane_count: y_domain_overrides.pop_back() while y_domain_overrides.size() < p_pane_count: - y_domain_overrides.append(YDomainOverride.new()) + var empty: Dictionary[AxisId, YDomainOverride] = {} + y_domain_overrides.append(empty) + + + # Returns null when no override exists for that (pane, axis). + func get_override(p_pane_index: int, p_y_axis_id: AxisId) -> YDomainOverride: + if p_pane_index < 0 or p_pane_index >= y_domain_overrides.size(): + return null + var pane_dict: Dictionary[AxisId, YDomainOverride] = y_domain_overrides[p_pane_index] + if p_y_axis_id in pane_dict: + return pane_dict[p_y_axis_id] + return null + + + func get_or_create_override(p_pane_index: int, p_y_axis_id: AxisId) -> YDomainOverride: + var pane_dict: Dictionary[AxisId, YDomainOverride] = y_domain_overrides[p_pane_index] + if p_y_axis_id in pane_dict: + return pane_dict[p_y_axis_id] + var override := YDomainOverride.new() + pane_dict[p_y_axis_id] = override + return override + + + func clear_pane(p_pane_index: int) -> void: + if p_pane_index < 0 or p_pane_index >= y_domain_overrides.size(): + return + y_domain_overrides[p_pane_index].clear() diff --git a/addons/tau-plot/plot/xy/xy_plot.gd b/addons/tau-plot/plot/xy/xy_plot.gd index 8e99409..66abd8f 100644 --- a/addons/tau-plot/plot/xy/xy_plot.gd +++ b/addons/tau-plot/plot/xy/xy_plot.gd @@ -1148,6 +1148,8 @@ func _update_xy_layout(p_pane_view_rects: Array[Rect2], p_pane_positions: Array[ func _apply_bar_domain_overrides_y() -> void: var pane_count := _domain_config.panes.size() for pane_index in range(pane_count): + _xy_domain_overrides.clear_pane(pane_index) + if pane_index >= _bar_series_ids_per_pane.size(): continue if _bar_series_ids_per_pane[pane_index].is_empty(): @@ -1157,11 +1159,6 @@ func _apply_bar_domain_overrides_y() -> void: if pane_bar_config == null: continue - var y_domain_override: YDomainOverride = _xy_domain_overrides.y_domain_overrides[pane_index] - - # Reset vertical overrides each recompute then re-apply if needed. - y_domain_override.reset() - if pane_bar_config.mode != TauBarConfig.BarMode.STACKED: continue @@ -1179,7 +1176,7 @@ func _apply_bar_domain_overrides_y() -> void: if stacked_y_cfg != null and stacked_y_cfg.range_override_enabled: continue - y_domain_override.target_y_axis_id = stacked_y_axis_id + var y_domain_override: YDomainOverride = _xy_domain_overrides.get_or_create_override(pane_index, stacked_y_axis_id) match pane_bar_config.stacked_normalization: TauBarConfig.StackedNormalization.NONE: From 70a83cffbb3fcf72c0a572ebd1b921542847a9e6 Mon Sep 17 00:00:00 2001 From: ze2j <46556066+ze2j@users.noreply.github.com> Date: Wed, 6 May 2026 07:13:22 +0100 Subject: [PATCH 19/23] Fix domain computation of stacked series --- addons/tau-plot/plot/xy/xy_domain.gd | 190 ++++++++++++++---- .../tau-plot/plot/xy/xy_domain_overrides.gd | 21 +- addons/tau-plot/plot/xy/xy_plot.gd | 9 +- 3 files changed, 175 insertions(+), 45 deletions(-) diff --git a/addons/tau-plot/plot/xy/xy_domain.gd b/addons/tau-plot/plot/xy/xy_domain.gd index c1f6197..4918d48 100644 --- a/addons/tau-plot/plot/xy/xy_domain.gd +++ b/addons/tau-plot/plot/xy/xy_domain.gd @@ -5,6 +5,7 @@ const XYDomainOverrides := preload("res://addons/tau-plot/plot/xy/xy_domain_over const SeriesAxisAssignment := preload("res://addons/tau-plot/plot/xy/series_axis_assignment.gd").SeriesAxisAssignment const AxisId = preload("res://addons/tau-plot/plot/xy/xy_axes.gd").AxisId const Axis = preload("res://addons/tau-plot/plot/xy/xy_axes.gd").Axis +const StackedNegativePolicy = preload("res://addons/tau-plot/plot/xy/stacked_negative_policy.gd").StackedNegativePolicy # Resolved domain for a single axis. @@ -66,6 +67,10 @@ class XYDomain extends RefCounted: var overrides: XYDomainOverrides = null var series_assignment: SeriesAxisAssignment = null + # Per-pane series ids. + var bar_series_ids_per_pane: Array[PackedInt64Array] = [] + var line_series_ids_per_pane: Array[PackedInt64Array] = [] + # Outputs var x_axis_domain: AxisDomain = AxisDomain.new() var x_categories: PackedStringArray = [] @@ -77,10 +82,15 @@ class XYDomain extends RefCounted: const _LOG_MIN_DOMAIN_RATIO: float = 1.1 # Minimum ratio between max/min for log scales - func _init(p_dataset: Dataset, p_config: TauXYConfig, p_series_assignment: SeriesAxisAssignment, p_overrides: XYDomainOverrides = null) -> void: + func _init(p_dataset: Dataset, p_config: TauXYConfig, p_series_assignment: SeriesAxisAssignment, + p_bar_series_ids_per_pane: Array[PackedInt64Array], + p_line_series_ids_per_pane: Array[PackedInt64Array], + p_overrides: XYDomainOverrides) -> void: config = p_config overrides = p_overrides series_assignment = p_series_assignment + bar_series_ids_per_pane = p_bar_series_ids_per_pane + line_series_ids_per_pane = p_line_series_ids_per_pane update_from_dataset(p_dataset) @@ -354,8 +364,6 @@ class XYDomain extends RefCounted: func _is_y_range_forced(p_pane_index: int, p_y_axis_id: AxisId) -> bool: - if overrides == null: - return false var ydo := overrides.get_override(p_pane_index, p_y_axis_id) if ydo == null: return false @@ -363,8 +371,6 @@ class XYDomain extends RefCounted: func _get_forced_y_min(p_pane_index: int, p_y_axis_id: AxisId) -> float: - if overrides == null: - return 0.0 var ydo := overrides.get_override(p_pane_index, p_y_axis_id) if ydo == null: return 0.0 @@ -372,37 +378,50 @@ class XYDomain extends RefCounted: func _get_forced_y_max(p_pane_index: int, p_y_axis_id: AxisId) -> float: - if overrides == null: - return 1.0 var ydo := overrides.get_override(p_pane_index, p_y_axis_id) if ydo == null: return 1.0 return ydo.force_y_max - func _must_stack_y_values(p_pane_index: int, p_y_axis_id: AxisId) -> bool: - if overrides == null: - return false - var ydo := overrides.get_override(p_pane_index, p_y_axis_id) - if ydo == null: - return false - return ydo.stack_y_values + # Series on the axis are partitioned into three groups: bar-stacked, + # line-stacked, and non-stacked. Each group contributes a Y range computed + # in isolation, and the axis range is the union of the three. When two + # stacked overlay types target the same axis the validator guarantees they + # share normalization and negative policy. + func _scan_series_y_range(p_dataset: Dataset, p_y_axis_series_ids: PackedInt64Array, p_pane_idx: int, p_y_axis_id: AxisId, p_is_log: bool) -> Vector2: + var ydo: YDomainOverride = overrides.get_override(p_pane_idx, p_y_axis_id) + var bar_partition: PackedInt64Array + var line_partition: PackedInt64Array + if ydo != null and ydo.bar_stack_active: + bar_partition = _intersect_with_pane_partition(p_y_axis_series_ids, bar_series_ids_per_pane, p_pane_idx) + if ydo != null and ydo.line_stack_active: + line_partition = _intersect_with_pane_partition(p_y_axis_series_ids, line_series_ids_per_pane, p_pane_idx) - func _scan_series_y_range(p_dataset: Dataset, p_y_axis_series_ids: PackedInt64Array, p_pane_idx: int, p_y_axis_id: AxisId, p_is_log: bool) -> Vector2: + var non_stacked := _subtract_partitions(p_y_axis_series_ids, bar_partition, line_partition) + + var range_acc := Vector2(INF, -INF) + range_acc = _union_range(range_acc, _scan_independent(p_dataset, non_stacked, p_is_log)) + + if not bar_partition.is_empty(): + range_acc = _union_range(range_acc, _scan_stacked(p_dataset, bar_partition, ydo.stacked_negative_policy, p_is_log)) + if not line_partition.is_empty(): + range_acc = _union_range(range_acc, _scan_stacked(p_dataset, line_partition, ydo.stacked_negative_policy, p_is_log)) + + return range_acc + + + # Independent (per-series) min/max scan: each sample contributes on its own. + # Honors the per-dataset-mode iteration shape. + func _scan_independent(p_dataset: Dataset, p_series_ids: PackedInt64Array, p_is_log: bool) -> Vector2: var y_min := INF var y_max := -INF match p_dataset.get_mode(): Dataset.Mode.SHARED_X: - for series_id in p_y_axis_series_ids: - if not p_dataset.has_series(series_id): - continue - - if _must_stack_y_values(p_pane_idx, p_y_axis_id): - return _scan_stacked_series_y_range(p_dataset, p_y_axis_series_ids, p_is_log) - - var sample_count := p_dataset.get_shared_sample_count() + var sample_count := p_dataset.get_shared_sample_count() + for series_id in p_series_ids: for sample_index in range(sample_count): var y_value := p_dataset.get_series_y(series_id, sample_index) if is_nan(y_value) or is_inf(y_value): @@ -413,10 +432,7 @@ class XYDomain extends RefCounted: y_max = maxf(y_max, y_value) Dataset.Mode.PER_SERIES_X: - for series_id in p_y_axis_series_ids: - if not p_dataset.has_series(series_id): - continue - + for series_id in p_series_ids: var sample_count := p_dataset.get_series_sample_count(series_id) for sample_index in range(sample_count): var y_value := p_dataset.get_series_y(series_id, sample_index) @@ -430,29 +446,133 @@ class XYDomain extends RefCounted: return Vector2(y_min, y_max) - func _scan_stacked_series_y_range(p_dataset: Dataset, p_y_axis_series_ids: PackedInt64Array, p_is_log: bool) -> Vector2: + func _scan_stacked(p_dataset: Dataset, p_series_ids: PackedInt64Array, p_policy: StackedNegativePolicy, p_is_log: bool) -> Vector2: + match p_policy: + StackedNegativePolicy.SKIP_NEGATIVES: + return _scan_stacked_skip_negatives(p_dataset, p_series_ids, p_policy, p_is_log) + + StackedNegativePolicy.DIVERGING: + return _scan_stacked_diverging(p_dataset, p_series_ids, p_policy, p_is_log) + + StackedNegativePolicy.SIGNED_SUM: + return _scan_stacked_signed_sum(p_dataset, p_series_ids, p_policy, p_is_log) + + _: + push_error("Unexpected StackedNegativePolicy: %d" % p_policy) + return Vector2() + + + func _scan_stacked_skip_negatives(p_dataset: Dataset, p_series_ids: PackedInt64Array, p_policy: StackedNegativePolicy, p_is_log: bool) -> Vector2: + var y_min := INF + var y_max := -INF + var sample_count := p_dataset.get_shared_sample_count() + + for sample_index in range(sample_count): + var pos_total := 0.0 + + for series_id in p_series_ids: + var y_value := p_dataset.get_series_y(series_id, sample_index) + if is_nan(y_value) or is_inf(y_value): + continue + if p_is_log and y_value <= 0.0: + continue + + # SKIP_NEGATIVES + if y_value >= 0.0: + pos_total += y_value + + # SKIP_NEGATIVES + y_min = minf(y_min, 0.0) + y_max = maxf(y_max, pos_total) + + return Vector2(y_min, y_max) + + + func _scan_stacked_diverging(p_dataset: Dataset, p_series_ids: PackedInt64Array, p_policy: StackedNegativePolicy, p_is_log: bool) -> Vector2: var y_min := INF var y_max := -INF var sample_count := p_dataset.get_shared_sample_count() for sample_index in range(sample_count): - var sum := 0.0 - for series_id in p_y_axis_series_ids: - if not p_dataset.has_series(series_id): + var pos_total := 0.0 + var neg_total := 0.0 + + for series_id in p_series_ids: + var y_value := p_dataset.get_series_y(series_id, sample_index) + if is_nan(y_value) or is_inf(y_value): + continue + if p_is_log and y_value <= 0.0: continue + # DIVERGING + if y_value >= 0.0: + pos_total += y_value + else: + neg_total += y_value + + # DIVERGING + y_min = minf(y_min, neg_total) + y_max = maxf(y_max, pos_total) + + return Vector2(y_min, y_max) + + + func _scan_stacked_signed_sum(p_dataset: Dataset, p_series_ids: PackedInt64Array, p_policy: StackedNegativePolicy, p_is_log: bool) -> Vector2: + var y_min := INF + var y_max := -INF + var sample_count := p_dataset.get_shared_sample_count() + + for sample_index in range(sample_count): + var signed_running := 0.0 + var signed_min_at_x := INF + var signed_max_at_x := -INF + + for series_id in p_series_ids: var y_value := p_dataset.get_series_y(series_id, sample_index) if is_nan(y_value) or is_inf(y_value): continue if p_is_log and y_value <= 0.0: continue - sum += y_value - y_min = minf(y_min, sum) - y_max = maxf(y_max, sum) + + # SIGNED_SUM + signed_running += y_value + signed_min_at_x = minf(signed_min_at_x, signed_running) + signed_max_at_x = maxf(signed_max_at_x, signed_running) + + # SIGNED_SUM + if signed_min_at_x != INF: + y_min = minf(y_min, signed_min_at_x) + y_max = maxf(y_max, signed_max_at_x) return Vector2(y_min, y_max) + static func _intersect_with_pane_partition(p_axis_series_ids: PackedInt64Array, + p_partition_per_pane: Array[PackedInt64Array], p_pane_idx: int) -> PackedInt64Array: + var out := PackedInt64Array() + if p_pane_idx >= p_partition_per_pane.size(): + return out + var partition: PackedInt64Array = p_partition_per_pane[p_pane_idx] + for sid in p_axis_series_ids: + if sid in partition: + out.append(sid) + return out + + + static func _subtract_partitions(p_axis_series_ids: PackedInt64Array, + p_a: PackedInt64Array, p_b: PackedInt64Array) -> PackedInt64Array: + var out := PackedInt64Array() + for sid in p_axis_series_ids: + if sid in p_a or sid in p_b: + continue + out.append(sid) + return out + + + static func _union_range(p_a: Vector2, p_b: Vector2) -> Vector2: + return Vector2(minf(p_a.x, p_b.x), maxf(p_a.y, p_b.y)) + + func _compute_continuous_shared_x(p_dataset: Dataset, p_is_log: bool) -> Vector2: var xmin := INF var xmax := -INF @@ -473,8 +593,6 @@ class XYDomain extends RefCounted: var xmax := -INF for series_id_v in p_series_ids: var series_id := series_id_v - if not p_dataset.has_series(series_id): - continue var sample_count := p_dataset.get_series_sample_count(series_id) for sample_index in range(sample_count): var x_value := float(p_dataset.get_series_x(series_id, sample_index)) diff --git a/addons/tau-plot/plot/xy/xy_domain_overrides.gd b/addons/tau-plot/plot/xy/xy_domain_overrides.gd index ea73630..8224277 100644 --- a/addons/tau-plot/plot/xy/xy_domain_overrides.gd +++ b/addons/tau-plot/plot/xy/xy_domain_overrides.gd @@ -1,21 +1,28 @@ const AxisId = preload("res://addons/tau-plot/plot/xy/xy_axes.gd").AxisId +const StackedNormalization = preload("res://addons/tau-plot/plot/xy/stacked_normalization.gd").StackedNormalization +const StackedNegativePolicy = preload("res://addons/tau-plot/plot/xy/stacked_negative_policy.gd").StackedNegativePolicy # Stacking and forced-range override for one (pane, y-axis) pair. +# +# bar_stack_active and line_stack_active are independent flags: each tells the +# domain scanner that the corresponding overlay type contributes a stacked +# cumulative on this axis. Both can be true at the same time, in which case +# their cumulatives are computed separately and unioned with the non-stacked +# range. Cross-overlay validation enforces a single shared +# stacked_normalization and a single shared stacked_negative_policy on the +# axis when both flags are true. class YDomainOverride extends RefCounted: var force_y_range: bool = false var force_y_min: float = 0.0 var force_y_max: float = 1.0 - var stack_y_values: bool = false + var bar_stack_active: bool = false + var line_stack_active: bool = false - - func reset() -> void: - force_y_range = false - force_y_min = 0.0 - force_y_max = 1.0 - stack_y_values = false + var stacked_normalization: StackedNormalization = StackedNormalization.NONE + var stacked_negative_policy: StackedNegativePolicy = StackedNegativePolicy.DIVERGING # Per-pane and per-axis Y domain overrides driven by renderers. diff --git a/addons/tau-plot/plot/xy/xy_plot.gd b/addons/tau-plot/plot/xy/xy_plot.gd index 66abd8f..ce28090 100644 --- a/addons/tau-plot/plot/xy/xy_plot.gd +++ b/addons/tau-plot/plot/xy/xy_plot.gd @@ -242,7 +242,8 @@ func setup( # Domain + layout creation _xy_domain_overrides = XYDomainOverrides.new() _xy_domain_overrides.init_panes(pane_count) - _xy_domain = XYDomain.new(_dataset, _domain_config, _series_assignment, _xy_domain_overrides) + _xy_domain = XYDomain.new(_dataset, _domain_config, _series_assignment, + _bar_series_ids_per_pane, _line_series_ids_per_pane, _xy_domain_overrides) _xy_layout = XYLayout.new(_xy_domain) # Connect style changed signals for programmatic mutation detection. @@ -1178,9 +1179,13 @@ func _apply_bar_domain_overrides_y() -> void: var y_domain_override: YDomainOverride = _xy_domain_overrides.get_or_create_override(pane_index, stacked_y_axis_id) + y_domain_override.bar_stack_active = true + y_domain_override.stacked_normalization = pane_bar_config.stacked_normalization + y_domain_override.stacked_negative_policy = pane_bar_config.stacked_negative_policy + match pane_bar_config.stacked_normalization: TauBarConfig.StackedNormalization.NONE: - y_domain_override.stack_y_values = true + pass TauBarConfig.StackedNormalization.FRACTION, TauBarConfig.StackedNormalization.PERCENT: var pinned := StackedPinnedRange.compute(pane_bar_config.stacked_normalization, pane_bar_config.stacked_negative_policy) From e965166a2361c3bd1dcb8e78761227c68d598927 Mon Sep 17 00:00:00 2001 From: ze2j <46556066+ze2j@users.noreply.github.com> Date: Wed, 6 May 2026 22:43:47 +0200 Subject: [PATCH 20/23] Validate stacked overlays --- .../tau-plot/plot/xy/line/line_validator.gd | 32 +++++++ addons/tau-plot/plot/xy/xy_plot_validator.gd | 84 +++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/addons/tau-plot/plot/xy/line/line_validator.gd b/addons/tau-plot/plot/xy/line/line_validator.gd index fc919d6..5488adc 100644 --- a/addons/tau-plot/plot/xy/line/line_validator.gd +++ b/addons/tau-plot/plot/xy/line/line_validator.gd @@ -1,6 +1,7 @@ # Dependencies const Dataset := preload("res://addons/tau-plot/model/dataset.gd").Dataset const PaneOverlayType = preload("res://addons/tau-plot/plot/xy/pane_overlay_type.gd").PaneOverlayType +const Axis = preload("res://addons/tau-plot/plot/xy/xy_axes.gd").Axis const LineVisualAttributes = preload("res://addons/tau-plot/plot/xy/line/line_visual_attributes.gd").LineVisualAttributes const LineVisualCallbacks = preload("res://addons/tau-plot/plot/xy/line/line_visual_callbacks.gd").LineVisualCallbacks const ValidationResult = preload("res://addons/tau-plot/plot/validation_result.gd").ValidationResult @@ -46,6 +47,9 @@ class LineValidator extends RefCounted: _validate_line_visuals(p_pane_index, line_config, p_line_overlay_bindings, p_result) + var is_shared_x := (p_dataset.get_mode() == Dataset.Mode.SHARED_X) + _validate_line_mode_constraints(p_pane_index, line_config, pane_cfg, p_line_overlay_bindings, is_shared_x, p_result) + #################################################################################################### # Private @@ -59,3 +63,31 @@ class LineValidator extends RefCounted: var binding: TauXYSeriesBinding = p_line_overlay_bindings[i] if binding.visual_attributes != null and binding.visual_attributes is not LineVisualAttributes: p_result.add_error("LineValidator: pane %d: series_id %d has visual_attributes that is not a LineVisualAttributes" % [p_pane_index, binding.series_id]) + + + static func _validate_line_mode_constraints(p_pane_index: int, p_line_config: TauLineConfig, p_pane_cfg: TauPaneConfig, p_line_overlay_bindings: Array[TauXYSeriesBinding], p_is_shared_x: bool, p_result: ValidationResult) -> void: + match p_line_config.mode: + TauLineConfig.LineMode.INDEPENDENT: + pass + + TauLineConfig.LineMode.STACKED: + # STACKED computes a per-X cumulative across series, so all series + # must share aligned X positions. + if not p_is_shared_x: + p_result.add_error("LineValidator: pane %d: STACKED mode requires SHARED_X dataset mode" % p_pane_index) + + if not p_line_overlay_bindings.is_empty(): + # All stacked line series must share the same y axis. + var first_y_axis_id := p_line_overlay_bindings[0].y_axis_id + for i in range(1, p_line_overlay_bindings.size()): + var binding: TauXYSeriesBinding = p_line_overlay_bindings[i] + if binding.y_axis_id != first_y_axis_id: + p_result.add_error("LineValidator: pane %d: STACKED mode requires all line series on the same y axis, but series_id %d uses %s (expected %s)" % [p_pane_index, binding.series_id, Axis.as_string(binding.y_axis_id), Axis.as_string(first_y_axis_id)]) + + # Cumulative sums on a logarithmic axis are not meaningful. + var y_axis_config: TauAxisConfig = p_pane_cfg.get_y_axis_config(first_y_axis_id) + if y_axis_config != null and y_axis_config.scale == TauAxisConfig.Scale.LOGARITHMIC: + p_result.add_error("LineValidator: pane %d: STACKED mode is incompatible with logarithmic y axis" % p_pane_index) + + _: + p_result.add_error("LineValidator: pane %d: unsupported line mode %d" % [p_pane_index, p_line_config.mode]) diff --git a/addons/tau-plot/plot/xy/xy_plot_validator.gd b/addons/tau-plot/plot/xy/xy_plot_validator.gd index 4b6ab72..e9ec14b 100644 --- a/addons/tau-plot/plot/xy/xy_plot_validator.gd +++ b/addons/tau-plot/plot/xy/xy_plot_validator.gd @@ -57,6 +57,14 @@ class XYPlotValidator extends RefCounted: if p_result.has_errors(): return false + # -- Cross-overlay stacking constraints -- + # When two stacked overlays share a Y axis in the same pane, their + # stacked normalization and negative policy must agree because both + # contribute to the same axis range computation. + _validate_cross_overlay_stacking(p_xy_config, p_series_bindings, p_result) + if p_result.has_errors(): + return false + # -- Warnings (never stop validation) -- _collect_warnings(p_xy_config, p_result) @@ -299,6 +307,82 @@ class XYPlotValidator extends RefCounted: LineValidator.validate(p_dataset, p_xy_config, pane_index, line_overlay_bindings, p_result) + #################################################################################################### + # Cross-overlay stacking constraints + #################################################################################################### + + ## Two stacked overlays sharing the same Y axis in the same pane both feed + ## the axis range computation. They must therefore agree on + ## [code]stacked_normalization[/code] (the pinned range vs data-derived + ## range cannot coexist) and on [code]stacked_negative_policy[/code] (the + ## range computation depends on a single coherent policy). + static func _validate_cross_overlay_stacking(p_xy_config: TauXYConfig, p_series_bindings: Array[TauXYSeriesBinding], p_result: ValidationResult) -> void: + # For each pane, resolve the y axis used by each stacked overlay. + # Only stacked overlays whose bindings are non-empty are considered: + # without bindings there is no axis to clash on. + var stacked_axis_by_pane_bar: Dictionary[int, AxisId] = {} + var stacked_axis_by_pane_line: Dictionary[int, AxisId] = {} + + for binding in p_series_bindings: + match binding.overlay_type: + TauXYSeriesBinding.PaneOverlayType.BAR: + if not stacked_axis_by_pane_bar.has(binding.pane_index): + var bar_cfg := _get_stacked_bar_config(p_xy_config, binding.pane_index) + if bar_cfg != null: + stacked_axis_by_pane_bar[binding.pane_index] = binding.y_axis_id + TauXYSeriesBinding.PaneOverlayType.LINE: + if not stacked_axis_by_pane_line.has(binding.pane_index): + var line_cfg := _get_stacked_line_config(p_xy_config, binding.pane_index) + if line_cfg != null: + stacked_axis_by_pane_line[binding.pane_index] = binding.y_axis_id + + # A clash exists when both a bar and a line stacked overlay land on the + # same axis in the same pane. The renderer-specific phase has already + # enforced "all stacked series of one overlay share a single axis", so + # the axis recorded above is unambiguous for each (pane, overlay) pair. + for pane_index in stacked_axis_by_pane_bar: + if not stacked_axis_by_pane_line.has(pane_index): + continue + var bar_axis: AxisId = stacked_axis_by_pane_bar[pane_index] + var line_axis: AxisId = stacked_axis_by_pane_line[pane_index] + if bar_axis != line_axis: + continue + + var pane_cfg: TauPaneConfig = p_xy_config.panes[pane_index] + var bar_cfg := pane_cfg.get_overlay_config(TauXYSeriesBinding.PaneOverlayType.BAR) as TauBarConfig + var line_cfg := pane_cfg.get_overlay_config(TauXYSeriesBinding.PaneOverlayType.LINE) as TauLineConfig + + if bar_cfg.stacked_normalization != line_cfg.stacked_normalization: + p_result.add_error("XYPlotValidator: pane %d, axis %s: stacked BAR and stacked LINE declare different stacked_normalization (BAR=%d, LINE=%d)" % [pane_index, Axis.as_string(bar_axis), int(bar_cfg.stacked_normalization), int(line_cfg.stacked_normalization)]) + + if bar_cfg.stacked_negative_policy != line_cfg.stacked_negative_policy: + p_result.add_error("XYPlotValidator: pane %d, axis %s: stacked BAR and stacked LINE declare different stacked_negative_policy (BAR=%d, LINE=%d)" % [pane_index, Axis.as_string(bar_axis), int(bar_cfg.stacked_negative_policy), int(line_cfg.stacked_negative_policy)]) + + + ## Returns the bar overlay config for the pane if it is in STACKED mode, + ## null otherwise. + static func _get_stacked_bar_config(p_xy_config: TauXYConfig, p_pane_index: int) -> TauBarConfig: + var pane_cfg: TauPaneConfig = p_xy_config.panes[p_pane_index] + var bar_cfg := pane_cfg.get_overlay_config(TauXYSeriesBinding.PaneOverlayType.BAR) as TauBarConfig + if bar_cfg == null: + return null + if bar_cfg.mode != TauBarConfig.BarMode.STACKED: + return null + return bar_cfg + + + ## Returns the line overlay config for the pane if it is in STACKED mode, + ## null otherwise. + static func _get_stacked_line_config(p_xy_config: TauXYConfig, p_pane_index: int) -> TauLineConfig: + var pane_cfg: TauPaneConfig = p_xy_config.panes[p_pane_index] + var line_cfg := pane_cfg.get_overlay_config(TauXYSeriesBinding.PaneOverlayType.LINE) as TauLineConfig + if line_cfg == null: + return null + if line_cfg.mode != TauLineConfig.LineMode.STACKED: + return null + return line_cfg + + #################################################################################################### # Warnings #################################################################################################### From a255422d5f50e13e1532aa26ef54ea8e99e03554 Mon Sep 17 00:00:00 2001 From: ze2j <46556066+ze2j@users.noreply.github.com> Date: Fri, 8 May 2026 12:40:02 +0200 Subject: [PATCH 21/23] Add tests for STACKED bars --- .../test_bar_stacked_negative_fraction.gd} | 152 ++--------- .../test_bar_stacked_negative_fraction.gd.uid | 1 + .../test_bar_stacked_negative_fraction.tscn} | 32 ++- .../stacked/test_bar_stacked_negative_none.gd | 253 ++++++++++++++++++ .../test_bar_stacked_negative_none.gd.uid | 1 + .../test_bar_stacked_negative_none.tscn | 83 ++++++ .../test_bar_stacked_negative_percent.gd | 253 ++++++++++++++++++ .../test_bar_stacked_negative_percent.gd.uid | 1 + .../test_bar_stacked_negative_percent.tscn | 83 ++++++ .../test_bar_stacking_order.gd | 0 .../test_bar_stacking_order.gd.uid | 0 .../test_bar_stacking_order.tscn | 2 +- .../test_bar_stacked_normalization.gd.uid | 1 - 13 files changed, 719 insertions(+), 143 deletions(-) rename addons/tau-plot/tests/bar/{stacked_normalization/test_bar_stacked_normalization.gd => stacked/test_bar_stacked_negative_fraction.gd} (60%) create mode 100644 addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_fraction.gd.uid rename addons/tau-plot/tests/bar/{stacked_normalization/test_bar_stacked_normalization.tscn => stacked/test_bar_stacked_negative_fraction.tscn} (69%) create mode 100644 addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_none.gd create mode 100644 addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_none.gd.uid create mode 100644 addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_none.tscn create mode 100644 addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_percent.gd create mode 100644 addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_percent.gd.uid create mode 100644 addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_percent.tscn rename addons/tau-plot/tests/bar/{stacking_order => stacked}/test_bar_stacking_order.gd (100%) rename addons/tau-plot/tests/bar/{stacking_order => stacked}/test_bar_stacking_order.gd.uid (100%) rename addons/tau-plot/tests/bar/{stacking_order => stacked}/test_bar_stacking_order.tscn (97%) delete mode 100644 addons/tau-plot/tests/bar/stacked_normalization/test_bar_stacked_normalization.gd.uid diff --git a/addons/tau-plot/tests/bar/stacked_normalization/test_bar_stacked_normalization.gd b/addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_fraction.gd similarity index 60% rename from addons/tau-plot/tests/bar/stacked_normalization/test_bar_stacked_normalization.gd rename to addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_fraction.gd index 784fd8b..1a1bcdd 100644 --- a/addons/tau-plot/tests/bar/stacked_normalization/test_bar_stacked_normalization.gd +++ b/addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_fraction.gd @@ -16,14 +16,14 @@ func _ready() -> void: func _setup_test_1() -> void: var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) - var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0]) - var y_a := PackedFloat64Array([5.0, 10.0, 15.0, 20.0]) - var y_b := PackedFloat64Array([25.0, 30.0, 35.0, 40.0]) - var y_c := PackedFloat64Array([30.0, 40.0, 50.0, 60.0]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0]) + var y_a := PackedFloat64Array([-5.0, -10.0, 15.0, -20.0, 20.0]) + var y_b := PackedFloat64Array([25.0, -30.0, -35.0, -40.0, 40.0]) + var y_c := PackedFloat64Array([30.0, 40.0, -50.0, -60.0, 60.0]) var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b, y_c]) - %TestPlot1.title = "[CONTINUOUS] with stacked_normalization = NONE" + %TestPlot1.title = "[CONTINUOUS] with stacked_negative_policy = SKIP_NEGATIVES" var x_axis := TauAxisConfig.new() x_axis.type = TauAxisConfig.Type.CONTINUOUS @@ -38,7 +38,8 @@ func _setup_test_1() -> void: var bar_config := TauBarConfig.new() bar_config.mode = TauBarConfig.BarMode.STACKED - bar_config.stacked_normalization = TauBarConfig.StackedNormalization.NONE + bar_config.stacked_normalization = TauBarConfig.StackedNormalization.FRACTION + bar_config.stacked_negative_policy = TauBarConfig.StackedNegativePolicy.SKIP_NEGATIVES var pane := TauPaneConfig.new() pane.y_left_axis = y_axis @@ -74,14 +75,14 @@ func _setup_test_1() -> void: func _setup_test_2() -> void: var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) - var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0]) - var y_a := PackedFloat64Array([5.0, 10.0, 15.0, 20.0]) - var y_b := PackedFloat64Array([25.0, 30.0, 35.0, 40.0]) - var y_c := PackedFloat64Array([30.0, 40.0, 50.0, 60.0]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0]) + var y_a := PackedFloat64Array([-5.0, -10.0, 15.0, -20.0, 20.0]) + var y_b := PackedFloat64Array([25.0, -30.0, -35.0, -40.0, 40.0]) + var y_c := PackedFloat64Array([30.0, 40.0, -50.0, -60.0, 60.0]) var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b, y_c]) - %TestPlot2.title = "[CONTINUOUS] with stacked_normalization = FRACTION" + %TestPlot2.title = "[CONTINUOUS] with stacked_negative_policy = DIVERGING" var x_axis := TauAxisConfig.new() x_axis.type = TauAxisConfig.Type.CONTINUOUS @@ -97,6 +98,7 @@ func _setup_test_2() -> void: var bar_config := TauBarConfig.new() bar_config.mode = TauBarConfig.BarMode.STACKED bar_config.stacked_normalization = TauBarConfig.StackedNormalization.FRACTION + bar_config.stacked_negative_policy = TauBarConfig.StackedNegativePolicy.DIVERGING var pane := TauPaneConfig.new() pane.y_left_axis = y_axis @@ -131,59 +133,7 @@ func _setup_test_2() -> void: #################################################################################################### func _setup_test_3() -> void: - var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) - var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0]) - var y_a := PackedFloat64Array([5.0, 10.0, 15.0, 20.0]) - var y_b := PackedFloat64Array([25.0, 30.0, 35.0, 40.0]) - var y_c := PackedFloat64Array([30.0, 40.0, 50.0, 60.0]) - - var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b, y_c]) - - %TestPlot3.title = "[CONTINUOUS] with stacked_normalization = PERCENT" - - var x_axis := TauAxisConfig.new() - x_axis.type = TauAxisConfig.Type.CONTINUOUS - x_axis.scale = TauAxisConfig.Scale.LINEAR - x_axis.domain_padding_mode = TauAxisConfig.DomainPaddingMode.DATA_UNITS - x_axis.domain_padding_min = 0.5 - x_axis.domain_padding_max = 0.5 - - var y_axis := TauAxisConfig.new() - y_axis.type = TauAxisConfig.Type.CONTINUOUS - y_axis.scale = TauAxisConfig.Scale.LINEAR - y_axis.format_tick_label = func(label: String) -> String: - return label + "%" - - var bar_config := TauBarConfig.new() - bar_config.mode = TauBarConfig.BarMode.STACKED - bar_config.stacked_normalization = TauBarConfig.StackedNormalization.PERCENT - - var pane := TauPaneConfig.new() - pane.y_left_axis = y_axis - pane.overlays = [bar_config] - - var config := TauXYConfig.new() - config.x_axis = x_axis - config.panes = [pane] - - var sb_a := TauXYSeriesBinding.new() - sb_a.series_id = dataset.get_series_id_by_index(0) - sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR - sb_a.y_axis_id = TauPlot.AxisId.LEFT - - var sb_b := TauXYSeriesBinding.new() - sb_b.series_id = dataset.get_series_id_by_index(1) - sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR - sb_b.y_axis_id = TauPlot.AxisId.LEFT - - var sb_c := TauXYSeriesBinding.new() - sb_c.series_id = dataset.get_series_id_by_index(2) - sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR - sb_c.y_axis_id = TauPlot.AxisId.LEFT - - var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] - - %TestPlot3.plot_xy(dataset, config, bindings) + pass #################################################################################################### # Test 4 @@ -191,14 +141,14 @@ func _setup_test_3() -> void: func _setup_test_4() -> void: var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) - var x := PackedStringArray(["One", "Two", "Three", "Four"]) - var y_a := PackedFloat64Array([5.0, 10.0, 15.0, 20.0]) - var y_b := PackedFloat64Array([25.0, 30.0, 35.0, 40.0]) - var y_c := PackedFloat64Array([30.0, 40.0, 50.0, 60.0]) + var x := PackedStringArray(["One", "Two", "Three", "Four", "Five"]) + var y_a := PackedFloat64Array([-5.0, -10.0, 15.0, -20.0, 20.0]) + var y_b := PackedFloat64Array([25.0, -30.0, -35.0, -40.0, 40.0]) + var y_c := PackedFloat64Array([30.0, 40.0, -50.0, -60.0, 60.0]) var dataset := TauPlot.Dataset.make_shared_x_categorical(series_names, x, [y_a, y_b, y_c]) - %TestPlot4.title = "[CATEGORICAL] with stacked_normalization = NONE" + %TestPlot4.title = "[CATEGORICAL] with stacked_negative_policy = SKIP_NEGATIVES" var x_axis := TauAxisConfig.new() x_axis.type = TauAxisConfig.Type.CATEGORICAL @@ -209,7 +159,8 @@ func _setup_test_4() -> void: var bar_config := TauBarConfig.new() bar_config.mode = TauBarConfig.BarMode.STACKED - bar_config.stacked_normalization = TauBarConfig.StackedNormalization.NONE + bar_config.stacked_normalization = TauBarConfig.StackedNormalization.FRACTION + bar_config.stacked_negative_policy = TauBarConfig.StackedNegativePolicy.SKIP_NEGATIVES var pane := TauPaneConfig.new() pane.y_left_axis = y_axis @@ -245,14 +196,14 @@ func _setup_test_4() -> void: func _setup_test_5() -> void: var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) - var x := PackedStringArray(["One", "Two", "Three", "Four"]) - var y_a := PackedFloat64Array([5.0, 10.0, 15.0, 20.0]) - var y_b := PackedFloat64Array([25.0, 30.0, 35.0, 40.0]) - var y_c := PackedFloat64Array([30.0, 40.0, 50.0, 60.0]) + var x := PackedStringArray(["One", "Two", "Three", "Four", "Five"]) + var y_a := PackedFloat64Array([-5.0, -10.0, 15.0, -20.0, 20.0]) + var y_b := PackedFloat64Array([25.0, -30.0, -35.0, -40.0, 40.0]) + var y_c := PackedFloat64Array([30.0, 40.0, -50.0, -60.0, 60.0]) var dataset := TauPlot.Dataset.make_shared_x_categorical(series_names, x, [y_a, y_b, y_c]) - %TestPlot5.title = "[CATEGORICAL] with stacked_normalization = FRACTION" + %TestPlot5.title = "[CATEGORICAL] with stacked_negative_policy = DIVERGING" var x_axis := TauAxisConfig.new() x_axis.type = TauAxisConfig.Type.CATEGORICAL @@ -264,6 +215,7 @@ func _setup_test_5() -> void: var bar_config := TauBarConfig.new() bar_config.mode = TauBarConfig.BarMode.STACKED bar_config.stacked_normalization = TauBarConfig.StackedNormalization.FRACTION + bar_config.stacked_negative_policy = TauBarConfig.StackedNegativePolicy.DIVERGING var pane := TauPaneConfig.new() pane.y_left_axis = y_axis @@ -298,52 +250,4 @@ func _setup_test_5() -> void: #################################################################################################### func _setup_test_6() -> void: - var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) - var x := PackedStringArray(["One", "Two", "Three", "Four"]) - var y_a := PackedFloat64Array([5.0, 10.0, 15.0, 20.0]) - var y_b := PackedFloat64Array([25.0, 30.0, 35.0, 40.0]) - var y_c := PackedFloat64Array([30.0, 40.0, 50.0, 60.0]) - - var dataset := TauPlot.Dataset.make_shared_x_categorical(series_names, x, [y_a, y_b, y_c]) - - %TestPlot6.title = "[CATEGORICAL] with stacked_normalization = PERCENT" - - var x_axis := TauAxisConfig.new() - x_axis.type = TauAxisConfig.Type.CATEGORICAL - - var y_axis := TauAxisConfig.new() - y_axis.type = TauAxisConfig.Type.CONTINUOUS - y_axis.scale = TauAxisConfig.Scale.LINEAR - y_axis.format_tick_label = func(label: String) -> String: - return label + "%" - - var bar_config := TauBarConfig.new() - bar_config.mode = TauBarConfig.BarMode.STACKED - bar_config.stacked_normalization = TauBarConfig.StackedNormalization.PERCENT - - var pane := TauPaneConfig.new() - pane.y_left_axis = y_axis - pane.overlays = [bar_config] - - var config := TauXYConfig.new() - config.x_axis = x_axis - config.panes = [pane] - - var sb_a := TauXYSeriesBinding.new() - sb_a.series_id = dataset.get_series_id_by_index(0) - sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR - sb_a.y_axis_id = TauPlot.AxisId.LEFT - - var sb_b := TauXYSeriesBinding.new() - sb_b.series_id = dataset.get_series_id_by_index(1) - sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR - sb_b.y_axis_id = TauPlot.AxisId.LEFT - - var sb_c := TauXYSeriesBinding.new() - sb_c.series_id = dataset.get_series_id_by_index(2) - sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR - sb_c.y_axis_id = TauPlot.AxisId.LEFT - - var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] - - %TestPlot6.plot_xy(dataset, config, bindings) + pass diff --git a/addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_fraction.gd.uid b/addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_fraction.gd.uid new file mode 100644 index 0000000..8491167 --- /dev/null +++ b/addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_fraction.gd.uid @@ -0,0 +1 @@ +uid://cupqyf70ivodk diff --git a/addons/tau-plot/tests/bar/stacked_normalization/test_bar_stacked_normalization.tscn b/addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_fraction.tscn similarity index 69% rename from addons/tau-plot/tests/bar/stacked_normalization/test_bar_stacked_normalization.tscn rename to addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_fraction.tscn index d2167ff..fec74e1 100644 --- a/addons/tau-plot/tests/bar/stacked_normalization/test_bar_stacked_normalization.tscn +++ b/addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_fraction.tscn @@ -1,7 +1,7 @@ -[gd_scene load_steps=3 format=3 uid="uid://b3ryyw2r4icth"] +[gd_scene load_steps=3 format=3 uid="uid://c4vb1a5ah2gsq"] -[ext_resource type="Script" uid="uid://bvr1lhw6xrucv" path="res://addons/tau-plot/tests/bar/stacked_normalization/test_bar_stacked_normalization.gd" id="1_13osl"] -[ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_t5ugu"] +[ext_resource type="Script" uid="uid://cupqyf70ivodk" path="res://addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_fraction.gd" id="1_ak25b"] +[ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_x74kx"] [node name="Bars" type="VBoxContainer"] anchors_preset = 15 @@ -9,12 +9,12 @@ anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 -script = ExtResource("1_13osl") +script = ExtResource("1_ak25b") [node name="Title" type="Label" parent="."] layout_mode = 2 theme_override_font_sizes/font_size = 32 -text = "Test stacked bars normalization" +text = "Test stacked bars negative policy with normalization = FRACTION" horizontal_alignment = 1 [node name="GridContainer" type="GridContainer" parent="."] @@ -30,8 +30,8 @@ layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 3 theme_type_variation = &"TauPlot" -script = ExtResource("2_t5ugu") -title = "[CONTINUOUS] with stacked_normalization = NONE" +script = ExtResource("2_x74kx") +title = "[CONTINUOUS] with stacked_negative_policy = SKIP_NEGATIVES" metadata/_custom_type_script = "uid://dnhvsf2wip771" [node name="TestPlot2" type="PanelContainer" parent="GridContainer"] @@ -40,8 +40,8 @@ layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 3 theme_type_variation = &"TauPlot" -script = ExtResource("2_t5ugu") -title = "[CONTINUOUS] with stacked_normalization = FRACTION" +script = ExtResource("2_x74kx") +title = "[CONTINUOUS] with stacked_negative_policy = DIVERGING" metadata/_custom_type_script = "uid://dnhvsf2wip771" [node name="TestPlot3" type="PanelContainer" parent="GridContainer"] @@ -50,8 +50,7 @@ layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 3 theme_type_variation = &"TauPlot" -script = ExtResource("2_t5ugu") -title = "[CONTINUOUS] with stacked_normalization = PERCENT" +script = ExtResource("2_x74kx") metadata/_custom_type_script = "uid://dnhvsf2wip771" [node name="TestPlot4" type="PanelContainer" parent="GridContainer"] @@ -60,8 +59,8 @@ layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 3 theme_type_variation = &"TauPlot" -script = ExtResource("2_t5ugu") -title = "[CATEGORICAL] with stacked_normalization = NONE" +script = ExtResource("2_x74kx") +title = "[CATEGORICAL] with stacked_negative_policy = SKIP_NEGATIVES" metadata/_custom_type_script = "uid://dnhvsf2wip771" [node name="TestPlot5" type="PanelContainer" parent="GridContainer"] @@ -70,8 +69,8 @@ layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 3 theme_type_variation = &"TauPlot" -script = ExtResource("2_t5ugu") -title = "[CATEGORICAL] with stacked_normalization = FRACTION" +script = ExtResource("2_x74kx") +title = "[CATEGORICAL] with stacked_negative_policy = DIVERGING" metadata/_custom_type_script = "uid://dnhvsf2wip771" [node name="TestPlot6" type="PanelContainer" parent="GridContainer"] @@ -80,6 +79,5 @@ layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 3 theme_type_variation = &"TauPlot" -script = ExtResource("2_t5ugu") -title = "[CATEGORICAL] with stacked_normalization = PERCENT" +script = ExtResource("2_x74kx") metadata/_custom_type_script = "uid://dnhvsf2wip771" diff --git a/addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_none.gd b/addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_none.gd new file mode 100644 index 0000000..6b6675e --- /dev/null +++ b/addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_none.gd @@ -0,0 +1,253 @@ +@tool +extends Control + +func _ready() -> void: + _setup_test_1() + _setup_test_2() + _setup_test_3() + _setup_test_4() + _setup_test_5() + _setup_test_6() + + +#################################################################################################### +# Test 1 +#################################################################################################### + +func _setup_test_1() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0]) + var y_a := PackedFloat64Array([-5.0, -10.0, 15.0, -20.0, 20.0]) + var y_b := PackedFloat64Array([25.0, -30.0, -35.0, -40.0, 40.0]) + var y_c := PackedFloat64Array([30.0, 40.0, -50.0, -60.0, 60.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b, y_c]) + + %TestPlot1.title = "[CONTINUOUS] with stacked_negative_policy = SKIP_NEGATIVES" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.domain_padding_mode = TauAxisConfig.DomainPaddingMode.DATA_UNITS + x_axis.domain_padding_min = 0.5 + x_axis.domain_padding_max = 0.5 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + + var bar_config := TauBarConfig.new() + bar_config.mode = TauBarConfig.BarMode.STACKED + bar_config.stacked_normalization = TauBarConfig.StackedNormalization.NONE + bar_config.stacked_negative_policy = TauBarConfig.StackedNegativePolicy.SKIP_NEGATIVES + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [bar_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] + + %TestPlot1.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 2 +#################################################################################################### + +func _setup_test_2() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0]) + var y_a := PackedFloat64Array([-5.0, -10.0, 15.0, -20.0, 20.0]) + var y_b := PackedFloat64Array([25.0, -30.0, -35.0, -40.0, 40.0]) + var y_c := PackedFloat64Array([30.0, 40.0, -50.0, -60.0, 60.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b, y_c]) + + %TestPlot2.title = "[CONTINUOUS] with stacked_negative_policy = DIVERGING" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.domain_padding_mode = TauAxisConfig.DomainPaddingMode.DATA_UNITS + x_axis.domain_padding_min = 0.5 + x_axis.domain_padding_max = 0.5 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + + var bar_config := TauBarConfig.new() + bar_config.mode = TauBarConfig.BarMode.STACKED + bar_config.stacked_normalization = TauBarConfig.StackedNormalization.NONE + bar_config.stacked_negative_policy = TauBarConfig.StackedNegativePolicy.DIVERGING + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [bar_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] + + %TestPlot2.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 3 +#################################################################################################### + +func _setup_test_3() -> void: + pass + +#################################################################################################### +# Test 4 +#################################################################################################### + +func _setup_test_4() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedStringArray(["One", "Two", "Three", "Four", "Five"]) + var y_a := PackedFloat64Array([-5.0, -10.0, 15.0, -20.0, 20.0]) + var y_b := PackedFloat64Array([25.0, -30.0, -35.0, -40.0, 40.0]) + var y_c := PackedFloat64Array([30.0, 40.0, -50.0, -60.0, 60.0]) + + var dataset := TauPlot.Dataset.make_shared_x_categorical(series_names, x, [y_a, y_b, y_c]) + + %TestPlot4.title = "[CATEGORICAL] with stacked_negative_policy = SKIP_NEGATIVES" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CATEGORICAL + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + + var bar_config := TauBarConfig.new() + bar_config.mode = TauBarConfig.BarMode.STACKED + bar_config.stacked_normalization = TauBarConfig.StackedNormalization.NONE + bar_config.stacked_negative_policy = TauBarConfig.StackedNegativePolicy.SKIP_NEGATIVES + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [bar_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] + + %TestPlot4.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 5 +#################################################################################################### + +func _setup_test_5() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedStringArray(["One", "Two", "Three", "Four", "Five"]) + var y_a := PackedFloat64Array([-5.0, -10.0, 15.0, -20.0, 20.0]) + var y_b := PackedFloat64Array([25.0, -30.0, -35.0, -40.0, 40.0]) + var y_c := PackedFloat64Array([30.0, 40.0, -50.0, -60.0, 60.0]) + + var dataset := TauPlot.Dataset.make_shared_x_categorical(series_names, x, [y_a, y_b, y_c]) + + %TestPlot5.title = "[CATEGORICAL] with stacked_negative_policy = DIVERGING" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CATEGORICAL + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + + var bar_config := TauBarConfig.new() + bar_config.mode = TauBarConfig.BarMode.STACKED + bar_config.stacked_normalization = TauBarConfig.StackedNormalization.NONE + bar_config.stacked_negative_policy = TauBarConfig.StackedNegativePolicy.DIVERGING + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [bar_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] + + %TestPlot5.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 6 +#################################################################################################### + +func _setup_test_6() -> void: + pass diff --git a/addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_none.gd.uid b/addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_none.gd.uid new file mode 100644 index 0000000..5eb42a0 --- /dev/null +++ b/addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_none.gd.uid @@ -0,0 +1 @@ +uid://dwn5xtyqivtr diff --git a/addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_none.tscn b/addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_none.tscn new file mode 100644 index 0000000..94ff99a --- /dev/null +++ b/addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_none.tscn @@ -0,0 +1,83 @@ +[gd_scene load_steps=3 format=3 uid="uid://dlcfw43nxd2lh"] + +[ext_resource type="Script" uid="uid://dwn5xtyqivtr" path="res://addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_none.gd" id="1_543ko"] +[ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_xjs2v"] + +[node name="Bars" type="VBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_543ko") + +[node name="Title" type="Label" parent="."] +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +text = "Test stacked bars negative policy with normalization = NONE" +horizontal_alignment = 1 + +[node name="GridContainer" type="GridContainer" parent="."] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/h_separation = 0 +theme_override_constants/v_separation = 0 +columns = 3 + +[node name="TestPlot1" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_xjs2v") +title = "[CONTINUOUS] with stacked_negative_policy = SKIP_NEGATIVES" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot2" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_xjs2v") +title = "[CONTINUOUS] with stacked_negative_policy = DIVERGING" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot3" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_xjs2v") +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot4" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_xjs2v") +title = "[CATEGORICAL] with stacked_negative_policy = SKIP_NEGATIVES" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot5" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_xjs2v") +title = "[CATEGORICAL] with stacked_negative_policy = DIVERGING" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot6" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_xjs2v") +metadata/_custom_type_script = "uid://dnhvsf2wip771" diff --git a/addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_percent.gd b/addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_percent.gd new file mode 100644 index 0000000..a8ab269 --- /dev/null +++ b/addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_percent.gd @@ -0,0 +1,253 @@ +@tool +extends Control + +func _ready() -> void: + _setup_test_1() + _setup_test_2() + _setup_test_3() + _setup_test_4() + _setup_test_5() + _setup_test_6() + + +#################################################################################################### +# Test 1 +#################################################################################################### + +func _setup_test_1() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0]) + var y_a := PackedFloat64Array([-5.0, -10.0, 15.0, -20.0, 20.0]) + var y_b := PackedFloat64Array([25.0, -30.0, -35.0, -40.0, 40.0]) + var y_c := PackedFloat64Array([30.0, 40.0, -50.0, -60.0, 60.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b, y_c]) + + %TestPlot1.title = "[CONTINUOUS] with stacked_negative_policy = SKIP_NEGATIVES" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.domain_padding_mode = TauAxisConfig.DomainPaddingMode.DATA_UNITS + x_axis.domain_padding_min = 0.5 + x_axis.domain_padding_max = 0.5 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + + var bar_config := TauBarConfig.new() + bar_config.mode = TauBarConfig.BarMode.STACKED + bar_config.stacked_normalization = TauBarConfig.StackedNormalization.PERCENT + bar_config.stacked_negative_policy = TauBarConfig.StackedNegativePolicy.SKIP_NEGATIVES + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [bar_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] + + %TestPlot1.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 2 +#################################################################################################### + +func _setup_test_2() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0]) + var y_a := PackedFloat64Array([-5.0, -10.0, 15.0, -20.0, 20.0]) + var y_b := PackedFloat64Array([25.0, -30.0, -35.0, -40.0, 40.0]) + var y_c := PackedFloat64Array([30.0, 40.0, -50.0, -60.0, 60.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b, y_c]) + + %TestPlot2.title = "[CONTINUOUS] with stacked_negative_policy = DIVERGING" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.domain_padding_mode = TauAxisConfig.DomainPaddingMode.DATA_UNITS + x_axis.domain_padding_min = 0.5 + x_axis.domain_padding_max = 0.5 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + + var bar_config := TauBarConfig.new() + bar_config.mode = TauBarConfig.BarMode.STACKED + bar_config.stacked_normalization = TauBarConfig.StackedNormalization.PERCENT + bar_config.stacked_negative_policy = TauBarConfig.StackedNegativePolicy.DIVERGING + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [bar_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] + + %TestPlot2.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 3 +#################################################################################################### + +func _setup_test_3() -> void: + pass + +#################################################################################################### +# Test 4 +#################################################################################################### + +func _setup_test_4() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedStringArray(["One", "Two", "Three", "Four", "Five"]) + var y_a := PackedFloat64Array([-5.0, -10.0, 15.0, -20.0, 20.0]) + var y_b := PackedFloat64Array([25.0, -30.0, -35.0, -40.0, 40.0]) + var y_c := PackedFloat64Array([30.0, 40.0, -50.0, -60.0, 60.0]) + + var dataset := TauPlot.Dataset.make_shared_x_categorical(series_names, x, [y_a, y_b, y_c]) + + %TestPlot4.title = "[CATEGORICAL] with stacked_negative_policy = SKIP_NEGATIVES" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CATEGORICAL + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + + var bar_config := TauBarConfig.new() + bar_config.mode = TauBarConfig.BarMode.STACKED + bar_config.stacked_normalization = TauBarConfig.StackedNormalization.PERCENT + bar_config.stacked_negative_policy = TauBarConfig.StackedNegativePolicy.SKIP_NEGATIVES + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [bar_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] + + %TestPlot4.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 5 +#################################################################################################### + +func _setup_test_5() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedStringArray(["One", "Two", "Three", "Four", "Five"]) + var y_a := PackedFloat64Array([-5.0, -10.0, 15.0, -20.0, 20.0]) + var y_b := PackedFloat64Array([25.0, -30.0, -35.0, -40.0, 40.0]) + var y_c := PackedFloat64Array([30.0, 40.0, -50.0, -60.0, 60.0]) + + var dataset := TauPlot.Dataset.make_shared_x_categorical(series_names, x, [y_a, y_b, y_c]) + + %TestPlot5.title = "[CATEGORICAL] with stacked_negative_policy = DIVERGING" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CATEGORICAL + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + + var bar_config := TauBarConfig.new() + bar_config.mode = TauBarConfig.BarMode.STACKED + bar_config.stacked_normalization = TauBarConfig.StackedNormalization.PERCENT + bar_config.stacked_negative_policy = TauBarConfig.StackedNegativePolicy.DIVERGING + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [bar_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.BAR + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] + + %TestPlot5.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 6 +#################################################################################################### + +func _setup_test_6() -> void: + pass diff --git a/addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_percent.gd.uid b/addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_percent.gd.uid new file mode 100644 index 0000000..5569c29 --- /dev/null +++ b/addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_percent.gd.uid @@ -0,0 +1 @@ +uid://54kryum6jpqn diff --git a/addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_percent.tscn b/addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_percent.tscn new file mode 100644 index 0000000..883707b --- /dev/null +++ b/addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_percent.tscn @@ -0,0 +1,83 @@ +[gd_scene load_steps=3 format=3 uid="uid://15u65yn0mbjv"] + +[ext_resource type="Script" uid="uid://54kryum6jpqn" path="res://addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_percent.gd" id="1_ru6as"] +[ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_1qp56"] + +[node name="Bars" type="VBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_ru6as") + +[node name="Title" type="Label" parent="."] +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +text = "Test stacked bars negative policy with normalization = PERCENT" +horizontal_alignment = 1 + +[node name="GridContainer" type="GridContainer" parent="."] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/h_separation = 0 +theme_override_constants/v_separation = 0 +columns = 3 + +[node name="TestPlot1" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_1qp56") +title = "[CONTINUOUS] with stacked_negative_policy = SKIP_NEGATIVES" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot2" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_1qp56") +title = "[CONTINUOUS] with stacked_negative_policy = DIVERGING" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot3" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_1qp56") +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot4" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_1qp56") +title = "[CATEGORICAL] with stacked_negative_policy = SKIP_NEGATIVES" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot5" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_1qp56") +title = "[CATEGORICAL] with stacked_negative_policy = DIVERGING" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot6" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_1qp56") +metadata/_custom_type_script = "uid://dnhvsf2wip771" diff --git a/addons/tau-plot/tests/bar/stacking_order/test_bar_stacking_order.gd b/addons/tau-plot/tests/bar/stacked/test_bar_stacking_order.gd similarity index 100% rename from addons/tau-plot/tests/bar/stacking_order/test_bar_stacking_order.gd rename to addons/tau-plot/tests/bar/stacked/test_bar_stacking_order.gd diff --git a/addons/tau-plot/tests/bar/stacking_order/test_bar_stacking_order.gd.uid b/addons/tau-plot/tests/bar/stacked/test_bar_stacking_order.gd.uid similarity index 100% rename from addons/tau-plot/tests/bar/stacking_order/test_bar_stacking_order.gd.uid rename to addons/tau-plot/tests/bar/stacked/test_bar_stacking_order.gd.uid diff --git a/addons/tau-plot/tests/bar/stacking_order/test_bar_stacking_order.tscn b/addons/tau-plot/tests/bar/stacked/test_bar_stacking_order.tscn similarity index 97% rename from addons/tau-plot/tests/bar/stacking_order/test_bar_stacking_order.tscn rename to addons/tau-plot/tests/bar/stacked/test_bar_stacking_order.tscn index f6539b7..679504f 100644 --- a/addons/tau-plot/tests/bar/stacking_order/test_bar_stacking_order.tscn +++ b/addons/tau-plot/tests/bar/stacked/test_bar_stacking_order.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=3 format=3 uid="uid://dtphakr4ucelg"] -[ext_resource type="Script" uid="uid://dc8xbkxehai5e" path="res://addons/tau-plot/tests/bar/stacking_order/test_bar_stacking_order.gd" id="1_6dmnp"] +[ext_resource type="Script" uid="uid://dc8xbkxehai5e" path="res://addons/tau-plot/tests/bar/stacked/test_bar_stacking_order.gd" id="1_6dmnp"] [ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_p3gyn"] [node name="Bars" type="VBoxContainer"] diff --git a/addons/tau-plot/tests/bar/stacked_normalization/test_bar_stacked_normalization.gd.uid b/addons/tau-plot/tests/bar/stacked_normalization/test_bar_stacked_normalization.gd.uid deleted file mode 100644 index 4518e76..0000000 --- a/addons/tau-plot/tests/bar/stacked_normalization/test_bar_stacked_normalization.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bvr1lhw6xrucv From 02e3e16655b319f29c79769b6061efc4cae5a414 Mon Sep 17 00:00:00 2001 From: ze2j <46556066+ze2j@users.noreply.github.com> Date: Mon, 11 May 2026 23:33:47 +0200 Subject: [PATCH 22/23] Implement stacked lines --- addons/tau-plot/plot/xy/bar/bar_renderer.gd | 151 ++------ addons/tau-plot/plot/xy/line/line_renderer.gd | 103 +++-- .../tau-plot/plot/xy/stacked_series_values.gd | 176 +++++++++ .../plot/xy/stacked_series_values.gd.uid | 1 + addons/tau-plot/plot/xy/xy_plot.gd | 131 ++++--- .../stacked/test_bar_stacked_negative_none.gd | 4 + .../test_line_stacked_negative_fraction.gd | 351 +++++++++++++++++ ...test_line_stacked_negative_fraction.gd.uid | 1 + .../test_line_stacked_negative_fraction.tscn | 85 +++++ .../test_line_stacked_negative_none.gd | 358 ++++++++++++++++++ .../test_line_stacked_negative_none.gd.uid | 1 + .../test_line_stacked_negative_none.tscn | 85 +++++ .../test_line_stacked_negative_percent.gd | 351 +++++++++++++++++ .../test_line_stacked_negative_percent.gd.uid | 1 + .../test_line_stacked_negative_percent.tscn | 83 ++++ .../line/stacked/test_line_stacking_order.gd | 349 +++++++++++++++++ .../stacked/test_line_stacking_order.gd.uid | 1 + .../stacked/test_line_stacking_order.tscn | 85 +++++ 18 files changed, 2118 insertions(+), 199 deletions(-) create mode 100644 addons/tau-plot/plot/xy/stacked_series_values.gd create mode 100644 addons/tau-plot/plot/xy/stacked_series_values.gd.uid create mode 100644 addons/tau-plot/tests/line/stacked/test_line_stacked_negative_fraction.gd create mode 100644 addons/tau-plot/tests/line/stacked/test_line_stacked_negative_fraction.gd.uid create mode 100644 addons/tau-plot/tests/line/stacked/test_line_stacked_negative_fraction.tscn create mode 100644 addons/tau-plot/tests/line/stacked/test_line_stacked_negative_none.gd create mode 100644 addons/tau-plot/tests/line/stacked/test_line_stacked_negative_none.gd.uid create mode 100644 addons/tau-plot/tests/line/stacked/test_line_stacked_negative_none.tscn create mode 100644 addons/tau-plot/tests/line/stacked/test_line_stacked_negative_percent.gd create mode 100644 addons/tau-plot/tests/line/stacked/test_line_stacked_negative_percent.gd.uid create mode 100644 addons/tau-plot/tests/line/stacked/test_line_stacked_negative_percent.tscn create mode 100644 addons/tau-plot/tests/line/stacked/test_line_stacking_order.gd create mode 100644 addons/tau-plot/tests/line/stacked/test_line_stacking_order.gd.uid create mode 100644 addons/tau-plot/tests/line/stacked/test_line_stacking_order.tscn diff --git a/addons/tau-plot/plot/xy/bar/bar_renderer.gd b/addons/tau-plot/plot/xy/bar/bar_renderer.gd index ac0cd50..bda77b0 100644 --- a/addons/tau-plot/plot/xy/bar/bar_renderer.gd +++ b/addons/tau-plot/plot/xy/bar/bar_renderer.gd @@ -8,6 +8,7 @@ const Axis = preload("res://addons/tau-plot/plot/xy/xy_axes.gd").Axis const VisualAttributes = preload("res://addons/tau-plot/plot/xy/visual_attributes.gd").VisualAttributes const BarVisualAttributes := preload("res://addons/tau-plot/plot/xy/bar/bar_visual_attributes.gd").BarVisualAttributes const BarHitRecord := preload("res://addons/tau-plot/plot/xy/bar/bar_hit_record.gd").BarHitRecord +const StackedSeriesValues := preload("res://addons/tau-plot/plot/xy/stacked_series_values.gd").StackedSeriesValues # Draws bar overlays from a XYLayout + Dataset. @@ -662,74 +663,14 @@ class BarRenderer extends Control: _draw_bar(p_pane_rect, center_px, zero_px, y_px, bar_width_px, color, series_index, i, x_value, y_value, y_value) - # Builds one X column's worth of stacked bar segments in dataset order, - # applying the active normalization and negative policy. The returned list - # drives the second-pass painter, which looks up segments by series_index. - func _compute_stacked_segments_at(p_series_count: int, p_sample_index: int) -> Array: - var normalization := _bar_config.stacked_normalization - var policy := _bar_config.stacked_negative_policy - - # DIVERGING needs positive and absolute-negative totals tracked - # separately so each half-stack normalizes against its own total. - var pos_total := 0.0 - var neg_total_abs := 0.0 - if normalization != TauBarConfig.StackedNormalization.NONE: - for series_index in range(p_series_count): - var series_id := _get_bar_series_id(series_index) - var y_value := _dataset.get_series_y(series_id, p_sample_index) - if is_nan(y_value) or is_inf(y_value): - continue - if y_value >= 0.0: - pos_total += y_value - else: - neg_total_abs += -y_value - - var pos_scale := 1.0 - var neg_scale := 1.0 - match normalization: - TauBarConfig.StackedNormalization.FRACTION: - pos_scale = 1.0 / pos_total if pos_total > 0.0 else 0.0 - neg_scale = 1.0 / neg_total_abs if neg_total_abs > 0.0 else 0.0 - - TauBarConfig.StackedNormalization.PERCENT: - pos_scale = 100.0 / pos_total if pos_total > 0.0 else 0.0 - neg_scale = 100.0 / neg_total_abs if neg_total_abs > 0.0 else 0.0 - - var segments: Array = [] - var accum_pos := 0.0 - var accum_neg := 0.0 - - for series_index in range(p_series_count): - var series_id := _get_bar_series_id(series_index) - var y_raw := _dataset.get_series_y(series_id, p_sample_index) - if is_nan(y_raw) or is_inf(y_raw): - continue - - match policy: - TauBarConfig.StackedNegativePolicy.SKIP_NEGATIVES: - if y_raw < 0.0: - continue - var y := y_raw * pos_scale - var y0 := accum_pos - var y1 := accum_pos + y - segments.append({"series_index": series_index, "y0": y0, "y1": y1}) - accum_pos = y1 - - TauBarConfig.StackedNegativePolicy.DIVERGING: - if y_raw >= 0.0: - var y := y_raw * pos_scale - var y0 := accum_pos - var y1 := accum_pos + y - segments.append({"series_index": series_index, "y0": y0, "y1": y1}) - accum_pos = y1 - else: - var y := y_raw * neg_scale - var y0 := accum_neg - var y1 := accum_neg + y - segments.append({"series_index": series_index, "y0": y0, "y1": y1}) - accum_neg = y1 - - return segments + # Builds the cumulative stacked values for every (series, sample) at once, + # delegating normalization and negative-policy handling to the shared + # helper. The two stacked painters look up y_plotted/y_baseline by + # (series_local, sample_index) instead of carrying their own pass. + func _compute_stacked_values() -> StackedSeriesValues: + return StackedSeriesValues.new(_dataset, _bar_series_ids, + _bar_config.stacked_normalization, + _bar_config.stacked_negative_policy) func _draw_stacked_bars(p_pane_rect: Rect2, p_series_count: int) -> void: @@ -759,41 +700,27 @@ class BarRenderer extends Control: var bar_width_px := _geometry_cache.compute_categorical_bar_width_px(p_pane_rect, n) bar_width_px = max(bar_width_px, _MIN_BAR_WIDTH_PX) - # First pass: compute all segments for all categories (in dataset order for stacking) - var all_segments: Array = [] # Array of arrays, one per category. FIXME Godot 4.5 does not support nested typed collections. - all_segments.resize(n) - for category_index in range(n): - all_segments[category_index] = _compute_stacked_segments_at(p_series_count, category_index) + var stacked_values := _compute_stacked_values() - # Second pass: paint in z_order (series first, then categories) var draw_order := _get_series_draw_order(p_series_count) for series_index: int in draw_order: for category_index in range(n): - var group_center_px := _layout.map_x_category_center_to_px(_pane_index, category_index) - var segments = all_segments[category_index] - - # Find the segment for this series at this category - var segment = null - for seg in segments: - if seg["series_index"] == series_index: - segment = seg - break - - if segment == null: - continue # This series had no valid data at this category + var y_plotted: float = stacked_values.get_y_plotted(series_index, category_index) + if is_nan(y_plotted): + continue - var y0_px := _layout.map_y_to_px(_pane_index, segment["y0"], stacked_axis_id) - var y1_px := _layout.map_y_to_px(_pane_index, segment["y1"], stacked_axis_id) + var group_center_px := _layout.map_x_category_center_to_px(_pane_index, category_index) + var y0_px := _layout.map_y_to_px(_pane_index, stacked_values.get_y_baseline(series_index, category_index), stacked_axis_id) + var y1_px := _layout.map_y_to_px(_pane_index, y_plotted, stacked_axis_id) var x_value: Variant = categories[category_index] - var y_value: float = segment["y1"] # Callbacks see the raw dataset value, not the stacked top. - var y_raw: float = _dataset.get_series_y(_get_bar_series_id(series_index), category_index) + var y_raw: float = stacked_values.get_y_raw(series_index, category_index) var base_color := _get_bar_color(series_index, category_index, x_value, y_raw) var alpha_override := _get_bar_alpha(series_index, category_index, x_value, y_raw) var color := _apply_alpha_override(base_color, alpha_override) - _draw_bar(p_pane_rect, group_center_px, y0_px, y1_px, bar_width_px, color, series_index, category_index, x_value, y_value, y_raw) + _draw_bar(p_pane_rect, group_center_px, y0_px, y1_px, bar_width_px, color, series_index, category_index, x_value, y_plotted, y_raw) # FIXME: the second pass here duplicates _draw_stacked_bars_categorical almost line-for-line. @@ -805,42 +732,21 @@ class BarRenderer extends Control: var resolved_bar_width_policy := _geometry_cache.get_resolved_bar_width_policy() - # First pass: compute all segments for all X positions (in dataset order for stacking) - var all_segments: Array = [] # Array of arrays, one per X position. FIXME Godot 4.5 does not support nested typed collections. - all_segments.resize(n) - for i in range(n): - var x_value := float(_dataset.get_shared_x(i)) - if is_nan(x_value) or is_inf(x_value): - all_segments[i] = [] - continue - if not _is_x_value_valid_for_scale(x_value): - all_segments[i] = [] - continue - - all_segments[i] = _compute_stacked_segments_at(p_series_count, i) - - # Second pass: paint in z_order (series first, then X positions) + var stacked_values := _compute_stacked_values() + var draw_order := _get_series_draw_order(p_series_count) for series_index: int in draw_order: for i in range(n): + var y_plotted: float = stacked_values.get_y_plotted(series_index, i) + if is_nan(y_plotted): + continue + var x_value := float(_dataset.get_shared_x(i)) if is_nan(x_value) or is_inf(x_value): continue if not _is_x_value_valid_for_scale(x_value): continue - var segments = all_segments[i] - - # Find the segment for this series at this X position - var segment = null - for seg in segments: - if seg["series_index"] == series_index: - segment = seg - break - - if segment == null: - continue # This series had no valid data at this X position - var group_center_px := _layout.map_x_to_px(_pane_index, x_value) var bar_width_px := 0.0 @@ -852,17 +758,16 @@ class BarRenderer extends Control: push_error("BarRenderer: bar_width_policy %d is not supported for STACKED + CONTINUOUS" % int(resolved_bar_width_policy)) return - var y0_px := _layout.map_y_to_px(_pane_index, segment["y0"], stacked_axis_id) - var y1_px := _layout.map_y_to_px(_pane_index, segment["y1"], stacked_axis_id) + var y0_px := _layout.map_y_to_px(_pane_index, stacked_values.get_y_baseline(series_index, i), stacked_axis_id) + var y1_px := _layout.map_y_to_px(_pane_index, y_plotted, stacked_axis_id) - var y_value: float = segment["y1"] # Callbacks see the raw dataset value, not the stacked top. - var y_raw: float = _dataset.get_series_y(_get_bar_series_id(series_index), i) + var y_raw: float = stacked_values.get_y_raw(series_index, i) var base_color := _get_bar_color(series_index, i, x_value, y_raw) var alpha_override := _get_bar_alpha(series_index, i, x_value, y_raw) var color := _apply_alpha_override(base_color, alpha_override) - _draw_bar(p_pane_rect, group_center_px, y0_px, y1_px, bar_width_px, color, series_index, i, x_value, y_value, y_raw) + _draw_bar(p_pane_rect, group_center_px, y0_px, y1_px, bar_width_px, color, series_index, i, x_value, y_plotted, y_raw) func _draw_independent_bars(p_pane_rect: Rect2, p_series_count: int) -> void: diff --git a/addons/tau-plot/plot/xy/line/line_renderer.gd b/addons/tau-plot/plot/xy/line/line_renderer.gd index c595cf1..1c324d3 100644 --- a/addons/tau-plot/plot/xy/line/line_renderer.gd +++ b/addons/tau-plot/plot/xy/line/line_renderer.gd @@ -7,6 +7,7 @@ const Axis = preload("res://addons/tau-plot/plot/xy/xy_axes.gd").Axis const VisualAttributes = preload("res://addons/tau-plot/plot/xy/visual_attributes.gd").VisualAttributes const LineVisualAttributes := preload("res://addons/tau-plot/plot/xy/line/line_visual_attributes.gd").LineVisualAttributes const LineHitRecord := preload("res://addons/tau-plot/plot/xy/line/line_hit_record.gd").LineHitRecord +const StackedSeriesValues := preload("res://addons/tau-plot/plot/xy/stacked_series_values.gd").StackedSeriesValues # Draws line overlays from an XYLayout + Dataset. @@ -77,6 +78,14 @@ const LineHitRecord := preload("res://addons/tau-plot/plot/xy/line/line_hit_reco # are colored consistently with the underlying segment endpoints so the # resulting interpolation matches the chosen interpolation mode. # +# STACKED mode: +# - Each polyline is drawn at the per-X cumulative top of its layer. +# Layer 0 sits at the bottom, ordered by dataset index. +# - Color and alpha callbacks always receive the original dataset value, +# never the cumulative or the normalized value. +# - LineHitRecord.y_plotted_value carries the cumulative top. +# LineHitRecord.y_raw_value carries the original dataset value. +# # LineValidator is expected to enforce binding-level typing constraints. class LineRenderer extends Control: # Number of sub-segments inserted between two consecutive samples by @@ -210,30 +219,35 @@ class LineRenderer extends Control: if series_count <= 0: return - var draw_order := _get_series_draw_order(series_count) + var stacked_values: StackedSeriesValues = null + if _line_config.mode == TauLineConfig.LineMode.STACKED: + stacked_values = StackedSeriesValues.new(_dataset, _line_series_ids, + _line_config.stacked_normalization, + _line_config.stacked_negative_policy) + var draw_order := _get_series_draw_order(series_count) for draw_rank in range(draw_order.size()): var series_index: int = draw_order[draw_rank] - _draw_series_independent(series_index) + _draw_series(series_index, stacked_values) - # Draws a single series as one or more polyline runs, respecting the - # active gap policy. Run emission follows these rules: + # Run emission rules, applied by both variants: # - A valid sample is appended to the current run. - # - An invalid sample (NaN/Inf X or Y, or a value forbidden by the - # active axis scale) is handled according to gap_policy: + # - An invalid sample (NaN/Inf X or Y, value forbidden by the active + # axis scale, or dropped by the negative policy in STACKED mode) + # is handled according to gap_policy: # - SKIP flushes the current run and starts a new one. # - BRIDGE drops the sample and keeps appending into the same run. - # - A run of fewer than two points is discarded (no polyline). - func _draw_series_independent(p_series_index: int) -> void: + # - A run of fewer than two points is discarded. + func _draw_series(p_series_index: int, p_stacked: StackedSeriesValues) -> void: var x_cfg := _get_x_axis_config() if x_cfg != null and x_cfg.type == TauAxisConfig.Type.CATEGORICAL: - _draw_series_categorical(p_series_index) + _draw_series_categorical(p_series_index, p_stacked) else: - _draw_series_continuous(p_series_index) + _draw_series_continuous(p_series_index, p_stacked) - func _draw_series_continuous(p_series_index: int) -> void: + func _draw_series_continuous(p_series_index: int, p_stacked: StackedSeriesValues) -> void: var series_id := _get_line_series_id(p_series_index) var global_series_index := _get_global_series_index(p_series_index) var width_px: float = _line_style.get_series_width_px(global_series_index) @@ -245,6 +259,10 @@ class LineRenderer extends Control: var bridge: bool = _line_config.gap_policy == TauLineConfig.GapPolicy.BRIDGE var interpolation: TauLineConfig.InterpolationMode = _line_config.interpolation_mode + var independent_y: PackedFloat64Array + if p_stacked == null: + independent_y = _build_independent_y_row(series_id) + var run := PackedVector2Array() var run_colors := PackedColorArray() # Parallel arrays describing the real samples appended to the current @@ -269,8 +287,16 @@ class LineRenderer extends Control: real_dataset_indices = PackedInt32Array() continue - var y_value := _dataset.get_series_y(series_id, i) - if is_nan(y_value) or is_inf(y_value) or not _is_y_value_valid_for_scale(series_id, y_value): + var y_plotted: float + var y_raw: float + if p_stacked != null: + y_plotted = p_stacked.get_y_plotted(p_series_index, i) + y_raw = p_stacked.get_y_raw(p_series_index, i) + else: + y_plotted = independent_y[i] + y_raw = y_plotted + + if is_nan(y_plotted): if not bridge: _finalize_run(run, run_colors, real_polyline_indices, real_dataset_indices, series_id, width_px, hover_width_px, interpolation, dash_px) run = PackedVector2Array() @@ -280,9 +306,9 @@ class LineRenderer extends Control: continue var x_px := _layout.map_x_to_px(_pane_index, x_value) - var y_px := _layout.map_y_to_px(_pane_index, y_value, y_axis_id) + var y_px := _layout.map_y_to_px(_pane_index, y_plotted, y_axis_id) var screen_pos := _layout.map_point_to_screen(x_px, y_px) - var sample_color := _resolve_sample_color(p_series_index, i, x_value, y_value) + var sample_color := _resolve_sample_color(p_series_index, i, x_value, y_raw) _append_with_interpolation(run, run_colors, screen_pos, sample_color, interpolation) # The real sample is always the last vertex appended by # _append_with_interpolation, regardless of the interpolation mode. @@ -293,15 +319,15 @@ class LineRenderer extends Control: record.series_id = series_id record.sample_index = i record.x_value = x_value - record.y_plotted_value = y_value - record.y_raw_value = y_value + record.y_plotted_value = y_plotted + record.y_raw_value = y_raw record.screen_position = screen_pos _hit_records.append(record) _finalize_run(run, run_colors, real_polyline_indices, real_dataset_indices, series_id, width_px, hover_width_px, interpolation, dash_px) - func _draw_series_categorical(p_series_index: int) -> void: + func _draw_series_categorical(p_series_index: int, p_stacked: StackedSeriesValues) -> void: var series_id := _get_line_series_id(p_series_index) var global_series_index := _get_global_series_index(p_series_index) var width_px: float = _line_style.get_series_width_px(global_series_index) @@ -313,6 +339,10 @@ class LineRenderer extends Control: var bridge: bool = _line_config.gap_policy == TauLineConfig.GapPolicy.BRIDGE var interpolation: TauLineConfig.InterpolationMode = _line_config.interpolation_mode + var independent_y: PackedFloat64Array + if p_stacked == null: + independent_y = _build_independent_y_row(series_id) + var categories := _layout.domain.x_categories var run := PackedVector2Array() var run_colors := PackedColorArray() @@ -321,8 +351,16 @@ class LineRenderer extends Control: var sample_count := _dataset.get_series_sample_count(series_id) for cat_idx in range(sample_count): - var y_value := _dataset.get_series_y(series_id, cat_idx) - if is_nan(y_value) or is_inf(y_value) or not _is_y_value_valid_for_scale(series_id, y_value): + var y_plotted: float + var y_raw: float + if p_stacked != null: + y_plotted = p_stacked.get_y_plotted(p_series_index, cat_idx) + y_raw = p_stacked.get_y_raw(p_series_index, cat_idx) + else: + y_plotted = independent_y[cat_idx] + y_raw = y_plotted + + if is_nan(y_plotted): if not bridge: _finalize_run(run, run_colors, real_polyline_indices, real_dataset_indices, series_id, width_px, hover_width_px, interpolation, dash_px) run = PackedVector2Array() @@ -332,10 +370,10 @@ class LineRenderer extends Control: continue var x_px := _layout.map_x_category_center_to_px(_pane_index, cat_idx) - var y_px := _layout.map_y_to_px(_pane_index, y_value, y_axis_id) + var y_px := _layout.map_y_to_px(_pane_index, y_plotted, y_axis_id) var screen_pos := _layout.map_point_to_screen(x_px, y_px) var x_value: Variant = categories[cat_idx] - var sample_color := _resolve_sample_color(p_series_index, cat_idx, x_value, y_value) + var sample_color := _resolve_sample_color(p_series_index, cat_idx, x_value, y_raw) _append_with_interpolation(run, run_colors, screen_pos, sample_color, interpolation) real_polyline_indices.append(run.size() - 1) real_dataset_indices.append(cat_idx) @@ -344,14 +382,31 @@ class LineRenderer extends Control: record.series_id = series_id record.sample_index = cat_idx record.x_value = x_value - record.y_plotted_value = y_value - record.y_raw_value = y_value + record.y_plotted_value = y_plotted + record.y_raw_value = y_raw record.screen_position = screen_pos _hit_records.append(record) _finalize_run(run, run_colors, real_polyline_indices, real_dataset_indices, series_id, width_px, hover_width_px, interpolation, dash_px) + # Marks dropped samples (NaN, Inf, log-axis violations) as NAN up front + # so the inner draw loop only needs a single is_nan() check per sample. + func _build_independent_y_row(p_series_id: int) -> PackedFloat64Array: + var sample_count := _dataset.get_series_sample_count(p_series_id) + var y_plotted := PackedFloat64Array() + y_plotted.resize(sample_count) + y_plotted.fill(NAN) + for i in range(sample_count): + var y := _dataset.get_series_y(p_series_id, i) + if is_nan(y) or is_inf(y): + continue + if not _is_y_value_valid_for_scale(p_series_id, y): + continue + y_plotted[i] = y + return y_plotted + + # Each appended sample (real or synthetic) gets the new sample's color. # Combined with linear interpolation by draw_polyline_colors() between # consecutive vertices, this places the color transition at the segment diff --git a/addons/tau-plot/plot/xy/stacked_series_values.gd b/addons/tau-plot/plot/xy/stacked_series_values.gd new file mode 100644 index 0000000..aff6c4b --- /dev/null +++ b/addons/tau-plot/plot/xy/stacked_series_values.gd @@ -0,0 +1,176 @@ +const Dataset := preload("res://addons/tau-plot/model/dataset.gd").Dataset +const StackedNormalization = preload("res://addons/tau-plot/plot/xy/stacked_normalization.gd").StackedNormalization +const StackedNegativePolicy = preload("res://addons/tau-plot/plot/xy/stacked_negative_policy.gd").StackedNegativePolicy + + +## Per-(series, sample) cumulative values for a stacked overlay. +## +## Built from an ordered list of stacked series sharing a SHARED_X dataset. +## Layer order matches the order of series_ids: layer 0 sits at the bottom. +## +## Values are read through [method get_y_plotted], [method get_y_baseline], +## and [method get_y_raw], all keyed by (series_local, sample_index). +## series_local matches the position of the corresponding series_id in the +## list passed at construction. NAN means the sample is dropped (NaN/Inf +## in the dataset, or removed by the active negative policy) and must be +## ignored by the caller. +class StackedSeriesValues extends RefCounted: + + var _series_count: int = 0 + var _sample_count: int = 0 + + # Flat layout sidesteps the copy-on-write of nested + # Array[PackedFloat64Array] element access in GDScript, where reading + # an element into a local yields a copy and breaks in-place mutation. + # Index = series_local * _sample_count + sample_index. + var _y_plotted: PackedFloat64Array = PackedFloat64Array() + var _y_baseline: PackedFloat64Array = PackedFloat64Array() + var _y_raw: PackedFloat64Array = PackedFloat64Array() + + + ## p_series_ids must already be in stacking order (layer 0 first) and + ## refer to series of the SHARED_X p_dataset. + func _init(p_dataset: Dataset, p_series_ids: PackedInt64Array, + p_normalization: StackedNormalization, + p_negative_policy: StackedNegativePolicy) -> void: + _series_count = p_series_ids.size() + _sample_count = p_dataset.get_shared_sample_count() + + var cell_count := _series_count * _sample_count + _y_plotted.resize(cell_count) + _y_plotted.fill(NAN) + _y_baseline.resize(cell_count) + _y_baseline.fill(NAN) + _y_raw.resize(cell_count) + _y_raw.fill(NAN) + + _fill_y_raw(p_dataset, p_series_ids) + + for sample_index in range(_sample_count): + var pos_scale := 1.0 + var neg_scale := 1.0 + if p_normalization != StackedNormalization.NONE: + var totals := _per_x_totals(sample_index, p_negative_policy) + var pos_total: float = totals.x + var neg_total_abs: float = totals.y + match p_normalization: + StackedNormalization.FRACTION: + pos_scale = 1.0 / pos_total if pos_total > 0.0 else 0.0 + neg_scale = 1.0 / neg_total_abs if neg_total_abs > 0.0 else 0.0 + StackedNormalization.PERCENT: + pos_scale = 100.0 / pos_total if pos_total > 0.0 else 0.0 + neg_scale = 100.0 / neg_total_abs if neg_total_abs > 0.0 else 0.0 + + match p_negative_policy: + StackedNegativePolicy.SKIP_NEGATIVES: + _accumulate_skip_negatives(sample_index, pos_scale) + StackedNegativePolicy.DIVERGING: + _accumulate_diverging(sample_index, pos_scale, neg_scale) + StackedNegativePolicy.SIGNED_SUM: + _accumulate_signed_sum(sample_index, pos_scale) + + + ## Painted top of this (series, sample) in axis units. + func get_y_plotted(p_series_local: int, p_sample_index: int) -> float: + return _y_plotted[p_series_local * _sample_count + p_sample_index] + + + ## Painted top of the layer below this (series, sample) in the same + ## half-stack, or 0.0 for the bottom-most contribution at this X. + func get_y_baseline(p_series_local: int, p_sample_index: int) -> float: + return _y_baseline[p_series_local * _sample_count + p_sample_index] + + + ## Original dataset value at this (series, sample), before stacking, + ## normalization, or accumulation. + func get_y_raw(p_series_local: int, p_sample_index: int) -> float: + return _y_raw[p_series_local * _sample_count + p_sample_index] + + + #################################################################################################### + # Private + #################################################################################################### + + # Negative-policy filtering belongs to the accumulation step, so this + # pass keeps every finite raw value regardless of sign. + func _fill_y_raw(p_dataset: Dataset, p_series_ids: PackedInt64Array) -> void: + for series_local in range(_series_count): + var series_id := p_series_ids[series_local] + var row_offset := series_local * _sample_count + for sample_index in range(_sample_count): + var v := p_dataset.get_series_y(series_id, sample_index) + if is_nan(v) or is_inf(v): + continue + _y_raw[row_offset + sample_index] = v + + + # DIVERGING is the only policy that needs the absolute negative total. + # The other policies leave it at zero. + func _per_x_totals(p_sample_index: int, p_policy: StackedNegativePolicy) -> Vector2: + var pos_total := 0.0 + var neg_total_abs := 0.0 + for series_local in range(_series_count): + var v: float = _y_raw[series_local * _sample_count + p_sample_index] + if is_nan(v): + continue + match p_policy: + StackedNegativePolicy.SKIP_NEGATIVES: + if v >= 0.0: + pos_total += v + StackedNegativePolicy.DIVERGING: + if v >= 0.0: + pos_total += v + else: + neg_total_abs += -v + StackedNegativePolicy.SIGNED_SUM: + pos_total += v + return Vector2(pos_total, neg_total_abs) + + + func _accumulate_skip_negatives(p_sample_index: int, p_pos_scale: float) -> void: + var accum_pos := 0.0 + for series_local in range(_series_count): + var idx := series_local * _sample_count + p_sample_index + var v: float = _y_raw[idx] + if is_nan(v) or v < 0.0: + continue + var scaled := v * p_pos_scale + _y_baseline[idx] = accum_pos + accum_pos += scaled + _y_plotted[idx] = accum_pos + + + func _accumulate_diverging(p_sample_index: int, p_pos_scale: float, p_neg_scale: float) -> void: + var accum_pos := 0.0 + var accum_neg := 0.0 + for series_local in range(_series_count): + var idx := series_local * _sample_count + p_sample_index + var v: float = _y_raw[idx] + if is_nan(v): + continue + if v >= 0.0: + var scaled := v * p_pos_scale + _y_baseline[idx] = accum_pos + accum_pos += scaled + _y_plotted[idx] = accum_pos + else: + var scaled := v * p_neg_scale + _y_baseline[idx] = accum_neg + accum_neg += scaled + _y_plotted[idx] = accum_neg + + + # A negative contribution dips the cumulative below the previous layer, + # producing the streamgraph pattern. The baseline of layer N is always + # the painted top of layer N-1, even when that top went down. + func _accumulate_signed_sum(p_sample_index: int, p_scale: float) -> void: + var accum := 0.0 + for series_local in range(_series_count): + var idx := series_local * _sample_count + p_sample_index + var v: float = _y_raw[idx] + if is_nan(v): + continue + var scaled := v * p_scale + _y_baseline[idx] = accum + accum += scaled + _y_plotted[idx] = accum diff --git a/addons/tau-plot/plot/xy/stacked_series_values.gd.uid b/addons/tau-plot/plot/xy/stacked_series_values.gd.uid new file mode 100644 index 0000000..29260d1 --- /dev/null +++ b/addons/tau-plot/plot/xy/stacked_series_values.gd.uid @@ -0,0 +1 @@ +uid://bse08nh8j638m diff --git a/addons/tau-plot/plot/xy/xy_plot.gd b/addons/tau-plot/plot/xy/xy_plot.gd index ce28090..bbd97a9 100644 --- a/addons/tau-plot/plot/xy/xy_plot.gd +++ b/addons/tau-plot/plot/xy/xy_plot.gd @@ -24,6 +24,8 @@ const XYDomain := preload("res://addons/tau-plot/plot/xy/xy_domain.gd").XYDomain const XYDomainOverrides := preload("res://addons/tau-plot/plot/xy/xy_domain_overrides.gd").XYDomainOverrides const YDomainOverride := preload("res://addons/tau-plot/plot/xy/xy_domain_overrides.gd").YDomainOverride const StackedPinnedRange := preload("res://addons/tau-plot/plot/xy/stacked_pinned_range.gd").StackedPinnedRange +const StackedNormalization = preload("res://addons/tau-plot/plot/xy/stacked_normalization.gd").StackedNormalization +const StackedNegativePolicy = preload("res://addons/tau-plot/plot/xy/stacked_negative_policy.gd").StackedNegativePolicy const XYLayout := preload("res://addons/tau-plot/plot/xy/xy_layout.gd").XYLayout const XYAxisTitleLayout := preload("res://addons/tau-plot/plot/xy/xy_axis_title_layout.gd").XYAxisTitleLayout const VisualAttributes = preload("res://addons/tau-plot/plot/xy/visual_attributes.gd").VisualAttributes @@ -522,10 +524,10 @@ func refresh(p_plot_global_position: Vector2, p_legend_position: Position) -> vo if has_any_bar: for pane_index in range(pane_count): - var pane_bar_config: TauBarConfig = _bar_config_per_pane[pane_index] if pane_index < _bar_config_per_pane.size() else null + var pane_bar_config: TauBarConfig = _bar_config_per_pane[pane_index] if pane_bar_config == null: continue - var prev_bar_config: TauBarConfig = _state.bar_config_per_pane[pane_index] if pane_index < _state.bar_config_per_pane.size() else null + var prev_bar_config: TauBarConfig = _state.bar_config_per_pane[pane_index] if not pane_bar_config.is_equal_to(prev_bar_config): if pane_bar_config.has_layout_affecting_change(prev_bar_config): _domain_dirty = true @@ -546,10 +548,10 @@ func refresh(p_plot_global_position: Vector2, p_legend_position: Position) -> vo if has_any_scatter: for pane_index in range(pane_count): - var pane_scatter_config: TauScatterConfig = _scatter_config_per_pane[pane_index] if pane_index < _scatter_config_per_pane.size() else null + var pane_scatter_config: TauScatterConfig = _scatter_config_per_pane[pane_index] if pane_scatter_config == null: continue - var prev_scatter_config: TauScatterConfig = _state.scatter_config_per_pane[pane_index] if pane_index < _state.scatter_config_per_pane.size() else null + var prev_scatter_config: TauScatterConfig = _state.scatter_config_per_pane[pane_index] if not pane_scatter_config.is_equal_to(prev_scatter_config): if pane_scatter_config.has_layout_affecting_change(prev_scatter_config): _domain_dirty = true @@ -570,10 +572,10 @@ func refresh(p_plot_global_position: Vector2, p_legend_position: Position) -> vo if has_any_line: for pane_index in range(pane_count): - var pane_line_config: TauLineConfig = _line_config_per_pane[pane_index] if pane_index < _line_config_per_pane.size() else null + var pane_line_config: TauLineConfig = _line_config_per_pane[pane_index] if pane_line_config == null: continue - var prev_line_config: TauLineConfig = _state.line_config_per_pane[pane_index] if pane_index < _state.line_config_per_pane.size() else null + var prev_line_config: TauLineConfig = _state.line_config_per_pane[pane_index] if not pane_line_config.is_equal_to(prev_line_config): if pane_line_config.has_layout_affecting_change(prev_line_config): _domain_dirty = true @@ -641,7 +643,7 @@ func refresh(p_plot_global_position: Vector2, p_legend_position: Position) -> vo # All TauBarStyle properties are visual-only, so only dirty the owning bar pane. if has_any_bar: for pane_index in range(pane_count): - var bar_config: TauBarConfig = _bar_config_per_pane[pane_index] if pane_index < _bar_config_per_pane.size() else null + var bar_config: TauBarConfig = _bar_config_per_pane[pane_index] if bar_config == null: continue var needs_bar_re_resolve := _styles_dirty @@ -653,7 +655,7 @@ func refresh(p_plot_global_position: Vector2, p_legend_position: Position) -> vo # Content mutation: user changed a property on the existing TauBarStyle. if not needs_bar_re_resolve: - var prev_bar_style: TauBarStyle = _state.bar_style_per_pane[pane_index] if pane_index < _state.bar_style_per_pane.size() else null + var prev_bar_style: TauBarStyle = _state.bar_style_per_pane[pane_index] if not bar_config.style.is_equal_to(prev_bar_style): needs_bar_re_resolve = true @@ -669,7 +671,7 @@ func refresh(p_plot_global_position: Vector2, p_legend_position: Position) -> vo # All TauScatterStyle properties are visual-only, so only dirty the owning scatter pane. if has_any_scatter: for pane_index in range(pane_count): - var scatter_config: TauScatterConfig = _scatter_config_per_pane[pane_index] if pane_index < _scatter_config_per_pane.size() else null + var scatter_config: TauScatterConfig = _scatter_config_per_pane[pane_index] if scatter_config == null: continue var needs_scatter_re_resolve := _styles_dirty @@ -681,7 +683,7 @@ func refresh(p_plot_global_position: Vector2, p_legend_position: Position) -> vo # Content mutation: user changed a property on the existing TauScatterStyle. if not needs_scatter_re_resolve: - var prev_scatter_style: TauScatterStyle = _state.scatter_style_per_pane[pane_index] if pane_index < _state.scatter_style_per_pane.size() else null + var prev_scatter_style: TauScatterStyle = _state.scatter_style_per_pane[pane_index] if not scatter_config.style.is_equal_to(prev_scatter_style): needs_scatter_re_resolve = true @@ -697,7 +699,7 @@ func refresh(p_plot_global_position: Vector2, p_legend_position: Position) -> vo # All TauLineStyle properties are visual-only, so only dirty the owning line pane. if has_any_line: for pane_index in range(pane_count): - var line_config: TauLineConfig = _line_config_per_pane[pane_index] if pane_index < _line_config_per_pane.size() else null + var line_config: TauLineConfig = _line_config_per_pane[pane_index] if line_config == null: continue var needs_line_re_resolve := _styles_dirty @@ -709,7 +711,7 @@ func refresh(p_plot_global_position: Vector2, p_legend_position: Position) -> vo # Content mutation: user changed a property on the existing TauLineStyle. if not needs_line_re_resolve: - var prev_line_style: TauLineStyle = _state.line_style_per_pane[pane_index] if pane_index < _state.line_style_per_pane.size() else null + var prev_line_style: TauLineStyle = _state.line_style_per_pane[pane_index] if not line_config.style.is_equal_to(prev_line_style): needs_line_re_resolve = true @@ -741,7 +743,7 @@ func refresh(p_plot_global_position: Vector2, p_legend_position: Position) -> vo # TauPaneStyle resource mutation (user changed a property on the assigned style). if not needs_re_resolve: - var prev_pane_style: TauPaneStyle = _state.pane_style_per_pane[pane_index] if pane_index < _state.pane_style_per_pane.size() else null + var prev_pane_style: TauPaneStyle = _state.pane_style_per_pane[pane_index] var user_style_check: TauPaneStyle = pane_config.style if user_style_check != null: if not user_style_check.is_equal_to(prev_pane_style): @@ -800,9 +802,11 @@ func refresh(p_plot_global_position: Vector2, p_legend_position: Position) -> vo if _hover_controller != null: _hover_controller.invalidate() - # Step 5: Apply bar-specific Y overrides (stacking normalization) only if bars active - if _domain_dirty and has_any_bar: - _apply_bar_domain_overrides_y() + # Step 5: Apply stacking Y overrides. Bar and line stacked overlays + # write to the same per-axis entry, with the cross-overlay validator + # guaranteeing they agree on the shared fields. + if _domain_dirty and (has_any_bar or has_any_line): + _apply_stacking_domain_overrides_y() # Step 6: Recompute domain from dataset if needed if _domain_dirty: @@ -1146,52 +1150,75 @@ func _update_xy_layout(p_pane_view_rects: Array[Rect2], p_pane_positions: Array[ _xy_layout.update() -func _apply_bar_domain_overrides_y() -> void: +func _apply_stacking_domain_overrides_y() -> void: var pane_count := _domain_config.panes.size() for pane_index in range(pane_count): _xy_domain_overrides.clear_pane(pane_index) + _apply_bar_stacking_for_pane(pane_index) + _apply_line_stacking_for_pane(pane_index) - if pane_index >= _bar_series_ids_per_pane.size(): - continue - if _bar_series_ids_per_pane[pane_index].is_empty(): - continue - var pane_bar_config: TauBarConfig = _bar_config_per_pane[pane_index] if pane_index < _bar_config_per_pane.size() else null - if pane_bar_config == null: - continue +func _apply_bar_stacking_for_pane(p_pane_index: int) -> void: + if _bar_series_ids_per_pane[p_pane_index].is_empty(): + return - if pane_bar_config.mode != TauBarConfig.BarMode.STACKED: - continue + var pane_bar_config: TauBarConfig = _bar_config_per_pane[p_pane_index] + if pane_bar_config.mode != TauBarConfig.BarMode.STACKED: + return - # Resolve the y-axis actually used by the stacked bar series. - # The validator enforces that all stacked bar series share the same y_axis_id, - # so checking the first one is enough. - var first_bar_sid: int = _bar_series_ids_per_pane[pane_index][0] - var stacked_y_axis_id: int = _series_assignment.get_y_axis_id_for_series(first_bar_sid, pane_index) - if stacked_y_axis_id == -1: - continue + # Stacked bar series in a pane share one y axis, so any series id resolves it. + var first_bar_sid: int = _bar_series_ids_per_pane[p_pane_index][0] + var stacked_y_axis_id: int = _series_assignment.get_y_axis_id_for_series(first_bar_sid, p_pane_index) - # Only check the axis that the stacked bars are bound to. - var pane_config: TauPaneConfig = _domain_config.panes[pane_index] - var stacked_y_cfg: TauAxisConfig = pane_config.get_y_axis_config(stacked_y_axis_id) - if stacked_y_cfg != null and stacked_y_cfg.range_override_enabled: - continue + var pane_config: TauPaneConfig = _domain_config.panes[p_pane_index] + var stacked_y_cfg: TauAxisConfig = pane_config.get_y_axis_config(stacked_y_axis_id) + # An explicit user range wins over any stacking-driven range. + if stacked_y_cfg.range_override_enabled: + return - var y_domain_override: YDomainOverride = _xy_domain_overrides.get_or_create_override(pane_index, stacked_y_axis_id) + var y_domain_override: YDomainOverride = _xy_domain_overrides.get_or_create_override(p_pane_index, stacked_y_axis_id) - y_domain_override.bar_stack_active = true - y_domain_override.stacked_normalization = pane_bar_config.stacked_normalization - y_domain_override.stacked_negative_policy = pane_bar_config.stacked_negative_policy + y_domain_override.bar_stack_active = true + y_domain_override.stacked_normalization = pane_bar_config.stacked_normalization + y_domain_override.stacked_negative_policy = pane_bar_config.stacked_negative_policy - match pane_bar_config.stacked_normalization: - TauBarConfig.StackedNormalization.NONE: - pass + _apply_pinned_range(y_domain_override, pane_bar_config.stacked_normalization, pane_bar_config.stacked_negative_policy) - TauBarConfig.StackedNormalization.FRACTION, TauBarConfig.StackedNormalization.PERCENT: - var pinned := StackedPinnedRange.compute(pane_bar_config.stacked_normalization, pane_bar_config.stacked_negative_policy) - y_domain_override.force_y_range = true - y_domain_override.force_y_min = pinned.x - y_domain_override.force_y_max = pinned.y - _: - push_error("_apply_bar_domain_overrides_y(): unexpected stacked normalization") +func _apply_line_stacking_for_pane(p_pane_index: int) -> void: + if _line_series_ids_per_pane[p_pane_index].is_empty(): + return + + var pane_line_config: TauLineConfig = _line_config_per_pane[p_pane_index] + if pane_line_config.mode != TauLineConfig.LineMode.STACKED: + return + + # Stacked line series in a pane share one y axis, so any series id resolves it. + var first_line_sid: int = _line_series_ids_per_pane[p_pane_index][0] + var stacked_y_axis_id: int = _series_assignment.get_y_axis_id_for_series(first_line_sid, p_pane_index) + + var pane_config: TauPaneConfig = _domain_config.panes[p_pane_index] + var stacked_y_cfg: TauAxisConfig = pane_config.get_y_axis_config(stacked_y_axis_id) + # An explicit user range wins over any stacking-driven range. + if stacked_y_cfg.range_override_enabled: + return + + var y_domain_override: YDomainOverride = _xy_domain_overrides.get_or_create_override(p_pane_index, stacked_y_axis_id) + + y_domain_override.line_stack_active = true + y_domain_override.stacked_normalization = pane_line_config.stacked_normalization + y_domain_override.stacked_negative_policy = pane_line_config.stacked_negative_policy + + _apply_pinned_range(y_domain_override, pane_line_config.stacked_normalization, pane_line_config.stacked_negative_policy) + + +# NONE keeps the range data-driven. FRACTION and PERCENT need a fixed +# range so every per-X stack fits exactly the available space. +func _apply_pinned_range(p_override: YDomainOverride, + p_normalization: StackedNormalization, p_policy: StackedNegativePolicy) -> void: + if p_normalization == StackedNormalization.NONE: + return + var pinned := StackedPinnedRange.compute(p_normalization, p_policy) + p_override.force_y_range = true + p_override.force_y_min = pinned.x + p_override.force_y_max = pinned.y diff --git a/addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_none.gd b/addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_none.gd index 6b6675e..c164b3c 100644 --- a/addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_none.gd +++ b/addons/tau-plot/tests/bar/stacked/test_bar_stacked_negative_none.gd @@ -35,6 +35,7 @@ func _setup_test_1() -> void: var y_axis := TauAxisConfig.new() y_axis.type = TauAxisConfig.Type.CONTINUOUS y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.tick_count_preferred = 15 var bar_config := TauBarConfig.new() bar_config.mode = TauBarConfig.BarMode.STACKED @@ -94,6 +95,7 @@ func _setup_test_2() -> void: var y_axis := TauAxisConfig.new() y_axis.type = TauAxisConfig.Type.CONTINUOUS y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.tick_count_preferred = 20 var bar_config := TauBarConfig.new() bar_config.mode = TauBarConfig.BarMode.STACKED @@ -161,6 +163,7 @@ func _setup_test_4() -> void: bar_config.mode = TauBarConfig.BarMode.STACKED bar_config.stacked_normalization = TauBarConfig.StackedNormalization.NONE bar_config.stacked_negative_policy = TauBarConfig.StackedNegativePolicy.SKIP_NEGATIVES + y_axis.tick_count_preferred = 15 var pane := TauPaneConfig.new() pane.y_left_axis = y_axis @@ -211,6 +214,7 @@ func _setup_test_5() -> void: var y_axis := TauAxisConfig.new() y_axis.type = TauAxisConfig.Type.CONTINUOUS y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.tick_count_preferred = 20 var bar_config := TauBarConfig.new() bar_config.mode = TauBarConfig.BarMode.STACKED diff --git a/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_fraction.gd b/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_fraction.gd new file mode 100644 index 0000000..35213a6 --- /dev/null +++ b/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_fraction.gd @@ -0,0 +1,351 @@ +@tool +extends Control + +func _ready() -> void: + _setup_test_1() + _setup_test_2() + _setup_test_3() + _setup_test_4() + _setup_test_5() + _setup_test_6() + + +#################################################################################################### +# Test 1 +#################################################################################################### + +func _setup_test_1() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0]) + var y_a := PackedFloat64Array([10.0, 5.0, 15.0, 5.0, 10.0]) + var y_b := PackedFloat64Array([25.0, 30.0, -20.0, 30.0, 25.0]) + var y_c := PackedFloat64Array([20.0, 30.0, 40.0, 30.0, 20.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b, y_c]) + + %TestPlot1.title = "[CONTINUOUS] with stacked_negative_policy = SKIP_NEGATIVES" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.domain_padding_mode = TauAxisConfig.DomainPaddingMode.DATA_UNITS + x_axis.domain_padding_min = 0.5 + x_axis.domain_padding_max = 0.5 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.STACKED + line_config.stacked_normalization = TauLineConfig.StackedNormalization.FRACTION + line_config.stacked_negative_policy = TauLineConfig.StackedNegativePolicy.SKIP_NEGATIVES + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] + + %TestPlot1.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 2 +#################################################################################################### + +func _setup_test_2() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0]) + var y_a := PackedFloat64Array([10.0, 5.0, 15.0, 5.0, 10.0]) + var y_b := PackedFloat64Array([25.0, 30.0, -20.0, 30.0, 25.0]) + var y_c := PackedFloat64Array([20.0, 30.0, 40.0, 30.0, 20.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b, y_c]) + + %TestPlot2.title = "[CONTINUOUS] with stacked_negative_policy = DIVERGING" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.domain_padding_mode = TauAxisConfig.DomainPaddingMode.DATA_UNITS + x_axis.domain_padding_min = 0.5 + x_axis.domain_padding_max = 0.5 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.STACKED + line_config.stacked_normalization = TauLineConfig.StackedNormalization.FRACTION + line_config.stacked_negative_policy = TauLineConfig.StackedNegativePolicy.DIVERGING + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] + + %TestPlot2.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 3 +#################################################################################################### + +func _setup_test_3() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0]) + var y_a := PackedFloat64Array([10.0, 5.0, 15.0, 5.0, 10.0]) + var y_b := PackedFloat64Array([25.0, 30.0, -20.0, 30.0, 25.0]) + var y_c := PackedFloat64Array([20.0, 30.0, 40.0, 30.0, 20.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b, y_c]) + + %TestPlot3.title = "[CONTINUOUS] with stacked_negative_policy = SIGNED_SUM" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.domain_padding_mode = TauAxisConfig.DomainPaddingMode.DATA_UNITS + x_axis.domain_padding_min = 0.5 + x_axis.domain_padding_max = 0.5 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.STACKED + line_config.stacked_normalization = TauLineConfig.StackedNormalization.FRACTION + line_config.stacked_negative_policy = TauLineConfig.StackedNegativePolicy.SIGNED_SUM + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] + + %TestPlot3.plot_xy(dataset, config, bindings) + +#################################################################################################### +# Test 4 +#################################################################################################### + +func _setup_test_4() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedStringArray(["One", "Two", "Three", "Four", "Five"]) + var y_a := PackedFloat64Array([10.0, 5.0, 15.0, 5.0, 10.0]) + var y_b := PackedFloat64Array([25.0, 30.0, -20.0, 30.0, 25.0]) + var y_c := PackedFloat64Array([20.0, 30.0, 40.0, 30.0, 20.0]) + + var dataset := TauPlot.Dataset.make_shared_x_categorical(series_names, x, [y_a, y_b, y_c]) + + %TestPlot4.title = "[CATEGORICAL] with stacked_negative_policy = SKIP_NEGATIVES" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CATEGORICAL + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.STACKED + line_config.stacked_normalization = TauLineConfig.StackedNormalization.FRACTION + line_config.stacked_negative_policy = TauLineConfig.StackedNegativePolicy.SKIP_NEGATIVES + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] + + %TestPlot4.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 5 +#################################################################################################### + +func _setup_test_5() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedStringArray(["One", "Two", "Three", "Four", "Five"]) + var y_a := PackedFloat64Array([10.0, 5.0, 15.0, 5.0, 10.0]) + var y_b := PackedFloat64Array([25.0, 30.0, -20.0, 30.0, 25.0]) + var y_c := PackedFloat64Array([20.0, 30.0, 40.0, 30.0, 20.0]) + + var dataset := TauPlot.Dataset.make_shared_x_categorical(series_names, x, [y_a, y_b, y_c]) + + %TestPlot5.title = "[CATEGORICAL] with stacked_negative_policy = DIVERGING" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CATEGORICAL + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.STACKED + line_config.stacked_normalization = TauLineConfig.StackedNormalization.FRACTION + line_config.stacked_negative_policy = TauLineConfig.StackedNegativePolicy.DIVERGING + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] + + %TestPlot5.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 6 +#################################################################################################### + +func _setup_test_6() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedStringArray(["One", "Two", "Three", "Four", "Five"]) + var y_a := PackedFloat64Array([10.0, 5.0, 15.0, 5.0, 10.0]) + var y_b := PackedFloat64Array([25.0, 30.0, -20.0, 30.0, 25.0]) + var y_c := PackedFloat64Array([20.0, 30.0, 40.0, 30.0, 20.0]) + + var dataset := TauPlot.Dataset.make_shared_x_categorical(series_names, x, [y_a, y_b, y_c]) + + %TestPlot6.title = "[CATEGORICAL] with stacked_negative_policy = SIGNED_SUM" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CATEGORICAL + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.STACKED + line_config.stacked_normalization = TauLineConfig.StackedNormalization.FRACTION + line_config.stacked_negative_policy = TauLineConfig.StackedNegativePolicy.SIGNED_SUM + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] + + %TestPlot6.plot_xy(dataset, config, bindings) diff --git a/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_fraction.gd.uid b/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_fraction.gd.uid new file mode 100644 index 0000000..8712f17 --- /dev/null +++ b/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_fraction.gd.uid @@ -0,0 +1 @@ +uid://t5fdpmq6toqn diff --git a/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_fraction.tscn b/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_fraction.tscn new file mode 100644 index 0000000..e6c96b9 --- /dev/null +++ b/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_fraction.tscn @@ -0,0 +1,85 @@ +[gd_scene load_steps=3 format=3 uid="uid://dfulk3tfeosmh"] + +[ext_resource type="Script" uid="uid://t5fdpmq6toqn" path="res://addons/tau-plot/tests/line/stacked/test_line_stacked_negative_fraction.gd" id="1_kstpx"] +[ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_71bbx"] + +[node name="Bars" type="VBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_kstpx") + +[node name="Title" type="Label" parent="."] +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +text = "Test stacked bars negative policy with normalization = FRACTION" +horizontal_alignment = 1 + +[node name="GridContainer" type="GridContainer" parent="."] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/h_separation = 0 +theme_override_constants/v_separation = 0 +columns = 3 + +[node name="TestPlot1" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_71bbx") +title = "[CONTINUOUS] with stacked_negative_policy = SKIP_NEGATIVES" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot2" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_71bbx") +title = "[CONTINUOUS] with stacked_negative_policy = DIVERGING" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot3" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_71bbx") +title = "[CONTINUOUS] with stacked_negative_policy = SIGNED_SUM" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot4" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_71bbx") +title = "[CATEGORICAL] with stacked_negative_policy = SKIP_NEGATIVES" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot5" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_71bbx") +title = "[CATEGORICAL] with stacked_negative_policy = DIVERGING" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot6" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_71bbx") +title = "[CATEGORICAL] with stacked_negative_policy = SIGNED_SUM" +metadata/_custom_type_script = "uid://dnhvsf2wip771" diff --git a/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_none.gd b/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_none.gd new file mode 100644 index 0000000..a4f44c6 --- /dev/null +++ b/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_none.gd @@ -0,0 +1,358 @@ +@tool +extends Control + +func _ready() -> void: + _setup_test_1() + _setup_test_2() + _setup_test_3() + _setup_test_4() + _setup_test_5() + _setup_test_6() + + +#################################################################################################### +# Test 1 +#################################################################################################### + +func _setup_test_1() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0]) + var y_a := PackedFloat64Array([10.0, 5.0, 15.0, 5.0, 10.0]) + var y_b := PackedFloat64Array([25.0, 30.0, -20.0, 30.0, 25.0]) + var y_c := PackedFloat64Array([20.0, 30.0, 40.0, 30.0, 20.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b, y_c]) + + %TestPlot1.title = "[CONTINUOUS] with stacked_negative_policy = SKIP_NEGATIVES" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.domain_padding_mode = TauAxisConfig.DomainPaddingMode.DATA_UNITS + x_axis.domain_padding_min = 0.5 + x_axis.domain_padding_max = 0.5 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.tick_count_preferred = 10 + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.STACKED + line_config.stacked_normalization = TauLineConfig.StackedNormalization.NONE + line_config.stacked_negative_policy = TauLineConfig.StackedNegativePolicy.SKIP_NEGATIVES + #line_config.gap_policy = TauLineConfig.GapPolicy.BRIDGE + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] + + %TestPlot1.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 2 +#################################################################################################### + +func _setup_test_2() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0]) + var y_a := PackedFloat64Array([10.0, 5.0, 15.0, 5.0, 10.0]) + var y_b := PackedFloat64Array([25.0, 30.0, -20.0, 30.0, 25.0]) + var y_c := PackedFloat64Array([20.0, 30.0, 40.0, 30.0, 20.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b, y_c]) + + %TestPlot2.title = "[CONTINUOUS] with stacked_negative_policy = DIVERGING" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.domain_padding_mode = TauAxisConfig.DomainPaddingMode.DATA_UNITS + x_axis.domain_padding_min = 0.5 + x_axis.domain_padding_max = 0.5 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.tick_count_preferred = 10 + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.STACKED + line_config.stacked_normalization = TauLineConfig.StackedNormalization.NONE + line_config.stacked_negative_policy = TauLineConfig.StackedNegativePolicy.DIVERGING + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] + + %TestPlot2.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 3 +#################################################################################################### + +func _setup_test_3() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0]) + var y_a := PackedFloat64Array([10.0, 5.0, 15.0, 5.0, 10.0]) + var y_b := PackedFloat64Array([25.0, 30.0, -20.0, 30.0, 25.0]) + var y_c := PackedFloat64Array([20.0, 30.0, 40.0, 30.0, 20.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b, y_c]) + + %TestPlot3.title = "[CONTINUOUS] with stacked_negative_policy = SIGNED_SUM" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.domain_padding_mode = TauAxisConfig.DomainPaddingMode.DATA_UNITS + x_axis.domain_padding_min = 0.5 + x_axis.domain_padding_max = 0.5 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.tick_count_preferred = 10 + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.STACKED + line_config.stacked_normalization = TauLineConfig.StackedNormalization.NONE + line_config.stacked_negative_policy = TauLineConfig.StackedNegativePolicy.SIGNED_SUM + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] + + %TestPlot3.plot_xy(dataset, config, bindings) + +#################################################################################################### +# Test 4 +#################################################################################################### + +func _setup_test_4() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedStringArray(["One", "Two", "Three", "Four", "Five"]) + var y_a := PackedFloat64Array([10.0, 5.0, 15.0, 5.0, 10.0]) + var y_b := PackedFloat64Array([25.0, 30.0, -20.0, 30.0, 25.0]) + var y_c := PackedFloat64Array([20.0, 30.0, 40.0, 30.0, 20.0]) + + var dataset := TauPlot.Dataset.make_shared_x_categorical(series_names, x, [y_a, y_b, y_c]) + + %TestPlot4.title = "[CATEGORICAL] with stacked_negative_policy = SKIP_NEGATIVES" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CATEGORICAL + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.tick_count_preferred = 10 + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.STACKED + line_config.stacked_normalization = TauLineConfig.StackedNormalization.NONE + line_config.stacked_negative_policy = TauLineConfig.StackedNegativePolicy.SKIP_NEGATIVES + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] + + %TestPlot4.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 5 +#################################################################################################### + +func _setup_test_5() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedStringArray(["One", "Two", "Three", "Four", "Five"]) + var y_a := PackedFloat64Array([10.0, 5.0, 15.0, 5.0, 10.0]) + var y_b := PackedFloat64Array([25.0, 30.0, -20.0, 30.0, 25.0]) + var y_c := PackedFloat64Array([20.0, 30.0, 40.0, 30.0, 20.0]) + + var dataset := TauPlot.Dataset.make_shared_x_categorical(series_names, x, [y_a, y_b, y_c]) + + %TestPlot5.title = "[CATEGORICAL] with stacked_negative_policy = DIVERGING" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CATEGORICAL + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.tick_count_preferred = 10 + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.STACKED + line_config.stacked_normalization = TauLineConfig.StackedNormalization.NONE + line_config.stacked_negative_policy = TauLineConfig.StackedNegativePolicy.DIVERGING + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] + + %TestPlot5.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 6 +#################################################################################################### + +func _setup_test_6() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedStringArray(["One", "Two", "Three", "Four", "Five"]) + var y_a := PackedFloat64Array([10.0, 5.0, 15.0, 5.0, 10.0]) + var y_b := PackedFloat64Array([25.0, 30.0, -20.0, 30.0, 25.0]) + var y_c := PackedFloat64Array([20.0, 30.0, 40.0, 30.0, 20.0]) + + var dataset := TauPlot.Dataset.make_shared_x_categorical(series_names, x, [y_a, y_b, y_c]) + + %TestPlot6.title = "[CATEGORICAL] with stacked_negative_policy = SIGNED_SUM" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CATEGORICAL + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.tick_count_preferred = 10 + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.STACKED + line_config.stacked_normalization = TauLineConfig.StackedNormalization.NONE + line_config.stacked_negative_policy = TauLineConfig.StackedNegativePolicy.SIGNED_SUM + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] + + %TestPlot6.plot_xy(dataset, config, bindings) diff --git a/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_none.gd.uid b/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_none.gd.uid new file mode 100644 index 0000000..d2952f7 --- /dev/null +++ b/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_none.gd.uid @@ -0,0 +1 @@ +uid://gq3klvlipxkr diff --git a/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_none.tscn b/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_none.tscn new file mode 100644 index 0000000..34ff9ea --- /dev/null +++ b/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_none.tscn @@ -0,0 +1,85 @@ +[gd_scene load_steps=3 format=3 uid="uid://ede28c5yqix3"] + +[ext_resource type="Script" uid="uid://gq3klvlipxkr" path="res://addons/tau-plot/tests/line/stacked/test_line_stacked_negative_none.gd" id="1_t3a0d"] +[ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_q7rrf"] + +[node name="Bars" type="VBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_t3a0d") + +[node name="Title" type="Label" parent="."] +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +text = "Test stacked bars negative policy with normalization = NONE" +horizontal_alignment = 1 + +[node name="GridContainer" type="GridContainer" parent="."] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/h_separation = 0 +theme_override_constants/v_separation = 0 +columns = 3 + +[node name="TestPlot1" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_q7rrf") +title = "[CONTINUOUS] with stacked_negative_policy = SKIP_NEGATIVES" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot2" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_q7rrf") +title = "[CONTINUOUS] with stacked_negative_policy = DIVERGING" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot3" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_q7rrf") +title = "[CONTINUOUS] with stacked_negative_policy = SIGNED_SUM" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot4" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_q7rrf") +title = "[CATEGORICAL] with stacked_negative_policy = SKIP_NEGATIVES" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot5" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_q7rrf") +title = "[CATEGORICAL] with stacked_negative_policy = DIVERGING" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot6" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_q7rrf") +title = "[CATEGORICAL] with stacked_negative_policy = SIGNED_SUM" +metadata/_custom_type_script = "uid://dnhvsf2wip771" diff --git a/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_percent.gd b/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_percent.gd new file mode 100644 index 0000000..87d8014 --- /dev/null +++ b/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_percent.gd @@ -0,0 +1,351 @@ +@tool +extends Control + +func _ready() -> void: + _setup_test_1() + _setup_test_2() + _setup_test_3() + _setup_test_4() + _setup_test_5() + _setup_test_6() + + +#################################################################################################### +# Test 1 +#################################################################################################### + +func _setup_test_1() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0]) + var y_a := PackedFloat64Array([10.0, 5.0, 15.0, 5.0, 10.0]) + var y_b := PackedFloat64Array([25.0, 30.0, -20.0, 30.0, 25.0]) + var y_c := PackedFloat64Array([20.0, 30.0, 40.0, 30.0, 20.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b, y_c]) + + %TestPlot1.title = "[CONTINUOUS] with stacked_negative_policy = SKIP_NEGATIVES" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.domain_padding_mode = TauAxisConfig.DomainPaddingMode.DATA_UNITS + x_axis.domain_padding_min = 0.5 + x_axis.domain_padding_max = 0.5 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.STACKED + line_config.stacked_normalization = TauLineConfig.StackedNormalization.PERCENT + line_config.stacked_negative_policy = TauLineConfig.StackedNegativePolicy.SKIP_NEGATIVES + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] + + %TestPlot1.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 2 +#################################################################################################### + +func _setup_test_2() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0]) + var y_a := PackedFloat64Array([10.0, 5.0, 15.0, 5.0, 10.0]) + var y_b := PackedFloat64Array([25.0, 30.0, -20.0, 30.0, 25.0]) + var y_c := PackedFloat64Array([20.0, 30.0, 40.0, 30.0, 20.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b, y_c]) + + %TestPlot2.title = "[CONTINUOUS] with stacked_negative_policy = DIVERGING" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.domain_padding_mode = TauAxisConfig.DomainPaddingMode.DATA_UNITS + x_axis.domain_padding_min = 0.5 + x_axis.domain_padding_max = 0.5 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.STACKED + line_config.stacked_normalization = TauLineConfig.StackedNormalization.PERCENT + line_config.stacked_negative_policy = TauLineConfig.StackedNegativePolicy.DIVERGING + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] + + %TestPlot2.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 3 +#################################################################################################### + +func _setup_test_3() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0]) + var y_a := PackedFloat64Array([10.0, 5.0, 15.0, 5.0, 10.0]) + var y_b := PackedFloat64Array([25.0, 30.0, -20.0, 30.0, 25.0]) + var y_c := PackedFloat64Array([20.0, 30.0, 40.0, 30.0, 20.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b, y_c]) + + %TestPlot3.title = "[CONTINUOUS] with stacked_negative_policy = SIGNED_SUM" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.domain_padding_mode = TauAxisConfig.DomainPaddingMode.DATA_UNITS + x_axis.domain_padding_min = 0.5 + x_axis.domain_padding_max = 0.5 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.STACKED + line_config.stacked_normalization = TauLineConfig.StackedNormalization.PERCENT + line_config.stacked_negative_policy = TauLineConfig.StackedNegativePolicy.SIGNED_SUM + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] + + %TestPlot3.plot_xy(dataset, config, bindings) + +#################################################################################################### +# Test 4 +#################################################################################################### + +func _setup_test_4() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedStringArray(["One", "Two", "Three", "Four", "Five"]) + var y_a := PackedFloat64Array([10.0, 5.0, 15.0, 5.0, 10.0]) + var y_b := PackedFloat64Array([25.0, 30.0, -20.0, 30.0, 25.0]) + var y_c := PackedFloat64Array([20.0, 30.0, 40.0, 30.0, 20.0]) + + var dataset := TauPlot.Dataset.make_shared_x_categorical(series_names, x, [y_a, y_b, y_c]) + + %TestPlot4.title = "[CATEGORICAL] with stacked_negative_policy = SKIP_NEGATIVES" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CATEGORICAL + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.STACKED + line_config.stacked_normalization = TauLineConfig.StackedNormalization.PERCENT + line_config.stacked_negative_policy = TauLineConfig.StackedNegativePolicy.SKIP_NEGATIVES + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] + + %TestPlot4.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 5 +#################################################################################################### + +func _setup_test_5() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedStringArray(["One", "Two", "Three", "Four", "Five"]) + var y_a := PackedFloat64Array([10.0, 5.0, 15.0, 5.0, 10.0]) + var y_b := PackedFloat64Array([25.0, 30.0, -20.0, 30.0, 25.0]) + var y_c := PackedFloat64Array([20.0, 30.0, 40.0, 30.0, 20.0]) + + var dataset := TauPlot.Dataset.make_shared_x_categorical(series_names, x, [y_a, y_b, y_c]) + + %TestPlot5.title = "[CATEGORICAL] with stacked_negative_policy = DIVERGING" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CATEGORICAL + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.STACKED + line_config.stacked_normalization = TauLineConfig.StackedNormalization.PERCENT + line_config.stacked_negative_policy = TauLineConfig.StackedNegativePolicy.DIVERGING + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] + + %TestPlot5.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 6 +#################################################################################################### + +func _setup_test_6() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedStringArray(["One", "Two", "Three", "Four", "Five"]) + var y_a := PackedFloat64Array([10.0, 5.0, 15.0, 5.0, 10.0]) + var y_b := PackedFloat64Array([25.0, 30.0, -20.0, 30.0, 25.0]) + var y_c := PackedFloat64Array([20.0, 30.0, 40.0, 30.0, 20.0]) + + var dataset := TauPlot.Dataset.make_shared_x_categorical(series_names, x, [y_a, y_b, y_c]) + + %TestPlot6.title = "[CATEGORICAL] with stacked_negative_policy = SIGNED_SUM" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CATEGORICAL + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.STACKED + line_config.stacked_normalization = TauLineConfig.StackedNormalization.PERCENT + line_config.stacked_negative_policy = TauLineConfig.StackedNegativePolicy.SIGNED_SUM + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b, sb_c] + + %TestPlot6.plot_xy(dataset, config, bindings) diff --git a/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_percent.gd.uid b/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_percent.gd.uid new file mode 100644 index 0000000..cd2cc01 --- /dev/null +++ b/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_percent.gd.uid @@ -0,0 +1 @@ +uid://bpbp8mimvrtki diff --git a/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_percent.tscn b/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_percent.tscn new file mode 100644 index 0000000..bd41d1e --- /dev/null +++ b/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_percent.tscn @@ -0,0 +1,83 @@ +[gd_scene load_steps=3 format=3 uid="uid://sifkw67vbj2h"] + +[ext_resource type="Script" uid="uid://bpbp8mimvrtki" path="res://addons/tau-plot/tests/line/stacked/test_line_stacked_negative_percent.gd" id="1_os7bv"] +[ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_tcxo6"] + +[node name="Bars" type="VBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_os7bv") + +[node name="Title" type="Label" parent="."] +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +text = "Test stacked bars negative policy with normalization = PERCENT" +horizontal_alignment = 1 + +[node name="GridContainer" type="GridContainer" parent="."] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/h_separation = 0 +theme_override_constants/v_separation = 0 +columns = 3 + +[node name="TestPlot1" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_tcxo6") +title = "[CONTINUOUS] with stacked_negative_policy = SKIP_NEGATIVES" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot2" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_tcxo6") +title = "[CONTINUOUS] with stacked_negative_policy = DIVERGING" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot3" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_tcxo6") +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot4" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_tcxo6") +title = "[CATEGORICAL] with stacked_negative_policy = SKIP_NEGATIVES" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot5" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_tcxo6") +title = "[CATEGORICAL] with stacked_negative_policy = DIVERGING" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot6" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_tcxo6") +metadata/_custom_type_script = "uid://dnhvsf2wip771" diff --git a/addons/tau-plot/tests/line/stacked/test_line_stacking_order.gd b/addons/tau-plot/tests/line/stacked/test_line_stacking_order.gd new file mode 100644 index 0000000..06f2d48 --- /dev/null +++ b/addons/tau-plot/tests/line/stacked/test_line_stacking_order.gd @@ -0,0 +1,349 @@ +@tool +extends Control + +func _ready() -> void: + _setup_test_1() + _setup_test_2() + _setup_test_3() + _setup_test_4() + _setup_test_5() + _setup_test_6() + + +#################################################################################################### +# Test 1 +#################################################################################################### + +func _setup_test_1() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0]) + var y_a := PackedFloat64Array([5.0, 10.0, 15.0, 20.0]) + var y_b := PackedFloat64Array([25.0, 30.0, 35.0, 40.0]) + var y_c := PackedFloat64Array([30.0, 40.0, 50.0, 60.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b, y_c]) + + %TestPlot1.title = "[CONTINUOUS] with stacked_normalization = NONE" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.domain_padding_mode = TauAxisConfig.DomainPaddingMode.DATA_UNITS + x_axis.domain_padding_min = 0.5 + x_axis.domain_padding_max = 0.5 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.STACKED + line_config.stacked_normalization = TauLineConfig.StackedNormalization.NONE + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_c, sb_a, sb_b] + + %TestPlot1.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 2 +#################################################################################################### + +func _setup_test_2() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0]) + var y_a := PackedFloat64Array([5.0, 10.0, 15.0, 20.0]) + var y_b := PackedFloat64Array([25.0, 30.0, 35.0, 40.0]) + var y_c := PackedFloat64Array([30.0, 40.0, 50.0, 60.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b, y_c]) + + %TestPlot2.title = "[CONTINUOUS] with stacked_normalization = FRACTION" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.domain_padding_mode = TauAxisConfig.DomainPaddingMode.DATA_UNITS + x_axis.domain_padding_min = 0.5 + x_axis.domain_padding_max = 0.5 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.STACKED + line_config.stacked_normalization = TauLineConfig.StackedNormalization.FRACTION + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_b, sb_a, sb_c] + + %TestPlot2.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 3 +#################################################################################################### + +func _setup_test_3() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0]) + var y_a := PackedFloat64Array([5.0, 10.0, 15.0, 20.0]) + var y_b := PackedFloat64Array([25.0, 30.0, 35.0, 40.0]) + var y_c := PackedFloat64Array([30.0, 40.0, 50.0, 60.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b, y_c]) + + %TestPlot3.title = "[CONTINUOUS] with stacked_normalization = PERCENT" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + x_axis.domain_padding_mode = TauAxisConfig.DomainPaddingMode.DATA_UNITS + x_axis.domain_padding_min = 0.5 + x_axis.domain_padding_max = 0.5 + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.format_tick_label = func(label: String) -> String: + return label + "%" + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.STACKED + line_config.stacked_normalization = TauLineConfig.StackedNormalization.PERCENT + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_c, sb_b] + + %TestPlot3.plot_xy(dataset, config, bindings) + +#################################################################################################### +# Test 4 +#################################################################################################### + +func _setup_test_4() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedStringArray(["One", "Two", "Three", "Four"]) + var y_a := PackedFloat64Array([5.0, 10.0, 15.0, 20.0]) + var y_b := PackedFloat64Array([25.0, 30.0, 35.0, 40.0]) + var y_c := PackedFloat64Array([30.0, 40.0, 50.0, 60.0]) + + var dataset := TauPlot.Dataset.make_shared_x_categorical(series_names, x, [y_a, y_b, y_c]) + + %TestPlot4.title = "[CATEGORICAL] with stacked_normalization = NONE" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CATEGORICAL + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.STACKED + line_config.stacked_normalization = TauLineConfig.StackedNormalization.NONE + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_b, sb_a, sb_c] + + %TestPlot4.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 5 +#################################################################################################### + +func _setup_test_5() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedStringArray(["One", "Two", "Three", "Four"]) + var y_a := PackedFloat64Array([5.0, 10.0, 15.0, 20.0]) + var y_b := PackedFloat64Array([25.0, 30.0, 35.0, 40.0]) + var y_c := PackedFloat64Array([30.0, 40.0, 50.0, 60.0]) + + var dataset := TauPlot.Dataset.make_shared_x_categorical(series_names, x, [y_a, y_b, y_c]) + + %TestPlot5.title = "[CATEGORICAL] with stacked_normalization = FRACTION" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CATEGORICAL + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.STACKED + line_config.stacked_normalization = TauLineConfig.StackedNormalization.FRACTION + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_c, sb_a, sb_b] + + %TestPlot5.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 6 +#################################################################################################### + +func _setup_test_6() -> void: + var series_names := PackedStringArray(["Series A", "Series B", "Series C"]) + var x := PackedStringArray(["One", "Two", "Three", "Four"]) + var y_a := PackedFloat64Array([5.0, 10.0, 15.0, 20.0]) + var y_b := PackedFloat64Array([25.0, 30.0, 35.0, 40.0]) + var y_c := PackedFloat64Array([30.0, 40.0, 50.0, 60.0]) + + var dataset := TauPlot.Dataset.make_shared_x_categorical(series_names, x, [y_a, y_b, y_c]) + + %TestPlot6.title = "[CATEGORICAL] with stacked_normalization = PERCENT" + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CATEGORICAL + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.format_tick_label = func(label: String) -> String: + return label + "%" + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.STACKED + line_config.stacked_normalization = TauLineConfig.StackedNormalization.PERCENT + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var sb_c := TauXYSeriesBinding.new() + sb_c.series_id = dataset.get_series_id_by_index(2) + sb_c.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_c.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_b, sb_c, sb_a] + + %TestPlot6.plot_xy(dataset, config, bindings) diff --git a/addons/tau-plot/tests/line/stacked/test_line_stacking_order.gd.uid b/addons/tau-plot/tests/line/stacked/test_line_stacking_order.gd.uid new file mode 100644 index 0000000..b7a4833 --- /dev/null +++ b/addons/tau-plot/tests/line/stacked/test_line_stacking_order.gd.uid @@ -0,0 +1 @@ +uid://cc44m4dwx745i diff --git a/addons/tau-plot/tests/line/stacked/test_line_stacking_order.tscn b/addons/tau-plot/tests/line/stacked/test_line_stacking_order.tscn new file mode 100644 index 0000000..47a2ab1 --- /dev/null +++ b/addons/tau-plot/tests/line/stacked/test_line_stacking_order.tscn @@ -0,0 +1,85 @@ +[gd_scene load_steps=3 format=3 uid="uid://dg287kd0yxvbr"] + +[ext_resource type="Script" uid="uid://cc44m4dwx745i" path="res://addons/tau-plot/tests/line/stacked/test_line_stacking_order.gd" id="1_if76y"] +[ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_e36ro"] + +[node name="Bars" type="VBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_if76y") + +[node name="Title" type="Label" parent="."] +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +text = "Test stacking order which is dataset order, not binding order." +horizontal_alignment = 1 + +[node name="GridContainer" type="GridContainer" parent="."] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/h_separation = 0 +theme_override_constants/v_separation = 0 +columns = 3 + +[node name="TestPlot1" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_e36ro") +title = "[CONTINUOUS] with stacked_normalization = NONE" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot2" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_e36ro") +title = "[CONTINUOUS] with stacked_normalization = FRACTION" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot3" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_e36ro") +title = "[CONTINUOUS] with stacked_normalization = PERCENT" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot4" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_e36ro") +title = "[CATEGORICAL] with stacked_normalization = NONE" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot5" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_e36ro") +title = "[CATEGORICAL] with stacked_normalization = FRACTION" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot6" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_e36ro") +title = "[CATEGORICAL] with stacked_normalization = PERCENT" +metadata/_custom_type_script = "uid://dnhvsf2wip771" From 3ab40d4322cb805d6e78c3d55ae93a37ad63ca33 Mon Sep 17 00:00:00 2001 From: ze2j <46556066+ze2j@users.noreply.github.com> Date: Wed, 13 May 2026 00:06:18 +0200 Subject: [PATCH 23/23] Fill to baseline --- addons/tau-plot/plot/xy/line/line_config.gd | 35 +++ addons/tau-plot/plot/xy/line/line_renderer.gd | 199 ++++++++++-- addons/tau-plot/plot/xy/line/line_style.gd | 75 ++++- .../tau-plot/plot/xy/line/line_validator.gd | 16 + .../line/area/test_line_fill_color_alpha.gd | 285 ++++++++++++++++++ .../area/test_line_fill_color_alpha.gd.uid | 1 + .../line/area/test_line_fill_color_alpha.tscn | 185 ++++++++++++ .../tests/line/area/test_line_fill_linear.gd | 279 +++++++++++++++++ .../line/area/test_line_fill_linear.gd.uid | 1 + .../line/area/test_line_fill_linear.tscn | 185 ++++++++++++ .../line/area/test_line_fill_logarithmic.gd | 276 +++++++++++++++++ .../area/test_line_fill_logarithmic.gd.uid | 1 + .../line/area/test_line_fill_logarithmic.tscn | 185 ++++++++++++ .../tests/line/dash/test_line_dash.tscn | 2 +- .../test_line_interpolation_linear.tscn | 2 +- .../test_line_interpolation_logarithmic.tscn | 2 +- ...est_line_visual_attributes_dash_alpha.tscn | 2 +- ...est_line_visual_attributes_dash_color.tscn | 2 +- ...st_line_visual_attributes_solid_alpha.tscn | 2 +- ...st_line_visual_attributes_solid_color.tscn | 2 +- ...test_line_visual_callbacks_dash_alpha.tscn | 2 +- ...test_line_visual_callbacks_dash_color.tscn | 2 +- ...est_line_visual_callbacks_solid_alpha.tscn | 2 +- ...est_line_visual_callbacks_solid_color.tscn | 2 +- .../line/log_scale/test_line_logarithmic.tscn | 2 +- .../nan_or_inf/test_line_nan_inf_bridge.gd | 16 + .../nan_or_inf/test_line_nan_inf_bridge.tscn | 18 +- .../line/nan_or_inf/test_line_nan_inf_skip.gd | 16 + .../nan_or_inf/test_line_nan_inf_skip.tscn | 2 +- .../test_line_stacked_negative_fraction.tscn | 2 +- .../test_line_stacked_negative_none.tscn | 2 +- .../test_line_stacked_negative_percent.tscn | 4 +- .../stacked/test_line_stacking_order.tscn | 2 +- 33 files changed, 1752 insertions(+), 57 deletions(-) create mode 100644 addons/tau-plot/tests/line/area/test_line_fill_color_alpha.gd create mode 100644 addons/tau-plot/tests/line/area/test_line_fill_color_alpha.gd.uid create mode 100644 addons/tau-plot/tests/line/area/test_line_fill_color_alpha.tscn create mode 100644 addons/tau-plot/tests/line/area/test_line_fill_linear.gd create mode 100644 addons/tau-plot/tests/line/area/test_line_fill_linear.gd.uid create mode 100644 addons/tau-plot/tests/line/area/test_line_fill_linear.tscn create mode 100644 addons/tau-plot/tests/line/area/test_line_fill_logarithmic.gd create mode 100644 addons/tau-plot/tests/line/area/test_line_fill_logarithmic.gd.uid create mode 100644 addons/tau-plot/tests/line/area/test_line_fill_logarithmic.tscn diff --git a/addons/tau-plot/plot/xy/line/line_config.gd b/addons/tau-plot/plot/xy/line/line_config.gd index e9e362b..d109c28 100644 --- a/addons/tau-plot/plot/xy/line/line_config.gd +++ b/addons/tau-plot/plot/xy/line/line_config.gd @@ -78,6 +78,37 @@ enum InterpolationMode } @export var interpolation_mode: InterpolationMode = InterpolationMode.LINEAR +## Strategy applied to fill the area between the line and a reference +## baseline. The visual appearance of the fill is controlled by +## [TauLineStyle]. +## +## NONE leaves the area below the line unfilled. +## +## TO_BASELINE fills the area between the line and the constant value +## [member fill_baseline]. When the curve crosses the baseline between two +## consecutive samples, the fill is split into multiple sub-polygons at the +## crossings, one per same-side run. +## +## STACKED is only meaningful when [member mode] is STACKED. Each stacked +## layer is filled between its own curve and the top of the layer below. +## The bottom layer is filled down to y = 0. +## +## This property is visual-only and does not affect layout or domain. +enum FillMode +{ + NONE, ## No area is filled. + TO_BASELINE, ## Fill between the line and [member fill_baseline]. + STACKED ## Fill each stacked layer between its curve and the layer below. +} +@export var fill_mode: FillMode = FillMode.NONE + +## Y value used as the reference baseline when [member fill_mode] is +## TO_BASELINE. Expressed in data units on the series y-axis. Ignored for +## any other [member fill_mode] value. +## +## This property is visual-only and does not affect layout or domain. +@export var fill_baseline: float = 0.0 + ## Maximum pixel distance from the cursor to a sample position for the sample ## to be considered a hover hit. ## @@ -130,6 +161,10 @@ func is_equal_to(p_other: TauPaneOverlayConfig) -> bool: return false if interpolation_mode != other.interpolation_mode: return false + if fill_mode != other.fill_mode: + return false + if fill_baseline != other.fill_baseline: + return false if hover_max_distance_px != other.hover_max_distance_px: return false diff --git a/addons/tau-plot/plot/xy/line/line_renderer.gd b/addons/tau-plot/plot/xy/line/line_renderer.gd index 1c324d3..3e1bd15 100644 --- a/addons/tau-plot/plot/xy/line/line_renderer.gd +++ b/addons/tau-plot/plot/xy/line/line_renderer.gd @@ -86,6 +86,19 @@ const StackedSeriesValues := preload("res://addons/tau-plot/plot/xy/stacked_seri # - LineHitRecord.y_plotted_value carries the cumulative top. # LineHitRecord.y_raw_value carries the original dataset value. # +# Area fill: +# - When TauLineConfig.fill_mode is TO_BASELINE, the area between the line +# and the constant TauLineConfig.fill_baseline is filled before the line +# is drawn. The polygon is built from the rendered polyline (after +# interpolation has materialized any synthetic vertices) and is split at +# every baseline crossing into same-side sub-polygons, each emitted as +# one draw_colored_polygon call. The line itself is unaffected by the +# split and remains one draw call per contiguous run. +# - Fill color resolution: when TauLineStyle.fill_color is the sentinel +# Color(0, 0, 0, 0), the per-series color from TauXYStyle.series_colors +# is used. Otherwise the flat TauLineStyle.fill_color wins. The resolved +# color's alpha channel is multiplied by TauLineStyle.fill_alpha. +# # LineValidator is expected to enforce binding-level typing constraints. class LineRenderer extends Control: # Number of sub-segments inserted between two consecutive samples by @@ -207,25 +220,17 @@ class LineRenderer extends Control: func _draw() -> void: _hit_records.clear() - if _line_style == null: - push_error("LineRenderer: resolved TauLineStyle is null.") - return - var pane_rect := _layout.get_pane_rect(_pane_index) if pane_rect.size.x <= 0.0 or pane_rect.size.y <= 0.0: return - var series_count := _get_line_series_count() - if series_count <= 0: - return - var stacked_values: StackedSeriesValues = null if _line_config.mode == TauLineConfig.LineMode.STACKED: stacked_values = StackedSeriesValues.new(_dataset, _line_series_ids, _line_config.stacked_normalization, _line_config.stacked_negative_policy) - var draw_order := _get_series_draw_order(series_count) + var draw_order := _get_series_draw_order(_get_line_series_count()) for draw_rank in range(draw_order.size()): var series_index: int = draw_order[draw_rank] _draw_series(series_index, stacked_values) @@ -240,8 +245,7 @@ class LineRenderer extends Control: # - BRIDGE drops the sample and keeps appending into the same run. # - A run of fewer than two points is discarded. func _draw_series(p_series_index: int, p_stacked: StackedSeriesValues) -> void: - var x_cfg := _get_x_axis_config() - if x_cfg != null and x_cfg.type == TauAxisConfig.Type.CATEGORICAL: + if _get_x_axis_config().type == TauAxisConfig.Type.CATEGORICAL: _draw_series_categorical(p_series_index, p_stacked) else: _draw_series_continuous(p_series_index, p_stacked) @@ -259,6 +263,11 @@ class LineRenderer extends Control: var bridge: bool = _line_config.gap_policy == TauLineConfig.GapPolicy.BRIDGE var interpolation: TauLineConfig.InterpolationMode = _line_config.interpolation_mode + # Resolved once per series: every run of this series fills against the + # same baseline and the same color. + var fill_color: Color = _resolve_series_fill_color(global_series_index) + var baseline_y_px: float = _resolve_fill_baseline_y_px(y_axis_id) + var independent_y: PackedFloat64Array if p_stacked == null: independent_y = _build_independent_y_row(series_id) @@ -280,7 +289,7 @@ class LineRenderer extends Control: var x_value: float = float(_dataset.get_shared_x(i)) if is_shared_x else float(_dataset.get_series_x(series_id, i)) if is_nan(x_value) or is_inf(x_value) or not _is_x_value_valid_for_scale(x_value): if not bridge: - _finalize_run(run, run_colors, real_polyline_indices, real_dataset_indices, series_id, width_px, hover_width_px, interpolation, dash_px) + _finalize_run(run, run_colors, real_polyline_indices, real_dataset_indices, series_id, width_px, hover_width_px, interpolation, dash_px, fill_color, baseline_y_px) run = PackedVector2Array() run_colors = PackedColorArray() real_polyline_indices = PackedInt32Array() @@ -298,7 +307,7 @@ class LineRenderer extends Control: if is_nan(y_plotted): if not bridge: - _finalize_run(run, run_colors, real_polyline_indices, real_dataset_indices, series_id, width_px, hover_width_px, interpolation, dash_px) + _finalize_run(run, run_colors, real_polyline_indices, real_dataset_indices, series_id, width_px, hover_width_px, interpolation, dash_px, fill_color, baseline_y_px) run = PackedVector2Array() run_colors = PackedColorArray() real_polyline_indices = PackedInt32Array() @@ -324,7 +333,7 @@ class LineRenderer extends Control: record.screen_position = screen_pos _hit_records.append(record) - _finalize_run(run, run_colors, real_polyline_indices, real_dataset_indices, series_id, width_px, hover_width_px, interpolation, dash_px) + _finalize_run(run, run_colors, real_polyline_indices, real_dataset_indices, series_id, width_px, hover_width_px, interpolation, dash_px, fill_color, baseline_y_px) func _draw_series_categorical(p_series_index: int, p_stacked: StackedSeriesValues) -> void: @@ -339,6 +348,11 @@ class LineRenderer extends Control: var bridge: bool = _line_config.gap_policy == TauLineConfig.GapPolicy.BRIDGE var interpolation: TauLineConfig.InterpolationMode = _line_config.interpolation_mode + # Resolved once per series: every run of this series fills against the + # same baseline and the same color. + var fill_color: Color = _resolve_series_fill_color(global_series_index) + var baseline_y_px: float = _resolve_fill_baseline_y_px(y_axis_id) + var independent_y: PackedFloat64Array if p_stacked == null: independent_y = _build_independent_y_row(series_id) @@ -362,7 +376,7 @@ class LineRenderer extends Control: if is_nan(y_plotted): if not bridge: - _finalize_run(run, run_colors, real_polyline_indices, real_dataset_indices, series_id, width_px, hover_width_px, interpolation, dash_px) + _finalize_run(run, run_colors, real_polyline_indices, real_dataset_indices, series_id, width_px, hover_width_px, interpolation, dash_px, fill_color, baseline_y_px) run = PackedVector2Array() run_colors = PackedColorArray() real_polyline_indices = PackedInt32Array() @@ -387,7 +401,7 @@ class LineRenderer extends Control: record.screen_position = screen_pos _hit_records.append(record) - _finalize_run(run, run_colors, real_polyline_indices, real_dataset_indices, series_id, width_px, hover_width_px, interpolation, dash_px) + _finalize_run(run, run_colors, real_polyline_indices, real_dataset_indices, series_id, width_px, hover_width_px, interpolation, dash_px, fill_color, baseline_y_px) # Marks dropped samples (NaN, Inf, log-axis violations) as NAN up front @@ -443,6 +457,12 @@ class LineRenderer extends Control: # For SMOOTH_MONOTONE the run is first replaced by its Fritsch-Carlson piecewise cubic resampling. # Runs of fewer than two points are silently dropped. # + # When p_fill_color has non-zero alpha and p_baseline_y_px is finite, + # the area between the rendered polyline and the horizontal baseline + # is filled before the line is drawn. The polygon is split at every + # baseline crossing into same-side sub-polygons, each emitted as one + # draw call. The line itself is unaffected by splitting. + # # Path selection per series (no hover): # - p_dash_px == 0: one draw_polyline_colors call. # - p_dash_px > 0: one draw_multiline_colors call after dash precomputation. @@ -466,7 +486,7 @@ class LineRenderer extends Control: # real_polyline_indices[k] is the index in p_run where the k-th real # sample landed. real_dataset_indices[k] is the dataset sample index for # that real sample. - func _finalize_run(p_run: PackedVector2Array, p_run_colors: PackedColorArray, p_real_polyline_indices: PackedInt32Array, p_real_dataset_indices: PackedInt32Array, p_series_id: int, p_width_px: float, p_hover_width_px: float, p_mode: TauLineConfig.InterpolationMode, p_dash_px: int) -> void: + func _finalize_run(p_run: PackedVector2Array, p_run_colors: PackedColorArray, p_real_polyline_indices: PackedInt32Array, p_real_dataset_indices: PackedInt32Array, p_series_id: int, p_width_px: float, p_hover_width_px: float, p_mode: TauLineConfig.InterpolationMode, p_dash_px: int, p_fill_color: Color, p_baseline_y_px: float) -> void: var polyline: PackedVector2Array = p_run var polyline_colors: PackedColorArray = p_run_colors var real_polyline_indices: PackedInt32Array = p_real_polyline_indices @@ -481,6 +501,11 @@ class LineRenderer extends Control: if polyline.size() < 2: return + # Fill is drawn first so the polyline lands on top of it. A NaN + # baseline or zero-alpha color means no fill for this run. + if not is_nan(p_baseline_y_px) and p_fill_color.a > 0.0: + _draw_fill_to_baseline(polyline, p_fill_color, p_baseline_y_px) + var dash_px: int = max(p_dash_px, 0) var slice_bounds := _resolve_hover_slice_bounds(real_polyline_indices, p_real_dataset_indices, p_series_id) @@ -678,6 +703,133 @@ class LineRenderer extends Control: if segments.size() >= 2: draw_multiline_colors(segments, segment_colors, p_width_px) + #################################################################################################### + # Area fill + #################################################################################################### + + # Resolves the per-series flat fill color, or fully transparent when no + # flat fill applies. Only TO_BASELINE produces a flat fill polygon. NONE + # leaves the area below the line unfilled, and STACKED is handled by the + # per-layer fill path rather than per-run flat fill. + func _resolve_series_fill_color(p_global_series_index: int) -> Color: + if _line_config.fill_mode != TauLineConfig.FillMode.TO_BASELINE: + return Color(0, 0, 0, 0) + return _line_style.get_series_fill_color(p_global_series_index, _xy_style) + + + # Returns the screen-space Y coordinate of the TO_BASELINE baseline, or + # NAN when no flat baseline applies. + func _resolve_fill_baseline_y_px(p_y_axis_id: AxisId) -> float: + if _line_config.fill_mode != TauLineConfig.FillMode.TO_BASELINE: + return NAN + var y_px: float = _layout.map_y_to_px(_pane_index, _line_config.fill_baseline, p_y_axis_id) + var screen_pos: Vector2 = _layout.map_point_to_screen(0.0, y_px) + return screen_pos.y + + + # Builds and draws the fill polygon between p_polyline and the horizontal + # line y = p_baseline_y_px in screen space. The polygon is split at every + # baseline crossing, producing one sub-polygon per same-side run, each + # emitted as a single draw_colored_polygon call. + # + # Crossing detection runs on the rendered polyline, so the synthetic + # vertices inserted by step interpolation and the sub-samples emitted by + # SMOOTH_MONOTONE are treated uniformly. A crossing point lies at the + # linear interpolation of the two flanking polyline vertices against + # the baseline. Both polyline orderings (ascending or descending screen + # X) are accepted because the builder never assumes a direction. + func _draw_fill_to_baseline(p_polyline: PackedVector2Array, p_fill_color: Color, p_baseline_y_px: float) -> void: + var n: int = p_polyline.size() + if n < 2: + return + + # Same-side accumulator. The current sub-polygon's top edge is the + # vertices buffered in `top`. `side` is the sign of (y - baseline_y) + # for the first non-on-baseline vertex of the run, and stays 0 until + # one is found. On-baseline vertices are appended to `top` and do + # not constrain `side`. + var top := PackedVector2Array() + var side: int = 0 + + for i in range(n): + var v: Vector2 = p_polyline[i] + var v_side: int = _baseline_side(v.y, p_baseline_y_px) + + if top.is_empty(): + top.append(v) + side = v_side + continue + + # Treat on-baseline as a degenerate crossing: the vertex sits on + # both half-planes, so it can extend the current run AND open a + # new one without a synthetic crossing. + if v_side == 0 or side == 0 or v_side == side: + top.append(v) + if side == 0: + side = v_side + continue + + # v_side and side are non-zero and opposite: a real crossing + # between top[-1] and v. The crossing point closes the current + # sub-polygon and seeds the next one. + var prev: Vector2 = top[top.size() - 1] + var crossing: Vector2 = _baseline_crossing(prev, v, p_baseline_y_px) + top.append(crossing) + _emit_fill_subpolygon(top, p_fill_color, p_baseline_y_px) + top = PackedVector2Array() + top.append(crossing) + top.append(v) + side = v_side + + _emit_fill_subpolygon(top, p_fill_color, p_baseline_y_px) + + + # Closes one sub-polygon by dropping its endpoints to the baseline and + # hands it to draw_colored_polygon. Runs that contain fewer than two + # points or that are entirely on the baseline contribute no area and + # are skipped. + func _emit_fill_subpolygon(p_top: PackedVector2Array, p_fill_color: Color, p_baseline_y_px: float) -> void: + var top_count: int = p_top.size() + if top_count < 2: + return + # A run sitting exactly on the baseline has zero area. + var any_off_baseline: bool = false + for k in range(top_count): + if p_top[k].y != p_baseline_y_px: + any_off_baseline = true + break + if not any_off_baseline: + return + + var polygon := PackedVector2Array() + polygon.resize(top_count + 2) + # Top edge in forward order, then closing edge down to the baseline. + for k in range(top_count): + polygon[k] = p_top[k] + polygon[top_count] = Vector2(p_top[top_count - 1].x, p_baseline_y_px) + polygon[top_count + 1] = Vector2(p_top[0].x, p_baseline_y_px) + draw_colored_polygon(polygon, p_fill_color) + + + # Returns +1 above the baseline (smaller screen Y), -1 below, 0 on it. + # Screen space is Y-down so "above the baseline in data space" means + # "smaller Y in screen space". + static func _baseline_side(p_y: float, p_baseline_y: float) -> int: + if p_y < p_baseline_y: + return 1 + if p_y > p_baseline_y: + return -1 + return 0 + + + # Linear interpolation along segment (p_a, p_b) at the parameter where + # y reaches p_baseline_y. Precondition: p_a.y and p_b.y straddle + # p_baseline_y with a non-zero gap, which is guaranteed by the caller. + static func _baseline_crossing(p_a: Vector2, p_b: Vector2, p_baseline_y: float) -> Vector2: + var t: float = (p_baseline_y - p_a.y) / (p_b.y - p_a.y) + return Vector2(p_a.x + t * (p_b.x - p_a.x), p_baseline_y) + + #################################################################################################### # Smooth-monotone (Fritsch-Carlson) resampling #################################################################################################### @@ -1046,20 +1198,11 @@ class LineRenderer extends Control: #################################################################################################### func _is_x_value_valid_for_scale(p_x_value: float) -> bool: - var x_cfg := _layout.domain.config.x_axis - if x_cfg == null: - return true - if x_cfg.scale == TauAxisConfig.Scale.LOGARITHMIC and p_x_value <= 0.0: - return false - return true + return _layout.domain.config.x_axis.scale != TauAxisConfig.Scale.LOGARITHMIC or p_x_value > 0.0 func _is_y_value_valid_for_scale(p_series_id: int, p_y_value: float) -> bool: var y_axis_id := _get_y_axis_id_for_series(p_series_id) var pane_cfg: TauPaneConfig = _layout.domain.config.panes[_pane_index] var y_cfg: TauAxisConfig = pane_cfg.get_y_axis_config(y_axis_id) - if y_cfg == null: - return true - if y_cfg.scale == TauAxisConfig.Scale.LOGARITHMIC and p_y_value <= 0.0: - return false - return true + return y_cfg.scale != TauAxisConfig.Scale.LOGARITHMIC or p_y_value > 0.0 diff --git a/addons/tau-plot/plot/xy/line/line_style.gd b/addons/tau-plot/plot/xy/line/line_style.gd index 4b2b486..f292938 100644 --- a/addons/tau-plot/plot/xy/line/line_style.gd +++ b/addons/tau-plot/plot/xy/line/line_style.gd @@ -50,6 +50,24 @@ const DEFAULT_HOVERED_LINE_WIDTHS_PX: Array[float] = [3.0] const DEFAULT_DASH_LENGTHS_PX: Array[int] = [0] @export var dash_lengths_px: Array[int] = [0] +## Flat color applied to the area defined by [member TauLineConfig.fill_mode]. +## The sentinel [code]Color(0, 0, 0, 0)[/code] means "derive from the series +## color supplied by [member TauXYStyle.series_colors]". Any other value +## becomes a uniform flat fill shared across all series in the overlay. +## [code]Color(0, 0, 0, 0)[/code] is therefore not a valid explicit fill +## color. +## +## The resolved color has its alpha channel multiplied by +## [member fill_alpha] before rasterization. +const DEFAULT_FILL_COLOR: Color = Color(0, 0, 0, 0) +@export var fill_color: Color = DEFAULT_FILL_COLOR + +## Multiplier applied to the alpha channel of the resolved fill color, +## regardless of whether the color came from [member fill_color] or from +## [member TauXYStyle.series_colors]. Valid range is [code][0.0, 1.0][/code]. +const DEFAULT_FILL_ALPHA: float = 0.2 +@export var fill_alpha: float = DEFAULT_FILL_ALPHA + #################################################################################################### # Helpers @@ -88,22 +106,47 @@ func get_series_dash_px(p_series_index: int) -> int: return max(entry, 0) +## Returns the resolved fill color for the given series index. +## +## When [member fill_color] is the sentinel [code]Color(0, 0, 0, 0)[/code] +## the per-series color from [param p_xy_style] is used. Otherwise the flat +## [member fill_color] wins regardless of the series. In both cases the +## alpha channel of the returned color is multiplied by [member fill_alpha], +## clamped to [code][0.0, 1.0][/code]. +func get_series_fill_color(p_series_index: int, p_xy_style: TauXYStyle) -> Color: + var base: Color + if fill_color == DEFAULT_FILL_COLOR: + base = p_xy_style.get_series_color(p_series_index) + else: + base = fill_color + base.a = clampf(base.a * fill_alpha, 0.0, 1.0) + return base + + #################################################################################################### # Cascade: theme loading (layer 2) #################################################################################################### ## Loads properties from the Godot theme attached to [param p_control]. ## -## All properties on this resource are per-series arrays. For each, a -## two-level indexed lookup applies at series granularity: +## The per-series array properties use a two-level indexed lookup at series +## granularity: ## 1. [code]_N[/code] sets the value for series N across all panes. ## 2. [code]_N_P[/code] overrides series N in pane P only. ## -## Theme key prefixes: +## Theme key prefixes for the per-series arrays: ## - [member line_widths_px]: [code]line_width_px[/code] ## - [member hovered_line_widths_px]: [code]line_hovered_width_px[/code] ## - [member dash_lengths_px]: [code]line_dash_px[/code] ## +## The scalar fill properties use a non-indexed base key plus a per-pane +## indexed key that overwrites the base value for the matching pane: +## - [member fill_color]: [code]line_fill_color[/code] and +## [code]line_fill_color_P[/code]. +## - [member fill_alpha]: [code]line_fill_alpha_percent[/code] and +## [code]line_fill_alpha_percent_P[/code]. Stored as a percentage in +## the theme because theme constants are integers. +## ## This method writes every property unconditionally because it is called on ## the resolved instance, not on the user-provided resource. func load_from_theme(p_control: Control, p_pane_index: int) -> void: @@ -190,6 +233,23 @@ func load_from_theme(p_control: Control, p_pane_index: int) -> void: dash_lengths_px[pane_dash_index] = max(int(p_control.get_theme_constant(key)), 0) pane_dash_index += 1 + # fill_color: non-indexed key first, then per-pane indexed key overwrites. + if p_control.has_theme_color(&"line_fill_color"): + fill_color = p_control.get_theme_color(&"line_fill_color") + var indexed_fill_color_key := StringName("line_fill_color_%d" % p_pane_index) + if p_control.has_theme_color(indexed_fill_color_key): + fill_color = p_control.get_theme_color(indexed_fill_color_key) + + # fill_alpha: stored as a percentage in the theme because theme constants + # are integers. Non-indexed key first, then per-pane indexed key overwrites. + if p_control.has_theme_constant(&"line_fill_alpha_percent"): + var alpha_percent := p_control.get_theme_constant(&"line_fill_alpha_percent") + fill_alpha = clampf(float(alpha_percent) / 100.0, 0.0, 1.0) + var indexed_fill_alpha_key := StringName("line_fill_alpha_percent_%d" % p_pane_index) + if p_control.has_theme_constant(indexed_fill_alpha_key): + var alpha_percent_p := p_control.get_theme_constant(indexed_fill_alpha_key) + fill_alpha = clampf(float(alpha_percent_p) / 100.0, 0.0, 1.0) + #################################################################################################### # Cascade: user overrides (layer 3) @@ -214,6 +274,11 @@ func apply_overrides_from(p_user_style: TauLineStyle) -> void: if _is_dash_lengths_overridden(p_user_style.dash_lengths_px): dash_lengths_px = p_user_style.dash_lengths_px.duplicate() + if p_user_style.fill_color != DEFAULT_FILL_COLOR: + fill_color = p_user_style.fill_color + if p_user_style.fill_alpha != DEFAULT_FILL_ALPHA: + fill_alpha = clampf(p_user_style.fill_alpha, 0.0, 1.0) + #################################################################################################### # Full cascade resolution @@ -262,6 +327,10 @@ func is_equal_to(p_other: TauLineStyle) -> bool: for i in range(dash_lengths_px.size()): if dash_lengths_px[i] != p_other.dash_lengths_px[i]: return false + if fill_color != p_other.fill_color: + return false + if fill_alpha != p_other.fill_alpha: + return false return true diff --git a/addons/tau-plot/plot/xy/line/line_validator.gd b/addons/tau-plot/plot/xy/line/line_validator.gd index 5488adc..f21e741 100644 --- a/addons/tau-plot/plot/xy/line/line_validator.gd +++ b/addons/tau-plot/plot/xy/line/line_validator.gd @@ -49,6 +49,7 @@ class LineValidator extends RefCounted: var is_shared_x := (p_dataset.get_mode() == Dataset.Mode.SHARED_X) _validate_line_mode_constraints(p_pane_index, line_config, pane_cfg, p_line_overlay_bindings, is_shared_x, p_result) + _validate_fill_constraints(p_pane_index, line_config, pane_cfg, p_line_overlay_bindings, p_result) #################################################################################################### @@ -91,3 +92,18 @@ class LineValidator extends RefCounted: _: p_result.add_error("LineValidator: pane %d: unsupported line mode %d" % [p_pane_index, p_line_config.mode]) + + + static func _validate_fill_constraints(p_pane_index: int, p_line_config: TauLineConfig, p_pane_cfg: TauPaneConfig, p_line_overlay_bindings: Array[TauXYSeriesBinding], p_result: ValidationResult) -> void: + if p_line_config.fill_mode != TauLineConfig.FillMode.TO_BASELINE: + return + if p_line_config.fill_baseline > 0.0: + return + # A non-positive baseline cannot be mapped on a logarithmic scale, + # so the fill is rejected as soon as any line series in this pane + # binds to such an axis. + for binding in p_line_overlay_bindings: + var y_axis_config: TauAxisConfig = p_pane_cfg.get_y_axis_config(binding.y_axis_id) + if y_axis_config.scale == TauAxisConfig.Scale.LOGARITHMIC: + p_result.add_error("LineValidator: pane %d: fill_mode TO_BASELINE requires fill_baseline > 0 on a logarithmic y axis, got %s" % [p_pane_index, p_line_config.fill_baseline]) + return diff --git a/addons/tau-plot/tests/line/area/test_line_fill_color_alpha.gd b/addons/tau-plot/tests/line/area/test_line_fill_color_alpha.gd new file mode 100644 index 0000000..2d3aa8c --- /dev/null +++ b/addons/tau-plot/tests/line/area/test_line_fill_color_alpha.gd @@ -0,0 +1,285 @@ +@tool +extends Control + +func _ready() -> void: + _setup_test_1() + _setup_test_2() + _setup_test_3() + _setup_test_4() + _setup_test_5() + _setup_test_6() + _setup_test_7() + _setup_test_8() + _setup_test_9() + _setup_test_10() + _setup_test_11() + _setup_test_12() + _setup_test_13() + _setup_test_14() + _setup_test_15() + +#################################################################################################### +# Helpers +#################################################################################################### + +func make_shared_x_continuous_plot(p_plot: TauPlot, p_title: String, p_interpolation: TauLineConfig.InterpolationMode) -> void: + var series_names := PackedStringArray(["Series A", "Series B"]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0]) + var y_a := PackedFloat64Array([10.0, 20.0, 50.0, 15.0, 10.0]) + var y_b := PackedFloat64Array([50.0, 40.0, 35.0, 5.0, 20.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b]) + + p_plot.title = p_title + p_plot.legend_enabled = true + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.tick_count_preferred = 10 + y_axis.overlap_strategy = TauAxisConfig.OverlapStrategy.NONE + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.INDEPENDENT + line_config.interpolation_mode = p_interpolation + line_config.fill_mode = TauLineConfig.FillMode.TO_BASELINE + line_config.fill_baseline = 10. + line_config.style.fill_color = Color(0.5, 0.5, 0.5) + line_config.style.fill_alpha = 0.5 + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b] + + p_plot.plot_xy(dataset, config, bindings) + + +func make_per_series_x_continuous_plot(p_plot: TauPlot, p_title: String, p_interpolation: TauLineConfig.InterpolationMode) -> void: + var series_names := PackedStringArray(["Series A", "Series B"]) + var x_a := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0]) + var y_a := PackedFloat64Array([10.0, 20.0, 50.0, 15.0, 10.0]) + var x_b := PackedFloat64Array([10.5, 11.5, 12.5, 13.5]) + var y_b := PackedFloat64Array([40.0, 35.0, 5.0, 20.0]) + + var dataset := TauPlot.Dataset.make_per_series_x_continuous(series_names, [x_a, x_b], [y_a, y_b]) + + p_plot.title = p_title + p_plot.legend_enabled = false + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.tick_count_preferred = 10 + y_axis.overlap_strategy = TauAxisConfig.OverlapStrategy.NONE + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.INDEPENDENT + line_config.interpolation_mode = p_interpolation + line_config.fill_mode = TauLineConfig.FillMode.TO_BASELINE + line_config.fill_baseline = 10. + line_config.style.fill_color = Color(0.5, 0.5, 0.5) + line_config.style.fill_alpha = 0.5 + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b] + + p_plot.plot_xy(dataset, config, bindings) + + +func make_shared_x_categorical_plot(p_plot: TauPlot, p_title: String, p_interpolation: TauLineConfig.InterpolationMode) -> void: + var series_names := PackedStringArray(["Series A", "Series B"]) + var x := PackedStringArray(["One", "Two", "Three", "Four", "Five"]) + var y_a := PackedFloat64Array([10.0, 20.0, 50.0, 15.0, 10.0]) + var y_b := PackedFloat64Array([50.0, 40.0, 35.0, 5.0, 20.0]) + + var dataset := TauPlot.Dataset.make_shared_x_categorical(series_names, x, [y_a, y_b]) + + p_plot.title = p_title + p_plot.legend_enabled = false + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CATEGORICAL + x_axis.scale = TauAxisConfig.Scale.LINEAR + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.tick_count_preferred = 10 + y_axis.overlap_strategy = TauAxisConfig.OverlapStrategy.NONE + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.INDEPENDENT + line_config.interpolation_mode = p_interpolation + line_config.fill_mode = TauLineConfig.FillMode.TO_BASELINE + line_config.fill_baseline = 10. + line_config.style.fill_color = Color(0.5, 0.5, 0.5) + line_config.style.fill_alpha = 0.5 + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b] + + p_plot.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 1 +#################################################################################################### + +func _setup_test_1() -> void: + make_shared_x_continuous_plot(%TestPlot1, "[LINEAR] SHARED_X + CONTINUOUS", TauLineConfig.InterpolationMode.LINEAR) + +#################################################################################################### +# Test 2 +#################################################################################################### + +func _setup_test_2() -> void: + make_per_series_x_continuous_plot(%TestPlot2, "[LINEAR] PER_SERIES_X + CONTINUOUS", TauLineConfig.InterpolationMode.LINEAR) + +#################################################################################################### +# Test 3 +#################################################################################################### + +func _setup_test_3() -> void: + make_shared_x_categorical_plot(%TestPlot3, "[LINEAR] SHARED_X + CATEGORICAL", TauLineConfig.InterpolationMode.LINEAR) + +#################################################################################################### +# Test 4 +#################################################################################################### + +func _setup_test_4() -> void: + make_shared_x_continuous_plot(%TestPlot4, "[STEP_BEFORE] SHARED_X + CONTINUOUS", TauLineConfig.InterpolationMode.STEP_BEFORE) + +#################################################################################################### +# Test 5 +#################################################################################################### + +func _setup_test_5() -> void: + make_per_series_x_continuous_plot(%TestPlot5, "[STEP_BEFORE] PER_SERIES_X + CONTINUOUS", TauLineConfig.InterpolationMode.STEP_BEFORE) + +#################################################################################################### +# Test 6 +#################################################################################################### + +func _setup_test_6() -> void: + make_shared_x_categorical_plot(%TestPlot6, "[STEP_BEFORE] SHARED_X + CATEGORICAL", TauLineConfig.InterpolationMode.STEP_BEFORE) + +#################################################################################################### +# Test 7 +#################################################################################################### + +func _setup_test_7() -> void: + make_shared_x_continuous_plot(%TestPlot7, "[STEP_MIDDLE] SHARED_X + CONTINUOUS", TauLineConfig.InterpolationMode.STEP_MIDDLE) + +#################################################################################################### +# Test 8 +#################################################################################################### + +func _setup_test_8() -> void: + make_per_series_x_continuous_plot(%TestPlot8, "[STEP_MIDDLE] PER_SERIES_X + CONTINUOUS", TauLineConfig.InterpolationMode.STEP_MIDDLE) + +#################################################################################################### +# Test 9 +#################################################################################################### + +func _setup_test_9() -> void: + make_shared_x_categorical_plot(%TestPlot9, "[STEP_MIDDLE] SHARED_X + CATEGORICAL", TauLineConfig.InterpolationMode.STEP_MIDDLE) + +#################################################################################################### +# Test 10 +#################################################################################################### + +func _setup_test_10() -> void: + make_shared_x_continuous_plot(%TestPlot10, "[STEP_AFTER] SHARED_X + CONTINUOUS", TauLineConfig.InterpolationMode.STEP_AFTER) + +#################################################################################################### +# Test 11 +#################################################################################################### + +func _setup_test_11() -> void: + make_per_series_x_continuous_plot(%TestPlot11, "[STEP_AFTER] PER_SERIES_X + CONTINUOUS", TauLineConfig.InterpolationMode.STEP_AFTER) + +#################################################################################################### +# Test 12 +#################################################################################################### + +func _setup_test_12() -> void: + make_shared_x_categorical_plot(%TestPlot12, "[STEP_AFTER] SHARED_X + CATEGORICAL", TauLineConfig.InterpolationMode.STEP_AFTER) + +#################################################################################################### +# Test 13 +#################################################################################################### + +func _setup_test_13() -> void: + make_shared_x_continuous_plot(%TestPlot13, "[SMOOTH_MONOTONE] SHARED_X + CONTINUOUS", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE) + +#################################################################################################### +# Test 14 +#################################################################################################### + +func _setup_test_14() -> void: + make_per_series_x_continuous_plot(%TestPlot14, "[SMOOTH_MONOTONE] PER_SERIES_X + CONTINUOUS", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE) + +#################################################################################################### +# Test 15 +#################################################################################################### + +func _setup_test_15() -> void: + make_shared_x_categorical_plot(%TestPlot15, "[SMOOTH_MONOTONE] SHARED_X + CATEGORICAL", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE) diff --git a/addons/tau-plot/tests/line/area/test_line_fill_color_alpha.gd.uid b/addons/tau-plot/tests/line/area/test_line_fill_color_alpha.gd.uid new file mode 100644 index 0000000..fe6ddef --- /dev/null +++ b/addons/tau-plot/tests/line/area/test_line_fill_color_alpha.gd.uid @@ -0,0 +1 @@ +uid://cwl6310pxi58 diff --git a/addons/tau-plot/tests/line/area/test_line_fill_color_alpha.tscn b/addons/tau-plot/tests/line/area/test_line_fill_color_alpha.tscn new file mode 100644 index 0000000..9338fd0 --- /dev/null +++ b/addons/tau-plot/tests/line/area/test_line_fill_color_alpha.tscn @@ -0,0 +1,185 @@ +[gd_scene load_steps=3 format=3 uid="uid://cmwd6dq8si671"] + +[ext_resource type="Script" uid="uid://cwl6310pxi58" path="res://addons/tau-plot/tests/line/area/test_line_fill_color_alpha.gd" id="1_3uc4y"] +[ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_s8dwp"] + +[node name="Lines" type="VBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_3uc4y") + +[node name="Title" type="Label" parent="."] +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +text = "Fill to baseline 10 with gray color and alpha = 0.5 for different interpolation modes" +horizontal_alignment = 1 + +[node name="GridContainer" type="GridContainer" parent="."] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/h_separation = 0 +theme_override_constants/v_separation = 0 +columns = 3 + +[node name="TestPlot1" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_s8dwp") +title = "[LINEAR] SHARED_X + CONTINUOUS" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot2" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_s8dwp") +title = "[LINEAR] PER_SERIES_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot3" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_s8dwp") +title = "[LINEAR] SHARED_X + CATEGORICAL" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot4" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_s8dwp") +title = "[STEP_BEFORE] SHARED_X + CONTINUOUS" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot5" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_s8dwp") +title = "[STEP_BEFORE] PER_SERIES_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot6" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_s8dwp") +title = "[STEP_BEFORE] SHARED_X + CATEGORICAL" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot7" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_s8dwp") +title = "[STEP_MIDDLE] SHARED_X + CONTINUOUS" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot8" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_s8dwp") +title = "[STEP_MIDDLE] PER_SERIES_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot9" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_s8dwp") +title = "[STEP_MIDDLE] SHARED_X + CATEGORICAL" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot10" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_s8dwp") +title = "[STEP_AFTER] SHARED_X + CONTINUOUS" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot11" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_s8dwp") +title = "[STEP_AFTER] PER_SERIES_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot12" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_s8dwp") +title = "[STEP_AFTER] SHARED_X + CATEGORICAL" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot13" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_s8dwp") +title = "[SMOOTH_MONOTONE] SHARED_X + CONTINUOUS" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot14" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_s8dwp") +title = "[SMOOTH_MONOTONE] PER_SERIES_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot15" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_s8dwp") +title = "[SMOOTH_MONOTONE] SHARED_X + CATEGORICAL" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" diff --git a/addons/tau-plot/tests/line/area/test_line_fill_linear.gd b/addons/tau-plot/tests/line/area/test_line_fill_linear.gd new file mode 100644 index 0000000..fb356c8 --- /dev/null +++ b/addons/tau-plot/tests/line/area/test_line_fill_linear.gd @@ -0,0 +1,279 @@ +@tool +extends Control + +func _ready() -> void: + _setup_test_1() + _setup_test_2() + _setup_test_3() + _setup_test_4() + _setup_test_5() + _setup_test_6() + _setup_test_7() + _setup_test_8() + _setup_test_9() + _setup_test_10() + _setup_test_11() + _setup_test_12() + _setup_test_13() + _setup_test_14() + _setup_test_15() + +#################################################################################################### +# Helpers +#################################################################################################### + +func make_shared_x_continuous_plot(p_plot: TauPlot, p_title: String, p_interpolation: TauLineConfig.InterpolationMode) -> void: + var series_names := PackedStringArray(["Series A", "Series B"]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0]) + var y_a := PackedFloat64Array([10.0, 20.0, 50.0, 15.0, 10.0]) + var y_b := PackedFloat64Array([50.0, 40.0, 35.0, 5.0, 20.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b]) + + p_plot.title = p_title + p_plot.legend_enabled = true + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.tick_count_preferred = 10 + y_axis.overlap_strategy = TauAxisConfig.OverlapStrategy.NONE + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.INDEPENDENT + line_config.interpolation_mode = p_interpolation + line_config.fill_mode = TauLineConfig.FillMode.TO_BASELINE + line_config.fill_baseline = 10. + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b] + + p_plot.plot_xy(dataset, config, bindings) + + +func make_per_series_x_continuous_plot(p_plot: TauPlot, p_title: String, p_interpolation: TauLineConfig.InterpolationMode) -> void: + var series_names := PackedStringArray(["Series A", "Series B"]) + var x_a := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0]) + var y_a := PackedFloat64Array([10.0, 20.0, 50.0, 15.0, 10.0]) + var x_b := PackedFloat64Array([10.5, 11.5, 12.5, 13.5]) + var y_b := PackedFloat64Array([40.0, 35.0, 5.0, 20.0]) + + var dataset := TauPlot.Dataset.make_per_series_x_continuous(series_names, [x_a, x_b], [y_a, y_b]) + + p_plot.title = p_title + p_plot.legend_enabled = false + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.tick_count_preferred = 10 + y_axis.overlap_strategy = TauAxisConfig.OverlapStrategy.NONE + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.INDEPENDENT + line_config.interpolation_mode = p_interpolation + line_config.fill_mode = TauLineConfig.FillMode.TO_BASELINE + line_config.fill_baseline = 10. + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b] + + p_plot.plot_xy(dataset, config, bindings) + + +func make_shared_x_categorical_plot(p_plot: TauPlot, p_title: String, p_interpolation: TauLineConfig.InterpolationMode) -> void: + var series_names := PackedStringArray(["Series A", "Series B"]) + var x := PackedStringArray(["One", "Two", "Three", "Four", "Five"]) + var y_a := PackedFloat64Array([10.0, 20.0, 50.0, 15.0, 10.0]) + var y_b := PackedFloat64Array([50.0, 40.0, 35.0, 5.0, 20.0]) + + var dataset := TauPlot.Dataset.make_shared_x_categorical(series_names, x, [y_a, y_b]) + + p_plot.title = p_title + p_plot.legend_enabled = false + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CATEGORICAL + x_axis.scale = TauAxisConfig.Scale.LINEAR + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LINEAR + y_axis.tick_count_preferred = 10 + y_axis.overlap_strategy = TauAxisConfig.OverlapStrategy.NONE + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.INDEPENDENT + line_config.interpolation_mode = p_interpolation + line_config.fill_mode = TauLineConfig.FillMode.TO_BASELINE + line_config.fill_baseline = 10. + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b] + + p_plot.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 1 +#################################################################################################### + +func _setup_test_1() -> void: + make_shared_x_continuous_plot(%TestPlot1, "[LINEAR] SHARED_X + CONTINUOUS", TauLineConfig.InterpolationMode.LINEAR) + +#################################################################################################### +# Test 2 +#################################################################################################### + +func _setup_test_2() -> void: + make_per_series_x_continuous_plot(%TestPlot2, "[LINEAR] PER_SERIES_X + CONTINUOUS", TauLineConfig.InterpolationMode.LINEAR) + +#################################################################################################### +# Test 3 +#################################################################################################### + +func _setup_test_3() -> void: + make_shared_x_categorical_plot(%TestPlot3, "[LINEAR] SHARED_X + CATEGORICAL", TauLineConfig.InterpolationMode.LINEAR) + +#################################################################################################### +# Test 4 +#################################################################################################### + +func _setup_test_4() -> void: + make_shared_x_continuous_plot(%TestPlot4, "[STEP_BEFORE] SHARED_X + CONTINUOUS", TauLineConfig.InterpolationMode.STEP_BEFORE) + +#################################################################################################### +# Test 5 +#################################################################################################### + +func _setup_test_5() -> void: + make_per_series_x_continuous_plot(%TestPlot5, "[STEP_BEFORE] PER_SERIES_X + CONTINUOUS", TauLineConfig.InterpolationMode.STEP_BEFORE) + +#################################################################################################### +# Test 6 +#################################################################################################### + +func _setup_test_6() -> void: + make_shared_x_categorical_plot(%TestPlot6, "[STEP_BEFORE] SHARED_X + CATEGORICAL", TauLineConfig.InterpolationMode.STEP_BEFORE) + +#################################################################################################### +# Test 7 +#################################################################################################### + +func _setup_test_7() -> void: + make_shared_x_continuous_plot(%TestPlot7, "[STEP_MIDDLE] SHARED_X + CONTINUOUS", TauLineConfig.InterpolationMode.STEP_MIDDLE) + +#################################################################################################### +# Test 8 +#################################################################################################### + +func _setup_test_8() -> void: + make_per_series_x_continuous_plot(%TestPlot8, "[STEP_MIDDLE] PER_SERIES_X + CONTINUOUS", TauLineConfig.InterpolationMode.STEP_MIDDLE) + +#################################################################################################### +# Test 9 +#################################################################################################### + +func _setup_test_9() -> void: + make_shared_x_categorical_plot(%TestPlot9, "[STEP_MIDDLE] SHARED_X + CATEGORICAL", TauLineConfig.InterpolationMode.STEP_MIDDLE) + +#################################################################################################### +# Test 10 +#################################################################################################### + +func _setup_test_10() -> void: + make_shared_x_continuous_plot(%TestPlot10, "[STEP_AFTER] SHARED_X + CONTINUOUS", TauLineConfig.InterpolationMode.STEP_AFTER) + +#################################################################################################### +# Test 11 +#################################################################################################### + +func _setup_test_11() -> void: + make_per_series_x_continuous_plot(%TestPlot11, "[STEP_AFTER] PER_SERIES_X + CONTINUOUS", TauLineConfig.InterpolationMode.STEP_AFTER) + +#################################################################################################### +# Test 12 +#################################################################################################### + +func _setup_test_12() -> void: + make_shared_x_categorical_plot(%TestPlot12, "[STEP_AFTER] SHARED_X + CATEGORICAL", TauLineConfig.InterpolationMode.STEP_AFTER) + +#################################################################################################### +# Test 13 +#################################################################################################### + +func _setup_test_13() -> void: + make_shared_x_continuous_plot(%TestPlot13, "[SMOOTH_MONOTONE] SHARED_X + CONTINUOUS", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE) + +#################################################################################################### +# Test 14 +#################################################################################################### + +func _setup_test_14() -> void: + make_per_series_x_continuous_plot(%TestPlot14, "[SMOOTH_MONOTONE] PER_SERIES_X + CONTINUOUS", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE) + +#################################################################################################### +# Test 15 +#################################################################################################### + +func _setup_test_15() -> void: + make_shared_x_categorical_plot(%TestPlot15, "[SMOOTH_MONOTONE] SHARED_X + CATEGORICAL", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE) diff --git a/addons/tau-plot/tests/line/area/test_line_fill_linear.gd.uid b/addons/tau-plot/tests/line/area/test_line_fill_linear.gd.uid new file mode 100644 index 0000000..f3f7280 --- /dev/null +++ b/addons/tau-plot/tests/line/area/test_line_fill_linear.gd.uid @@ -0,0 +1 @@ +uid://buxrc2ogu8cne diff --git a/addons/tau-plot/tests/line/area/test_line_fill_linear.tscn b/addons/tau-plot/tests/line/area/test_line_fill_linear.tscn new file mode 100644 index 0000000..2b4eb0d --- /dev/null +++ b/addons/tau-plot/tests/line/area/test_line_fill_linear.tscn @@ -0,0 +1,185 @@ +[gd_scene load_steps=3 format=3 uid="uid://cpswbt2gwdeki"] + +[ext_resource type="Script" uid="uid://buxrc2ogu8cne" path="res://addons/tau-plot/tests/line/area/test_line_fill_linear.gd" id="1_pel2m"] +[ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_fkav2"] + +[node name="Lines" type="VBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_pel2m") + +[node name="Title" type="Label" parent="."] +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +text = "Fill to baseline 10 with different interpolation modes (linear scale)" +horizontal_alignment = 1 + +[node name="GridContainer" type="GridContainer" parent="."] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/h_separation = 0 +theme_override_constants/v_separation = 0 +columns = 3 + +[node name="TestPlot1" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_fkav2") +title = "[LINEAR] SHARED_X + CONTINUOUS" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot2" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_fkav2") +title = "[LINEAR] PER_SERIES_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot3" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_fkav2") +title = "[LINEAR] SHARED_X + CATEGORICAL" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot4" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_fkav2") +title = "[STEP_BEFORE] SHARED_X + CONTINUOUS" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot5" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_fkav2") +title = "[STEP_BEFORE] PER_SERIES_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot6" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_fkav2") +title = "[STEP_BEFORE] SHARED_X + CATEGORICAL" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot7" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_fkav2") +title = "[STEP_MIDDLE] SHARED_X + CONTINUOUS" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot8" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_fkav2") +title = "[STEP_MIDDLE] PER_SERIES_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot9" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_fkav2") +title = "[STEP_MIDDLE] SHARED_X + CATEGORICAL" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot10" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_fkav2") +title = "[STEP_AFTER] SHARED_X + CONTINUOUS" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot11" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_fkav2") +title = "[STEP_AFTER] PER_SERIES_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot12" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_fkav2") +title = "[STEP_AFTER] SHARED_X + CATEGORICAL" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot13" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_fkav2") +title = "[SMOOTH_MONOTONE] SHARED_X + CONTINUOUS" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot14" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_fkav2") +title = "[SMOOTH_MONOTONE] PER_SERIES_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot15" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_fkav2") +title = "[SMOOTH_MONOTONE] SHARED_X + CATEGORICAL" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" diff --git a/addons/tau-plot/tests/line/area/test_line_fill_logarithmic.gd b/addons/tau-plot/tests/line/area/test_line_fill_logarithmic.gd new file mode 100644 index 0000000..7abf6c4 --- /dev/null +++ b/addons/tau-plot/tests/line/area/test_line_fill_logarithmic.gd @@ -0,0 +1,276 @@ +@tool +extends Control + +func _ready() -> void: + _setup_test_1() + _setup_test_2() + _setup_test_3() + _setup_test_4() + _setup_test_5() + _setup_test_6() + _setup_test_7() + _setup_test_8() + _setup_test_9() + _setup_test_10() + _setup_test_11() + _setup_test_12() + _setup_test_13() + _setup_test_14() + _setup_test_15() + +#################################################################################################### +# Helpers +#################################################################################################### + +func make_shared_x_continuous_plot(p_plot: TauPlot, p_title: String, p_interpolation: TauLineConfig.InterpolationMode) -> void: + var series_names := PackedStringArray(["Series A", "Series B"]) + var x := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0]) + var y_a := PackedFloat64Array([100.0, 400.0, 2500.0, 225.0, 100.0]) + var y_b := PackedFloat64Array([2500.0, 1600.0, 1225.0, 25.0, 400.0]) + + var dataset := TauPlot.Dataset.make_shared_x_continuous(series_names, x, [y_a, y_b]) + + p_plot.title = p_title + p_plot.legend_enabled = true + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LOGARITHMIC + y_axis.overlap_strategy = TauAxisConfig.OverlapStrategy.NONE + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.INDEPENDENT + line_config.interpolation_mode = p_interpolation + line_config.fill_mode = TauLineConfig.FillMode.TO_BASELINE + line_config.fill_baseline = 100. + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b] + + p_plot.plot_xy(dataset, config, bindings) + + +func make_per_series_x_continuous_plot(p_plot: TauPlot, p_title: String, p_interpolation: TauLineConfig.InterpolationMode) -> void: + var series_names := PackedStringArray(["Series A", "Series B"]) + var x_a := PackedFloat64Array([10.0, 11.0, 12.0, 13.0, 14.0]) + var y_a := PackedFloat64Array([100.0, 400.0, 2500.0, 225.0, 100.0]) + var x_b := PackedFloat64Array([10.5, 11.5, 12.5, 13.5]) + var y_b := PackedFloat64Array([1600.0, 1225.0, 25.0, 400.0]) + + var dataset := TauPlot.Dataset.make_per_series_x_continuous(series_names, [x_a, x_b], [y_a, y_b]) + + p_plot.title = p_title + p_plot.legend_enabled = false + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CONTINUOUS + x_axis.scale = TauAxisConfig.Scale.LINEAR + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LOGARITHMIC + y_axis.overlap_strategy = TauAxisConfig.OverlapStrategy.NONE + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.INDEPENDENT + line_config.interpolation_mode = p_interpolation + line_config.fill_mode = TauLineConfig.FillMode.TO_BASELINE + line_config.fill_baseline = 100. + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b] + + p_plot.plot_xy(dataset, config, bindings) + + +func make_shared_x_categorical_plot(p_plot: TauPlot, p_title: String, p_interpolation: TauLineConfig.InterpolationMode) -> void: + var series_names := PackedStringArray(["Series A", "Series B"]) + var x := PackedStringArray(["One", "Two", "Three", "Four", "Five"]) + var y_a := PackedFloat64Array([100.0, 400.0, 2500.0, 225.0, 100.0]) + var y_b := PackedFloat64Array([2500.0, 1600.0, 1225.0, 25.0, 400.0]) + + var dataset := TauPlot.Dataset.make_shared_x_categorical(series_names, x, [y_a, y_b]) + + p_plot.title = p_title + p_plot.legend_enabled = false + + var x_axis := TauAxisConfig.new() + x_axis.type = TauAxisConfig.Type.CATEGORICAL + x_axis.scale = TauAxisConfig.Scale.LINEAR + + var y_axis := TauAxisConfig.new() + y_axis.type = TauAxisConfig.Type.CONTINUOUS + y_axis.scale = TauAxisConfig.Scale.LOGARITHMIC + y_axis.overlap_strategy = TauAxisConfig.OverlapStrategy.NONE + + var line_config := TauLineConfig.new() + line_config.mode = TauLineConfig.LineMode.INDEPENDENT + line_config.interpolation_mode = p_interpolation + line_config.fill_mode = TauLineConfig.FillMode.TO_BASELINE + line_config.fill_baseline = 100. + + var pane := TauPaneConfig.new() + pane.y_left_axis = y_axis + pane.overlays = [line_config] + + var config := TauXYConfig.new() + config.x_axis = x_axis + config.panes = [pane] + + var sb_a := TauXYSeriesBinding.new() + sb_a.series_id = dataset.get_series_id_by_index(0) + sb_a.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_a.y_axis_id = TauPlot.AxisId.LEFT + + var sb_b := TauXYSeriesBinding.new() + sb_b.series_id = dataset.get_series_id_by_index(1) + sb_b.overlay_type = TauXYSeriesBinding.PaneOverlayType.LINE + sb_b.y_axis_id = TauPlot.AxisId.LEFT + + var bindings: Array[TauXYSeriesBinding] = [sb_a, sb_b] + + p_plot.plot_xy(dataset, config, bindings) + + +#################################################################################################### +# Test 1 +#################################################################################################### + +func _setup_test_1() -> void: + make_shared_x_continuous_plot(%TestPlot1, "[LINEAR] SHARED_X + CONTINUOUS", TauLineConfig.InterpolationMode.LINEAR) + +#################################################################################################### +# Test 2 +#################################################################################################### + +func _setup_test_2() -> void: + make_per_series_x_continuous_plot(%TestPlot2, "[LINEAR] PER_SERIES_X + CONTINUOUS", TauLineConfig.InterpolationMode.LINEAR) + +#################################################################################################### +# Test 3 +#################################################################################################### + +func _setup_test_3() -> void: + make_shared_x_categorical_plot(%TestPlot3, "[LINEAR] SHARED_X + CATEGORICAL", TauLineConfig.InterpolationMode.LINEAR) + +#################################################################################################### +# Test 4 +#################################################################################################### + +func _setup_test_4() -> void: + make_shared_x_continuous_plot(%TestPlot4, "[STEP_BEFORE] SHARED_X + CONTINUOUS", TauLineConfig.InterpolationMode.STEP_BEFORE) + +#################################################################################################### +# Test 5 +#################################################################################################### + +func _setup_test_5() -> void: + make_per_series_x_continuous_plot(%TestPlot5, "[STEP_BEFORE] PER_SERIES_X + CONTINUOUS", TauLineConfig.InterpolationMode.STEP_BEFORE) + +#################################################################################################### +# Test 6 +#################################################################################################### + +func _setup_test_6() -> void: + make_shared_x_categorical_plot(%TestPlot6, "[STEP_BEFORE] SHARED_X + CATEGORICAL", TauLineConfig.InterpolationMode.STEP_BEFORE) + +#################################################################################################### +# Test 7 +#################################################################################################### + +func _setup_test_7() -> void: + make_shared_x_continuous_plot(%TestPlot7, "[STEP_MIDDLE] SHARED_X + CONTINUOUS", TauLineConfig.InterpolationMode.STEP_MIDDLE) + +#################################################################################################### +# Test 8 +#################################################################################################### + +func _setup_test_8() -> void: + make_per_series_x_continuous_plot(%TestPlot8, "[STEP_MIDDLE] PER_SERIES_X + CONTINUOUS", TauLineConfig.InterpolationMode.STEP_MIDDLE) + +#################################################################################################### +# Test 9 +#################################################################################################### + +func _setup_test_9() -> void: + make_shared_x_categorical_plot(%TestPlot9, "[STEP_MIDDLE] SHARED_X + CATEGORICAL", TauLineConfig.InterpolationMode.STEP_MIDDLE) + +#################################################################################################### +# Test 10 +#################################################################################################### + +func _setup_test_10() -> void: + make_shared_x_continuous_plot(%TestPlot10, "[STEP_AFTER] SHARED_X + CONTINUOUS", TauLineConfig.InterpolationMode.STEP_AFTER) + +#################################################################################################### +# Test 11 +#################################################################################################### + +func _setup_test_11() -> void: + make_per_series_x_continuous_plot(%TestPlot11, "[STEP_AFTER] PER_SERIES_X + CONTINUOUS", TauLineConfig.InterpolationMode.STEP_AFTER) + +#################################################################################################### +# Test 12 +#################################################################################################### + +func _setup_test_12() -> void: + make_shared_x_categorical_plot(%TestPlot12, "[STEP_AFTER] SHARED_X + CATEGORICAL", TauLineConfig.InterpolationMode.STEP_AFTER) + +#################################################################################################### +# Test 13 +#################################################################################################### + +func _setup_test_13() -> void: + make_shared_x_continuous_plot(%TestPlot13, "[SMOOTH_MONOTONE] SHARED_X + CONTINUOUS", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE) + +#################################################################################################### +# Test 14 +#################################################################################################### + +func _setup_test_14() -> void: + make_per_series_x_continuous_plot(%TestPlot14, "[SMOOTH_MONOTONE] PER_SERIES_X + CONTINUOUS", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE) + +#################################################################################################### +# Test 15 +#################################################################################################### + +func _setup_test_15() -> void: + make_shared_x_categorical_plot(%TestPlot15, "[SMOOTH_MONOTONE] SHARED_X + CATEGORICAL", TauLineConfig.InterpolationMode.SMOOTH_MONOTONE) diff --git a/addons/tau-plot/tests/line/area/test_line_fill_logarithmic.gd.uid b/addons/tau-plot/tests/line/area/test_line_fill_logarithmic.gd.uid new file mode 100644 index 0000000..e1d2e0a --- /dev/null +++ b/addons/tau-plot/tests/line/area/test_line_fill_logarithmic.gd.uid @@ -0,0 +1 @@ +uid://gmom2vph7l32 diff --git a/addons/tau-plot/tests/line/area/test_line_fill_logarithmic.tscn b/addons/tau-plot/tests/line/area/test_line_fill_logarithmic.tscn new file mode 100644 index 0000000..87f0b64 --- /dev/null +++ b/addons/tau-plot/tests/line/area/test_line_fill_logarithmic.tscn @@ -0,0 +1,185 @@ +[gd_scene load_steps=3 format=3 uid="uid://bdxoyoq0ffkui"] + +[ext_resource type="Script" uid="uid://gmom2vph7l32" path="res://addons/tau-plot/tests/line/area/test_line_fill_logarithmic.gd" id="1_241pk"] +[ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_s8dwp"] + +[node name="Lines" type="VBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_241pk") + +[node name="Title" type="Label" parent="."] +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +text = "Fill to baseline 100 with different interpolation modes (log scale)" +horizontal_alignment = 1 + +[node name="GridContainer" type="GridContainer" parent="."] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/h_separation = 0 +theme_override_constants/v_separation = 0 +columns = 3 + +[node name="TestPlot1" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_s8dwp") +title = "[LINEAR] SHARED_X + CONTINUOUS" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot2" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_s8dwp") +title = "[LINEAR] PER_SERIES_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot3" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_s8dwp") +title = "[LINEAR] SHARED_X + CATEGORICAL" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot4" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_s8dwp") +title = "[STEP_BEFORE] SHARED_X + CONTINUOUS" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot5" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_s8dwp") +title = "[STEP_BEFORE] PER_SERIES_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot6" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_s8dwp") +title = "[STEP_BEFORE] SHARED_X + CATEGORICAL" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot7" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_s8dwp") +title = "[STEP_MIDDLE] SHARED_X + CONTINUOUS" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot8" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_s8dwp") +title = "[STEP_MIDDLE] PER_SERIES_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot9" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_s8dwp") +title = "[STEP_MIDDLE] SHARED_X + CATEGORICAL" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot10" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_s8dwp") +title = "[STEP_AFTER] SHARED_X + CONTINUOUS" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot11" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_s8dwp") +title = "[STEP_AFTER] PER_SERIES_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot12" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_s8dwp") +title = "[STEP_AFTER] SHARED_X + CATEGORICAL" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot13" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_s8dwp") +title = "[SMOOTH_MONOTONE] SHARED_X + CONTINUOUS" +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot14" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_s8dwp") +title = "[SMOOTH_MONOTONE] PER_SERIES_X + CONTINUOUS" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" + +[node name="TestPlot15" type="PanelContainer" parent="GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_type_variation = &"TauPlot" +script = ExtResource("2_s8dwp") +title = "[SMOOTH_MONOTONE] SHARED_X + CATEGORICAL" +legend_enabled = false +metadata/_custom_type_script = "uid://dnhvsf2wip771" diff --git a/addons/tau-plot/tests/line/dash/test_line_dash.tscn b/addons/tau-plot/tests/line/dash/test_line_dash.tscn index d7b61ae..d8464bb 100644 --- a/addons/tau-plot/tests/line/dash/test_line_dash.tscn +++ b/addons/tau-plot/tests/line/dash/test_line_dash.tscn @@ -3,7 +3,7 @@ [ext_resource type="Script" uid="uid://ccebd3cs615t7" path="res://addons/tau-plot/tests/line/dash/test_line_dash.gd" id="1_4q4uh"] [ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_mp1bp"] -[node name="Bars" type="VBoxContainer"] +[node name="Lines" type="VBoxContainer"] anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 diff --git a/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_linear.tscn b/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_linear.tscn index 8db02d7..3e93800 100644 --- a/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_linear.tscn +++ b/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_linear.tscn @@ -3,7 +3,7 @@ [ext_resource type="Script" uid="uid://dagnw8rnu7ils" path="res://addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_linear.gd" id="1_ahkji"] [ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_nry5o"] -[node name="Bars" type="VBoxContainer"] +[node name="Lines" type="VBoxContainer"] anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 diff --git a/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_logarithmic.tscn b/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_logarithmic.tscn index 7557394..4623344 100644 --- a/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_logarithmic.tscn +++ b/addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_logarithmic.tscn @@ -3,7 +3,7 @@ [ext_resource type="Script" uid="uid://bsivl5p06vdek" path="res://addons/tau-plot/tests/line/interpolation_mode/test_line_interpolation_logarithmic.gd" id="1_g1sf8"] [ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_u5as0"] -[node name="Bars" type="VBoxContainer"] +[node name="Lines" type="VBoxContainer"] anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 diff --git a/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_alpha.tscn b/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_alpha.tscn index 911e9f1..1f256ee 100644 --- a/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_alpha.tscn +++ b/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_alpha.tscn @@ -3,7 +3,7 @@ [ext_resource type="Script" uid="uid://vi4vdovvscly" path="res://addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_alpha.gd" id="1_lv2ey"] [ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_touct"] -[node name="Bars" type="VBoxContainer"] +[node name="Lines" type="VBoxContainer"] anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 diff --git a/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_color.tscn b/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_color.tscn index c77ef9a..9cfb98d 100644 --- a/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_color.tscn +++ b/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_color.tscn @@ -3,7 +3,7 @@ [ext_resource type="Script" uid="uid://5dh41vpm1xd3" path="res://addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_dash_color.gd" id="1_yv5ay"] [ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_le50r"] -[node name="Bars" type="VBoxContainer"] +[node name="Lines" type="VBoxContainer"] anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 diff --git a/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_alpha.tscn b/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_alpha.tscn index 45a5473..4427421 100644 --- a/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_alpha.tscn +++ b/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_alpha.tscn @@ -3,7 +3,7 @@ [ext_resource type="Script" uid="uid://vro08bntv7uo" path="res://addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_alpha.gd" id="1_u76at"] [ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_hy82u"] -[node name="Bars" type="VBoxContainer"] +[node name="Lines" type="VBoxContainer"] anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 diff --git a/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_color.tscn b/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_color.tscn index 1d39731..e2c244e 100644 --- a/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_color.tscn +++ b/addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_color.tscn @@ -3,7 +3,7 @@ [ext_resource type="Script" uid="uid://c78fi08wv7vqd" path="res://addons/tau-plot/tests/line/line_attributes/test_line_visual_attributes_solid_color.gd" id="1_7vjol"] [ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_lc0kd"] -[node name="Bars" type="VBoxContainer"] +[node name="Lines" type="VBoxContainer"] anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 diff --git a/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_alpha.tscn b/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_alpha.tscn index 7dc3e02..b768b86 100644 --- a/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_alpha.tscn +++ b/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_alpha.tscn @@ -3,7 +3,7 @@ [ext_resource type="Script" uid="uid://d1ehkjmoa0dhx" path="res://addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_alpha.gd" id="1_w8xoc"] [ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_wvy6q"] -[node name="Bars" type="VBoxContainer"] +[node name="Lines" type="VBoxContainer"] anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 diff --git a/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_color.tscn b/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_color.tscn index 41f03e1..b8c603d 100644 --- a/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_color.tscn +++ b/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_color.tscn @@ -3,7 +3,7 @@ [ext_resource type="Script" uid="uid://tgw2e12ovgd" path="res://addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_dash_color.gd" id="1_mx55d"] [ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_0c0l8"] -[node name="Bars" type="VBoxContainer"] +[node name="Lines" type="VBoxContainer"] anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 diff --git a/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_alpha.tscn b/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_alpha.tscn index 4363503..5ab41cc 100644 --- a/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_alpha.tscn +++ b/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_alpha.tscn @@ -3,7 +3,7 @@ [ext_resource type="Script" uid="uid://byunb5b3q5f0n" path="res://addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_alpha.gd" id="1_8ct1l"] [ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_ronnj"] -[node name="Bars" type="VBoxContainer"] +[node name="Lines" type="VBoxContainer"] anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 diff --git a/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_color.tscn b/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_color.tscn index 2a93ff8..588c974 100644 --- a/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_color.tscn +++ b/addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_color.tscn @@ -3,7 +3,7 @@ [ext_resource type="Script" uid="uid://bnuv0vxxw14ea" path="res://addons/tau-plot/tests/line/line_callbacks/test_line_visual_callbacks_solid_color.gd" id="1_lev7j"] [ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_xbeyi"] -[node name="Bars" type="VBoxContainer"] +[node name="Lines" type="VBoxContainer"] anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 diff --git a/addons/tau-plot/tests/line/log_scale/test_line_logarithmic.tscn b/addons/tau-plot/tests/line/log_scale/test_line_logarithmic.tscn index 280d768..15a14a6 100644 --- a/addons/tau-plot/tests/line/log_scale/test_line_logarithmic.tscn +++ b/addons/tau-plot/tests/line/log_scale/test_line_logarithmic.tscn @@ -3,7 +3,7 @@ [ext_resource type="Script" uid="uid://b5nqjwb6flndn" path="res://addons/tau-plot/tests/line/log_scale/test_line_logarithmic.gd" id="1_k4moc"] [ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_erpj8"] -[node name="Bars" type="VBoxContainer"] +[node name="Lines" type="VBoxContainer"] anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 diff --git a/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_bridge.gd b/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_bridge.gd index ca687e2..c27744c 100644 --- a/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_bridge.gd +++ b/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_bridge.gd @@ -37,6 +37,8 @@ func _setup_test_1() -> void: var line_config := TauLineConfig.new() line_config.gap_policy = TauLineConfig.GapPolicy.BRIDGE + line_config.fill_mode = TauLineConfig.FillMode.TO_BASELINE + line_config.fill_baseline = 0. var pane_config := TauPaneConfig.new() pane_config.y_left_axis = y_axis @@ -88,6 +90,8 @@ func _setup_test_2() -> void: var line_config := TauLineConfig.new() line_config.gap_policy = TauLineConfig.GapPolicy.BRIDGE + line_config.fill_mode = TauLineConfig.FillMode.TO_BASELINE + line_config.fill_baseline = 0. var pane := TauPaneConfig.new() pane.y_left_axis = y_axis @@ -138,6 +142,8 @@ func _setup_test_3() -> void: var line_config := TauLineConfig.new() line_config.gap_policy = TauLineConfig.GapPolicy.BRIDGE + line_config.fill_mode = TauLineConfig.FillMode.TO_BASELINE + line_config.fill_baseline = 0. var pane_config := TauPaneConfig.new() pane_config.y_left_axis = y_axis @@ -189,6 +195,8 @@ func _setup_test_4() -> void: var line_config := TauLineConfig.new() line_config.gap_policy = TauLineConfig.GapPolicy.BRIDGE + line_config.fill_mode = TauLineConfig.FillMode.TO_BASELINE + line_config.fill_baseline = 0. var pane := TauPaneConfig.new() pane.y_left_axis = y_axis @@ -239,6 +247,8 @@ func _setup_test_5() -> void: var line_config := TauLineConfig.new() line_config.gap_policy = TauLineConfig.GapPolicy.BRIDGE + line_config.fill_mode = TauLineConfig.FillMode.TO_BASELINE + line_config.fill_baseline = 0. var pane_config := TauPaneConfig.new() pane_config.y_left_axis = y_axis @@ -290,6 +300,8 @@ func _setup_test_6() -> void: var line_config := TauLineConfig.new() line_config.gap_policy = TauLineConfig.GapPolicy.BRIDGE + line_config.fill_mode = TauLineConfig.FillMode.TO_BASELINE + line_config.fill_baseline = 0. var pane := TauPaneConfig.new() pane.y_left_axis = y_axis @@ -340,6 +352,8 @@ func _setup_test_7() -> void: var line_config := TauLineConfig.new() line_config.gap_policy = TauLineConfig.GapPolicy.BRIDGE + line_config.fill_mode = TauLineConfig.FillMode.TO_BASELINE + line_config.fill_baseline = 1. var pane_config := TauPaneConfig.new() pane_config.y_left_axis = y_axis @@ -391,6 +405,8 @@ func _setup_test_8() -> void: var line_config := TauLineConfig.new() line_config.gap_policy = TauLineConfig.GapPolicy.BRIDGE + line_config.fill_mode = TauLineConfig.FillMode.TO_BASELINE + line_config.fill_baseline = 1. var pane := TauPaneConfig.new() pane.y_left_axis = y_axis diff --git a/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_bridge.tscn b/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_bridge.tscn index bc0c825..0cd12ca 100644 --- a/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_bridge.tscn +++ b/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_bridge.tscn @@ -3,7 +3,7 @@ [ext_resource type="Script" uid="uid://cym0c8dl4swxu" path="res://addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_bridge.gd" id="1_156gm"] [ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_v7edd"] -[node name="Scatters" type="VBoxContainer"] +[node name="Lines" type="VBoxContainer"] anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 @@ -31,7 +31,7 @@ size_flags_horizontal = 3 size_flags_vertical = 3 theme_type_variation = &"TauPlot" script = ExtResource("2_v7edd") -title = "[SHARED_X] 2nd and 4th x values are invalid" +title = "[SHARED_X] 3rd and 4th x values are invalid" metadata/_custom_type_script = "uid://dnhvsf2wip771" [node name="TestPlot2" type="PanelContainer" parent="GridContainer"] @@ -41,7 +41,7 @@ size_flags_horizontal = 3 size_flags_vertical = 3 theme_type_variation = &"TauPlot" script = ExtResource("2_v7edd") -title = "[PER_SERIES_X] 2nd and 4th x values are invalid for series A" +title = "[PER_SERIES_X] 3rd and 4th x values are invalid for series A" metadata/_custom_type_script = "uid://dnhvsf2wip771" [node name="TestPlot3" type="PanelContainer" parent="GridContainer"] @@ -51,7 +51,7 @@ size_flags_horizontal = 3 size_flags_vertical = 3 theme_type_variation = &"TauPlot" script = ExtResource("2_v7edd") -title = "[SHARED_X] 2nd and 4th y values are invalid for series A" +title = "[SHARED_X] 3rd and 4th y values are invalid for series A" metadata/_custom_type_script = "uid://dnhvsf2wip771" [node name="TestPlot4" type="PanelContainer" parent="GridContainer"] @@ -61,7 +61,7 @@ size_flags_horizontal = 3 size_flags_vertical = 3 theme_type_variation = &"TauPlot" script = ExtResource("2_v7edd") -title = "[PER_SERIES_X] 2nd and 4th y values are invalid for series A" +title = "[PER_SERIES_X] 3rd and 4th y values are invalid for series A" metadata/_custom_type_script = "uid://dnhvsf2wip771" [node name="TestPlot5" type="PanelContainer" parent="GridContainer"] @@ -71,7 +71,7 @@ size_flags_horizontal = 3 size_flags_vertical = 3 theme_type_variation = &"TauPlot" script = ExtResource("2_v7edd") -title = "[LOG][SHARED_X] 2nd and 4th x values are negative" +title = "[LOG][SHARED_X] 3rd and 4th x values are invalid" metadata/_custom_type_script = "uid://dnhvsf2wip771" [node name="TestPlot6" type="PanelContainer" parent="GridContainer"] @@ -81,7 +81,7 @@ size_flags_horizontal = 3 size_flags_vertical = 3 theme_type_variation = &"TauPlot" script = ExtResource("2_v7edd") -title = "[LOG][PER_SERIES_X] 2nd and 4th x values are negative for series A" +title = "[LOG][PER_SERIES_X] 3rd and 4th x values are invalid for series A" metadata/_custom_type_script = "uid://dnhvsf2wip771" [node name="TestPlot7" type="PanelContainer" parent="GridContainer"] @@ -91,7 +91,7 @@ size_flags_horizontal = 3 size_flags_vertical = 3 theme_type_variation = &"TauPlot" script = ExtResource("2_v7edd") -title = "[LOG][SHARED_X] 2nd and 4th y values are negative for series A" +title = "[LOG][SHARED_X] 3rd and 4th y values are invalid for series A" metadata/_custom_type_script = "uid://dnhvsf2wip771" [node name="TestPlot8" type="PanelContainer" parent="GridContainer"] @@ -101,5 +101,5 @@ size_flags_horizontal = 3 size_flags_vertical = 3 theme_type_variation = &"TauPlot" script = ExtResource("2_v7edd") -title = "[LOG][PER_SERIES_X] 2nd and 4th y values are negative for series A" +title = "[LOG][PER_SERIES_X] 3rd and 4th y values are invalid for series A" metadata/_custom_type_script = "uid://dnhvsf2wip771" diff --git a/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_skip.gd b/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_skip.gd index 1537c8a..af2f4b4 100644 --- a/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_skip.gd +++ b/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_skip.gd @@ -37,6 +37,8 @@ func _setup_test_1() -> void: var line_config := TauLineConfig.new() line_config.gap_policy = TauLineConfig.GapPolicy.SKIP + line_config.fill_mode = TauLineConfig.FillMode.TO_BASELINE + line_config.fill_baseline = 0. var pane_config := TauPaneConfig.new() pane_config.y_left_axis = y_axis @@ -88,6 +90,8 @@ func _setup_test_2() -> void: var line_config := TauLineConfig.new() line_config.gap_policy = TauLineConfig.GapPolicy.SKIP + line_config.fill_mode = TauLineConfig.FillMode.TO_BASELINE + line_config.fill_baseline = 0. var pane := TauPaneConfig.new() pane.y_left_axis = y_axis @@ -138,6 +142,8 @@ func _setup_test_3() -> void: var line_config := TauLineConfig.new() line_config.gap_policy = TauLineConfig.GapPolicy.SKIP + line_config.fill_mode = TauLineConfig.FillMode.TO_BASELINE + line_config.fill_baseline = 0. var pane_config := TauPaneConfig.new() pane_config.y_left_axis = y_axis @@ -189,6 +195,8 @@ func _setup_test_4() -> void: var line_config := TauLineConfig.new() line_config.gap_policy = TauLineConfig.GapPolicy.SKIP + line_config.fill_mode = TauLineConfig.FillMode.TO_BASELINE + line_config.fill_baseline = 0. var pane := TauPaneConfig.new() pane.y_left_axis = y_axis @@ -239,6 +247,8 @@ func _setup_test_5() -> void: var line_config := TauLineConfig.new() line_config.gap_policy = TauLineConfig.GapPolicy.SKIP + line_config.fill_mode = TauLineConfig.FillMode.TO_BASELINE + line_config.fill_baseline = 0. var pane_config := TauPaneConfig.new() pane_config.y_left_axis = y_axis @@ -290,6 +300,8 @@ func _setup_test_6() -> void: var line_config := TauLineConfig.new() line_config.gap_policy = TauLineConfig.GapPolicy.SKIP + line_config.fill_mode = TauLineConfig.FillMode.TO_BASELINE + line_config.fill_baseline = 0. var pane := TauPaneConfig.new() pane.y_left_axis = y_axis @@ -340,6 +352,8 @@ func _setup_test_7() -> void: var line_config := TauLineConfig.new() line_config.gap_policy = TauLineConfig.GapPolicy.SKIP + line_config.fill_mode = TauLineConfig.FillMode.TO_BASELINE + line_config.fill_baseline = 1. var pane_config := TauPaneConfig.new() pane_config.y_left_axis = y_axis @@ -391,6 +405,8 @@ func _setup_test_8() -> void: var line_config := TauLineConfig.new() line_config.gap_policy = TauLineConfig.GapPolicy.SKIP + line_config.fill_mode = TauLineConfig.FillMode.TO_BASELINE + line_config.fill_baseline = 1. var pane := TauPaneConfig.new() pane.y_left_axis = y_axis diff --git a/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_skip.tscn b/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_skip.tscn index 9e7f315..7e1e985 100644 --- a/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_skip.tscn +++ b/addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_skip.tscn @@ -3,7 +3,7 @@ [ext_resource type="Script" uid="uid://bokuk27l1c8du" path="res://addons/tau-plot/tests/line/nan_or_inf/test_line_nan_inf_skip.gd" id="1_e73yd"] [ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_5c7d4"] -[node name="Scatters" type="VBoxContainer"] +[node name="Lines" type="VBoxContainer"] anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 diff --git a/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_fraction.tscn b/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_fraction.tscn index e6c96b9..8a48488 100644 --- a/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_fraction.tscn +++ b/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_fraction.tscn @@ -3,7 +3,7 @@ [ext_resource type="Script" uid="uid://t5fdpmq6toqn" path="res://addons/tau-plot/tests/line/stacked/test_line_stacked_negative_fraction.gd" id="1_kstpx"] [ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_71bbx"] -[node name="Bars" type="VBoxContainer"] +[node name="Lines" type="VBoxContainer"] anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 diff --git a/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_none.tscn b/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_none.tscn index 34ff9ea..44a7b54 100644 --- a/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_none.tscn +++ b/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_none.tscn @@ -3,7 +3,7 @@ [ext_resource type="Script" uid="uid://gq3klvlipxkr" path="res://addons/tau-plot/tests/line/stacked/test_line_stacked_negative_none.gd" id="1_t3a0d"] [ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_q7rrf"] -[node name="Bars" type="VBoxContainer"] +[node name="Lines" type="VBoxContainer"] anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 diff --git a/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_percent.tscn b/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_percent.tscn index bd41d1e..a4e6b3e 100644 --- a/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_percent.tscn +++ b/addons/tau-plot/tests/line/stacked/test_line_stacked_negative_percent.tscn @@ -3,7 +3,7 @@ [ext_resource type="Script" uid="uid://bpbp8mimvrtki" path="res://addons/tau-plot/tests/line/stacked/test_line_stacked_negative_percent.gd" id="1_os7bv"] [ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_tcxo6"] -[node name="Bars" type="VBoxContainer"] +[node name="Lines" type="VBoxContainer"] anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 @@ -51,6 +51,7 @@ size_flags_horizontal = 3 size_flags_vertical = 3 theme_type_variation = &"TauPlot" script = ExtResource("2_tcxo6") +title = "[CONTINUOUS] with stacked_negative_policy = SIGNED_SUM" metadata/_custom_type_script = "uid://dnhvsf2wip771" [node name="TestPlot4" type="PanelContainer" parent="GridContainer"] @@ -80,4 +81,5 @@ size_flags_horizontal = 3 size_flags_vertical = 3 theme_type_variation = &"TauPlot" script = ExtResource("2_tcxo6") +title = "[CATEGORICAL] with stacked_negative_policy = SIGNED_SUM" metadata/_custom_type_script = "uid://dnhvsf2wip771" diff --git a/addons/tau-plot/tests/line/stacked/test_line_stacking_order.tscn b/addons/tau-plot/tests/line/stacked/test_line_stacking_order.tscn index 47a2ab1..bab0427 100644 --- a/addons/tau-plot/tests/line/stacked/test_line_stacking_order.tscn +++ b/addons/tau-plot/tests/line/stacked/test_line_stacking_order.tscn @@ -3,7 +3,7 @@ [ext_resource type="Script" uid="uid://cc44m4dwx745i" path="res://addons/tau-plot/tests/line/stacked/test_line_stacking_order.gd" id="1_if76y"] [ext_resource type="Script" uid="uid://dnhvsf2wip771" path="res://addons/tau-plot/plot/plot.gd" id="2_e36ro"] -[node name="Bars" type="VBoxContainer"] +[node name="Lines" type="VBoxContainer"] anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0