1use crate::Result;
2use crate::json::from_value_ref;
3use crate::model::{
4 QuadrantChartAxisLabelData, QuadrantChartBorderLineData, QuadrantChartDiagramLayout,
5 QuadrantChartPointData, QuadrantChartQuadrantData, QuadrantChartTextData,
6};
7use crate::text::TextMeasurer;
8use serde::Deserialize;
9use serde_json::Value;
10use std::collections::BTreeMap;
11
12#[derive(Debug, Clone, Default, Deserialize)]
13#[serde(rename_all = "camelCase")]
14struct QuadrantChartStyles {
15 radius: Option<f64>,
16 color: Option<String>,
17 stroke_color: Option<String>,
18 stroke_width: Option<String>,
19}
20
21#[derive(Debug, Clone, Deserialize)]
22#[serde(rename_all = "camelCase")]
23struct QuadrantChartPointModel {
24 text: String,
25 x: f64,
26 y: f64,
27 #[serde(default)]
28 class_name: Option<String>,
29 #[serde(default)]
30 styles: QuadrantChartStyles,
31}
32
33#[derive(Debug, Clone, Deserialize)]
34#[serde(rename_all = "camelCase")]
35struct QuadrantChartQuadrantsModel {
36 quadrant1_text: String,
37 quadrant2_text: String,
38 quadrant3_text: String,
39 quadrant4_text: String,
40}
41
42#[derive(Debug, Clone, Deserialize)]
43#[serde(rename_all = "camelCase")]
44struct QuadrantChartAxesModel {
45 x_axis_left_text: String,
46 x_axis_right_text: String,
47 y_axis_bottom_text: String,
48 y_axis_top_text: String,
49}
50
51#[derive(Debug, Clone, Deserialize)]
52#[serde(rename_all = "camelCase")]
53struct QuadrantChartModel {
54 #[serde(default)]
55 title: Option<String>,
56 quadrants: QuadrantChartQuadrantsModel,
57 axes: QuadrantChartAxesModel,
58 #[serde(default)]
59 points: Vec<QuadrantChartPointModel>,
60 #[serde(default)]
61 classes: BTreeMap<String, QuadrantChartStyles>,
62}
63
64#[derive(Debug, Clone)]
65struct QuadrantChartConfig {
66 chart_width: f64,
67 chart_height: f64,
68 title_padding: f64,
69 title_font_size: f64,
70 quadrant_padding: f64,
71 x_axis_label_padding: f64,
72 y_axis_label_padding: f64,
73 x_axis_label_font_size: f64,
74 y_axis_label_font_size: f64,
75 quadrant_label_font_size: f64,
76 quadrant_text_top_padding: f64,
77 point_text_padding: f64,
78 point_label_font_size: f64,
79 point_radius: f64,
80 x_axis_position: String,
81 y_axis_position: String,
82 quadrant_internal_border_stroke_width: f64,
83 quadrant_external_border_stroke_width: f64,
84}
85
86fn json_f64(v: &Value) -> Option<f64> {
87 v.as_f64()
88 .or_else(|| v.as_i64().map(|n| n as f64))
89 .or_else(|| v.as_u64().map(|n| n as f64))
90}
91
92fn config_f64(cfg: &Value, path: &[&str]) -> Option<f64> {
93 let mut cur = cfg;
94 for key in path {
95 cur = cur.get(*key)?;
96 }
97 json_f64(cur)
98}
99
100fn config_string(cfg: &Value, path: &[&str]) -> Option<String> {
101 let mut cur = cfg;
102 for key in path {
103 cur = cur.get(*key)?;
104 }
105 cur.as_str().map(|s| s.to_string())
106}
107
108fn default_quadrant_config(effective_config: &Value) -> QuadrantChartConfig {
109 QuadrantChartConfig {
110 chart_width: config_f64(effective_config, &["quadrantChart", "chartWidth"])
111 .unwrap_or(500.0),
112 chart_height: config_f64(effective_config, &["quadrantChart", "chartHeight"])
113 .unwrap_or(500.0),
114 title_padding: config_f64(effective_config, &["quadrantChart", "titlePadding"])
115 .unwrap_or(10.0),
116 title_font_size: config_f64(effective_config, &["quadrantChart", "titleFontSize"])
117 .unwrap_or(20.0),
118 quadrant_padding: config_f64(effective_config, &["quadrantChart", "quadrantPadding"])
119 .unwrap_or(5.0),
120 x_axis_label_padding: config_f64(effective_config, &["quadrantChart", "xAxisLabelPadding"])
121 .unwrap_or(5.0),
122 y_axis_label_padding: config_f64(effective_config, &["quadrantChart", "yAxisLabelPadding"])
123 .unwrap_or(5.0),
124 x_axis_label_font_size: config_f64(
125 effective_config,
126 &["quadrantChart", "xAxisLabelFontSize"],
127 )
128 .unwrap_or(16.0),
129 y_axis_label_font_size: config_f64(
130 effective_config,
131 &["quadrantChart", "yAxisLabelFontSize"],
132 )
133 .unwrap_or(16.0),
134 quadrant_label_font_size: config_f64(
135 effective_config,
136 &["quadrantChart", "quadrantLabelFontSize"],
137 )
138 .unwrap_or(16.0),
139 quadrant_text_top_padding: config_f64(
140 effective_config,
141 &["quadrantChart", "quadrantTextTopPadding"],
142 )
143 .unwrap_or(5.0),
144 point_text_padding: config_f64(effective_config, &["quadrantChart", "pointTextPadding"])
145 .unwrap_or(5.0),
146 point_label_font_size: config_f64(
147 effective_config,
148 &["quadrantChart", "pointLabelFontSize"],
149 )
150 .unwrap_or(12.0),
151 point_radius: config_f64(effective_config, &["quadrantChart", "pointRadius"])
152 .unwrap_or(5.0),
153 x_axis_position: config_string(effective_config, &["quadrantChart", "xAxisPosition"])
154 .unwrap_or_else(|| "top".to_string()),
155 y_axis_position: config_string(effective_config, &["quadrantChart", "yAxisPosition"])
156 .unwrap_or_else(|| "left".to_string()),
157 quadrant_internal_border_stroke_width: config_f64(
158 effective_config,
159 &["quadrantChart", "quadrantInternalBorderStrokeWidth"],
160 )
161 .unwrap_or(1.0),
162 quadrant_external_border_stroke_width: config_f64(
163 effective_config,
164 &["quadrantChart", "quadrantExternalBorderStrokeWidth"],
165 )
166 .unwrap_or(2.0),
167 }
168}
169
170#[derive(Debug, Clone)]
171struct QuadrantThemeConfig {
172 quadrant1_fill: String,
173 quadrant2_fill: String,
174 quadrant3_fill: String,
175 quadrant4_fill: String,
176 quadrant1_text_fill: String,
177 quadrant2_text_fill: String,
178 quadrant3_text_fill: String,
179 quadrant4_text_fill: String,
180 quadrant_point_fill: String,
181 quadrant_point_text_fill: String,
182 quadrant_x_axis_text_fill: String,
183 quadrant_y_axis_text_fill: String,
184 quadrant_title_fill: String,
185 quadrant_internal_border_stroke_fill: String,
186 quadrant_external_border_stroke_fill: String,
187}
188
189fn parse_hex_rgb(s: &str) -> Option<(u8, u8, u8)> {
190 let t = s.trim().strip_prefix('#').unwrap_or(s.trim());
191 if t.len() != 6 || !t.chars().all(|c| c.is_ascii_hexdigit()) {
192 return None;
193 }
194 let r = u8::from_str_radix(&t[0..2], 16).ok()?;
195 let g = u8::from_str_radix(&t[2..4], 16).ok()?;
196 let b = u8::from_str_radix(&t[4..6], 16).ok()?;
197 Some((r, g, b))
198}
199
200fn invert_hex_rgb(hex: &str) -> Option<String> {
201 let (r, g, b) = parse_hex_rgb(hex)?;
202 Some(format!("#{:02x}{:02x}{:02x}", 255 - r, 255 - g, 255 - b))
203}
204
205fn adjust_hex_rgb(hex: &str, delta: i16) -> Option<String> {
206 let (r, g, b) = parse_hex_rgb(hex)?;
207 let adj = |c: u8| -> u8 {
208 let v = c as i16 + delta;
209 v.clamp(0, 255) as u8
210 };
211 Some(format!("#{:02x}{:02x}{:02x}", adj(r), adj(g), adj(b)))
212}
213
214fn default_quadrant_theme() -> QuadrantThemeConfig {
215 let quadrant1_fill = "#ECECFF".to_string();
220 let primary_text = invert_hex_rgb(&quadrant1_fill).unwrap_or_else(|| "#131300".to_string());
221 QuadrantThemeConfig {
222 quadrant2_fill: adjust_hex_rgb(&quadrant1_fill, 5).unwrap_or_else(|| "#f1f1ff".to_string()),
223 quadrant3_fill: adjust_hex_rgb(&quadrant1_fill, 10)
224 .unwrap_or_else(|| "#f6f6ff".to_string()),
225 quadrant4_fill: adjust_hex_rgb(&quadrant1_fill, 15)
226 .unwrap_or_else(|| "#fbfbff".to_string()),
227 quadrant1_text_fill: primary_text.clone(),
228 quadrant2_text_fill: adjust_hex_rgb(&primary_text, -5)
229 .unwrap_or_else(|| "#0e0e00".to_string()),
230 quadrant3_text_fill: adjust_hex_rgb(&primary_text, -10)
231 .unwrap_or_else(|| "#090900".to_string()),
232 quadrant4_text_fill: adjust_hex_rgb(&primary_text, -15)
233 .unwrap_or_else(|| "#040400".to_string()),
234 quadrant_point_fill: "hsl(240, 100%, NaN%)".to_string(),
235 quadrant_point_text_fill: primary_text.clone(),
236 quadrant_x_axis_text_fill: primary_text.clone(),
237 quadrant_y_axis_text_fill: primary_text.clone(),
238 quadrant_title_fill: primary_text,
239 quadrant_internal_border_stroke_fill: "rgb(199, 199, 241)".to_string(),
240 quadrant_external_border_stroke_fill: "rgb(199, 199, 241)".to_string(),
241 quadrant1_fill,
242 }
243}
244
245fn quadrant_theme_with_overrides(effective_config: &Value) -> QuadrantThemeConfig {
246 let mut theme = default_quadrant_theme();
247
248 let set = |field: &mut String, key: &str| {
251 if let Some(v) = config_string(effective_config, &["themeVariables", key]) {
252 *field = v;
253 }
254 };
255
256 set(&mut theme.quadrant1_fill, "quadrant1Fill");
257 set(&mut theme.quadrant2_fill, "quadrant2Fill");
258 set(&mut theme.quadrant3_fill, "quadrant3Fill");
259 set(&mut theme.quadrant4_fill, "quadrant4Fill");
260
261 set(&mut theme.quadrant1_text_fill, "quadrant1TextFill");
262 set(&mut theme.quadrant2_text_fill, "quadrant2TextFill");
263 set(&mut theme.quadrant3_text_fill, "quadrant3TextFill");
264 set(&mut theme.quadrant4_text_fill, "quadrant4TextFill");
265
266 set(&mut theme.quadrant_point_fill, "quadrantPointFill");
267 set(&mut theme.quadrant_point_text_fill, "quadrantPointTextFill");
268 set(
269 &mut theme.quadrant_x_axis_text_fill,
270 "quadrantXAxisTextFill",
271 );
272 set(
273 &mut theme.quadrant_y_axis_text_fill,
274 "quadrantYAxisTextFill",
275 );
276 set(&mut theme.quadrant_title_fill, "quadrantTitleFill");
277
278 set(
279 &mut theme.quadrant_internal_border_stroke_fill,
280 "quadrantInternalBorderStrokeFill",
281 );
282 set(
283 &mut theme.quadrant_external_border_stroke_fill,
284 "quadrantExternalBorderStrokeFill",
285 );
286
287 theme
288}
289
290fn scale_linear(domain: (f64, f64), range: (f64, f64), v: f64) -> f64 {
291 let (d0, d1) = domain;
292 let (r0, r1) = range;
293 if d1 == d0 {
294 return r0;
295 }
296 let t = (v - d0) / (d1 - d0);
297 r0 + t * (r1 - r0)
298}
299
300pub fn layout_quadrantchart_diagram(
301 model: &Value,
302 effective_config: &Value,
303 _text_measurer: &dyn TextMeasurer,
304) -> Result<QuadrantChartDiagramLayout> {
305 let model: QuadrantChartModel = from_value_ref(model)?;
306
307 let cfg = default_quadrant_config(effective_config);
308 let theme = quadrant_theme_with_overrides(effective_config);
309
310 let title_text = model.title.as_deref().unwrap_or("").trim();
311 let show_title = !title_text.is_empty();
312
313 let show_x_axis = !model.axes.x_axis_left_text.trim().is_empty()
314 || !model.axes.x_axis_right_text.trim().is_empty();
315 let show_y_axis = !model.axes.y_axis_top_text.trim().is_empty()
316 || !model.axes.y_axis_bottom_text.trim().is_empty();
317
318 let x_axis_position = if model.points.is_empty() {
319 cfg.x_axis_position.as_str()
320 } else {
321 "bottom"
322 };
323
324 let x_axis_space_calc = cfg.x_axis_label_padding * 2.0 + cfg.x_axis_label_font_size;
325 let x_axis_space_top = if x_axis_position == "top" && show_x_axis {
326 x_axis_space_calc
327 } else {
328 0.0
329 };
330 let x_axis_space_bottom = if x_axis_position == "bottom" && show_x_axis {
331 x_axis_space_calc
332 } else {
333 0.0
334 };
335
336 let y_axis_space_calc = cfg.y_axis_label_padding * 2.0 + cfg.y_axis_label_font_size;
337 let y_axis_space_left = if cfg.y_axis_position == "left" && show_y_axis {
338 y_axis_space_calc
339 } else {
340 0.0
341 };
342 let y_axis_space_right = if cfg.y_axis_position == "right" && show_y_axis {
343 y_axis_space_calc
344 } else {
345 0.0
346 };
347
348 let title_space_top = if show_title {
349 cfg.title_font_size + cfg.title_padding * 2.0
350 } else {
351 0.0
352 };
353
354 let quadrant_left = cfg.quadrant_padding + y_axis_space_left;
355 let quadrant_top = cfg.quadrant_padding + x_axis_space_top + title_space_top;
356 let quadrant_width =
357 cfg.chart_width - cfg.quadrant_padding * 2.0 - y_axis_space_left - y_axis_space_right;
358 let quadrant_height = cfg.chart_height
359 - cfg.quadrant_padding * 2.0
360 - x_axis_space_top
361 - x_axis_space_bottom
362 - title_space_top;
363 let quadrant_half_width = quadrant_width / 2.0;
364 let quadrant_half_height = quadrant_height / 2.0;
365
366 let mut quadrants: Vec<QuadrantChartQuadrantData> = vec![
367 QuadrantChartQuadrantData {
368 x: quadrant_left + quadrant_half_width,
369 y: quadrant_top,
370 width: quadrant_half_width,
371 height: quadrant_half_height,
372 fill: theme.quadrant1_fill.clone(),
373 text: QuadrantChartTextData {
374 text: model.quadrants.quadrant1_text,
375 fill: theme.quadrant1_text_fill.clone(),
376 x: 0.0,
377 y: 0.0,
378 font_size: cfg.quadrant_label_font_size,
379 vertical_pos: "center".to_string(),
380 horizontal_pos: "middle".to_string(),
381 rotation: 0.0,
382 },
383 },
384 QuadrantChartQuadrantData {
385 x: quadrant_left,
386 y: quadrant_top,
387 width: quadrant_half_width,
388 height: quadrant_half_height,
389 fill: theme.quadrant2_fill.clone(),
390 text: QuadrantChartTextData {
391 text: model.quadrants.quadrant2_text,
392 fill: theme.quadrant2_text_fill.clone(),
393 x: 0.0,
394 y: 0.0,
395 font_size: cfg.quadrant_label_font_size,
396 vertical_pos: "center".to_string(),
397 horizontal_pos: "middle".to_string(),
398 rotation: 0.0,
399 },
400 },
401 QuadrantChartQuadrantData {
402 x: quadrant_left,
403 y: quadrant_top + quadrant_half_height,
404 width: quadrant_half_width,
405 height: quadrant_half_height,
406 fill: theme.quadrant3_fill.clone(),
407 text: QuadrantChartTextData {
408 text: model.quadrants.quadrant3_text,
409 fill: theme.quadrant3_text_fill.clone(),
410 x: 0.0,
411 y: 0.0,
412 font_size: cfg.quadrant_label_font_size,
413 vertical_pos: "center".to_string(),
414 horizontal_pos: "middle".to_string(),
415 rotation: 0.0,
416 },
417 },
418 QuadrantChartQuadrantData {
419 x: quadrant_left + quadrant_half_width,
420 y: quadrant_top + quadrant_half_height,
421 width: quadrant_half_width,
422 height: quadrant_half_height,
423 fill: theme.quadrant4_fill.clone(),
424 text: QuadrantChartTextData {
425 text: model.quadrants.quadrant4_text,
426 fill: theme.quadrant4_text_fill.clone(),
427 x: 0.0,
428 y: 0.0,
429 font_size: cfg.quadrant_label_font_size,
430 vertical_pos: "center".to_string(),
431 horizontal_pos: "middle".to_string(),
432 rotation: 0.0,
433 },
434 },
435 ];
436 for q in &mut quadrants {
437 q.text.x = q.x + q.width / 2.0;
438 if model.points.is_empty() {
439 q.text.y = q.y + q.height / 2.0;
440 q.text.horizontal_pos = "middle".to_string();
441 } else {
442 q.text.y = q.y + cfg.quadrant_text_top_padding;
443 q.text.horizontal_pos = "top".to_string();
444 }
445 }
446
447 let draw_x_axis_labels_in_middle = !model.axes.x_axis_right_text.trim().is_empty();
448 let draw_y_axis_labels_in_middle = !model.axes.y_axis_top_text.trim().is_empty();
449
450 let mut axis_labels: Vec<QuadrantChartAxisLabelData> = Vec::new();
451 if !model.axes.x_axis_left_text.trim().is_empty() && show_x_axis {
452 axis_labels.push(QuadrantChartAxisLabelData {
453 text: model.axes.x_axis_left_text,
454 fill: theme.quadrant_x_axis_text_fill.clone(),
455 x: quadrant_left
456 + if draw_x_axis_labels_in_middle {
457 quadrant_half_width / 2.0
458 } else {
459 0.0
460 },
461 y: if x_axis_position == "top" {
462 cfg.x_axis_label_padding + title_space_top
463 } else {
464 cfg.x_axis_label_padding + quadrant_top + quadrant_height + cfg.quadrant_padding
465 },
466 font_size: cfg.x_axis_label_font_size,
467 vertical_pos: if draw_x_axis_labels_in_middle {
468 "center".to_string()
469 } else {
470 "left".to_string()
471 },
472 horizontal_pos: "top".to_string(),
473 rotation: 0.0,
474 });
475 }
476 if !model.axes.x_axis_right_text.trim().is_empty() && show_x_axis {
477 axis_labels.push(QuadrantChartAxisLabelData {
478 text: model.axes.x_axis_right_text,
479 fill: theme.quadrant_x_axis_text_fill.clone(),
480 x: quadrant_left
481 + quadrant_half_width
482 + if draw_x_axis_labels_in_middle {
483 quadrant_half_width / 2.0
484 } else {
485 0.0
486 },
487 y: if x_axis_position == "top" {
488 cfg.x_axis_label_padding + title_space_top
489 } else {
490 cfg.x_axis_label_padding + quadrant_top + quadrant_height + cfg.quadrant_padding
491 },
492 font_size: cfg.x_axis_label_font_size,
493 vertical_pos: if draw_x_axis_labels_in_middle {
494 "center".to_string()
495 } else {
496 "left".to_string()
497 },
498 horizontal_pos: "top".to_string(),
499 rotation: 0.0,
500 });
501 }
502 if !model.axes.y_axis_bottom_text.trim().is_empty() && show_y_axis {
503 axis_labels.push(QuadrantChartAxisLabelData {
504 text: model.axes.y_axis_bottom_text,
505 fill: theme.quadrant_y_axis_text_fill.clone(),
506 x: if cfg.y_axis_position == "left" {
507 cfg.y_axis_label_padding
508 } else {
509 cfg.y_axis_label_padding + quadrant_left + quadrant_width + cfg.quadrant_padding
510 },
511 y: quadrant_top + quadrant_height
512 - if draw_y_axis_labels_in_middle {
513 quadrant_half_height / 2.0
514 } else {
515 0.0
516 },
517 font_size: cfg.y_axis_label_font_size,
518 vertical_pos: if draw_y_axis_labels_in_middle {
519 "center".to_string()
520 } else {
521 "left".to_string()
522 },
523 horizontal_pos: "top".to_string(),
524 rotation: -90.0,
525 });
526 }
527 if !model.axes.y_axis_top_text.trim().is_empty() && show_y_axis {
528 axis_labels.push(QuadrantChartAxisLabelData {
529 text: model.axes.y_axis_top_text,
530 fill: theme.quadrant_y_axis_text_fill.clone(),
531 x: if cfg.y_axis_position == "left" {
532 cfg.y_axis_label_padding
533 } else {
534 cfg.y_axis_label_padding + quadrant_left + quadrant_width + cfg.quadrant_padding
535 },
536 y: quadrant_top + quadrant_half_height
537 - if draw_y_axis_labels_in_middle {
538 quadrant_half_height / 2.0
539 } else {
540 0.0
541 },
542 font_size: cfg.y_axis_label_font_size,
543 vertical_pos: if draw_y_axis_labels_in_middle {
544 "center".to_string()
545 } else {
546 "left".to_string()
547 },
548 horizontal_pos: "top".to_string(),
549 rotation: -90.0,
550 });
551 }
552
553 let half_external_border_width = cfg.quadrant_external_border_stroke_width / 2.0;
554 let border_lines = vec![
555 QuadrantChartBorderLineData {
556 stroke_fill: theme.quadrant_external_border_stroke_fill.clone(),
557 stroke_width: cfg.quadrant_external_border_stroke_width,
558 x1: quadrant_left - half_external_border_width,
559 y1: quadrant_top,
560 x2: quadrant_left + quadrant_width + half_external_border_width,
561 y2: quadrant_top,
562 },
563 QuadrantChartBorderLineData {
564 stroke_fill: theme.quadrant_external_border_stroke_fill.clone(),
565 stroke_width: cfg.quadrant_external_border_stroke_width,
566 x1: quadrant_left + quadrant_width,
567 y1: quadrant_top + half_external_border_width,
568 x2: quadrant_left + quadrant_width,
569 y2: quadrant_top + quadrant_height - half_external_border_width,
570 },
571 QuadrantChartBorderLineData {
572 stroke_fill: theme.quadrant_external_border_stroke_fill.clone(),
573 stroke_width: cfg.quadrant_external_border_stroke_width,
574 x1: quadrant_left - half_external_border_width,
575 y1: quadrant_top + quadrant_height,
576 x2: quadrant_left + quadrant_width + half_external_border_width,
577 y2: quadrant_top + quadrant_height,
578 },
579 QuadrantChartBorderLineData {
580 stroke_fill: theme.quadrant_external_border_stroke_fill.clone(),
581 stroke_width: cfg.quadrant_external_border_stroke_width,
582 x1: quadrant_left,
583 y1: quadrant_top + half_external_border_width,
584 x2: quadrant_left,
585 y2: quadrant_top + quadrant_height - half_external_border_width,
586 },
587 QuadrantChartBorderLineData {
588 stroke_fill: theme.quadrant_internal_border_stroke_fill.clone(),
589 stroke_width: cfg.quadrant_internal_border_stroke_width,
590 x1: quadrant_left + quadrant_half_width,
591 y1: quadrant_top + half_external_border_width,
592 x2: quadrant_left + quadrant_half_width,
593 y2: quadrant_top + quadrant_height - half_external_border_width,
594 },
595 QuadrantChartBorderLineData {
596 stroke_fill: theme.quadrant_internal_border_stroke_fill.clone(),
597 stroke_width: cfg.quadrant_internal_border_stroke_width,
598 x1: quadrant_left + half_external_border_width,
599 y1: quadrant_top + quadrant_half_height,
600 x2: quadrant_left + quadrant_width - half_external_border_width,
601 y2: quadrant_top + quadrant_half_height,
602 },
603 ];
604
605 let mut points: Vec<QuadrantChartPointData> = Vec::new();
606 for p in model.points {
607 let class_styles = p
608 .class_name
609 .as_deref()
610 .and_then(|name| model.classes.get(name));
611
612 let radius = p
613 .styles
614 .radius
615 .or_else(|| class_styles.and_then(|c| c.radius))
616 .unwrap_or(cfg.point_radius);
617 let fill = p
618 .styles
619 .color
620 .clone()
621 .or_else(|| class_styles.and_then(|c| c.color.clone()))
622 .unwrap_or_else(|| theme.quadrant_point_fill.clone());
623 let stroke_color = p
624 .styles
625 .stroke_color
626 .clone()
627 .or_else(|| class_styles.and_then(|c| c.stroke_color.clone()))
628 .unwrap_or_else(|| theme.quadrant_point_fill.clone());
629 let stroke_width = p
630 .styles
631 .stroke_width
632 .clone()
633 .or_else(|| class_styles.and_then(|c| c.stroke_width.clone()))
634 .unwrap_or_else(|| "0px".to_string());
635
636 let x = scale_linear(
637 (0.0, 1.0),
638 (quadrant_left, quadrant_width + quadrant_left),
639 p.x,
640 );
641 let y = scale_linear(
642 (0.0, 1.0),
643 (quadrant_height + quadrant_top, quadrant_top),
644 p.y,
645 );
646 points.push(QuadrantChartPointData {
647 x,
648 y,
649 fill: fill.clone(),
650 radius,
651 stroke_color,
652 stroke_width,
653 text: QuadrantChartTextData {
654 text: p.text,
655 fill: theme.quadrant_point_text_fill.clone(),
656 x,
657 y: y + cfg.point_text_padding,
658 font_size: cfg.point_label_font_size,
659 vertical_pos: "center".to_string(),
660 horizontal_pos: "top".to_string(),
661 rotation: 0.0,
662 },
663 });
664 }
665
666 let title = if show_title {
667 Some(QuadrantChartTextData {
668 text: title_text.to_string(),
669 fill: theme.quadrant_title_fill,
670 font_size: cfg.title_font_size,
671 horizontal_pos: "top".to_string(),
672 vertical_pos: "center".to_string(),
673 rotation: 0.0,
674 y: cfg.title_padding,
675 x: cfg.chart_width / 2.0,
676 })
677 } else {
678 None
679 };
680
681 Ok(QuadrantChartDiagramLayout {
682 width: cfg.chart_width,
683 height: cfg.chart_height,
684 title,
685 quadrants,
686 border_lines,
687 points,
688 axis_labels,
689 })
690}