Add support for histogram charts

finchie
Apr 25, 2024, 4:24 AM
ZEN3WUPDVQWI7LPTEG3WQA5QSEHU74CUXVFJJFIJNSIFC7N2CMEAC

Dependencies

  • [2] YA5ITLOV Add support for Sankey diagrams
  • [3] PVUQYWZE Live-reload generated site using Trunk
  • [4] 7CVIL7UJ Create simple metadata parser
  • [5] JVYWRCPT Add basic chart visualisation
  • [6] LOR3KOXG Parse JSON output from `cargo build --timings`
  • [7] B2L26LOA Store index of dependency nodes
  • [8] ZPFD3275 Switch from `cargo_metadata`+`petgraph` to `guppy`
  • [9] C43IWI7G Move visualization logic into separate module
  • [*] OPTMCUTB Use timings `duration` to set size of rendered node

Change contents

  • replacement in src/visualize/mod.rs at line 3
    [3.76][2.1237:1302]()
    use charming::series::{Graph, GraphLayoutForce, Sankey, Series};
    [3.76]
    [2.1302]
    use charming::series::{Bar, Graph, GraphLayoutForce, Sankey, Series};
  • edit in src/visualize/mod.rs at line 8
    [3.215]
    [2.1340]
    mod histogram;
  • edit in src/visualize/mod.rs at line 15
    [2.1365]
    [3.274]
    Histogram,
  • edit in src/visualize/mod.rs at line 35
    [2.1543]
    [3.915]
    Style::Histogram => Bar::new()
    // TODO: charming does not support relative bar widths, but echarts does.
    // This is an exact value, NOT a percentage, and is wrong - each bar overlaps
    // itself. This is "fine" for a histogram, so leaving for now,
    // but should change eventually.
    .bar_width(100)
    .data(histogram::data(timings))
    .into(),
  • replacement in src/visualize/mod.rs at line 45
    [3.923][3.923:968]()
    let chart = Chart::new().series(series);
    [3.923]
    [3.968]
    let mut chart = Chart::new().series(series);
    // Use custom axes for histogram
    if matches!(style, Style::Histogram) {
    let (x_axis, y_axis) = histogram::axes(timings);
    chart = chart.x_axis(x_axis).y_axis(y_axis);
    }
  • file addition: histogram.rs (----------)
    [3.21]
    use crate::timings;
    use charming::{component::Axis, datatype::DataPoint, element::AxisType};
    const BUCKET_COUNT: usize = 10;
    fn find_min_max_duration(pkg_durations: &[f64]) -> (f64, f64) {
    assert!(!pkg_durations.is_empty());
    let mut min = f64::MAX;
    let mut max = f64::MIN;
    for duration in pkg_durations {
    min = duration.min(min);
    max = duration.max(max);
    }
    assert_ne!(min, f64::MAX);
    assert_ne!(max, f64::MIN);
    assert!(min.is_sign_positive());
    assert!(max.is_sign_positive());
    (min, max)
    }
    pub fn axes(timings: &timings::Output) -> (Axis, Axis) {
    let pkg_durations: Vec<f64> = timings.pkg_times().collect();
    let (min_duration, max_duration) = find_min_max_duration(&pkg_durations);
    let bucket_width = (max_duration - min_duration) / (BUCKET_COUNT as f64);
    let mut x_labels = Vec::with_capacity(BUCKET_COUNT);
    for bucket_index in 0..BUCKET_COUNT {
    // The start time is offset, first bucket starts at min_duration
    let start_time = min_duration + (bucket_width * (bucket_index as f64));
    // The label is the start time rounded to 2 decimal places
    x_labels.push(format!("{start_time:.2}"));
    }
    let x_axis = Axis::new().type_(AxisType::Category).data(x_labels);
    let y_axis = Axis::new().type_(AxisType::Value);
    (x_axis, y_axis)
    }
    pub fn data(timings: &timings::Output) -> Vec<DataPoint> {
    let pkg_durations: Vec<f64> = timings.pkg_times().collect();
    let (min_duration, max_duration) = find_min_max_duration(&pkg_durations);
    // Make sure to start the buckets at min, not 0
    let bucket_width = (max_duration - min_duration) / (BUCKET_COUNT as f64);
    let mut buckets = [0_u64; BUCKET_COUNT];
    for duration in pkg_durations {
    let relative_duration = duration - min_duration;
    let remainder = relative_duration % bucket_width;
    // Calculate the nearest multiple of bucket_size
    let next_multiple = (relative_duration - remainder) / bucket_width;
    // Make sure we have an integer before casting
    assert_eq!(next_multiple, next_multiple.floor());
    let bucket_index = next_multiple as usize;
    // Increment the frequency of the relevant bucket
    buckets[bucket_index] += 1;
    }
    // Convert the buckets into `charming::datatype::DataPoint`s
    buckets
    .into_iter()
    .map(|bucket| (bucket as i64).into())
    .collect()
    }
  • edit in src/timings.rs at line 149
    [11.181]
    [3.2689]
    }
    // TODO: this returns each total package time, but it would be interesting to filter by
    // crate type (lib, binary, proc_macro), target, build script runs etc
    pub fn pkg_times<'s>(&'s self) -> impl Iterator<Item = f64> + 's {
    self.repr
    .values()
    .map(|messages| messages.iter().map(|msg| msg.duration).sum())
  • replacement in src/main.rs at line 14
    [3.897][2.1544:1622]()
    visualize::for_style(visualize::Style::Sankey, &package_graph, &timings);
    [3.897]
    [3.924]
    visualize::for_style(visualize::Style::Histogram, &package_graph, &timings);