Proposal
Here is a sketch of what it could look like:
class_name TauLineConfig extends TauPaneOverlayConfig
enum LineMode
{
INDEPENDENT, ## Each series is drawn independently. Lines at the same X may overlap.
STACKED ## Values at the same X are summed across series.
}
@export var mode: LineMode = LineMode.INDEPENDENT
enum InterpolationMode
{
LINEAR, ## Straight segment between consecutive samples.
STEP_BEFORE, ## Vertical jump at the previous sample's X, then horizontal.
STEP_AFTER, ## Horizontal first, then vertical jump at the next sample's X.
STEP_MIDDLE, ## Jump at the pixel midpoint between the two X positions.
SMOOTH_MONOTONE ## Fritsch-Carlson monotone cubic, no overshoot between samples.
}
## Per-series cycle, read as interpolation_modes[series_index % size].
## An empty array means LINEAR everywhere.
@export var interpolation_modes: Array[InterpolationMode] = [InterpolationMode.LINEAR]
enum GapPolicy
{
SKIP, ## Break the polyline at the invalid sample.
BRIDGE ## Drop the invalid sample and connect the surrounding valid ones.
}
## Per-series cycle. Applies to NaN, infinite, and values forbidden by the
## active axis scale, such as a non-positive value on a logarithmic axis.
@export var gap_policies: Array[GapPolicy] = [GapPolicy.SKIP]
## Normalization applied to the stacked sums when mode is STACKED.
@export var stacked_normalization: StackedNormalization = StackedNormalization.NONE
## How negative values enter the cumulative when mode is STACKED.
## SIGNED_SUM dips below the previous layer, DIVERGING splits each X into an
## upper and a lower stack anchored at zero, SKIP_NEGATIVES drops them.
@export var stacked_negative_policy: StackedNegativePolicy = StackedNegativePolicy.SIGNED_SUM
## Per-series cycle of hover pixel gates. A 2D distance gate in NEAREST mode,
## an x-only gate in X_ALIGNED mode.
@export var hover_max_distances_px: Array[int] = [10]
## Visual style applied to this overlay. Never null.
@export var style: TauLineStyle = TauLineStyle.new()
And the style:
class_name TauLineStyle extends Resource
## Per-series cycle of line widths in pixels in the normal state.
@export var line_widths_px: Array[float] = [2.0]
## Per-series cycle of line widths for the two segments adjacent to the hovered
## sample. Clamped at draw time to at least the series base width.
@export var hovered_line_widths_px: Array[float] = [3.0]
## Per-series cycle of dash lengths in pixels. Zero means solid.
@export var dash_lengths_px: Array[int] = [0]
## Per-series cycle of fills. Unlike the arrays above, this one merges with the
## theme entry by entry and field by field. A null entry leaves that position
## to the theme.
@export var fills: Array[TauLineFill] = []
The fill is its own resource, so a series can be filled while the next one is not:
class_name TauLineFill extends Resource
enum FillMode
{
NONE, ## Leave the area unfilled.
TO_BASELINE, ## Fill between the line and the constant level fill_baseline.
STACKED ## Fill down to the top of the layer below. Needs LineMode.STACKED.
}
@export var fill_mode: FillMode = FillMode.NONE
## Reference Y level for TO_BASELINE, in data units on the series y axis.
@export var fill_baseline: float = 0.0
## Sentinel meaning "derive from TauXYStyle.series_colors".
const NO_COLOR: Color = Color(0, 0, 0, 0)
@export var color: Color = NO_COLOR
## Multiplier applied to the alpha of the resolved fill.
@export var alpha: float = 0.5
## Painted over the fill area, taking the place of color when set.
@export var texture: Texture2D = null
enum FillTextureMode
{
STRETCH, ## Fit the texture across the fill once, as a gradient or a band.
TILE ## Repeat the texture at its native pixel size, for a motif.
}
@export var texture_mode: FillTextureMode = FillTextureMode.STRETCH
enum FillStretchSpan
{
LINE, ## Position inside the band, from the line to the baseline.
VALUE_Y, ## The Y value of the point, so a color stays tied to a value.
VALUE_X, ## The X value of the point.
MAGNITUDE ## Distance from fill_baseline, either side. Not available with STACKED.
}
## What a stretched texture's color stands for. The texture is read as a color
## scale between its two edges, so a gradient can be a single pixel wide.
@export var stretch_span: FillStretchSpan = FillStretchSpan.LINE
enum StretchRangePolicy
{
DOMAIN, ## Span the whole series, from its lowest value to its highest.
CUSTOM ## Use the fixed window set in stretch_range.
}
## Where VALUE_Y, VALUE_X and MAGNITUDE read their low and high ends.
## LINE never reads them.
@export var stretch_range_policy: StretchRangePolicy = StretchRangePolicy.DOMAIN
@export var stretch_range: Vector2 = Vector2.ZERO
## Uniform scale of the tile grid in TILE mode. 1.0 is the native pixel size.
@export var tile_scale: float = 1.0
## Rotation of the tile grid in TILE mode, turned around the pane center.
@export var tile_rotation_deg: float = 0.0
## Screen-space translation of the tile grid in TILE mode, applied after
## rotation. Animate it to scroll the pattern.
@export var tile_offset_px: Vector2 = Vector2.ZERO
LineVisualAttributes carries only the inherited buffers color_buffer and alpha_buffer.
At draw time the final fill color of a series is resolved in this order:
- nothing is painted when
fill_mode is NONE.
texture if non-null.
color if different from NO_COLOR.
- else the series color from
TauXYStyle.series_colors.
Whatever survives has its alpha channel multiplied by alpha before rendering.
Behavior:
- Fill only is supported.
STACKED mode requires a SHARED_X dataset, all line series of the pane on the same y axis, and a non-logarithmic y axis.
FillMode.STACKED needs the overlay to be in LineMode.STACKED, otherwise the fill is dropped.
- Line overlays prefer
X_ALIGNED when TauHoverConfig.hover_mode is AUTO, since a line chart is read along the x axis.
- Hover reports both the plotted Y (the cumulative top, normalized when asked) and the raw dataset value.
- Everything in
TauLineStyle and TauLineFill goes through the usual three-layer cascade, with per-series and per-pane theme keys.
Edits:
- changed
dash_px scalar to dash_lengths_px array (indexed by series index)
- changed
line_width_px scalar to line_widths_px array (indexed by series index)
- realigned the sketch with the implementation:
interpolation_mode, gap_policy and hover_max_distance_px became per-series cycles, hovered_line_width_px became hovered_line_widths_px, stacked_negative_policy was added, and every fill setting moved out of the config and the style into a per-series TauLineFill, where fill_anchor was replaced by texture_mode, stretch_span and the tile transform
Proposal
Here is a sketch of what it could look like:
And the style:
The fill is its own resource, so a series can be filled while the next one is not:
LineVisualAttributescarries only the inherited bufferscolor_bufferandalpha_buffer.At draw time the final fill color of a series is resolved in this order:
fill_modeisNONE.textureif non-null.colorif different fromNO_COLOR.TauXYStyle.series_colors.Whatever survives has its alpha channel multiplied by
alphabefore rendering.Behavior:
STACKEDmode requires aSHARED_Xdataset, all line series of the pane on the same y axis, and a non-logarithmic y axis.FillMode.STACKEDneeds the overlay to be inLineMode.STACKED, otherwise the fill is dropped.X_ALIGNEDwhenTauHoverConfig.hover_modeisAUTO, since a line chart is read along the x axis.TauLineStyleandTauLineFillgoes through the usual three-layer cascade, with per-series and per-pane theme keys.Edits:
dash_pxscalar todash_lengths_pxarray (indexed by series index)line_width_pxscalar toline_widths_pxarray (indexed by series index)interpolation_mode,gap_policyandhover_max_distance_pxbecame per-series cycles,hovered_line_width_pxbecamehovered_line_widths_px,stacked_negative_policywas added, and every fill setting moved out of the config and the style into a per-seriesTauLineFill, wherefill_anchorwas replaced bytexture_mode,stretch_spanand the tile transform