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
use crate::Precision;
use crate::chart::Chart;
use crate::core::context::PanelContext;
use crate::core::layer::{MarkRenderer, RenderBackend, TextConfig};
use crate::core::utils::IntoParallelizable;
use crate::error::ChartonError;
use crate::mark::text::MarkText;
use crate::visual::color::SingleColor;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
// ============================================================================
// MARK RENDERING (Text implementation)
// ============================================================================
impl MarkRenderer for Chart<MarkText> {
/// Renders text marks by mapping data points to text elements in the backend.
/// Uses row-based parallel processing for optimal performance.
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("MarkText 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 primary coordinates
let x_norms = x_scale
.scale_type()
.normalize_column(x_scale, df_source.column(&x_enc.field)?);
let y_norms = y_scale
.scale_type()
.normalize_column(y_scale, df_source.column(&y_enc.field)?);
// Pre-normalize color aesthetics if mapping exists
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())
});
// --- STEP 3: PARALLEL PROCESSING ---
// We process rows independently to handle high-density text labels efficiently.
let render_configs: Vec<TextConfig> = (0..row_count)
.maybe_into_par_iter()
.filter_map(|i| {
// Extract normalized coordinates
let x_n = x_norms[i]?;
let y_n = y_norms[i]?;
// 1. Position: Transform normalized [0,1] to screen pixel space
let (px, py) = context.coord.transform(x_n, y_n, &context.panel);
// 2. Aesthetic Resolution: Resolve color using data mapping or fallback
let fill = self.resolve_color_from_value(
color_norms.as_ref().and_then(|n| n[i]),
context,
&mark_config.color,
);
// 3. Text Content Resolution:
// Prioritize data field from encoding, fallback to static mark text.
let content = if let Some(ref text_enc) = self.encoding.text {
df_source
.column(&text_enc.field)
.ok()?
.get_str(i)
.unwrap_or_default()
.to_string()
} else {
mark_config.text.clone()
};
Some(TextConfig {
x: px as Precision,
y: py as Precision,
text: content,
font_size: mark_config.font_size as Precision,
font_family: mark_config.font_family.clone(),
color: fill,
text_anchor: mark_config.text_anchor.to_string(),
font_weight: mark_config.font_weight.to_string(),
opacity: mark_config.opacity as Precision,
})
})
.collect();
// --- STEP 4: SEQUENTIAL DRAW DISPATCH ---
// Dispatch draw calls to the backend in deterministic data order.
for config in render_configs {
backend.draw_text(config);
}
Ok(())
}
}
impl Chart<MarkText> {
/// Re-using the optimized color resolution logic.
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
}
}
}