use super::funejson::Value; use super::charts_axis_axis_from_ticks::{axis_to_value, line_to_value}; use super::charts_axis_band_axis::band_axis; use super::charts_axis_grid_lines::grid_lines; use super::charts_axis_number_axis::number_axis; 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; use super::charts_scale_band_scale::band_scale; use super::charts_scale_linear_scale::linear_scale; use super::charts_scale_nice_domain::nice_domain; use super::charts_shape_bar_rects::{bar_rects, rect_to_value}; use super::charts_shape_types::BarSpec; use super::charts_stack::stack; use super::charts_ticks_nice_ticks::nice_ticks; use super::charts_ticks_tick_step::tick_step; use super::math_round_float::round_float; // 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; const CATEGORY_PADDING_INNER: f64 = 0.2; const CATEGORY_PADDING_OUTER: f64 = 0.1; const SERIES_PADDING: f64 = 0.05; /// A whole bar chart as geometry, grouped or stacked, by the fixed rules in /// the README. /// /// # Panics /// Panics on an unknown mode, no categories or series, a series of the wrong /// length, repeated categories or series names, or a chart too small for its /// margins. pub fn bar_chart(spec: &BarChartSpec) -> BarChart { let (width, height, categories, series) = (spec.width, spec.height, &spec.categories, &spec.series); if spec.mode != "grouped" && spec.mode != "stacked" { panic!("mode must be grouped or stacked, received {}", spec.mode); } if categories.is_empty() { panic!("a bar chart needs at least one category"); } if series.is_empty() { panic!("a bar chart needs at least one series"); } for s in series { if s.values.len() != categories.len() { panic!( "series \"{}\" has {} values but there are {} categories", s.name, s.values.len(), categories.len() ); } } let names: Vec = series.iter().map(|s| s.name.clone()).collect(); let mut legend: Vec = Vec::new(); let mut top = TOP_WITHOUT_LEGEND; if series.len() > 1 { 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 stacked = if spec.mode == "stacked" { let values: Vec> = series.iter().map(|s| s.values.clone()).collect(); Some(stack(&values, "diverging")) } else { None }; let (mut lo, mut hi) = (0.0f64, 0.0f64); for (i, s) in series.iter().enumerate() { for (j, v) in s.values.iter().enumerate() { let ends = match &stacked { None => vec![*v], Some(st) => vec![st[i][j].y0, st[i][j].y1], }; for e in ends { if e < lo { lo = e; } if e > hi { hi = e; } } } } if lo == hi { 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 = |v: f64| linear_scale(&y_domain, &y_range, v, false); 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_axis = band_axis(categories, &x_range, CATEGORY_PADDING_INNER, CATEGORY_PADDING_OUTER, "bottom", bottom_y, TICK_SIZE); let colors = palette("okabe-ito", series.len() as i64, true); let mut specs: Vec = Vec::new(); let mut meta: Vec<(String, String, f64, String)> = Vec::new(); for (i, s) in series.iter().enumerate() { for (j, c) in categories.iter().enumerate() { let outer = band_scale(categories, &x_range, c, CATEGORY_PADDING_INNER, CATEGORY_PADDING_OUTER, 0.5); let bar = match &stacked { Some(st) => BarSpec { band: outer.start, thickness: outer.width, base: y(st[i][j].y0), value: y(st[i][j].y1) }, None if series.len() == 1 => { BarSpec { band: outer.start, thickness: outer.width, base: y(0.0), value: y(s.values[j]) } } None => { let inner = band_scale(&names, &[outer.start, outer.start + outer.width], &s.name, SERIES_PADDING, 0.0, 0.5); BarSpec { band: inner.start, thickness: inner.width, base: y(0.0), value: y(s.values[j]) } } }; specs.push(bar); meta.push((s.name.clone(), c.clone(), s.values[j], colors[i].clone())); } } let rects = bar_rects(&specs, "vertical"); let bars: Vec = meta .into_iter() .zip(rects) .map(|((series, category, value, color), rect)| ChartBar { series, category, value, color, rect }) .collect(); BarChart { width: round_float(width, 2), height: round_float(height, 2), grid_lines: grid_lines(&y_axis, plot.width), plot, x_axis, y_axis, bars, colors, legend, } } pub fn bar_chart_spec_from_value(v: &Value) -> BarChartSpec { BarChartSpec { width: v.get("width").as_f64(), height: v.get("height").as_f64(), categories: v.get("categories").as_arr().iter().map(|c| c.as_str().to_string()).collect(), series: v .get("series") .as_arr() .iter() .map(|s| BarSeries { name: s.get("name").as_str().to_string(), values: s.get("values").as_arr().iter().map(|x| x.as_f64()).collect(), }) .collect(), mode: v.get("mode").as_str().to_string(), } } pub fn bar_chart_to_value(c: &BarChart) -> 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())), ( "bars", Value::Arr( c.bars .iter() .map(|b| { Value::obj(vec![ ("series", Value::str(&b.series)), ("category", Value::str(&b.category)), ("value", Value::Float(b.value)), ("color", Value::str(&b.color)), ("rect", rect_to_value(&b.rect)), ]) }) .collect(), ), ), ("colors", Value::Arr(c.colors.iter().map(|s| Value::str(s)).collect())), ("legend", Value::Arr(c.legend.iter().map(legend_item_to_value).collect())), ]) } pub fn fune_vector(args: &[Value]) -> Value { bar_chart_to_value(&bar_chart(&bar_chart_spec_from_value(&args[0]))) }