1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
use crate::Precision;
use crate::chart::Chart;
use crate::core::context::PanelContext;
use crate::core::layer::{LineConfig, MarkRenderer, RenderBackend};
use crate::core::utils::IntoParallelizable;
use crate::error::ChartonError;
use crate::mark::rule::MarkRule;
use crate::visual::color::SingleColor;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
// ============================================================================
// MARK RENDERING (Rule Implementation)
// ============================================================================
impl MarkRenderer for Chart<MarkRule> {
/// Renders "Rule" marks, which are straight line segments typically used for
/// axis rulers, error bars, or range indicators.
///
/// This implementation uses a row-independent parallel approach (similar to PointMark)
/// because each rule is a standalone geometry that doesn't require connecting
/// points or path interpolation.
fn render_marks(
&self,
backend: &mut dyn RenderBackend,
context: &PanelContext,
) -> Result<(), ChartonError> {
let df_source = &self.data;
let row_count = df_source.height();
if row_count == 0 {
return Ok(());
}
// --- STEP 1: SPECIFICATION VALIDATION ---
let x_enc = self
.encoding
.x
.as_ref()
.ok_or_else(|| ChartonError::Encoding("X encoding is missing".into()))?;
let y_enc = self
.encoding
.y
.as_ref()
.ok_or_else(|| ChartonError::Encoding("Y encoding is missing".into()))?;
let mark_config = self
.mark
.as_ref()
.ok_or_else(|| ChartonError::Mark("MarkRule configuration is missing".into()))?;
// --- STEP 2: POSITION & AESTHETIC NORMALIZATION ---
let x_scale = context.coord.get_x_scale();
let y_scale = context.coord.get_y_scale();
// Vectorized normalization of coordinates
let x_norms = x_scale
.scale_type()
.normalize_column(x_scale, df_source.column(&x_enc.field)?);
let y1_norms = y_scale
.scale_type()
.normalize_column(y_scale, df_source.column(&y_enc.field)?);
// Normalize Y2 if provided; defaults to 1.0 (top of the scale) for vertical rules
let y2_norms = self.encoding.y2.as_ref().map(|e| {
y_scale
.scale_type()
.normalize_column(y_scale, df_source.column(&e.field).unwrap())
});
// Pre-normalize color aesthetics for data-driven mapping
let color_norms = context.spec.aesthetics.color.as_ref().map(|m| {
let s = m.scale_impl.as_ref();
s.scale_type()
.normalize_column(s, df_source.column(&m.field).unwrap())
});
let is_flipped = context.coord.is_flipped();
// --- STEP 3: PARALLEL GEOMETRY PROJECTION ---
let render_configs: Vec<LineConfig> = (0..row_count)
.maybe_into_par_iter()
.filter_map(|i| {
let x_n = x_norms[i]?;
let yn1 = y1_norms[i]?;
let yn2 = y2_norms.as_ref().and_then(|ns| ns[i]).unwrap_or(1.0);
// Transform normalized coordinates [0, 1] to screen pixel space
let (p1_x, p1_y) = context.coord.transform(x_n, yn1, &context.panel);
let (p2_x, p2_y) = context.coord.transform(x_n, yn2, &context.panel);
// Determine orientation based on coordinate system flipping:
// Standard: Constant X, Y spans from y1 to y2 (Vertical Rule)
// Flipped: Constant Y, X spans from x1 to x2 (Horizontal Rule)
let (x1, y1, x2, y2) = if !is_flipped {
(p1_x, p1_y, p1_x, p2_y)
} else {
(p1_x, p1_y, p2_x, p1_y)
};
// Resolve color: Priority is Data Mapping > Mark Config Fallback
let final_color = self.resolve_color_from_value(
color_norms.as_ref().and_then(|n| n[i]),
context,
&mark_config.color,
);
Some(LineConfig {
x1: x1 as Precision,
y1: y1 as Precision,
x2: x2 as Precision,
y2: y2 as Precision,
color: final_color,
width: mark_config.stroke_width as Precision,
opacity: mark_config.opacity as Precision,
dash: vec![],
})
})
.collect();
// --- STEP 4: SEQUENTIAL DRAW DISPATCH ---
// Lines are drawn in original data order to maintain deterministic Z-indexing.
for config in render_configs {
backend.draw_line(config);
}
Ok(())
}
}
impl Chart<MarkRule> {
/// Reusable aesthetic color resolver.
fn resolve_color_from_value(
&self,
val: Option<f64>,
context: &PanelContext,
fallback: &SingleColor,
) -> SingleColor {
if let (Some(v), Some(mapping)) = (val, &context.spec.aesthetics.color) {
let s_trait = mapping.scale_impl.as_ref();
s_trait
.mapper()
.as_ref()
.map(|m| m.map_to_color(v, s_trait.logical_max()))
.unwrap_or(*fallback)
} else {
*fallback
}
}
}