1use std::{marker::PhantomData, rc::Rc};
14
15use gloo_events::EventListener;
16use wasm_bindgen::JsCast;
17use web_sys::{Element, SvgElement};
18use yew::prelude::*;
19
20use crate::series::Scalar;
21
22#[derive(Debug, PartialEq)]
25pub struct NormalisedValue(pub f32);
26
27pub trait Scale {
29 type Scalar: Scalar;
30
31 fn ticks(&self) -> Vec<Tick>;
33
34 fn normalise(&self, value: Self::Scalar) -> NormalisedValue;
43}
44
45#[derive(Debug, PartialEq)]
48pub struct Tick {
49 pub location: NormalisedValue,
52
53 pub label: Option<String>,
55}
56
57pub enum Msg {
58 Resize,
59}
60
61#[derive(Clone, PartialEq)]
62pub enum Orientation {
63 Left,
64 Right,
65 Bottom,
66 Top,
67}
68
69#[derive(Properties, Clone)]
70pub struct Props<S: Scalar> {
71 pub name: String,
73 pub orientation: Orientation,
75 pub x1: f32,
77 pub y1: f32,
79 pub xy2: f32,
82 pub tick_len: f32,
84 #[prop_or_default]
86 pub title: Option<String>,
87 pub scale: Rc<dyn Scale<Scalar = S>>,
89}
90
91impl<S: Scalar> PartialEq for Props<S> {
92 fn eq(&self, other: &Self) -> bool {
93 self.name == other.name
94 && self.orientation == other.orientation
95 && self.x1 == other.x1
96 && self.y1 == other.y1
97 && self.xy2 == other.xy2
98 && self.tick_len == other.tick_len
99 && self.title == other.title
100 && std::ptr::eq(
101 &*self.scale as *const _ as *const u8,
104 &*other.scale as *const _ as *const u8,
105 )
106 }
107}
108
109pub struct Axis<S: Scalar> {
110 phantom: PhantomData<S>,
111 _resize_listener: EventListener,
112 svg: NodeRef,
113}
114
115impl<S: Scalar + 'static> Component for Axis<S> {
116 type Message = Msg;
117
118 type Properties = Props<S>;
119
120 fn create(ctx: &Context<Self>) -> Self {
121 let on_resize = ctx.link().callback(|_: Event| Msg::Resize);
122 Axis {
123 phantom: PhantomData,
124 _resize_listener: EventListener::new(&gloo_utils::window(), "resize", move |e| {
125 on_resize.emit(e.clone())
126 }),
127 svg: NodeRef::default(),
128 }
129 }
130
131 fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
132 match msg {
133 Msg::Resize => true,
134 }
135 }
136
137 fn changed(&mut self, _ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
138 true
139 }
140
141 fn view(&self, ctx: &Context<Self>) -> Html {
142 let p = ctx.props();
143
144 fn title(x: f32, y: f32, baseline: &str, title: &str) -> Html {
145 html! {
146 <text
147 x={x.to_string()} y={y.to_string()}
148 dominant-baseline={baseline.to_string()}
149 text-anchor={"middle"}
150 transform-origin={format!("{} {}", x, y)}
151 class="title" >
152 {title}
153 </text>
154 }
155 }
156
157 let class = match p.orientation {
158 Orientation::Left => "left",
159 Orientation::Right => "right",
160 Orientation::Bottom => "bottom",
161 Orientation::Top => "top",
162 };
163
164 if p.orientation == Orientation::Left || p.orientation == Orientation::Right {
165 let scale = p.xy2 - p.y1;
166 let x = p.x1;
167 let to_x = if p.orientation == Orientation::Left {
168 x - p.tick_len
169 } else {
170 x + p.tick_len
171 };
172
173 html! {
174 <svg ref={self.svg.clone()} class={classes!("axis", class, p.name.to_owned())}>
175 <line x1={p.x1.to_string()} y1={p.y1.to_string()} x2={p.x1.to_string()} y2={p.xy2.to_string()} class="line" />
176 { for (p.scale.ticks().iter()).map(|Tick { location: NormalisedValue(normalised_location), label }| {
177 let y = (p.xy2 - (normalised_location * scale)) as u32;
178 html! {
179 <>
180 <line x1={x.to_string()} y1={y.to_string()} x2={to_x.to_string()} y2={y.to_string()} class="tick" />
181 if let Some(l) = label {
182 <text x={to_x.to_string()} y={y.to_string()} text-anchor={if p.orientation == Orientation::Left {"end"} else {"start"}} class="text">{l.to_string()}</text>
183 }
184 </>
185 }
186 }) }
187 { for p.title.as_ref().map(|t| {
188 let title_distance = p.tick_len * 2.0;
189 let x = if p.orientation == Orientation::Left {
190 p.x1 - title_distance
191 } else {
192 p.x1 + title_distance
193 };
194 let y = p.y1 + ((p.xy2 - p.y1) * 0.5);
195 title(x, y, "auto",t)
196 })}
197 </svg>
198 }
199 } else {
200 let scale = p.xy2 - p.x1;
201 let y = p.y1;
202 let (to_y, baseline) = if p.orientation == Orientation::Top {
203 (y - p.tick_len, "auto")
204 } else {
205 (y + p.tick_len, "hanging")
206 };
207
208 html! {
209 <svg ref={self.svg.clone()} class={classes!("axis", class, p.name.to_owned())}>
210 <line x1={p.x1.to_string()} y1={p.y1.to_string()} x2={p.xy2.to_string()} y2={p.y1.to_string()} class="line" />
211 { for(p.scale.ticks().iter()).map(|Tick { location: NormalisedValue(normalised_location), label }| {
212 let x = p.x1 + normalised_location * scale;
213 html! {
214 <>
215 <line x1={x.to_string()} y1={y.to_string()} x2={x.to_string()} y2={to_y.to_string()} class="tick" />
216 if let Some(l) = label {
217 <text x={x.to_string()} y={to_y.to_string()} text-anchor="middle" transform-origin={format!("{} {}", x, to_y)} dominant-baseline={baseline.to_string()} class="text">{l.to_string()}</text>
218 }
219 </>
220 }
221 }) }
222 { for p.title.as_ref().map(|t| {
223 let title_distance = p.tick_len * 2.0;
224 let y = if p.orientation == Orientation::Top {
225 p.y1 - title_distance
226 } else {
227 p.y1 + title_distance
228 };
229 let x = p.x1 + ((p.xy2 - p.x1) * 0.5);
230 title(x, y, baseline, t)
231 })}
232 </svg>
233 }
234 }
235 }
236
237 fn rendered(&mut self, ctx: &Context<Self>, _first_render: bool) {
238 let p = ctx.props();
239
240 let element = self.svg.cast::<Element>().unwrap();
241 if let Some(svg_element) = element
242 .first_child()
243 .and_then(|n| n.dyn_into::<SvgElement>().ok())
244 {
245 let bounding_rect = svg_element.get_bounding_client_rect();
246 let scale = if p.orientation == Orientation::Left || p.orientation == Orientation::Right
247 {
248 let height = bounding_rect.height() as f32;
249 (p.xy2 - p.y1) / height
250 } else {
251 let width = bounding_rect.width() as f32;
252 (p.xy2 - p.x1) / width
253 };
254 let font_size = scale * 100.0;
255 let _ = element.set_attribute("font-size", &format!("{}%", &font_size));
256 let _ = element.set_attribute("style", &format!("stroke-width: {}", scale));
257 }
258 }
259}