use super::funejson::Value; use super::math_round_float::round_float; // d3-axis's default gap between the end of a tick mark and its label. const LABEL_PADDING: f64 = 3.0; /// A pixel coordinate rounded to 2 places, the precision every axis output uses. pub fn axis_pixel(value: f64) -> f64 { round_float(value, 2) } pub fn axis_line(x1: f64, y1: f64, x2: f64, y2: f64) -> Line { Line { x1: axis_pixel(x1), y1: axis_pixel(y1), x2: axis_pixel(x2), y2: axis_pixel(y2) } } pub fn check_orient(orient: &str) { if !matches!(orient, "bottom" | "top" | "left" | "right") { panic!("orient must be bottom, top, left or right, received \"{}\"", orient); } } /// Axis geometry from tick positions and labels, for any scale (d3-axis layout). /// /// # Panics /// Panics on an unknown orient, positions and labels of different lengths, or /// a range without exactly two values. pub fn axis_from_ticks(positions: &[f64], labels: &[String], range: &[f64], orient: &str, position: f64, tick_size: f64) -> Axis { check_orient(orient); if positions.len() != labels.len() { panic!( "positions and labels must be the same length, received {} and {}", positions.len(), labels.len() ); } if range.len() != 2 { panic!("range must have exactly 2 values, [from, to]; got {}", range.len()); } let horizontal = orient == "bottom" || orient == "top"; let sign = if orient == "bottom" || orient == "right" { 1.0 } else { -1.0 }; let mark_end = position + sign * tick_size; let label_at = position + sign * (tick_size + LABEL_PADDING); let ticks = positions .iter() .zip(labels) .map(|(&p, label)| { if horizontal { AxisTick { position: axis_pixel(p), label: label.clone(), tick: axis_line(p, position, p, mark_end), label_x: axis_pixel(p), label_y: axis_pixel(label_at), anchor: "middle".to_string(), baseline: if orient == "bottom" { "hanging" } else { "alphabetic" }.to_string(), } } else { AxisTick { position: axis_pixel(p), label: label.clone(), tick: axis_line(position, p, mark_end, p), label_x: axis_pixel(label_at), label_y: axis_pixel(p), anchor: if orient == "left" { "end" } else { "start" }.to_string(), baseline: "middle".to_string(), } } }) .collect(); let line = if horizontal { axis_line(range[0], position, range[1], position) } else { axis_line(position, range[0], position, range[1]) }; Axis { orient: orient.to_string(), position: axis_pixel(position), line, ticks } } pub fn line_to_value(l: &Line) -> Value { Value::obj(vec![ ("x1", Value::Float(l.x1)), ("y1", Value::Float(l.y1)), ("x2", Value::Float(l.x2)), ("y2", Value::Float(l.y2)), ]) } pub fn line_from_value(v: &Value) -> Line { Line { x1: v.get("x1").as_f64(), y1: v.get("y1").as_f64(), x2: v.get("x2").as_f64(), y2: v.get("y2").as_f64() } } pub fn axis_to_value(a: &Axis) -> Value { Value::obj(vec![ ("orient", Value::str(&a.orient)), ("position", Value::Float(a.position)), ("line", line_to_value(&a.line)), ( "ticks", Value::Arr( a.ticks .iter() .map(|t| { Value::obj(vec![ ("position", Value::Float(t.position)), ("label", Value::str(&t.label)), ("tick", line_to_value(&t.tick)), ("labelX", Value::Float(t.label_x)), ("labelY", Value::Float(t.label_y)), ("anchor", Value::str(&t.anchor)), ("baseline", Value::str(&t.baseline)), ]) }) .collect(), ), ), ]) } pub fn axis_from_value(v: &Value) -> Axis { Axis { orient: v.get("orient").as_str().to_string(), position: v.get("position").as_f64(), line: line_from_value(v.get("line")), ticks: v .get("ticks") .as_arr() .iter() .map(|t| AxisTick { position: t.get("position").as_f64(), label: t.get("label").as_str().to_string(), tick: line_from_value(t.get("tick")), label_x: t.get("labelX").as_f64(), label_y: t.get("labelY").as_f64(), anchor: t.get("anchor").as_str().to_string(), baseline: t.get("baseline").as_str().to_string(), }) .collect(), } } /// A JSON list of numbers or strings, for the vector adapters. pub fn axis_floats(v: &Value) -> Vec { v.as_arr().iter().map(|x| x.as_f64()).collect() } pub fn axis_strings(v: &Value) -> Vec { v.as_arr().iter().map(|x| x.as_str().to_string()).collect() } pub fn fune_vector(args: &[Value]) -> Value { axis_to_value(&axis_from_ticks( &axis_floats(&args[0]), &axis_strings(&args[1]), &axis_floats(&args[2]), args[3].as_str(), args[4].as_f64(), args[5].as_f64(), )) }