use zenith_core::{Diagnostic, FontStyle};
use zenith_layout::{ShapeRequest, TextDirection, TextLayoutEngine};
use crate::ir::{Color, SceneCommand};
use super::super::NodeCtx;
use super::super::text::run_to_scene_glyphs;
use super::frame::PlotArea;
use super::scale::Tick;
#[derive(Clone, Copy)]
pub(super) struct AxisColors {
pub(super) axis: Color,
pub(super) grid: Color,
pub(super) label: Color,
}
pub(super) fn format_tick_label(value: f64) -> String {
let rounded = (value * 1e10).round() / 1e10;
if rounded.fract() == 0.0 && rounded.abs() < 1e15 {
format!("{}", rounded as i64)
} else {
let s = format!("{:.10}", rounded);
let s = s.trim_end_matches('0');
let s = s.trim_end_matches('.');
s.to_owned()
}
}
pub(super) fn emit_gridlines_and_labels(
plot: &PlotArea,
y_ticks: &[Tick],
colors: AxisColors,
chart_id: &str,
cx: NodeCtx,
commands: &mut Vec<SceneCommand>,
diagnostics: &mut Vec<Diagnostic>,
) {
if plot.w <= 0.0 || plot.h <= 0.0 {
return;
}
let label_families = [String::from("Noto Sans")];
for tick in y_ticks {
let eps = 0.5;
if tick.pixel < plot.y - eps || tick.pixel > plot.y + plot.h + eps {
continue;
}
commands.push(SceneCommand::StrokeLine {
x1: plot.x,
y1: tick.pixel,
x2: plot.x + plot.w,
y2: tick.pixel,
color: colors.grid,
stroke_width: 1.0,
stroke_dash: None,
stroke_gap: None,
stroke_linecap: None,
});
let label = format_tick_label(tick.value);
let req = ShapeRequest {
text: &label,
families: &label_families,
weight: 400,
style: FontStyle::Normal,
font_size: 9.0,
direction: TextDirection::Ltr,
};
match cx.engine.shape_with_fallback(&req, cx.fonts) {
Err(e) => {
diagnostics.push(Diagnostic::advisory(
"scene.text_unshaped",
format!(
"chart '{}' axis tick label '{}' could not be shaped: {}",
chart_id, label, e.message
),
None,
Some(chart_id.to_owned()),
));
}
Ok(result) => {
let total_advance: f64 = result.runs.iter().map(|r| r.advance_width as f64).sum();
let ascent: f64 = result.runs.first().map(|r| r.ascent as f64).unwrap_or(0.0);
let mut label_x = plot.x - 4.0 - total_advance;
let baseline_y = tick.pixel + ascent * 0.5;
for run in result.runs {
let advance = run.advance_width as f64;
let glyphs = run_to_scene_glyphs(&run);
commands.push(SceneCommand::DrawGlyphRun {
x: label_x,
y: baseline_y,
font_id: run.font_id.clone(),
font_size: run.font_size,
color: colors.label,
stroke_color: None,
stroke_width: None,
link: None,
selectable: true,
glyphs,
});
label_x += advance;
}
}
}
}
}
pub(super) fn emit_axis_lines(
plot: &PlotArea,
axis_color: Color,
commands: &mut Vec<SceneCommand>,
) {
if plot.w <= 0.0 || plot.h <= 0.0 {
return;
}
commands.push(SceneCommand::StrokeLine {
x1: plot.x,
y1: plot.y,
x2: plot.x,
y2: plot.y + plot.h,
color: axis_color,
stroke_width: 1.0,
stroke_dash: None,
stroke_gap: None,
stroke_linecap: None,
});
commands.push(SceneCommand::StrokeLine {
x1: plot.x,
y1: plot.y + plot.h,
x2: plot.x + plot.w,
y2: plot.y + plot.h,
color: axis_color,
stroke_width: 1.0,
stroke_dash: None,
stroke_gap: None,
stroke_linecap: None,
});
}
#[cfg(test)]
mod tests {
use super::format_tick_label;
#[test]
fn format_integer() {
assert_eq!(format_tick_label(0.0), "0");
assert_eq!(format_tick_label(42.0), "42");
assert_eq!(format_tick_label(-10.0), "-10");
assert_eq!(format_tick_label(100.0), "100");
}
#[test]
fn format_non_integer() {
assert_eq!(format_tick_label(42.5), "42.5");
assert_eq!(format_tick_label(-0.25), "-0.25");
assert_eq!(format_tick_label(1.1), "1.1");
}
#[test]
fn format_trailing_zero_trimmed() {
assert_eq!(format_tick_label(1.0), "1");
assert_eq!(format_tick_label(1.5), "1.5");
}
}