#![allow(unused_assignments)]
#![allow(unused_variables)]
#![allow(dead_code)]
#![allow(clippy::explicit_counter_loop, clippy::needless_range_loop)]
use animate::{
easing::{get_easing, Easing},
interpolate::lerp,
BaseLine, CanvasContext, Point, Rect, Size, TextStyle, TextWeight,
};
use dataflow::*;
use std::{cell::RefCell, fmt};
use crate::*;
#[derive(Default, Clone)]
pub struct PolarPoint<D> {
color: Fill,
highlight_color: Fill,
index: usize,
old_value: Option<D>,
value: Option<D>,
old_radius: f64,
old_angle: f64,
old_point_radius: f64,
radius: f64,
angle: f64,
point_radius: f64,
center: Point<f64>,
}
impl<C, D> Drawable<C> for PolarPoint<D>
where
C: CanvasContext,
{
fn draw(&self, ctx: &C, percent: f64, highlight: bool) {
let r = lerp(self.old_radius, self.radius, percent);
let a = lerp(self.old_angle, self.angle, percent);
let pr = lerp(self.old_point_radius, self.point_radius, percent);
let p = utils::polar2cartesian(&self.center, r, a);
if highlight {
match &self.highlight_color {
Fill::Solid(color) => {
ctx.set_fill_color(*color);
ctx.begin_path();
ctx.arc(p.x, p.y, 2. * pr, 0., TAU, false);
ctx.fill();
}
Fill::Gradient(gradient) => {
ctx.set_fill_gradient(gradient);
ctx.begin_path();
ctx.arc(p.x, p.y, 2. * pr, 0., TAU, false);
ctx.fill();
}
Fill::None => {}
}
}
match &self.color {
Fill::Solid(color) => {
ctx.set_fill_color(*color);
ctx.begin_path();
ctx.arc(p.x, p.y, pr, 0., TAU, false);
ctx.fill();
ctx.stroke();
}
Fill::Gradient(gradient) => {
ctx.set_fill_gradient(gradient);
ctx.begin_path();
ctx.arc(p.x, p.y, pr, 0., TAU, false);
ctx.fill();
ctx.stroke();
}
Fill::None => {}
}
}
}
impl<D> Entity for PolarPoint<D> {
fn free(&mut self) {}
fn save(&self) {
}
}
#[derive(Default, Clone)]
struct RadarChartProperties {
center: Point<f64>,
radius: f64,
angle_interval: f64,
xlabels: Vec<String>,
ylabels: Vec<String>,
ymax_value: f64,
ylabel_hop: f64,
ylabel_formatter: Option<ValueFormatter>,
bounding_boxes: Vec<Rect<f64>>,
}
pub struct RadarChart<C, M, D>
where
C: CanvasContext,
M: fmt::Display,
D: fmt::Display + Copy,
{
props: RefCell<RadarChartProperties>,
base: BaseChart<C, PolarPoint<D>, M, D, RadarChartOptions>,
}
impl<C, M, D> RadarChart<C, M, D>
where
C: CanvasContext,
M: fmt::Display,
D: fmt::Display + Copy + Into<f64> + Ord + Default,
{
pub fn new(options: RadarChartOptions) -> Self {
Self {
props: Default::default(),
base: BaseChart::new(options),
}
}
pub fn get_angle(&self, entity_index: usize) -> f64 {
let props = self.props.borrow();
(entity_index as f64) * props.angle_interval - PI_2
}
pub fn value2radius(&self, value: Option<D>) -> f64 {
match value {
Some(value) => {
let props = self.props.borrow();
value.into() * props.radius / props.ymax_value
}
None => 0.0,
}
}
fn calculate_bounding_boxes(&self) {
if !self.base.options.tooltip.enabled {
return;
}
let channels = self.base.channels.borrow();
let channel_count = channels.len();
let entity_count = {
let channel = channels.get(0).unwrap();
channel.entities.len()
};
let mut props = self.props.borrow_mut();
props
.bounding_boxes
.resize(entity_count, Default::default());
for idx in 0..entity_count {
let mut min_x = f64::MAX;
let mut min_y = f64::MAX;
let mut max_x = -f64::MAX;
let mut max_y = -f64::MAX;
let mut count = 0;
for jdx in 0..channel_count {
let channel = channels.get(jdx).unwrap();
if channel.state == Visibility::Hidden || channel.state == Visibility::Hiding {
continue;
}
let channel = channels.get(jdx).unwrap();
let pp = channel.entities.get(idx).unwrap();
if pp.value.is_none() {
continue;
}
let cp = utils::polar2cartesian(&pp.center, pp.radius, pp.angle);
min_x = min_x.min(cp.x);
min_y = min_y.min(cp.y);
max_x = max_x.max(cp.x);
max_y = max_y.max(cp.y);
count += 1;
}
props.bounding_boxes[idx] = if count > 0 {
Rect::new(
Point::new(min_x, min_y),
Size::new(max_x - min_x, max_y - min_y),
)
} else {
unimplemented!()
};
}
}
fn draw_text(&self, ctx: &C, text: &str, radius: f64, angle: f64, font_size: f64) {
let props = self.props.borrow();
let w = ctx.measure_text(text).width;
let x = props.center.x + angle.cos() * (props.radius + 0.8 * w) - (0.5 * w);
let y = props.center.y + angle.sin() * (props.radius + 0.8 * font_size) + (0.5 * font_size);
ctx.fill_text(text, x, y);
}
fn get_entity_group_index(&self, x: f64, y: f64) -> i64 {
let props = self.props.borrow();
let p = Point::new(x - props.center.x, y - props.center.y);
if p.distance_to(Point::zero()) >= props.radius {
return -1;
}
let angle = p.y.atan2(p.x);
let channels = self.base.channels.borrow();
let channel = channels.first().unwrap();
let points = &channel.entities;
for idx in points.len()..0 {
if props.bounding_boxes.get(idx).is_none() {
continue;
}
let delta = angle - points[idx].angle;
if delta.abs() < 0.5 * props.angle_interval {
return idx as i64;
}
if (delta + TAU).abs() < 0.5 * props.angle_interval {
return idx as i64;
}
}
-1
}
fn channel_visibility_changed(&self, index: usize) {
let mut channels = self.base.channels.borrow_mut();
let channel = channels.get_mut(index).unwrap();
let visible = channel.state == Visibility::Showing || channel.state == Visibility::Shown;
let marker_size = self.base.options.channel.markers.size;
for entity in channel.entities.iter_mut() {
if visible {
entity.radius = self.value2radius(entity.value);
entity.point_radius = marker_size;
} else {
entity.radius = 0.0;
entity.point_radius = 0.;
}
}
self.calculate_bounding_boxes();
}
}
impl<C, M, D> Chart<C, M, D, PolarPoint<D>> for RadarChart<C, M, D>
where
C: CanvasContext,
M: fmt::Display,
D: fmt::Display + Copy + Into<f64> + Ord + Default,
{
fn calculate_drawing_sizes(&self, ctx: &C) {
self.base.calculate_drawing_sizes(ctx);
let mut props = self.props.borrow_mut();
props.xlabels = self
.base
.data
.frames
.iter()
.map(|item| item.metric.to_string())
.collect();
props.angle_interval = TAU / props.xlabels.len() as f64;
let xlabel_font_size = self.base.options.xaxis.labels.style.fontsize.unwrap();
let factor = 1. + ((props.xlabels.len() >> 1) as f64 * props.angle_interval - PI_2).sin();
{
let rect = &self.base.props.borrow().area;
props.radius = rect.size.width.min(rect.size.height) / factor
- factor * (xlabel_font_size + AXIS_LABEL_MARGIN as f64);
props.center = Point::new(
rect.origin.x + rect.size.width / 2.,
rect.origin.y + rect.size.height / factor,
);
}
let yaxis = &self.base.options.yaxis;
let yinterval = match yaxis.interval {
Some(yinterval) => yinterval,
None => {
let ymin_interval = yaxis.min_interval.unwrap_or(0.0);
props.ymax_value = utils::find_max_value(&self.base.data).into();
let yinterval = utils::calculate_interval(props.ymax_value, 3, Some(ymin_interval));
props.ymax_value = (props.ymax_value / yinterval).ceil() * yinterval;
yinterval
}
};
props.ylabel_formatter = yaxis.labels.formatter;
if props.ylabel_formatter.is_none() {
let a = |x: f64| -> String { x.to_string() };
props.ylabel_formatter = Some(a);
}
let mut baseprops = self.base.props.borrow_mut();
baseprops.entity_value_formatter = props.ylabel_formatter;
props.ylabels.clear();
let ylabel_formatter = props.ylabel_formatter.unwrap();
let mut value = 0.0;
while value <= props.ymax_value {
props.ylabels.push(ylabel_formatter(value));
value += yinterval;
}
props.ylabel_hop = props.radius / (props.ylabels.len() as f64 - 1.);
baseprops.tooltip_value_formatter =
if let Some(value_formatter) = self.base.options.tooltip.value_formatter {
Some(value_formatter)
} else {
Some(ylabel_formatter)
}
}
fn set_stream(&mut self, stream: DataStream<M, D>) {
self.base.data = stream;
self.create_channels(0, self.base.data.meta.len());
}
fn draw(&self, ctx: &C) {
self.base.dispose();
self.calculate_drawing_sizes(ctx);
self.update_channel(0);
self.calculate_bounding_boxes();
self.draw_frame(ctx, None);
}
fn resize(&self, w: f64, h: f64) {
self.base.resize(w, h);
}
fn draw_axes_and_grid(&self, ctx: &C) {
let props = self.props.borrow();
let xlabel_count = props.xlabels.len();
let ylabel_count = props.ylabels.len();
let mut line_width = self.base.options.xaxis.grid_line_width;
if line_width > 0. {
ctx.set_line_width(line_width);
ctx.set_stroke_color(self.base.options.xaxis.grid_line_color);
ctx.begin_path();
let mut radius = props.radius;
for idx in 1..ylabel_count {
let mut angle = -PI_2 + props.angle_interval;
ctx.move_to(props.center.x, props.center.y - radius);
for jdx in 0..xlabel_count {
let point = utils::polar2cartesian(&props.center, radius, angle);
ctx.line_to(point.x, point.y);
angle += props.angle_interval;
}
radius -= props.ylabel_hop;
}
ctx.stroke();
}
line_width = self.base.options.yaxis.grid_line_width;
if line_width > 0. {
ctx.set_line_width(line_width);
ctx.set_stroke_color(self.base.options.yaxis.grid_line_color);
ctx.begin_path();
let mut angle = -PI_2;
for idx in 0..xlabel_count {
let point = utils::polar2cartesian(&props.center, props.radius, angle);
ctx.move_to(props.center.x, props.center.y);
ctx.line_to(point.x, point.y);
angle += props.angle_interval;
}
ctx.stroke();
}
let style = &self.base.options.yaxis.labels.style;
let x = props.center.x - AXIS_LABEL_MARGIN as f64;
let mut y = props.center.y - props.ylabel_hop;
ctx.set_fill_color(style.color);
let fontfamily = match &style.fontfamily {
Some(val) => val.as_str(),
None => DEFAULT_FONT_FAMILY,
};
ctx.set_font(
fontfamily,
style.fontstyle.unwrap_or(TextStyle::Normal),
TextWeight::Normal,
style.fontsize.unwrap_or(12.),
);
ctx.set_text_baseline(BaseLine::Middle);
for idx in 1..ylabel_count - 1 {
let text = props.ylabels[idx].as_str();
let w = ctx.measure_text(text).width;
ctx.fill_text(props.ylabels[idx].as_str(), x - w, y - 4.);
y -= props.ylabel_hop;
}
let style = &self.base.options.xaxis.labels.style;
ctx.set_fill_color(style.color);
let fontfamily = match &style.fontfamily {
Some(val) => val.as_str(),
None => DEFAULT_FONT_FAMILY,
};
ctx.set_font(
fontfamily,
style.fontstyle.unwrap_or(TextStyle::Normal),
TextWeight::Normal,
style.fontsize.unwrap_or(12.),
);
ctx.set_text_baseline(BaseLine::Middle);
let font_size = style.fontsize.unwrap();
let mut angle = -PI_2;
let radius = props.radius + AXIS_LABEL_MARGIN as f64;
for idx in 0..xlabel_count {
self.draw_text(ctx, props.xlabels[idx].as_str(), radius, angle, font_size);
angle += props.angle_interval;
}
}
fn draw_frame(&self, ctx: &C, time: Option<i64>) {
self.base.draw_frame(ctx, time);
self.draw_axes_and_grid(ctx);
let mut percent = self.base.calculate_percent(time);
if percent >= 1.0 {
percent = 1.0;
let mut channels = self.base.channels.borrow_mut();
for channel in channels.iter_mut() {
if channel.state == Visibility::Showing {
channel.state = Visibility::Shown;
} else if channel.state == Visibility::Hiding {
channel.state = Visibility::Hidden;
}
}
}
let props = self.base.props.borrow();
let ease = match props.easing {
Some(val) => val,
None => get_easing(Easing::Linear),
};
self.draw_channels(ctx, ease(percent));
self.base.draw_title(ctx);
if percent < 1.0 {
} else if time.is_some() {
self.base.animation_end();
}
}
fn draw_channels(&self, ctx: &C, percent: f64) -> bool {
let props = self.props.borrow();
let mut focused_channel_index = self.base.props.borrow().focused_channel_index;
focused_channel_index = -1;
let fill_opacity = self.base.options.channel.fill_opacity;
let channel_line_width = self.base.options.channel.line_width;
let marker_options = &self.base.options.channel.markers;
let marker_size = marker_options.size;
let point_count = props.xlabels.len();
let channels = self.base.channels.borrow();
let mut focused_entity_index = self.base.props.borrow().focused_entity_index;
focused_entity_index = -1;
let mut idx = 0;
for channel in channels.iter() {
let scale = if idx as i64 != focused_channel_index {
1.
} else {
2.
};
idx += 1;
if channel.state == Visibility::Hidden {
continue;
}
if fill_opacity > 0. {
let chennel_fill = self.base.change_fill_alpha(&channel.fill, fill_opacity);
let should_fill = match &chennel_fill {
Fill::Solid(color) => {
ctx.set_fill_color(*color);
true
}
Fill::Gradient(gradient) => {
ctx.set_fill_gradient(gradient);
true
}
Fill::None => false,
};
if should_fill {
ctx.begin_path();
for jdx in 0..point_count {
let entity = channel.entities.get(jdx).unwrap();
let radius = lerp(entity.old_radius, entity.radius, percent);
let angle = lerp(entity.old_angle, entity.angle, percent);
let p = utils::polar2cartesian(&props.center, radius, angle);
if jdx > 0 {
ctx.line_to(p.x, p.y);
} else {
ctx.move_to(p.x, p.y);
}
}
ctx.close_path();
ctx.fill();
}
}
ctx.set_line_width(scale * channel_line_width);
let should_stroke = match &channel.fill {
Fill::Solid(color) => {
ctx.set_stroke_color(*color);
true
}
Fill::Gradient(gradient) => {
ctx.set_stroke_gradient(gradient);
true
}
Fill::None => false,
};
if should_stroke {
ctx.begin_path();
for jdx in 0..point_count {
let entity = channel.entities.get(jdx).unwrap();
let radius = lerp(entity.old_radius, entity.radius, percent);
let angle = lerp(entity.old_angle, entity.angle, percent);
let p = utils::polar2cartesian(&props.center, radius, angle);
if jdx > 0 {
ctx.line_to(p.x, p.y);
} else {
ctx.move_to(p.x, p.y);
}
}
ctx.close_path();
ctx.stroke();
}
if marker_size > 0. {
let fill_color = if let Some(color) = &marker_options.fill_color {
color
} else {
&channel.fill
};
let stroke_color = if let Some(color) = &marker_options.stroke_color {
color
} else {
&channel.fill
};
match fill_color {
Fill::Solid(color) => {
ctx.set_fill_color(*color);
}
Fill::Gradient(gradient) => {
ctx.set_fill_gradient(gradient);
}
Fill::None => {}
}
match stroke_color {
Fill::Solid(color) => {
ctx.set_stroke_color(*color);
}
Fill::Gradient(gradient) => {
ctx.set_stroke_gradient(gradient);
}
Fill::None => {}
}
ctx.set_line_width(scale * marker_options.line_width);
for p in channel.entities.iter() {
if marker_options.enabled {
p.draw(ctx, percent, p.index as i64 == focused_entity_index);
} else if p.index as i64 == focused_entity_index {
p.draw(ctx, percent, true);
}
}
}
}
false
}
fn update_channel(&self, _: usize) {
let entity_count = self.base.data.frames.len();
let mut channels = self.base.channels.borrow_mut();
let props = self.props.borrow();
let mut idx = 0;
for channel in channels.iter_mut() {
let color = self.base.get_fill(idx);
let highlight_color = self.base.get_highlight_color(&color);
channel.fill = color.clone();
channel.highlight = highlight_color.clone();
let visible =
channel.state == Visibility::Showing || channel.state == Visibility::Shown;
for jdx in 0..entity_count {
let mut entity = channel.entities.get_mut(jdx).unwrap();
entity.index = jdx;
entity.center = props.center;
entity.radius = if visible {
self.value2radius(entity.value)
} else {
0.0
};
entity.angle = self.get_angle(jdx);
entity.color = color.clone();
entity.highlight_color = highlight_color.clone();
}
idx += 1;
}
}
fn create_entity(
&self,
channel_index: usize,
entity_index: usize,
value: Option<D>,
color: Fill,
highlight_color: Fill,
) -> PolarPoint<D> {
let props = self.props.borrow();
let angle = self.get_angle(entity_index);
let point_radius = self.base.options.channel.markers.size as f64;
let radius = self.value2radius(value);
PolarPoint {
index: entity_index,
value,
old_value: None,
color,
highlight_color,
center: props.center,
old_radius: 0.,
old_angle: angle,
old_point_radius: 0.,
radius,
angle,
point_radius,
}
}
fn create_channels(&self, start: usize, end: usize) {
let mut start = start;
let mut result = Vec::new();
let count = self.base.data.frames.len();
let meta = &self.base.data.meta;
while start < end {
let channel = meta.get(start).unwrap();
let name = channel.name.as_str();
let color = self.base.get_fill(start);
let highlight = self.base.get_highlight_color(&color);
let entities = self.create_entities(start, 0, count, color.clone(), highlight.clone());
result.push(ChartChannel::new(name, color, highlight, entities));
start += 1;
}
let mut channels = self.base.channels.borrow_mut();
*channels = result;
}
fn create_entities(
&self,
channel_index: usize,
start: usize,
end: usize,
color: Fill,
highlight: Fill,
) -> Vec<PolarPoint<D>> {
let mut start = start;
let mut result = Vec::new();
while start < end {
let frame = self.base.data.frames.get(start).unwrap();
let value = frame.data.get(channel_index as u64);
let entity = match frame.data.get(channel_index as u64) {
Some(value) => {
let value = *value;
self.create_entity(
channel_index,
start,
Some(value),
color.clone(),
highlight.clone(),
)
}
None => {
self.create_entity(channel_index, start, None, color.clone(), highlight.clone())
}
};
result.push(entity);
start += 1;
}
result
}
fn get_tooltip_position(&self, tooltip_width: f64, tooltip_height: f64) -> Point<f64> {
let props = self.props.borrow();
let focused_entity_index = self.base.props.borrow().focused_entity_index;
let bounding_box = &props.bounding_boxes[focused_entity_index as usize];
let offset = self.base.options.channel.markers.size as f64 * 2. + 5.;
let origin = bounding_box.origin;
let mut x = origin.x + bounding_box.width() + offset;
let y = origin.y + ((bounding_box.height() - tooltip_height) / 2.).trunc();
let width = self.base.props.borrow().width;
if x + tooltip_width > width {
x = origin.x - tooltip_width - offset;
}
Point::new(x, y)
}
}