Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::charts_axis_axis_from_ticks::{axis_to_value, line_to_value};
use super::charts_axis_grid_lines::grid_lines;
use super::charts_axis_number_axis::number_axis;
use super::charts_axis_time_axis::time_axis;
use super::charts_extent::extent; ← from charts.extent ^1.0.0 · built alongside by fune
use super::charts_format_format_tick::format_tick;
use super::charts_layout_legend_rows::{legend_item_to_value, legend_rows};
use super::charts_layout_plot_area::{plot_area, plot_area_to_value};
use super::charts_layout_types::{LegendItem, Margins};
use super::charts_palette::palette; ← from charts.palette ^1.0.0 · built alongside by fune
use super::charts_scale_linear_scale::linear_scale;
use super::charts_scale_nice_domain::nice_domain;
use super::charts_scale_time_scale::time_scale;
use super::charts_shape_line_path::line_path;
use super::charts_shape_monotone_curve::monotone_curve;
use super::charts_shape_step_path::step_path;
use super::charts_shape_types::Point;
use super::charts_ticks_nice_ticks::nice_ticks;
use super::charts_ticks_tick_step::tick_step;
use super::math_round_float::round_float; ← from math.round-float ^1.0.0 · built alongside by fune
// Fixed layout constants; the README explains each.
const CHAR_WIDTH: f64 = 7.0;
const SWATCH: f64 = 10.0;
const LEGEND_GAP: f64 = 16.0;
const LEGEND_ROW: f64 = 18.0;
const EDGE: f64 = 8.0;
const TICK_SIZE: f64 = 6.0;
const LABEL_PADDING: f64 = 3.0;
const TOP_WITHOUT_LEGEND: f64 = 12.0;
const RIGHT: f64 = 20.0;
const BOTTOM: f64 = 30.0;
/// A whole line chart as geometry, by the fixed rules in the README.
///
/// # Panics
/// Panics on no series, both or neither of x and dates, a series of the wrong
/// length, no values, fewer than two different x values, or a chart too small
/// for its margins.
pub fn line_chart(spec: &LineChartSpec) -> LineChart {
let (width, height, series) = (spec.width, spec.height, &spec.series);
if series.is_empty() {
panic!("a line chart needs at least one series");
}
if spec.x.is_none() == spec.dates.is_none() {
panic!("give exactly one of x and dates");
}
let count = match (&spec.x, &spec.dates) {
(Some(x), _) => x.len(),
(_, Some(d)) => d.len(),
_ => 0,
};
for s in series {
if s.values.len() != count {
panic!("series \"{}\" has {} values but there are {} x values", s.name, s.values.len(), count);
}
}
let mut legend: Vec<LegendItem> = Vec::new();
let mut top = TOP_WITHOUT_LEGEND;
if series.len() > 1 {
let names: Vec<String> = series.iter().map(|s| s.name.clone()).collect();
let rows = legend_rows(&names, width - 2.0 * EDGE, CHAR_WIDTH, SWATCH, LEGEND_GAP, LEGEND_ROW);
legend = rows
.iter()
.map(|it| LegendItem {
label: it.label.clone(),
row: it.row,
x: round_float(it.x + EDGE, 2),
y: round_float(it.y + EDGE, 2),
width: it.width,
text_x: round_float(it.text_x + EDGE, 2),
})
.collect();
top = EDGE + (rows[rows.len() - 1].row + 1) as f64 * LEGEND_ROW + EDGE;
}
let all: Vec<Option<f64>> = series.iter().flat_map(|s| s.values.iter().cloned()).collect();
let found = match extent(&all) {
None => panic!("a line chart needs at least one value to plot"),
Some(r) => r,
};
let (mut lo, mut hi) = (found[0], found[1]);
if spec.y_zero {
lo = lo.min(0.0);
hi = hi.max(0.0);
}
if lo == hi {
lo -= 1.0;
hi += 1.0;
}
let y_count = (((height - top - BOTTOM) / 50.0).floor() as i64).max(2);
let y_domain = nice_domain(&[lo, hi], y_count).domain;
let y_step = tick_step(y_domain[0], y_domain[1], y_count);
let widest = nice_ticks(y_domain[0], y_domain[1], y_count)
.iter()
.map(|v| format_tick(*v, y_step).chars().count())
.max()
.unwrap_or(0);
let left = TICK_SIZE + LABEL_PADDING + widest as f64 * CHAR_WIDTH + EDGE;
let plot = plot_area(width, height, &Margins { top, right: RIGHT, bottom: BOTTOM, left });
let bottom_y = plot.y + plot.height;
let y_range = vec![bottom_y, plot.y];
let y_axis = number_axis(&y_domain, &y_range, y_count, "left", plot.x, TICK_SIZE);
let x_range = vec![plot.x, plot.x + plot.width];
let x_count = ((plot.width / 80.0).floor() as i64).max(2);
let (x_axis, xs) = match (&spec.x, &spec.dates) {
(Some(x), _) => {
let opt: Vec<Option<f64>> = x.iter().map(|v| Some(*v)).collect();
let d = match extent(&opt) {
Some(d) if d[0] != d[1] => d,
_ => panic!("a line chart needs at least two different x values"),
};
let xs: Vec<f64> = x.iter().map(|v| linear_scale(&d, &x_range, *v, false)).collect();
(number_axis(&d, &x_range, x_count, "bottom", bottom_y, TICK_SIZE), xs)
}
(_, Some(dates)) => {
let first = dates.iter().min().cloned();
let last = dates.iter().max().cloned();
let (first, last) = match (first, last) {
(Some(f), Some(l)) if f != l => (f, l),
_ => panic!("a line chart needs at least two different x values"),
};
let domain = vec![first, last];
let xs: Vec<f64> = dates.iter().map(|d| time_scale(&domain, &x_range, d, false)).collect();
(time_axis(&domain, &x_range, x_count, "bottom", bottom_y, TICK_SIZE), xs)
}
_ => unreachable!(),
};
let colors = palette("okabe-ito", series.len() as i64, true);
let lines: Vec<SeriesPath> = series
.iter()
.enumerate()
.map(|(i, s)| {
let points: Vec<Point> = s
.values
.iter()
.enumerate()
.map(|(j, v)| Point { x: xs[j], y: v.map(|v| linear_scale(&y_domain, &y_range, v, false)) })
.collect();
let path = match spec.curve.as_str() {
"monotone" => monotone_curve(&points),
"step" => step_path(&points, "middle"),
_ => line_path(&points),
};
SeriesPath { name: s.name.clone(), color: colors[i].clone(), path }
})
.collect();
LineChart {
width: round_float(width, 2),
height: round_float(height, 2),
grid_lines: grid_lines(&y_axis, plot.width),
plot,
x_axis,
y_axis,
lines,
legend,
}
}
fn opt_floats(v: &Value) -> Vec<Option<f64>> {
v.as_arr().iter().map(|x| if x.is_null() { None } else { Some(x.as_f64()) }).collect()
}
pub fn line_chart_spec_from_value(v: &Value) -> LineChartSpec {
LineChartSpec {
width: v.get("width").as_f64(),
height: v.get("height").as_f64(),
x: if v.get("x").is_null() { None } else { Some(v.get("x").as_arr().iter().map(|x| x.as_f64()).collect()) },
dates: if v.get("dates").is_null() {
None
} else {
Some(v.get("dates").as_arr().iter().map(|x| x.as_str().to_string()).collect())
},
series: v
.get("series")
.as_arr()
.iter()
.map(|s| LineSeries { name: s.get("name").as_str().to_string(), values: opt_floats(s.get("values")) })
.collect(),
curve: v.get("curve").as_str().to_string(),
y_zero: v.get("yZero").as_bool(),
}
}
pub fn line_chart_to_value(c: &LineChart) -> Value {
Value::obj(vec![
("width", Value::Float(c.width)),
("height", Value::Float(c.height)),
("plot", plot_area_to_value(&c.plot)),
("xAxis", axis_to_value(&c.x_axis)),
("yAxis", axis_to_value(&c.y_axis)),
("gridLines", Value::Arr(c.grid_lines.iter().map(line_to_value).collect())),
(
"lines",
Value::Arr(
c.lines
.iter()
.map(|l| {
Value::obj(vec![
("name", Value::str(&l.name)),
("color", Value::str(&l.color)),
("path", Value::str(&l.path)),
])
})
.collect(),
),
),
("legend", Value::Arr(c.legend.iter().map(legend_item_to_value).collect())),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
line_chart_to_value(&line_chart(&line_chart_spec_from_value(&args[0])))
}