use leptos::prelude::*;
use crate::context::{
use_axis_data_index, use_chart_context, use_hovered_item, HeatmapPlotContext,
};
use crate::engine::{resolve_mark_color, resolve_series_label};
use crate::{LabelLocation, SeriesDef, TooltipTrigger, HEATMAP_ITEM_SERIES_ID};
#[derive(Clone, Debug, PartialEq)]
pub struct ItemTooltipData {
pub series_id: String,
pub data_index: usize,
pub label: String,
pub value: f64,
pub formatted_value: String,
pub color: String,
}
#[derive(Clone, Debug, PartialEq)]
pub struct AxisTooltipRow {
pub series_id: String,
pub label: String,
pub value: f64,
pub formatted_value: String,
pub color: String,
}
#[derive(Clone, Debug, PartialEq)]
pub struct AxisTooltipData {
pub axis_label: String,
pub rows: Vec<AxisTooltipRow>,
}
#[derive(Clone, Copy, Default)]
pub struct ChartTooltipContext {
pub trigger: RwSignal<TooltipTrigger>,
pub hide_x_header: RwSignal<bool>,
}
impl ChartTooltipContext {
pub fn new(trigger: TooltipTrigger, hide_x_header: bool) -> Self {
Self {
trigger: RwSignal::new(trigger),
hide_x_header: RwSignal::new(hide_x_header),
}
}
}
pub fn provide_tooltip_context(trigger: TooltipTrigger, hide_x_header: bool) {
provide_context(ChartTooltipContext::new(trigger, hide_x_header));
}
fn format_value(series: &SeriesDef, value: f64) -> String {
if let Some(formatter) = series.value_formatter.as_ref() {
return formatter.run((value,));
}
if (value - value.round()).abs() < f64::EPSILON {
format!("{:.0}", value)
} else {
format!("{:.2}", value)
}
}
pub fn use_item_tooltip() -> Signal<Option<ItemTooltipData>> {
let ctx = use_chart_context();
let hovered = use_hovered_item();
Signal::derive(move || {
let item = hovered.get()?;
if item.series_id == HEATMAP_ITEM_SERIES_ID {
let heatmap = use_context::<HeatmapPlotContext>()?;
let cell = heatmap.cells.get(item.data_index)?;
let x_label = ctx
.x_axes
.first()
.and_then(|a| a.data.as_ref())
.and_then(|cats| cats.get(cell.x).cloned())
.unwrap_or_else(|| cell.x.to_string());
let y_label = ctx
.y_axes
.first()
.and_then(|a| a.data.as_ref())
.and_then(|cats| cats.get(cell.y).cloned())
.unwrap_or_else(|| cell.y.to_string());
let color = crate::engine::resolve_color(
cell.value,
&heatmap.color_scale,
crate::engine::heatmap_value_domain(
&heatmap.cells,
heatmap.value_min,
heatmap.value_max,
),
);
return Some(ItemTooltipData {
series_id: item.series_id.clone(),
data_index: item.data_index,
label: format!("{y_label} × {x_label}"),
value: cell.value,
formatted_value: format!("{:.1}", cell.value),
color,
});
}
let projected = ctx.projected.as_ref()?;
let series_idx = ctx.series.iter().position(|s| s.id == item.series_id)?;
let series_def = ctx.series.get(series_idx)?;
let projected_series = projected.series.iter().find(|s| s.id == item.series_id)?;
let value = projected_series.data.get(item.data_index).copied()?;
let category = projected
.categories
.get(item.data_index)
.map(String::as_str);
let x_axis = ctx.x_axes.first();
let y_axis = ctx.y_axes.first();
let color = resolve_mark_color(
series_idx,
series_def,
&ctx.palette,
category,
Some(value),
x_axis,
y_axis,
);
Some(ItemTooltipData {
series_id: item.series_id.clone(),
data_index: item.data_index,
label: resolve_series_label(series_def, LabelLocation::Tooltip),
value,
formatted_value: format_value(series_def, value),
color,
})
})
}
pub fn use_axis_tooltip() -> Signal<Option<AxisTooltipData>> {
let ctx = use_chart_context();
let axis_index = use_axis_data_index();
Signal::derive(move || {
let idx = axis_index.get()?;
let projected = ctx.projected.as_ref()?;
let category = projected.categories.get(idx)?.clone();
let rows: Vec<AxisTooltipRow> = ctx
.series
.iter()
.enumerate()
.filter_map(|(series_idx, series_def)| {
let projected_series = projected.series.iter().find(|s| s.id == series_def.id)?;
let value = projected_series.data.get(idx).copied()?;
let color = resolve_mark_color(
series_idx,
series_def,
&ctx.palette,
Some(category.as_str()),
Some(value),
ctx.x_axes.first(),
ctx.y_axes.first(),
);
Some(AxisTooltipRow {
series_id: series_def.id.clone(),
label: resolve_series_label(series_def, LabelLocation::Tooltip),
value,
formatted_value: format_value(series_def, value),
color,
})
})
.collect();
if rows.is_empty() {
return None;
}
Some(AxisTooltipData {
axis_label: category,
rows,
})
})
}