Skip to content

Commit

Permalink
add rudimentary line chart
Browse files Browse the repository at this point in the history
  • Loading branch information
Ralf Grubenmann committed Mar 17, 2024
1 parent 893c042 commit 9f53c68
Show file tree
Hide file tree
Showing 4 changed files with 238 additions and 121 deletions.
133 changes: 12 additions & 121 deletions src/bar.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{axis::YAxis, ChartColor, Palette, CATPPUCCIN_COLORS};
use crate::{axis::YAxis, utils, ChartColor, Palette, CATPPUCCIN_COLORS};
use leptos::{svg::*, *};
use leptos_use::*;
use num_traits::ToPrimitive;
Expand All @@ -17,85 +17,6 @@ impl Default for BarChartOptions {
}
}

#[derive(Clone, Debug, PartialEq)]
struct TickSpacing {
min_point: f64,
max_point: f64,
spacing: f64,
num_ticks: u8,
}

#[allow(clippy::collapsible_else_if)]
fn nice_num(num: f64, round: bool) -> f64 {
let exponent = num.log10().floor();
let fraction = num / 10.0f64.powf(exponent);
let nice_fraction = if round {
if fraction < 1.5 {
1.0
} else if fraction < 3.0 {
2.0
} else if fraction < 7.0 {
5.0
} else {
10.0
}
} else {
if fraction <= 1.0 {
1.0
} else if fraction <= 2.0 {
2.0
} else if fraction <= 5.0 {
5.0
} else {
10.0
}
};
nice_fraction * 10.0f64.powf(exponent)
}

fn nice_ticks(min: f64, max: f64, max_ticks: u8) -> TickSpacing {
let range = nice_num(max - min, false);
let spacing = nice_num(range / (max_ticks - 1) as f64, true);
let min_point = (min / spacing).floor() * spacing;
let max_point = (max / spacing).ceil() * spacing;
let num_ticks = ((max_point - min_point) / spacing) as u8 + 1;
TickSpacing {
min_point,
max_point,
spacing,
num_ticks,
}
}

#[allow(clippy::ptr_arg)]
fn get_min_max<T>(values: &Vec<T>) -> (f64, f64)
where
T: ToPrimitive + Clone + PartialOrd + 'static,
{
let min_max = values
.iter()
.map(|v| v.to_f64().unwrap())
.fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), v| {
(f64::min(a, v), f64::max(b, v))
});
(
if min_max.0 < 0.0 { min_max.0 } else { 0.0 },
if min_max.1 > 0.0 { min_max.1 } else { 0.0 },
)
}

fn get_ticks(ticks: &TickSpacing) -> Vec<(f64, String)> {
(0..ticks.num_ticks)
.map(|i| ticks.min_point + i as f64 * ticks.spacing)
.map(move |tick| {
(
100.0 - (tick - ticks.min_point) / (ticks.max_point - ticks.min_point) * 100.0,
format!("{}", tick),
)
})
.collect::<Vec<(f64, String)>>()
}

/// Simple responsive bar chart
///
/// Example:
Expand Down Expand Up @@ -135,7 +56,7 @@ where
let vals = values.clone();
let num_bars = create_memo(move |_| vals.get().len());
let vals = values.clone();
let min_max = create_memo(move |_| vals.with(get_min_max));
let min_max = create_memo(move |_| vals.with(utils::get_min_max));
let values = create_memo(move |_| {
values
.get()
Expand All @@ -145,8 +66,9 @@ where
.collect::<Vec<(usize, f64)>>()
});
let max_ticks = options.max_ticks;
let tick_config = create_memo(move |_| nice_ticks(min_max.get().0, min_max.get().1, max_ticks));
let ticks = create_memo(move |_| tick_config.with(get_ticks));
let tick_config =
create_memo(move |_| utils::nice_ticks(min_max.get().0, min_max.get().1, max_ticks));
let ticks = create_memo(move |_| tick_config.with(utils::get_ticks));

view! {
<svg {..attrs}>
Expand Down Expand Up @@ -212,15 +134,18 @@ where
vector-effect="non-scaling-stroke"
x=move || {
format!(
"{}%", (15.0 + 85.0 / num_bars.get() as f64 * (i as f64 +
0.5))
"{}%",
(15.0 + 85.0 / num_bars.get() as f64 * (i as f64 + 0.5)),
)
}

y=move || {
format!(
"{}%", (100.0 - 100.0 * (v - tick_config.get().min_point) /
(tick_config.get().max_point - tick_config.get().min_point))
"{}%",
(100.0
- 100.0 * (v - tick_config.get().min_point)
/ (tick_config.get().max_point
- tick_config.get().min_point)),
)
}

Expand All @@ -238,37 +163,3 @@ where
</svg>
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn min_max() {
let values = vec![-4, 10, 0, 50, 2, -6, 7];
assert_eq!(get_min_max(&values), (-6.0, 50.0));
let values = vec![-4.9, 10.0, 0.8, 50.2, 2.7, -6.3, 7.5];
assert_eq!(get_min_max(&values), (-6.3, 50.2));
let values = vec![-4, -10, -3, -50, -2, -6, -7];
assert_eq!(get_min_max(&values), (-50.0, 0.0));
let values = vec![4, 10, 2, 50, 2, 6, 7];
assert_eq!(get_min_max(&values), (0.0, 50.0));
}

#[test]
fn ticks() {
let ticks = nice_ticks(-10.0, 10.0, 10);
assert_eq!(ticks.min_point, -10.0);
assert_eq!(ticks.max_point, 10.0);
assert_eq!(ticks.spacing, 2.0);
assert_eq!(ticks.num_ticks, 11);

let ticks = get_ticks(&ticks);
assert_eq!(ticks[0].0, 100.0);
assert_eq!(ticks[0].1, "-10");
assert_eq!(ticks[4].0, 60.0);
assert_eq!(ticks[4].1, "-2");
assert_eq!(ticks[10].0, 0.0);
assert_eq!(ticks[10].1, "10");
}
}
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ pub mod axis;
pub mod bar;
pub mod color;
pub mod legend;
pub mod line;
pub mod pie;
pub mod point;
pub mod utils;

pub use bar::{BarChart, BarChartOptions};
pub use color::{CalculatedColor, ChartColor, Color, Gradient, Palette, CATPPUCCIN_COLORS};
pub use line::{LineChart, LineChartOptions};
pub use pie::{PieChart, PieChartOptions};
pub use point::{Point, Series};
109 changes: 109 additions & 0 deletions src/line.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
use std::cmp;

use crate::{axis::YAxis, utils, ChartColor, Color, Palette, CATPPUCCIN_COLORS};
use itertools::Itertools;
use leptos::{svg::*, *};
use leptos_use::*;
use num_traits::ToPrimitive;

pub struct LineChartOptions {
pub max_ticks: u8,
pub color: Box<dyn ChartColor>,
}

impl Default for LineChartOptions {
fn default() -> Self {
Self {
max_ticks: 5u8,
color: Box::new(Palette(vec![Color::Hex("#dd3333")])),
}
}
}

#[component]
pub fn LineChart<T, U>(
values: MaybeSignal<Vec<(T, U)>>,
options: Box<LineChartOptions>,
#[prop(attrs)] attrs: Vec<(&'static str, Attribute)>,
) -> impl IntoView
where
T: ToPrimitive + Clone + PartialOrd + 'static,
U: ToPrimitive + Clone + PartialOrd + 'static,
{
let values = create_memo(move |_| {
values
.get()
.into_iter()
.map(|(x, y)| (x.to_f64().unwrap(), y.to_f64().unwrap()))
.collect::<Vec<(f64, f64)>>()
});
let min_max = create_memo(move |_| {
values.get().iter().fold(
(
(f64::INFINITY, f64::NEG_INFINITY),
(f64::INFINITY, f64::NEG_INFINITY),
),
|((acc_min_x, acc_max_x), (acc_min_y, acc_max_y)), (x, y)| {
(
(f64::min(acc_min_x, *x), f64::max(acc_max_x, *x)),
(f64::min(acc_min_y, *y), f64::max(acc_max_y, *y)),
)
},
)
});
let max_ticks = options.max_ticks;
let tick_config =
create_memo(move |_| utils::nice_ticks(min_max.get().1 .0, min_max.get().1 .1, max_ticks));
let ticks = create_memo(move |_| tick_config.with(utils::get_ticks));
view! {
<svg {..attrs}>
<YAxis ticks=ticks/>
<svg
x="10%"
y="10%"
width="90%"
height="80%"
viewBox="0 0 100 100"
preserveAspectRatio="none"
>
<g transform="matrix(1 0 0 -1 0 100)">
<defs>
<linearGradient id="gradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop
offset="0%"
stop-color=String::from(options.color.color_for_index(0, 2))
></stop>
<stop
offset="100%"
stop-color=String::from(options.color.color_for_index(1, 2))
></stop>
</linearGradient>
</defs>
<polyline
fill="none"
style="stroke:url(#gradient)"
stroke-width="0.1"
stroke-linejoin="round"
points=move || {
values
.get()
.into_iter()
.map(|(x, y)| (
100.0 * (x - min_max.get().0.0)
/ (min_max.get().0.1 - min_max.get().0.0),
100.0 * (y - tick_config.get().min_point)
/ (tick_config.get().max_point
- tick_config.get().min_point),
))
.map(|(x, y)| format!("{},{}", x, y))
.intersperse(" ".to_string())
.collect::<String>()
}
>
</polyline>

</g>
</svg>
</svg>
}
}
Loading

0 comments on commit 9f53c68

Please sign in to comment.