Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow custom axis scales through common component #7

Merged
merged 4 commits into from
Jan 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion examples/basic/index.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Typical settings for all charts

.time-axis-x {
.some-x-axis {
.text {
transform: rotate(45deg);
}
.line {
stroke: grey;
}
Expand Down
33 changes: 20 additions & 13 deletions examples/basic/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
use std::{
ops::Add,
ops::{Range, Sub},
ops::{Add, Sub},
rc::Rc,
};

use chrono::{DateTime, Duration, Utc};
use chrono::{Duration, Utc};
use yew::prelude::*;
use yew_chart::{
horizontal_series::{self, HorizontalSeries, SeriesData, SeriesDataLabelled},
horizontal_time_axis::HorizontalTimeAxis,
horizontal_axis::{self, HorizontalAxis},
time_axis_scale::TimeAxisScale,
linear_axis_scale::LinearAxisScale,
vertical_axis::{self, VerticalAxis},
axis::AxisScale,
};

const WIDTH: u32 = 533;
Expand All @@ -20,7 +22,8 @@ const TICK_LENGTH: u32 = 10;
struct App {
data_set: Rc<SeriesData>,
data_set_labels: Rc<SeriesDataLabelled>,
time: Range<DateTime<Utc>>,
vertical_axis_scale: Rc<dyn AxisScale>,
horizontal_axis_scale: Rc<dyn AxisScale>,
}

impl Component for App {
Expand All @@ -31,6 +34,7 @@ impl Component for App {
fn create(_ctx: &Context<Self>) -> Self {
let end_date = Utc::now();
let start_date = end_date.sub(Duration::days(4));
let time = start_date..end_date;
App {
data_set: Rc::new(vec![
(start_date.timestamp() as f32, 1.0),
Expand All @@ -44,7 +48,8 @@ impl Component for App {
5.0,
horizontal_series::label("Label"),
)]),
time: start_date..end_date,
horizontal_axis_scale: Rc::new(TimeAxisScale::for_range(time, Duration::days(1), None)),
vertical_axis_scale: Rc::new(LinearAxisScale::for_range(0.0..5.0, 0.5))
}
}

Expand All @@ -60,21 +65,23 @@ impl Component for App {
name="some-series"
data={Rc::clone(&self.data_set)}
data_labels={Some(Rc::clone(&self.data_set_labels))}
horizontal_scale={self.time.start.timestamp() as f32..self.time.end.timestamp() as f32}
horizontal_scale_step={Duration::days(1).num_seconds() as f32}
vertical_scale={0.0..5.0}
horizontal_scale={Rc::clone(&self.horizontal_axis_scale)}
horizontal_scale_step={Duration::days(2).num_seconds() as f32}
vertical_scale={Rc::clone(&self.vertical_axis_scale)}
x={MARGIN} y={MARGIN} width={WIDTH - (MARGIN * 2)} height={HEIGHT - (MARGIN * 2)} />

<VerticalAxis
name="some-y-axis"
orientation={vertical_axis::Orientation::Left}
scale={0.0..5.0} scale_step={0.5}
scale={Rc::clone(&self.vertical_axis_scale)}
x1={MARGIN} y1={MARGIN} y2={HEIGHT - MARGIN}
tick_len={TICK_LENGTH}
title={"Some Y thing".to_string()} />

<HorizontalTimeAxis
time={self.time.to_owned()} time_step={Duration::days(1)}

<HorizontalAxis
name="some-x-axis"
orientation={horizontal_axis::Orientation::Bottom}
scale={Rc::clone(&self.horizontal_axis_scale)}
x1={MARGIN} y1={HEIGHT - MARGIN} x2={WIDTH - MARGIN}
tick_len={TICK_LENGTH} />

Expand Down
5 changes: 4 additions & 1 deletion examples/scatter/index.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Typical settings for all charts

.time-axis-x {
.some-x-axis {
.text {
transform: rotate(45deg);
}
.line {
stroke: grey;
}
Expand Down
34 changes: 20 additions & 14 deletions examples/scatter/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
use std::{
ops::Add,
ops::{Range, Sub},
ops::{Add, Sub},
rc::Rc,
};

use chrono::{DateTime, Duration, Utc};
use chrono::{Duration, Utc};
use yew::prelude::*;
use yew_chart::{
horizontal_series::{self, HorizontalSeries, SeriesData, SeriesDataLabelled},
horizontal_time_axis::HorizontalTimeAxis,
horizontal_axis::{self, HorizontalAxis},
time_axis_scale::TimeAxisScale,
linear_axis_scale::LinearAxisScale,
vertical_axis::{self, VerticalAxis},
axis::AxisScale,
};

const WIDTH: u32 = 533;
Expand All @@ -20,7 +22,8 @@ const TICK_LENGTH: u32 = 10;
struct App {
data_set: Rc<SeriesData>,
data_set_labels: Rc<SeriesDataLabelled>,
time: Range<DateTime<Utc>>,
vertical_axis_scale: Rc<dyn AxisScale>,
horizontal_axis_scale: Rc<dyn AxisScale>,
}

impl Component for App {
Expand All @@ -31,7 +34,7 @@ impl Component for App {
fn create(_ctx: &Context<Self>) -> Self {
let end_date = Utc::now();
let start_date = end_date.sub(Duration::days(4));

let time = start_date..end_date;
App {
data_set: Rc::new(vec![]),
data_set_labels: Rc::new(vec![
Expand Down Expand Up @@ -61,7 +64,8 @@ impl Component for App {
horizontal_series::label("Label"),
),
]),
time: start_date..end_date,
horizontal_axis_scale: Rc::new(TimeAxisScale::for_range(time, Duration::days(1), None)),
vertical_axis_scale: Rc::new(LinearAxisScale::for_range(0.0..5.0, 0.5))
}
}

Expand All @@ -77,21 +81,23 @@ impl Component for App {
name="some-series"
data={Rc::clone(&self.data_set)}
data_labels={Some(Rc::clone(&self.data_set_labels))}
horizontal_scale={self.time.start.timestamp() as f32..self.time.end.timestamp() as f32}
horizontal_scale_step={Duration::days(1).num_seconds() as f32}
vertical_scale={0.0..5.0}
horizontal_scale={Rc::clone(&self.horizontal_axis_scale)}
horizontal_scale_step={Duration::days(2).num_seconds() as f32}
vertical_scale={Rc::clone(&self.vertical_axis_scale)}
x={MARGIN} y={MARGIN} width={WIDTH - (MARGIN * 2)} height={HEIGHT - (MARGIN * 2)} />

<VerticalAxis
name="some-y-axis"
orientation={vertical_axis::Orientation::Left}
scale={0.0..5.0} scale_step={0.5}
scale={Rc::clone(&self.vertical_axis_scale)}
x1={MARGIN} y1={MARGIN} y2={HEIGHT - MARGIN}
tick_len={TICK_LENGTH}
title={"Some Y thing".to_string()} />

<HorizontalTimeAxis
time={self.time.to_owned()} time_step={Duration::days(1)}

<HorizontalAxis
name="some-x-axis"
orientation={horizontal_axis::Orientation::Bottom}
scale={Rc::clone(&self.horizontal_axis_scale)}
x1={MARGIN} y1={HEIGHT - MARGIN} x2={WIDTH - MARGIN}
tick_len={TICK_LENGTH} />

Expand Down
30 changes: 30 additions & 0 deletions src/axis.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/// Axis scaled value, expected to be between 0 and 1
/// except in the case where the value is outside of the axis range
pub struct NormalisedValue(pub f32);
huntc marked this conversation as resolved.
Show resolved Hide resolved

/// Specifies a generic scale on which axes and data can be rendered
pub trait AxisScale {
huntc marked this conversation as resolved.
Show resolved Hide resolved
/// Provides the list of [ticks](AxisTick) that should be rendered along the axis
fn ticks(&self) -> Vec<AxisTick>;

/// Normalises a value within the axis scale to a number between 0 and 1,
/// where 0 represents the minimum value of the scale, and 1 the maximum
///
/// For example, for a linear scale between 50 and 100:
/// - normalise(50) -> 0
/// - normalise(60) -> 0.2
/// - normalise(75) -> 0.5
/// - normalise(100) -> 1
fn normalise(&self, value: f32) -> NormalisedValue;
}

/// An axis tick, specifying a label to be displayed at some normalised
/// position along the axis
pub struct AxisTick {
/// normalised location between zero and one along the axis specifying
/// the position at which the tick should be rendered
pub location: NormalisedValue,

/// text label that should be rendered alongside the tick
pub label: String,
}
57 changes: 33 additions & 24 deletions src/horizontal_axis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@
/// * line - the axis line
/// * tick - the axis tick line
/// * text - the axis text
use std::ops::Range;
use std::rc::Rc;

use gloo_events::EventListener;
use wasm_bindgen::JsCast;
use web_sys::{Element, SvgElement};
use yew::prelude::*;

use crate::axis::{AxisScale, AxisTick, NormalisedValue};

pub enum Msg {
Resize,
}
Expand All @@ -27,17 +29,34 @@ pub enum Orientation {
Top,
}

#[derive(Properties, Clone, PartialEq)]
#[derive(Properties, Clone)]
pub struct Props {
pub name: String,
pub orientation: Orientation,
pub scale: Range<f32>,
pub scale_step: f32,
pub x1: u32,
pub x2: u32,
pub y1: u32,
pub tick_len: u32,
pub title: Option<String>,
pub scale: Rc<dyn AxisScale>,
}

impl PartialEq for Props {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
&& self.orientation == other.orientation
&& self.x1 == other.x1
&& self.x2 == other.x2
&& self.y1 == other.y1
&& self.tick_len == other.tick_len
&& self.title == other.title
&& std::ptr::eq(
// test reference equality, avoiding issues with vtables discussed in
// https://github.com/rust-lang/rust/issues/46139
&*self.scale as *const _ as *const u8,
&*other.scale as *const _ as *const u8,
)
RoryStokes marked this conversation as resolved.
Show resolved Hide resolved
}
}

pub struct HorizontalAxis {
Expand Down Expand Up @@ -70,33 +89,23 @@ impl Component for HorizontalAxis {
fn view(&self, ctx: &Context<Self>) -> Html {
let p = ctx.props();

let range_from = &p.scale.start;
let range_to = &p.scale.end;
let range_step = &p.scale_step;

let range = range_to - range_from;
let scale = (p.x2 - p.x1) as f32 / range;

let range_from = (range_from * 100.0) as u32;
let range_to = (range_to * 100.0) as u32;
let range_step = (range_step * 100.0) as u32;
let scale = (p.x2 - p.x1) as f32;
let y = p.y1;
let to_y = if p.orientation == Orientation::Top {
y - p.tick_len
} else {
y + p.tick_len
};

html! {
<svg ref={self.svg.clone()} class={classes!("axis-x", p.name.to_owned())}>
<line x1={p.x1.to_string()} y1={p.y1.to_string()} x2={p.x2.to_string()} y2={p.y1.to_string()} class="line" />
{ for (range_from..=range_to).step_by(range_step as usize).map(|i| {
let i = i as f32 / 100.0;
let y = p.y1;
let to_y = if p.orientation == Orientation::Top {
y - p.tick_len
} else {
y + p.tick_len
};
let x = (p.x1 as f32 + (i as f32 + p.scale.start) * scale) as u32;
{ for(p.scale.ticks().iter()).map(|AxisTick { location: NormalisedValue(normalised_location), label }| {
let x = p.x1 as f32 + normalised_location * scale;
html! {
<>
<line x1={x.to_string()} y1={y.to_string()} x2={x.to_string()} y2={to_y.to_string()} class="tick" />
<text x={(x + 1).to_string()} y={to_y.to_string()} text-anchor="start" class="text">{i}</text>
<text x={(x + 1.0).to_string()} y={to_y.to_string()} text-anchor="start" transform-origin={format!("{} {}", x, to_y + 1)} class="text">{label.to_string()}</text>
</>
}
}) }
Expand Down
Loading