use crate::aes::Aesthetic;
use crate::data::Value;
use crate::guide::config::GuideLegend;
use crate::render::backend::{
DrawBackend, LineStyle, Linetype, PointStyle, RectStyle, TextAnchor, TextStyle,
};
use crate::render::{Rect, RenderError};
use crate::scale::ScaleSet;
use crate::theme::{LegendPosition, Theme};
const LEGEND_AESTHETICS: &[Aesthetic] = &[
Aesthetic::Color,
Aesthetic::Fill,
Aesthetic::Shape,
Aesthetic::Linetype,
Aesthetic::Size,
Aesthetic::Alpha,
Aesthetic::Linewidth,
];
pub(crate) fn legend_scales_for(
scales: &ScaleSet,
suppressed: &std::collections::HashSet<Aesthetic>,
) -> Vec<Aesthetic> {
let mut legend_scales: Vec<Aesthetic> = Vec::new();
for aes in LEGEND_AESTHETICS {
if suppressed.contains(aes) {
continue;
}
if let Some(scale) = scales.get(aes) {
if !scale.breaks().is_empty() {
if *aes == Aesthetic::Fill && legend_scales.contains(&Aesthetic::Color) {
continue;
}
legend_scales.push(aes.clone());
}
}
}
legend_scales
}
#[deprecated(
since = "0.23.0",
note = "unused internally since 0.23 (superseded by the crate-internal draw_legend_planned, \
sipemu/ggplot-rs#155); no direct public replacement — legends are drawn automatically \
by GGPlot's render methods. Will be removed in a later minor release."
)]
pub fn draw_legend(
scales: &ScaleSet,
theme: &Theme,
plot_area: &Rect,
backend: &mut dyn DrawBackend,
guide: &GuideLegend,
suppressed: &std::collections::HashSet<Aesthetic>,
) -> Result<(), RenderError> {
if matches!(theme.legend_position, LegendPosition::None) {
return Ok(());
}
let mut legend_scales: Vec<&Aesthetic> = Vec::new();
for aes in LEGEND_AESTHETICS {
if suppressed.contains(aes) {
continue;
}
if let Some(scale) = scales.get(aes) {
if !scale.breaks().is_empty() {
if *aes == Aesthetic::Fill && legend_scales.contains(&&Aesthetic::Color) {
continue;
}
legend_scales.push(aes);
}
}
}
if legend_scales.is_empty() {
return Ok(());
}
let (legend_x, legend_y, mut is_horizontal) = legend_position(theme, plot_area);
if let Some(dir) = theme.legend_direction {
is_horizontal = matches!(dir, crate::theme::LegendDirection::Horizontal);
}
let mut offset_y = legend_y;
let mut offset_x = legend_x;
for aes in &legend_scales {
let scale = scales.get(aes).unwrap();
if scale.is_discrete() {
if is_horizontal {
let width = draw_discrete_legend_at(
scales, aes, scale, theme, offset_x, offset_y, backend, guide, true,
)?;
offset_x += width + theme.legend_spacing * 2.0;
} else {
let height = draw_discrete_legend_at(
scales, aes, scale, theme, offset_x, offset_y, backend, guide, false,
)?;
offset_y += height + theme.legend_spacing * 2.0;
}
} else {
if matches!(aes, Aesthetic::Color | Aesthetic::Fill) {
let height =
draw_continuous_legend_at(scale, theme, offset_x, offset_y, backend, guide)?;
if is_horizontal {
offset_x += theme.legend_key_width
+ theme.legend_text.size * 6.0
+ theme.legend_spacing * 2.0;
} else {
offset_y += height + theme.legend_spacing * 2.0;
}
} else {
let height = draw_discrete_legend_at(
scales,
aes,
scale,
theme,
offset_x,
offset_y,
backend,
guide,
is_horizontal,
)?;
if is_horizontal {
offset_x += theme.legend_key_width
+ theme.legend_text.size * 6.0
+ theme.legend_spacing * 2.0;
} else {
offset_y += height + theme.legend_spacing * 2.0;
}
}
}
}
Ok(())
}
pub(crate) fn legend_position(theme: &Theme, plot_area: &Rect) -> (f64, f64, bool) {
match theme.legend_position {
LegendPosition::Right => (
plot_area.x + plot_area.width + theme.legend_margin.left,
plot_area.y + theme.legend_margin.top,
false,
),
LegendPosition::Left => (
theme.legend_margin.left,
plot_area.y + theme.legend_margin.top,
false,
),
LegendPosition::Top => (
plot_area.x + theme.legend_margin.left,
theme.legend_margin.top,
true,
),
LegendPosition::Bottom => (
plot_area.x + theme.legend_margin.left,
plot_area.y + plot_area.height + theme.legend_margin.top + 30.0,
true,
),
LegendPosition::None => (0.0, 0.0, false),
LegendPosition::Inside(fx, fy) => (
plot_area.x + fx * plot_area.width,
plot_area.y + (1.0 - fy) * plot_area.height,
false,
),
}
}
#[allow(clippy::too_many_arguments)]
fn draw_discrete_legend_at(
scales: &ScaleSet,
aes: &Aesthetic,
scale: &dyn crate::scale::Scale,
theme: &Theme,
legend_x: f64,
legend_y: f64,
backend: &mut dyn DrawBackend,
guide: &GuideLegend,
is_horizontal: bool,
) -> Result<f64, RenderError> {
let mut breaks = scale.breaks();
if breaks.is_empty() {
return Ok(0.0);
}
if guide.reverse {
breaks.reverse();
}
let item_height = theme.legend_key_height;
let swatch_size = theme.legend_key_width;
let title = guide.title.as_deref().unwrap_or_else(|| scale.name());
let legend_family = if theme.legend_title.family.is_empty() {
None
} else {
Some(theme.legend_title.family.clone())
};
let title_offset = if !title.is_empty() {
backend.draw_text(
title,
(legend_x, legend_y),
&TextStyle {
color: theme.legend_title.color,
size: theme.legend_title.size,
anchor: TextAnchor::Start,
angle: 0.0,
family: legend_family,
face: theme.legend_title.face,
},
)?;
theme.legend_title.size + 4.0
} else {
0.0
};
let items_y = legend_y + title_offset;
if theme.legend_background.visible {
let total_height = breaks.len() as f64 * item_height;
let total_width = swatch_size + theme.legend_spacing + theme.legend_text.size * 6.0;
if let Some(fill) = theme.legend_background.fill {
backend.draw_rect(
(legend_x - 2.0, items_y - 2.0),
(legend_x + total_width + 2.0, items_y + total_height + 2.0),
&RectStyle {
fill: Some(fill),
stroke: theme.legend_background.color,
stroke_width: theme.legend_background.width,
alpha: 1.0,
clip: false,
},
)?;
}
}
let mut cur_x = legend_x;
for (i, (pos, label)) in breaks.iter().enumerate() {
let x = if is_horizontal { cur_x } else { legend_x };
let y = if is_horizontal {
items_y
} else {
items_y + i as f64 * item_height
};
let center_x = x + swatch_size / 2.0;
let center_y = y + swatch_size / 2.0;
if theme.legend_key.visible {
if let Some(fill) = theme.legend_key.fill {
backend.draw_rect(
(x, y),
(x + swatch_size, y + swatch_size),
&RectStyle {
fill: Some(fill),
stroke: theme.legend_key.color,
stroke_width: theme.legend_key.width,
alpha: 1.0,
clip: false,
},
)?;
}
}
draw_legend_swatch(
backend,
scales,
aes,
scale,
theme,
label,
*pos,
x,
y,
center_x,
center_y,
swatch_size,
)?;
let label_family = if theme.legend_text.family.is_empty() {
None
} else {
Some(theme.legend_text.family.clone())
};
backend.draw_text(
label,
(x + swatch_size + theme.legend_spacing, center_y),
&TextStyle {
color: theme.legend_text.color,
size: theme.legend_text.size,
anchor: TextAnchor::Start,
angle: 0.0,
family: label_family,
face: theme.legend_text.face,
},
)?;
if is_horizontal {
cur_x = x
+ swatch_size
+ theme.legend_spacing
+ (label.chars().count() as f64) * theme.legend_text.size * 0.62
+ theme.legend_spacing * 3.0;
}
}
if is_horizontal {
Ok(cur_x - legend_x) } else {
Ok(title_offset + breaks.len() as f64 * item_height)
}
}
fn draw_continuous_legend_at(
scale: &dyn crate::scale::Scale,
theme: &Theme,
legend_x: f64,
legend_y: f64,
backend: &mut dyn DrawBackend,
guide: &GuideLegend,
) -> Result<f64, RenderError> {
let breaks = scale.breaks();
if breaks.is_empty() {
return Ok(0.0);
}
let bar_width = theme.legend_key_width;
let bar_height = theme.legend_key_height * 8.0;
let title = guide.title.as_deref().unwrap_or_else(|| scale.name());
let cont_family = if theme.legend_title.family.is_empty() {
None
} else {
Some(theme.legend_title.family.clone())
};
let title_offset = if !title.is_empty() {
backend.draw_text(
title,
(legend_x, legend_y),
&TextStyle {
color: theme.legend_title.color,
size: theme.legend_title.size,
anchor: TextAnchor::Start,
angle: 0.0,
family: cont_family,
face: theme.legend_title.face,
},
)?;
theme.legend_title.size + 4.0
} else {
0.0
};
let bar_top = legend_y + title_offset;
if theme.legend_background.visible {
let total_width = bar_width + theme.legend_spacing + theme.legend_text.size * 6.0;
if let Some(fill) = theme.legend_background.fill {
backend.draw_rect(
(legend_x - 2.0, bar_top - 2.0),
(legend_x + total_width + 2.0, bar_top + bar_height + 2.0),
&RectStyle {
fill: Some(fill),
stroke: theme.legend_background.color,
stroke_width: theme.legend_background.width,
alpha: 1.0,
clip: false,
},
)?;
}
}
let (data_min, data_max) = scale.domain().unwrap_or((0.0, 1.0));
let n_slices = 50;
let slice_height = bar_height / n_slices as f64;
for i in 0..n_slices {
let t = 1.0 - i as f64 / n_slices as f64;
let data_val = data_min + t * (data_max - data_min);
let color = scale
.map_to_color(&Value::Float(data_val))
.unwrap_or((127, 127, 127));
let sy = bar_top + i as f64 * slice_height;
backend.draw_rect(
(legend_x, sy),
(legend_x + bar_width, sy + slice_height + 0.5),
&RectStyle {
fill: Some(color),
stroke: None,
stroke_width: 0.0,
alpha: 1.0,
clip: false,
},
)?;
}
let border = theme.legend_bar_border_line();
let border_style = LineStyle {
color: border.color,
width: border.width,
alpha: 1.0,
linetype: border.linetype,
..Default::default()
};
if border.visible {
backend.draw_line(
&[
(legend_x, bar_top),
(legend_x + bar_width, bar_top),
(legend_x + bar_width, bar_top + bar_height),
(legend_x, bar_top + bar_height),
(legend_x, bar_top),
],
&border_style,
)?;
}
let tick_len = 3.0;
for (pos, label) in &breaks {
let tick_y = bar_top + bar_height * (1.0 - pos);
backend.draw_line(
&[
(legend_x + bar_width, tick_y),
(legend_x + bar_width + tick_len, tick_y),
],
&border_style,
)?;
let tick_family = if theme.legend_text.family.is_empty() {
None
} else {
Some(theme.legend_text.family.clone())
};
backend.draw_text(
label,
(
legend_x + bar_width + tick_len + theme.legend_spacing,
tick_y,
),
&TextStyle {
color: theme.legend_text.color,
size: theme.legend_text.size,
anchor: TextAnchor::Start,
angle: 0.0,
family: tick_family,
face: theme.legend_text.face,
},
)?;
}
Ok(title_offset + bar_height)
}
#[allow(clippy::too_many_arguments)]
fn draw_legend_swatch(
backend: &mut dyn DrawBackend,
scales: &ScaleSet,
aes: &Aesthetic,
scale: &dyn crate::scale::Scale,
theme: &Theme,
label: &str,
pos: f64,
x: f64,
y: f64,
center_x: f64,
center_y: f64,
swatch_size: f64,
) -> Result<(), RenderError> {
let value = Value::Str(label.to_string());
match aes {
Aesthetic::Color | Aesthetic::Fill => {
let color = scales.map_color(aes, &value).unwrap_or((127, 127, 127));
backend.draw_rect(
(x, y),
(x + swatch_size, y + swatch_size),
&RectStyle {
fill: Some(color),
stroke: None,
stroke_width: 0.0,
alpha: 1.0,
clip: false,
},
)?;
}
Aesthetic::Shape => {
let shape = scales
.map_shape(&value)
.unwrap_or(crate::render::backend::PointShape::Circle);
backend.draw_shape(
(center_x, center_y),
swatch_size / 3.0,
&PointStyle {
color: theme.geom.ink.unwrap_or((50, 50, 50)),
alpha: 1.0,
filled: true,
shape,
},
)?;
}
Aesthetic::Linetype => {
let lt = scales.map_linetype(&value).unwrap_or(Linetype::Solid);
backend.draw_line(
&[(x + 2.0, center_y), (x + swatch_size - 2.0, center_y)],
&LineStyle {
color: theme.geom.ink.unwrap_or((50, 50, 50)),
width: 1.5,
alpha: 1.0,
linetype: lt,
..Default::default()
},
)?;
}
Aesthetic::Size => {
let size = scale
.map_fraction_to_size(pos)
.or_else(|| scales.map_size(&value))
.unwrap_or(3.0);
backend.draw_shape(
(center_x, center_y),
size.min(swatch_size / 2.0),
&PointStyle {
color: theme.geom.ink.unwrap_or((50, 50, 50)),
alpha: 1.0,
filled: true,
shape: crate::render::backend::PointShape::Circle,
},
)?;
}
Aesthetic::Alpha => {
let alpha = scale
.map_fraction_to_alpha(pos)
.or_else(|| scales.map_alpha(&value))
.unwrap_or(1.0);
backend.draw_rect(
(x, y),
(x + swatch_size, y + swatch_size),
&RectStyle {
fill: Some(theme.geom.ink.unwrap_or((50, 50, 50))),
stroke: None,
stroke_width: 0.0,
alpha,
clip: false,
},
)?;
}
Aesthetic::Linewidth => {
let width = scale.map_fraction_to_size(pos).unwrap_or(1.0);
backend.draw_line(
&[(x + 2.0, center_y), (x + swatch_size - 2.0, center_y)],
&LineStyle {
color: theme.geom.ink.unwrap_or((50, 50, 50)),
width: width.min(swatch_size),
alpha: 1.0,
linetype: Linetype::Solid,
..Default::default()
},
)?;
}
_ => {}
}
Ok(())
}
#[derive(Clone, Debug)]
pub(crate) struct LegendEntry {
pub break_idx: usize,
pub row: usize,
pub x_offset: f64,
pub label_override: Option<String>,
}
#[derive(Clone, Debug)]
pub(crate) struct LegendOverflow {
pub dropped: usize,
pub row: usize,
pub x_offset: f64,
pub text: String,
}
#[derive(Clone, Debug)]
pub(crate) struct LegendPlan {
pub rows: usize,
pub entries: Vec<LegendEntry>,
pub overflow: Option<LegendOverflow>,
pub cols: usize,
pub col_pitch: f64,
}
fn legend_pitch(label: &str, theme: &Theme) -> f64 {
let swatch = theme.legend_key_width;
let spacing = theme.legend_spacing;
let text_size = theme.legend_text.size;
let legacy =
swatch + spacing + (label.chars().count() as f64) * text_size * 0.62 + spacing * 3.0;
let measured = swatch + spacing + crate::render::text::text_width(label, text_size) + 8.0;
legacy.max(measured)
}
pub(crate) fn plan_legend(
breaks: &[(f64, String)],
guide: &GuideLegend,
theme: &Theme,
avail_width: f64,
avail_height: f64,
is_horizontal: bool,
) -> Option<LegendPlan> {
let n = breaks.len();
if n == 0 {
return None;
}
if guide.nrow.is_some() || guide.ncol.is_some() {
let (rows, cols) = match (guide.nrow, guide.ncol) {
(Some(nrow), Some(ncol)) => (nrow, ncol),
(Some(nrow), None) => (nrow.max(1), n.div_ceil(nrow.max(1))),
(None, Some(ncol)) => (n.div_ceil(ncol.max(1)), ncol.max(1)),
(None, None) => unreachable!("checked above"),
};
let rows = rows.max(1);
let cols = cols.max(1);
let col_pitch = breaks
.iter()
.map(|(_, l)| legend_pitch(l, theme))
.fold(0.0_f64, f64::max);
let entries = (0..n)
.map(|i| {
let (row, col) = if guide.byrow {
(i / cols, i % cols)
} else {
(i % rows, i / rows)
};
LegendEntry {
break_idx: i,
row,
x_offset: col as f64 * col_pitch,
label_override: None,
}
})
.collect();
return Some(LegendPlan {
rows,
entries,
overflow: None,
cols,
col_pitch,
});
}
if !is_horizontal {
return None;
}
let pitches: Vec<f64> = breaks.iter().map(|(_, l)| legend_pitch(l, theme)).collect();
let total_width: f64 = pitches.iter().sum();
if total_width <= avail_width.max(0.0) {
return None; }
let item_height = theme.legend_key_height.max(1.0);
let row_limit = guide
.max_rows
.unwrap_or_else(|| 1 + (avail_height / 3.0 / item_height).floor().max(0.0) as usize)
.max(1);
let mut entries: Vec<LegendEntry> = Vec::with_capacity(n);
let mut row = 0usize;
let mut cur_x = 0.0_f64;
for (i, &p) in pitches.iter().enumerate() {
if cur_x > 0.0 && cur_x + p > avail_width {
if row + 1 >= row_limit {
break; }
row += 1;
cur_x = 0.0;
}
entries.push(LegendEntry {
break_idx: i,
row,
x_offset: cur_x,
label_override: None,
});
cur_x += p;
}
if entries.len() >= n {
return Some(LegendPlan {
rows: row + 1,
entries,
overflow: None,
cols: 1,
col_pitch: 0.0,
});
}
let mut dropped = n - entries.len();
loop {
let text = format!("+{dropped} more");
let more_pitch = legend_pitch(&text, theme);
let row_count = entries.iter().filter(|e| e.row == row).count();
let last_x = entries
.iter()
.filter(|e| e.row == row)
.map(|e| e.x_offset + pitches[e.break_idx])
.fold(0.0_f64, f64::max);
if last_x + more_pitch <= avail_width || row_count == 0 {
return Some(LegendPlan {
rows: row + 1,
entries,
overflow: Some(LegendOverflow {
dropped,
row,
x_offset: last_x,
text,
}),
cols: 1,
col_pitch: 0.0,
});
}
entries.pop();
dropped += 1;
}
}
fn discrete_guide_count(scales: &ScaleSet, legend_scales: &[Aesthetic]) -> usize {
legend_scales
.iter()
.filter(|aes| scales.get(aes).map(|s| s.is_discrete()).unwrap_or(false))
.count()
.max(1)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn plan_all_legends(
scales: &ScaleSet,
guide: &GuideLegend,
theme: &Theme,
suppressed: &std::collections::HashSet<Aesthetic>,
avail_width: f64,
avail_height: f64,
is_horizontal: bool,
warnings: &mut Vec<String>,
) -> std::collections::HashMap<Aesthetic, LegendPlan> {
let legend_scales = legend_scales_for(scales, suppressed);
let n_discrete = discrete_guide_count(scales, &legend_scales);
let per_guide_width = if is_horizontal {
(avail_width / n_discrete as f64).max(0.0)
} else {
avail_width
};
let mut plans = std::collections::HashMap::new();
for aes in &legend_scales {
let Some(scale) = scales.get(aes) else {
continue;
};
if !scale.is_discrete() {
continue;
}
let breaks = scale.breaks();
let validated_guide = guide.validated(breaks.len(), warnings);
if let Some(plan) = plan_legend(
&breaks,
&validated_guide,
theme,
per_guide_width,
avail_height,
is_horizontal,
) {
plans.insert(aes.clone(), plan);
}
}
plans
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn draw_legend_planned(
scales: &ScaleSet,
theme: &Theme,
plot_area: &Rect,
legend_avail_width: f64,
backend: &mut dyn DrawBackend,
guide: &GuideLegend,
suppressed: &std::collections::HashSet<Aesthetic>,
legend_plans: &std::collections::HashMap<Aesthetic, LegendPlan>,
) -> Result<(), RenderError> {
if matches!(theme.legend_position, LegendPosition::None) {
return Ok(());
}
let legend_scales = legend_scales_for(scales, suppressed);
if legend_scales.is_empty() {
return Ok(());
}
let (legend_x, legend_y, mut is_horizontal) = legend_position(theme, plot_area);
if let Some(dir) = theme.legend_direction {
is_horizontal = matches!(dir, crate::theme::LegendDirection::Horizontal);
}
let n_discrete = discrete_guide_count(scales, &legend_scales);
let per_guide_width = legend_avail_width / n_discrete as f64;
let mut offset_y = legend_y;
let mut offset_x = legend_x;
for aes in &legend_scales {
let scale = scales.get(aes).unwrap();
if !scale.is_discrete() {
if matches!(aes, Aesthetic::Color | Aesthetic::Fill) {
let height =
draw_continuous_legend_at(scale, theme, offset_x, offset_y, backend, guide)?;
if is_horizontal {
offset_x += theme.legend_key_width
+ theme.legend_text.size * 6.0
+ theme.legend_spacing * 2.0;
} else {
offset_y += height + theme.legend_spacing * 2.0;
}
} else {
let height = draw_discrete_legend_at(
scales,
aes,
scale,
theme,
offset_x,
offset_y,
backend,
guide,
is_horizontal,
)?;
if is_horizontal {
offset_x += theme.legend_key_width
+ theme.legend_text.size * 6.0
+ theme.legend_spacing * 2.0;
} else {
offset_y += height + theme.legend_spacing * 2.0;
}
}
continue;
}
match legend_plans.get(aes) {
None => {
if is_horizontal {
let width = draw_discrete_legend_at(
scales, aes, scale, theme, offset_x, offset_y, backend, guide, true,
)?;
offset_x += width + theme.legend_spacing * 2.0;
} else {
let height = draw_discrete_legend_at(
scales, aes, scale, theme, offset_x, offset_y, backend, guide, false,
)?;
offset_y += height + theme.legend_spacing * 2.0;
}
}
Some(plan) => {
let height = draw_discrete_legend_grid(
scales, aes, scale, theme, offset_x, offset_y, backend, guide, plan,
)?;
if is_horizontal {
offset_x += per_guide_width;
} else {
offset_y += height + theme.legend_spacing * 2.0;
}
}
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn draw_discrete_legend_grid(
scales: &ScaleSet,
aes: &Aesthetic,
scale: &dyn crate::scale::Scale,
theme: &Theme,
legend_x: f64,
legend_y: f64,
backend: &mut dyn DrawBackend,
guide: &GuideLegend,
plan: &LegendPlan,
) -> Result<f64, RenderError> {
let mut breaks = scale.breaks();
if breaks.is_empty() {
return Ok(0.0);
}
if guide.reverse {
breaks.reverse();
}
let swatch_size = theme.legend_key_width;
let item_height = theme.legend_key_height;
let title = guide.title.as_deref().unwrap_or_else(|| scale.name());
let legend_family = if theme.legend_title.family.is_empty() {
None
} else {
Some(theme.legend_title.family.clone())
};
let title_offset = if !title.is_empty() {
backend.draw_text(
title,
(legend_x, legend_y),
&TextStyle {
color: theme.legend_title.color,
size: theme.legend_title.size,
anchor: TextAnchor::Start,
angle: 0.0,
family: legend_family,
face: theme.legend_title.face,
},
)?;
theme.legend_title.size + 4.0
} else {
0.0
};
let items_y = legend_y + title_offset;
for entry in &plan.entries {
let (pos, label) = &breaks[entry.break_idx];
let x = legend_x + entry.x_offset;
let y = items_y + entry.row as f64 * item_height;
let center_x = x + swatch_size / 2.0;
let center_y = y + swatch_size / 2.0;
if theme.legend_key.visible {
if let Some(fill) = theme.legend_key.fill {
backend.draw_rect(
(x, y),
(x + swatch_size, y + swatch_size),
&RectStyle {
fill: Some(fill),
stroke: theme.legend_key.color,
stroke_width: theme.legend_key.width,
alpha: 1.0,
clip: false,
},
)?;
}
}
draw_legend_swatch(
backend,
scales,
aes,
scale,
theme,
label,
*pos,
x,
y,
center_x,
center_y,
swatch_size,
)?;
let label_family = if theme.legend_text.family.is_empty() {
None
} else {
Some(theme.legend_text.family.clone())
};
let display = entry.label_override.as_deref().unwrap_or(label.as_str());
backend.draw_text_with_title(
display,
(x + swatch_size + theme.legend_spacing, center_y),
&TextStyle {
color: theme.legend_text.color,
size: theme.legend_text.size,
anchor: TextAnchor::Start,
angle: 0.0,
family: label_family,
face: theme.legend_text.face,
},
entry.label_override.as_deref().map(|_| label.as_str()),
)?;
}
if let Some(ov) = &plan.overflow {
let x = legend_x + ov.x_offset;
let y = items_y + ov.row as f64 * item_height + swatch_size / 2.0;
let label_family = if theme.legend_text.family.is_empty() {
None
} else {
Some(theme.legend_text.family.clone())
};
backend.draw_text(
&ov.text,
(x, y),
&TextStyle {
color: theme.legend_text.color,
size: theme.legend_text.size,
anchor: TextAnchor::Start,
angle: 0.0,
family: label_family,
face: theme.legend_text.face,
},
)?;
}
Ok(title_offset + plan.rows as f64 * item_height)
}