Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
265 views
in Technique[技术] by (71.8m points)

rust - How to plot a series with a date on the x-axis and time on the y-axis?

I want to plot points, where the x-coordinate is a Date and the y-coordinate is a Time (like a NaiveTime from Chrono).

I think this will be easiest using the Plotters crate. Unfortunately, it doesn't support NaiveTime on the y-axis by default, as shown by this GitHub issue. However, the documentation at https://plotters-rs.github.io/book/basic/basic_data_plotting.html?highlight=date#time-series-chart states that

In theory Plotters supports any data type to be axis. The only requirement is to implement the axis mapping traits.

So that sounds good.

Therefore, I adapted their Stock.rs example, and got to the following.

use chrono::{Date, Duration, ParseError, DateTime, Utc, NaiveTime};
use chrono::offset::{Local, TimeZone};
use plotters::prelude::*;

fn parse_datetime(t: &str) -> Date<Local> {
    Local
        .datetime_from_str(&format!("{} 0:0", t), "%Y-%m-%d %H:%M")
        .unwrap()
        .date()
}

/// Workaround attempt: use a mock Date in order to get a DateTime instead of a NaiveTime
fn parse_time_as_datetime(t: &str) -> Result<DateTime<Local>, ParseError> {
    return match Local.datetime_from_str(&format!("2020-01-01 {}", t), "%Y-%m-%d %H:%M.%S") {
        Ok(date) => Ok(date),
        Err(e) => { println!("{}", e); Err(e) },
    };
}

fn parse_time(t: &str) -> Result<NaiveTime, ParseError> {
    return match Local.datetime_from_str(t, "%M:%S%.f") {
        Ok(date) => Ok(date.time()),
        Err(e) => { println!("{}", e); Err(e) },
    };
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let data = get_data();
    let root = BitMapBackend::new("stock-example.png", (1024, 768)).into_drawing_area();
    root.fill(&WHITE)?;

    let (from_date, to_date) = (
        parse_datetime(&data[0].0) + Duration::days(1),
        parse_datetime(&data[4].0) - Duration::days(1),
    );

    let y_min = parse_time("9:30.0").unwrap();
    let y_max = parse_time("13:00.0").unwrap();
    // Workaround attempt: use DateTime instead of NaiveTime
    // let y_min = Local.datetime_from_str("2020-01-01 0:0", "%Y-%m-%d %H:%M").unwrap().date();
    // let y_max = Local.datetime_from_str("2020-01-02 0:1", "%Y-%m-%d %H:%M").unwrap().date();

    let mut chart = ChartBuilder::on(&root)
        .x_label_area_size(40)
        .y_label_area_size(40)
        .caption("Time", ("sans-serif", 30.0).into_font())
        .build_cartesian_2d(from_date..to_date, y_min..y_max)?;

    chart.configure_mesh().light_line_style(&WHITE).draw()?;

    chart.draw_series(
        data.iter()
            .map(|x| Circle::new((parse_datetime(x.0), parse_time(x.1).unwrap()), 5, BLUE.filled())),
    )?;

    Ok(())
}

fn get_data() -> Vec<(&'static str, &'static str, f32, f32, f32)> {
    return vec![
        ("2019-04-18", "10:11.5", 16.0, 121.3018, 123.3700),
        ("2019-04-22", "10:52.2", 15.0, 122.5700, 123.7600),
        ("2019-04-23", "12:23.5", 14.0, 123.8300, 125.4400),
        ("2019-04-24", "10:15.0", 13.0, 124.5200, 125.0100),
        ("2019-04-25", "10:43.9", 12.0, 128.8300, 129.1500),
    ];
}

Result:

error[E0277]: the trait bound `std::ops::Range<NaiveTime>: plotters::prelude::Ranged` is not satisfied
  --> src/main.rs:47:49
   |
47 |         .build_cartesian_2d(from_date..to_date, y_min..y_max)?;
   |                                                 ^^^^^^^^^^^^ the trait `plotters::prelude::Ranged` is not implemented for `std::ops::Range<NaiveTime>`

Now, how to implement the Trait. ... except that in https://docs.rs/plotters/0.3.0/src/plotters/coord/ranged1d/types/datetime.rs.html#600 it seems to be already implemented. So, we can do .build_cartesian_2d(from_date..to_date, RangedDateTime(y_min, y_max))?;

which fails with

52  |         .build_cartesian_2d(from_date..to_date, RangedDateTime(y_min, y_max))?;
    |                                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use struct literal syntax instead: `RangedDateTime { 0: val, 1: val }`

So we change https://docs.rs/plotters/0.3.0/src/plotters/coord/ranged1d/types/datetime.rs.html#581 to make the struct fields public: pub struct RangedDateTime<DT: Datelike + Timelike + TimeValue>(pub DT, pub DT);. It fails with

52 |         .build_cartesian_2d(from_date..to_date, RangedDateTime(y_min, y_max))?;
   |                                                 ^^^^^^^^^^^^^^ the trait `ranged1d::types::datetime::TimeValue` is not implemented for `NaiveTime`

However, we cannot implement TimeValue for NaiveTime, because this is the current definition:

pub trait TimeValue: Eq {
    type DateType: Datelike + PartialOrd;
    // ...
}

and NaiveTime is not DateLike. I don't know how to go from here, I considered the following options:

  • Add TimeLike to the restriction of the associated type of TimeValue,
  • Remove DateLike from the asscociated type of TimeValue,
  • Add a separate trait DateTimeValue which can have TimeLike as its DateType.
question from:https://stackoverflow.com/questions/65937447/how-to-plot-a-series-with-a-date-on-the-x-axis-and-time-on-the-y-axis

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

In this case it is easier to use Duration from Chrono instead, because Plotters does have a RangedDuration implemented as well, it's just a bit hidden.

Note you can use custom axes label formatters, so you can still format it as time of day if you want.

Fixed example:

#![feature(allocator_api)]

use chrono::{Date, Duration, ParseError, NaiveTime};
use chrono::offset::{Local, TimeZone};
use plotters::prelude::*;

fn parse_datetime(t: &str) -> Date<Local> {
    Local
        .datetime_from_str(&format!("{} 0:0", t), "%Y-%m-%d %H:%M")
        .unwrap()
        .date()
}

fn parse_time(t: &str) -> Result<Duration, ParseError> {
    return match Local.datetime_from_str(&format!("2020-01-01 0:{}", t), "%Y-%m-%d %H:%M:%S%.f") {
        Ok(date) => Ok(date.time().signed_duration_since(NaiveTime::from_hms(0, 0, 0))),
        Err(e) => { println!("{}", e); Err(e) },
    };
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let data = get_data();
    let root = BitMapBackend::new("stock-example.png", (1024, 768)).into_drawing_area();
    root.fill(&WHITE)?;

    let (from_date, to_date) = (
        parse_datetime(&data[0].0) - Duration::days(1),
        parse_datetime(&data[data.len() - 1].0) + Duration::days(1),
    );

    let y_min = parse_time("9:30.0").unwrap();
    let y_max = parse_time("13:00.0").unwrap();

    let mut chart = ChartBuilder::on(&root)
        .x_label_area_size(40)
        .y_label_area_size(50)
        .caption("Time", ("sans-serif", 30.0).into_font())
        .build_cartesian_2d(from_date..to_date, y_min..y_max)?;

    chart.configure_mesh()
        .light_line_style(&WHITE)
        .y_label_formatter(&|y| format!("{:02}:{:02}", y.num_minutes(), y.num_seconds() % 60))
        .x_label_formatter(&|x| x.naive_local().to_string())
        .draw()?;

    chart.draw_series(
        data.iter()
            .map(|x| Circle::new((parse_datetime(x.0), parse_time(x.1).unwrap()), 5, BLUE.filled())),
    )?;

    Ok(())
}

fn get_data() -> Vec<(&'static str, &'static str, f32, f32, f32)> {
    return vec![
        ("2019-04-18", "10:11.5", 16.0, 121.3018, 123.3700),
        ("2019-04-22", "10:52.2", 15.0, 122.5700, 123.7600),
        ("2019-04-23", "12:23.5", 14.0, 123.8300, 125.4400),
        ("2019-04-24", "10:15.0", 13.0, 124.5200, 125.0100),
        ("2019-04-25", "10:43.9", 12.0, 128.8300, 129.1500),
    ];
}

enter image description here


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

2.1m questions

2.1m answers

60 comments

57.0k users

...