use crate::{
aspect_ratio::KnownAspectRatio,
debug::DebugRect,
inner::InnerLayout,
layout::{EdgeLayout, Layout},
overlay::tooltip::Tooltip,
projection::Projection,
series::{RenderData, UseData},
state::{PreState, State},
use_watched_node::{use_watched_node, UseWatchedNode},
AspectRatio, Padding, Series, Tick,
};
use leptos::{html::Div, prelude::*};
pub const FONT_HEIGHT: f64 = 16.0;
pub const FONT_WIDTH: f64 = 10.0;
#[component]
pub fn Chart<T: Send + Sync + 'static, X: Tick, Y: Tick>(
#[prop(into)]
aspect_ratio: Signal<AspectRatio>,
#[prop(into, optional)]
font_height: Option<Signal<f64>>,
#[prop(into, optional)]
font_width: Option<Signal<f64>>,
#[prop(into, optional)]
debug: Signal<bool>,
#[prop(into, optional)]
padding: Option<Signal<Padding>>,
#[prop(into, optional)]
top: Vec<EdgeLayout<X>>,
#[prop(into, optional)]
right: Vec<EdgeLayout<Y>>,
#[prop(into, optional)]
bottom: Vec<EdgeLayout<X>>,
#[prop(into, optional)]
left: Vec<EdgeLayout<Y>>,
#[prop(into, optional)]
inner: Vec<InnerLayout<X, Y>>,
#[prop(into, optional)]
tooltip: Tooltip<X, Y>,
#[prop(into)]
series: Series<T, X, Y>,
#[prop(into)]
data: Signal<Vec<T>>,
) -> impl IntoView {
let root = NodeRef::<Div>::new();
let watch = use_watched_node(root);
let have_dimensions = Memo::new(move |_| watch.bounds.get().is_some());
let width = Memo::new(move |_| watch.bounds.get().unwrap_or_default().width());
let height = Memo::new(move |_| watch.bounds.get().unwrap_or_default().height());
let calc = AspectRatio::known_signal(aspect_ratio, width, height);
let env_size = move || {
if aspect_ratio.get().is_env() {
"100%"
} else {
"fit-content"
}
};
let debug = Memo::new(move |_| debug.get());
let font_height = Memo::new(move |_| font_height.map(|f| f.get()).unwrap_or(FONT_HEIGHT));
let font_width = Memo::new(move |_| font_width.map(|f| f.get()).unwrap_or(FONT_WIDTH));
let padding = Memo::new(move |_| {
padding
.map(|p| p.get())
.unwrap_or_else(move || Padding::from(font_width.get()))
});
let mut top = top;
let mut left = left;
top.reverse();
left.reverse();
let data = UseData::new(series, data);
let pre = PreState::new(debug.into(), font_height, font_width, padding.into(), data);
view! {
<div
node_ref=root
class="_chartistry"
style:width=env_size
style:height=env_size
style="overflow: visible;">
<DebugRect label="Chart" debug=debug />
<Show when=move || have_dimensions.get() fallback=|| view!(<p>"Loading..."</p>)>
{view!{<RenderChart
watch=watch.clone()
pre_state=pre.clone()
aspect_ratio=calc
top=top.clone()
right=right.clone()
bottom=bottom.clone()
left=left.clone()
inner=inner.clone()
tooltip=tooltip.clone()
/>}.into_any()}
</Show>
</div>
}
}
#[component]
fn RenderChart<X: Tick, Y: Tick>(
watch: UseWatchedNode,
pre_state: PreState<X, Y>,
aspect_ratio: Memo<KnownAspectRatio>,
top: Vec<EdgeLayout<X>>,
right: Vec<EdgeLayout<Y>>,
bottom: Vec<EdgeLayout<X>>,
left: Vec<EdgeLayout<Y>>,
inner: Vec<InnerLayout<X, Y>>,
tooltip: Tooltip<X, Y>,
) -> impl IntoView {
let debug = pre_state.debug;
let (layout, edges) = Layout::compose(&top, &right, &bottom, &left, aspect_ratio, &pre_state);
let projection = {
let range_x = pre_state.data.range_x;
let range_y = pre_state.data.range_y;
let includes_bars = pre_state.data.includes_bars;
Memo::new(move |_| {
let mut inner = layout.inner.get();
if includes_bars.get() {
let half = layout.x_width.get() / 2.0;
inner = inner.shrink(0.0, half, 0.0, half);
}
Projection::new(inner, range_x.get().positions(), range_y.get().positions())
})
};
let state = State::new(pre_state, &watch, layout, projection);
let edges = edges
.into_iter()
.map(|r| r.render(state.clone()).into_any())
.collect_view()
.into_any();
let inner = inner
.into_iter()
.map(|opt| opt.into_use(&state).render(state.clone()).into_any())
.collect_view()
.into_any();
let outer = state.layout.outer;
view! {
<svg
width=move || format!("{}px", outer.get().width())
height=move || format!("{}px", outer.get().height())
viewBox=move || outer.with(|outer| format!("0 0 {} {}", outer.width(), outer.height()))
style="display: block; overflow: visible;">
<DebugRect label="RenderChart" debug=debug bounds=vec![outer.into()] />
<CommonDefs />
{inner}
{edges}
{view!{<RenderData state=state.clone() />}.into_any()}
</svg>
<Tooltip tooltip=tooltip state=state />
}
.into_any()
}
#[component]
fn CommonDefs() -> impl IntoView {
view! {
<defs>
<marker
id="marker_axis_arrow"
markerUnits="strokeWidth"
markerWidth=7
markerHeight=8
refX=0
refY=4
orient="auto">
<path d="M0,0 L0,8 L7,4 z" fill="context-stroke" />
</marker>
</defs>
}
}