I think I may have run into a bug in the plotters
crate, but it is such a common task on a widely used crate that I think I must be doing something wrong. I'm unable to draw a histogram of floating point data; indeed, all the documentation and online samples I've been able to find use integers for the domain.
Here's my code. I've trimmed it down as much as I can to a standalone sample. It runs fine, but displays only the axes and labels, no data. If I cast the domain values to i32
, it displays the bars (see the 2 commented out lines).
Should I file a bug report on plotters-rs/plotters? Or can you spot the error in my code?!
By the way, these bugs are possibly related (closest I could find): #470 histograms fail silently, #300 histogram drawn with negative width rects
Thank you!
Tim
use anyhow::{bail, Result};
use ndarray::prelude::*;
use ndarray_rand::{rand_distr::StandardNormal, RandomExt};
use ndarray_stats::histogram::{strategies::Auto, Grid, GridBuilder, HistogramExt};
use noisy_float::types::{r64, R64};
use plotters::{prelude::*, style::full_palette as palette};
use std::{fmt, ops::Range};
fn main() -> Result<()> {
// Histogram wants a 2D array, and it's fine for the second dimension to be 1.
let a = Array::<f64, _>::random((1_000, 1), StandardNormal).mapv(r64);
let a = a * 5.3 + 1.65;
let grid = GridBuilder::<Auto<R64>>::from_array(&a).unwrap().build();
println!("grid: shape {:?}, {grid:?}", grid.shape());
// FIXME: Is this really necessary? How do people normally do this?
let ranges = extract_grid_ranges(&grid)?;
println!("\nranges: {} {ranges:?}", ranges.len());
let hist = a.histogram(grid);
println!("\nhist: counts {:?}", hist.counts());
let root_area = BitMapBackend::new("histogram.png", (800, 600)).into_drawing_area();
root_area.fill(&palette::GREY_300).unwrap();
let mut ctx = ChartBuilder::on(&root_area)
.set_label_area_size(LabelAreaPosition::Left, 40)
.set_label_area_size(LabelAreaPosition::Right, 40)
.set_label_area_size(LabelAreaPosition::Bottom, 40)
.caption("Histogram from Grid", ("sans-serif", 40))
.build_cartesian_2d((-20.0..20.0).step(1.0), 0..130)
// INTEGER WORKS: .build_cartesian_2d(-20..20, 0..130)
.unwrap();
ctx.configure_mesh().draw().unwrap();
let counts = hist.counts();
let data = ranges
.iter()
.zip(counts.iter())
.map(|(r, c)| (r.start, *c as i32))
// INTEGER WORKS: .map(|(r, c)| (r.start.raw() as i32, *c as i32))
.collect::<Vec<_>>();
println!("\nCharting data: {data:?}");
ctx.draw_series(
Histogram::vertical(&ctx)
.style(BLUE.filled())
.margin(2)
.data(data),
)
.unwrap();
root_area.present()?;
Ok(())
}
fn grid_size<A>(grid: &Grid<A>) -> Result<usize>
where
A: Ord,
{
if grid.ndim() != 1 {
bail!("Only implemented for 1-dimensional grids, sorry");
}
Ok(*grid.shape().first().unwrap())
}
fn extract_grid_ranges<A>(grid: &Grid<A>) -> Result<Vec<Range<A>>>
where
A: Ord + Clone + fmt::Debug,
{
let size = grid_size(grid)?;
Ok((0..size)
.flat_map(|i| grid.index(&[i]))
.collect::<Vec<Range<A>>>())
}