plotters-rs / plotters

A rust drawing library for high quality data plotting for both WASM and native, statically and realtimely 🦀 📈🚀
https://plotters-rs.github.io/home/
MIT License
3.92k stars 281 forks source link

[Feature Request] Dynamic dispatch should be allows as long as the plot is accepting the same data points #121

Open mlange-42 opened 4 years ago

mlange-42 commented 4 years ago

If the chart is defined to accept same data type, even though the axis is using different types of decorator, those chart types should be cvt into a trait object.

==== Original issue (one of valid use case)

First, many thanks for providing this great library!

Learning Rust for 2 weeks now and not really grasping the type system yet, this may be more of a Rust question than a plotters question.

I try to create a chart context with optional log-scaled y axis:

let mut cc = if y_log {
    ChartBuilder::on(&root)
                .build_ranged(
                    (xlim.0)..(xlim.1),
                    LogRange((ylim.0)..(ylim.1)),
                ).unwrap()
} else {
    ChartBuilder::on(&root)
                .build_ranged(
                    (xlim.0)..(xlim.1),
                    (ylim.0)..(ylim.1),
                ).unwrap()
} 

Now, my problem is that this doesn't compile due to:

expected type `plotters::chart::context::ChartContext<'_, _, plotters::coord::ranged::RangedCoord<_, plotters::coord::numeric::RangedCoordf64>>`
 found struct `plotters::chart::context::ChartContext<'_, _, plotters::coord::ranged::RangedCoord<_, plotters::coord::logarithmic::LogCoord<f64>>>`

When I use the condition in build_ranged

if y_log { LogRange((ylim.0)..(ylim.1)) } else { (ylim.0)..(ylim.1) }

I get a type mismatch between std::ops::Range and plotters::coord::logarithmic::LogRange.

I understand that I probably have to specify an explicit type for cc, probably with some dyn Trait in ChartContext's generics. However, due to my still very limited Rust experience, I can't get it to work.

Many thanks for your help!

38 commented 4 years ago

Hmm, this is something unsupported yet. You just spotted the root cause of this.

The cc variable doesn't have a valid type, since it can not be ChartContext<RangedCoord<_, :RangedCoordf64>> and ChartContext<'_, _, RangedCoord<_, LogCoord<f64>>> at the same time.

One way to address that (not only in Rust but also other programming languages) is using dynamic dispatch, which is, in Rust term, a trait object. However, Plotters doesn't have a trait for those two types, so the compiler don't know how to do that.

One work around is to use static dispatch instead (use template function).

Hope this is helpful, and dynamic dispatch on charts with same data type should be supported. Thanks again to come up with this problem, hopefully Plotters will support dynamic dispatch i the future release.

Thanks for trying Plotters and hope you enjoy both the crate and Rust language.

I am changing this issue to a feature request if you don't mind.

Cheers!

mlange-42 commented 4 years ago

Many thanks for the reply!

My solution so far was complete doubling of the plotting code, as you can see here: https://github.com/mlange-42/easy_graph/blob/master/src/ui/chart.rs#L406

Not really nice, but good to know that my limited Rust experience was not the sole reason.

And yes, I really enjoy Rust as well as plotters.

38 commented 4 years ago

doubling of the plotting code

Yep, you got that. To make it nicer, you can make duplicated code reusable with a template function.

That is very often used trick in both Rust and C++ I believe. Since in C++ it's also not recommended to frequently use dynamic dispatch, or AKA virtual method functions.

nwtnni commented 2 years ago

For anyone else running into this issue, the trait bounds I ended up with to get a linear or logarithmic Y-axis were:

use std::convert::identity;
use std::ops::Range;
use std::fmt::Debug;

use plotters::chart::ChartBuilder;
use plotters::coord::combinators::IntoLogRange;
use plotters::coord::ranged1d::AsRangedCoord;
use plotters::coord::ranged1d::DefaultFormatting;
use plotters::coord::ranged1d::Ranged;
use plotters::coord::ranged1d::ValueFormatter;

pub fn plot_linear() {
    plot(identity);
}

pub fn plot_logarithmic() {
    plot(IntoLogRange::log_scale);
}

fn plot<S>(
    scale: fn(Range<u64>) -> S,
)
where
    S: AsRangedCoord<Value = u64>,
    S::CoordDescType: ValueFormatter<u64> + Ranged<FormatOption = DefaultFormatting>,
    S::Value: Debug,
{
    ChartBuilder::on(...)
        .build_cartesian_2d(..., scale(0u64..100u64))?;   
}