1use super::canvas;
14use super::color::*;
15use super::common::*;
16use super::component::*;
17use super::font::measure_max_text_width_family;
18use super::params::*;
19use super::theme::{get_default_theme_name, get_theme, Theme, DEFAULT_Y_AXIS_WIDTH};
20use super::util::*;
21use super::Canvas;
22use crate::charts::measure_text_width_family;
23use charts_rs_derive::Chart;
24use std::sync::Arc;
25
26#[derive(Clone, Debug, Default)]
27pub struct HeatmapData {
28 pub index: usize,
29 pub value: f32,
30}
31
32impl From<(usize, f32)> for HeatmapData {
33 fn from(value: (usize, f32)) -> Self {
34 HeatmapData {
35 index: value.0,
36 value: value.1,
37 }
38 }
39}
40
41#[derive(Clone, Debug, Default)]
42pub struct HeatmapSeries {
43 pub data: Vec<HeatmapData>,
44 pub min: f32,
45 pub max: f32,
46 pub min_color: Color,
47 pub max_color: Color,
48 pub min_font_color: Color,
49 pub max_font_color: Color,
50}
51
52impl HeatmapSeries {
53 fn get_color(&self, value: f32) -> Color {
54 if value < self.min {
55 return self.min_color;
56 }
57 if value > self.max {
58 return self.max_color;
59 }
60 let percent = (value - self.min) / (self.max - self.min);
61 let get_value = |max: u8, min: u8| {
62 let offset = max.abs_diff(min);
63 let offset = (offset as f32 * percent) as u8;
64 if max > min {
65 min + offset
66 } else {
67 min - offset
68 }
69 };
70 Color {
71 r: get_value(self.max_color.r, self.min_color.r),
72 g: get_value(self.max_color.g, self.min_color.g),
73 b: get_value(self.max_color.b, self.min_color.b),
74 a: get_value(self.max_color.a, self.min_color.a),
75 }
76 }
77}
78
79#[derive(Clone, Debug, Default, Chart)]
80pub struct HeatmapChart {
81 pub width: f32,
82 pub height: f32,
83 pub x: f32,
84 pub y: f32,
85 pub margin: Box,
86 series_list: Vec<Series>,
88 pub series: HeatmapSeries,
89 pub font_family: String,
90 pub background_color: Color,
91 pub is_light: bool,
92
93 pub title_text: String,
95 pub title_font_size: f32,
96 pub title_font_color: Color,
97 pub title_font_weight: Option<String>,
98 pub title_margin: Option<Box>,
99 pub title_align: Align,
100 pub title_height: f32,
101
102 pub sub_title_text: String,
104 pub sub_title_font_size: f32,
105 pub sub_title_font_color: Color,
106 pub sub_title_font_weight: Option<String>,
107 pub sub_title_margin: Option<Box>,
108 pub sub_title_align: Align,
109 pub sub_title_height: f32,
110
111 pub legend_font_size: f32,
113 pub legend_font_color: Color,
114 pub legend_font_weight: Option<String>,
115 pub legend_align: Align,
116 pub legend_margin: Option<Box>,
117 pub legend_category: LegendCategory,
118 pub legend_show: Option<bool>,
119
120 pub x_axis_data: Vec<String>,
122 pub x_axis_height: f32,
123 pub x_axis_stroke_color: Color,
124 pub x_axis_font_size: f32,
125 pub x_axis_font_color: Color,
126 pub x_axis_font_weight: Option<String>,
127 pub x_axis_name_gap: f32,
128 pub x_axis_name_rotate: f32,
129 pub x_axis_margin: Option<Box>,
130 pub x_axis_hidden: bool,
131 pub x_boundary_gap: Option<bool>,
132
133 pub y_axis_hidden: bool,
135 pub y_axis_data: Vec<String>,
136 y_axis_configs: Vec<YAxisConfig>,
137
138 grid_stroke_color: Color,
140 grid_stroke_width: f32,
141
142 pub series_stroke_width: f32,
144 pub series_label_font_color: Color,
145 pub series_label_font_size: f32,
146 pub series_label_font_weight: Option<String>,
147 pub series_label_formatter: String,
148 pub series_colors: Vec<Color>,
149 pub series_symbol: Option<Symbol>,
150 pub series_smooth: bool,
151 pub series_fill: bool,
152}
153
154impl HeatmapChart {
155 fn fill_default(&mut self) {
156 if self.y_axis_configs[0].axis_stroke_color.is_zero() {
157 self.y_axis_configs[0].axis_stroke_color = self.x_axis_stroke_color;
158 }
159 self.y_axis_configs[0].axis_name_align = Some(Align::Center);
160 self.y_axis_configs[0].axis_split_number += 1;
161 if self.series.max_color.is_zero() {
162 self.series.max_color = (191, 68, 76).into();
163 }
164 if self.series.min_color.is_zero() {
165 self.series.min_color = (240, 217, 156).into();
166 }
167 if self.series.min_font_color.is_zero() {
168 self.series.min_font_color = (70, 70, 70).into();
169 }
170 if self.series.max_font_color.is_zero() {
171 self.series.max_font_color = (238, 238, 238).into();
172 }
173 if self.series.max == 0.0 {
174 let mut max = 0.0;
175 for item in self.series.data.iter() {
176 if item.value > max {
177 max = item.value
178 }
179 }
180 self.series.max = max;
181 }
182 }
183 pub fn from_json(data: &str) -> canvas::Result<HeatmapChart> {
185 let mut h = HeatmapChart {
186 ..Default::default()
187 };
188 let value = h.fill_option(data)?;
189 if let Some(y_axis_data) = get_string_slice_from_value(&value, "y_axis_data") {
190 h.y_axis_data = y_axis_data;
191 }
192 if let Some(value) = value.get("series") {
193 if let Some(min) = get_f32_from_value(value, "min") {
194 h.series.min = min;
195 }
196 if let Some(max) = get_f32_from_value(value, "max") {
197 h.series.max = max;
198 }
199 if let Some(min_color) = get_color_from_value(value, "min_color") {
200 h.series.min_color = min_color;
201 }
202 if let Some(max_color) = get_color_from_value(value, "max_color") {
203 h.series.max_color = max_color;
204 }
205 if let Some(min_font_color) = get_color_from_value(value, "min_font_color") {
206 h.series.min_font_color = min_font_color;
207 }
208 if let Some(max_font_color) = get_color_from_value(value, "max_font_color") {
209 h.series.max_font_color = max_font_color;
210 }
211 if let Some(data) = value.get("data") {
212 let mut values = vec![];
213 if let Some(arr) = data.as_array() {
214 for item in arr.iter() {
215 if let Some(arr) = item.as_array() {
216 if arr.len() != 2 {
217 continue;
218 }
219 values.push(HeatmapData {
220 index: arr[0].as_i64().unwrap_or_default() as usize,
221 value: arr[1].as_f64().unwrap_or_default() as f32,
222 });
223 }
224 }
225 }
226 h.series.data = values;
227 }
228 }
229 h.fill_default();
230 if let Some(x_axis_hidden) = get_bool_from_value(&value, "x_axis_hidden") {
231 h.x_axis_hidden = x_axis_hidden;
232 }
233 if let Some(y_axis_hidden) = get_bool_from_value(&value, "y_axis_hidden") {
234 h.y_axis_hidden = y_axis_hidden;
235 }
236 Ok(h)
237 }
238 pub fn new(
240 series_data: Vec<(usize, f32)>,
241 x_axis_data: Vec<String>,
242 y_axis_data: Vec<String>,
243 ) -> HeatmapChart {
244 HeatmapChart::new_with_theme(
245 series_data,
246 x_axis_data,
247 y_axis_data,
248 &get_default_theme_name(),
249 )
250 }
251 pub fn new_with_theme(
253 series_data: Vec<(usize, f32)>,
254 x_axis_data: Vec<String>,
255 y_axis_data: Vec<String>,
256 theme: &str,
257 ) -> HeatmapChart {
258 let mut h = HeatmapChart {
259 x_axis_data,
260 y_axis_data,
261 ..Default::default()
262 };
263 let mut max = 0.0_f32;
264 let mut data = vec![];
265 for item in series_data.iter() {
266 if item.1 > max {
267 max = item.1;
268 }
269 data.push((*item).into());
270 }
271 h.series.data = data;
272 let theme = get_theme(theme);
273 h.fill_theme(theme);
274 h.fill_default();
275 h
276 }
277 pub fn svg(&self) -> canvas::Result<String> {
279 let mut c = Canvas::new_width_xy(self.width, self.height, self.x, self.y);
280
281 if self.x_axis_data.is_empty() || self.y_axis_data.is_empty() {
282 return Err(canvas::Error::Params {
283 message: "x axis or y axis can not be empty".to_string(),
284 });
285 }
286
287 self.render_background(c.child(Box::default()));
288 let mut x_axis_height = self.x_axis_height;
289 if self.x_axis_hidden {
290 x_axis_height = 0.0;
291 }
292
293 c.margin = self.margin.clone();
294
295 let title_height = self.render_title(c.child(Box::default()));
296
297 let legend_height = self.render_legend(c.child(Box::default()));
298 let axis_top = if legend_height > title_height {
300 legend_height
301 } else {
302 title_height
303 };
304 let axis_height = c.height() - x_axis_height - axis_top;
305
306 if axis_top > 0.0 {
308 c = c.child(Box {
309 top: axis_top,
310 ..Default::default()
311 });
312 }
313 let mut y_axis_width = 0.0;
314 if !self.y_axis_hidden {
315 let max_text_width_box = measure_max_text_width_family(
316 &self.font_family,
317 self.y_axis_configs[0].axis_font_size,
318 self.y_axis_data.iter().map(|item| item.as_str()).collect(),
319 )?;
320 y_axis_width = max_text_width_box.width() + self.margin.left;
321 let mut y_axis_data = self.y_axis_data.clone();
323 y_axis_data.reverse();
324 self.render_y_axis(
325 c.child_left_top(Box::default()),
326 y_axis_data,
327 axis_height,
328 y_axis_width,
329 0,
330 );
331 }
332 let axis_width = c.width() - y_axis_width;
333 if !self.x_axis_hidden {
335 self.render_x_axis(
336 c.child(Box {
337 top: c.height() - x_axis_height,
338 left: y_axis_width,
339 ..Default::default()
340 }),
341 self.x_axis_data.clone(),
342 axis_width,
343 );
344 }
345 let mut data = vec![None; self.x_axis_data.len() * self.y_axis_data.len()];
346 for item in self.series.data.iter() {
347 if item.index < data.len() {
348 data[item.index] = Some(item.value);
349 }
350 }
351
352 let x_unit = (axis_width - 1.0) / self.x_axis_data.len() as f32;
353 let y_unit = (axis_height - 1.0) / self.y_axis_data.len() as f32;
354 let mut c1 = c.child(Box {
355 left: y_axis_width + 1.0,
356 ..Default::default()
357 });
358 let y_axis_count = self.y_axis_data.len();
359 for i in 0..y_axis_count {
360 for j in 0..self.x_axis_data.len() {
361 let index = i * self.y_axis_data.len() + j;
362 let x = x_unit * j as f32;
363 let y = y_unit * (y_axis_count - i - 1) as f32;
365 let mut text = "".to_string();
366 let mut font_color = self.series.min_font_color;
367 let color = if let Some(value) = data[index] {
368 let percent = (value - self.series.min) / (self.series.max - self.series.min);
369 if percent >= 0.8 {
370 font_color = self.series.max_font_color;
371 }
372
373 text = format_series_value(value, &self.series_label_formatter);
374 self.series.get_color(value)
375 } else {
376 let mut color_index = j;
377 if i % 2 != 0 {
378 color_index += 1;
379 }
380 let mut color = self.background_color;
381 let offset = 20;
382 if color.is_light() {
383 color.r -= offset;
384 color.g -= offset;
385 color.b -= offset;
386 } else {
387 color.r += offset;
388 color.g += offset;
389 color.b += offset;
390 }
391 if color_index % 2 != 0 {
392 color = color.with_alpha(100);
393 }
394 color
395 };
396 c1.rect(Rect {
397 color: Some(color),
398 fill: Some(color),
399 left: x,
400 top: y,
401 width: x_unit,
402 height: y_unit,
403 ..Default::default()
404 });
405 if !text.is_empty() {
406 let mut x1 = x + x_unit / 2.0;
407 let y1 = y + y_unit / 2.0;
408 if let Ok(b) = measure_text_width_family(
409 &self.font_family,
410 self.series_label_font_size,
411 &text,
412 ) {
413 x1 -= b.width() / 2.0;
414 }
415 c1.text(Text {
416 text,
417 font_family: Some(self.font_family.clone()),
418 font_color: Some(font_color),
419 font_size: Some(self.series_label_font_size),
420 font_weight: self.series_label_font_weight.clone(),
421 dominant_baseline: Some("central".to_string()),
422 x: Some(x1),
423 y: Some(y1),
424 ..Default::default()
425 });
426 }
427 }
428 }
429
430 c.svg()
431 }
432}
433
434#[cfg(test)]
435mod tests {
436 use crate::THEME_DARK;
437
438 use super::HeatmapChart;
439 use pretty_assertions::assert_eq;
440
441 #[test]
442 fn heatmap_chart_basic() {
443 let x_axis_data = vec![
444 "12a", "1a", "2a", "3a", "4a", "5a", "6a", "7a", "8a", "9a", "10a", "11a", "12p", "1p",
445 "2p", "3p", "4p", "5p", "6p", "7p", "8p", "9p", "10p", "11p",
446 ]
447 .iter()
448 .map(|item| item.to_string())
449 .collect();
450 let y_axis_data = [
451 "Saturday",
452 "Friday",
453 "Thursday",
454 "Wednesday",
455 "Tuesday",
456 "Monday",
457 "Sunday",
458 ]
459 .iter()
460 .map(|item| item.to_string())
461 .collect();
462 let mut heatmap_chart = HeatmapChart::new(
463 vec![
464 (0, 9.0),
465 (1, 3.0),
466 (7, 3.0),
467 (12, 3.0),
468 (24, 12.0),
469 (28, 10.0),
470 (31, 8.0),
471 (50, 4.0),
472 (63, 2.0),
473 ],
474 x_axis_data,
475 y_axis_data,
476 );
477 heatmap_chart.width = 800.0;
478 heatmap_chart.series.max = 10.0;
479
480 assert_eq!(
481 include_str!("../../asset/heatmap_chart/basic.svg"),
482 heatmap_chart.svg().unwrap()
483 );
484 }
485
486 #[test]
487 fn heatmap_chart_dark() {
488 let x_axis_data = vec![
489 "12a", "1a", "2a", "3a", "4a", "5a", "6a", "7a", "8a", "9a", "10a", "11a", "12p", "1p",
490 "2p", "3p", "4p", "5p", "6p", "7p", "8p", "9p", "10p", "11p",
491 ]
492 .iter()
493 .map(|item| item.to_string())
494 .collect();
495 let y_axis_data = [
496 "Saturday",
497 "Friday",
498 "Thursday",
499 "Wednesday",
500 "Tuesday",
501 "Monday",
502 "Sunday",
503 ]
504 .iter()
505 .map(|item| item.to_string())
506 .collect();
507 let mut heatmap_chart = HeatmapChart::new_with_theme(
508 vec![
509 (0, 9.0),
510 (1, 3.0),
511 (7, 3.0),
512 (12, 3.0),
513 (24, 12.0),
514 (28, 10.0),
515 (31, 8.0),
516 (50, 4.0),
517 (63, 2.0),
518 ],
519 x_axis_data,
520 y_axis_data,
521 THEME_DARK,
522 );
523 heatmap_chart.width = 800.0;
524 heatmap_chart.series.max = 10.0;
525
526 assert_eq!(
527 include_str!("../../asset/heatmap_chart/basic_dark.svg"),
528 heatmap_chart.svg().unwrap()
529 );
530 }
531
532 #[test]
533 fn heatmap_chart_no_axis() {
534 let x_axis_data = vec![
535 "12a", "1a", "2a", "3a", "4a", "5a", "6a", "7a", "8a", "9a", "10a", "11a", "12p", "1p",
536 "2p", "3p", "4p", "5p", "6p", "7p", "8p", "9p", "10p", "11p",
537 ]
538 .iter()
539 .map(|item| item.to_string())
540 .collect();
541 let y_axis_data = [
542 "Saturday",
543 "Friday",
544 "Thursday",
545 "Wednesday",
546 "Tuesday",
547 "Monday",
548 "Sunday",
549 ]
550 .iter()
551 .map(|item| item.to_string())
552 .collect();
553 let mut heatmap_chart = HeatmapChart::new(
554 vec![
555 (0, 9.0),
556 (1, 3.0),
557 (7, 3.0),
558 (12, 3.0),
559 (24, 12.0),
560 (28, 10.0),
561 (31, 8.0),
562 (50, 4.0),
563 (63, 2.0),
564 ],
565 x_axis_data,
566 y_axis_data,
567 );
568 heatmap_chart.width = 800.0;
569 heatmap_chart.series.max = 10.0;
570 heatmap_chart.x_axis_hidden = true;
571 heatmap_chart.y_axis_hidden = true;
572
573 assert_eq!(
574 include_str!("../../asset/heatmap_chart/no_axis.svg"),
575 heatmap_chart.svg().unwrap()
576 );
577 }
578}