mirror of
https://github.com/duhanbalci/dreport.git
synced 2026-07-01 18:39:16 +00:00
fixes
This commit is contained in:
@@ -129,10 +129,23 @@ pub struct LineChartLayout {
|
||||
pub show_labels: bool,
|
||||
pub label_font: f64,
|
||||
pub label_color: String,
|
||||
pub smooth: bool,
|
||||
/// X axis line endpoints
|
||||
pub x_axis_y: f64,
|
||||
pub x_axis_x1: f64,
|
||||
pub x_axis_x2: f64,
|
||||
/// Vertical reference lines
|
||||
pub ref_lines: Vec<RefLineLayout>,
|
||||
}
|
||||
|
||||
pub struct RefLineLayout {
|
||||
pub x: f64,
|
||||
pub y1: f64,
|
||||
pub y2: f64,
|
||||
pub color: String,
|
||||
pub width: f64,
|
||||
pub dash: bool,
|
||||
pub label: Option<String>,
|
||||
}
|
||||
|
||||
pub struct PieSlice {
|
||||
@@ -223,6 +236,10 @@ pub trait ChartDataSource {
|
||||
fn inner_radius(&self) -> Option<f64>;
|
||||
fn show_points(&self) -> Option<bool>;
|
||||
fn line_width(&self) -> Option<f64>;
|
||||
fn curve_type(&self) -> Option<&str>;
|
||||
fn reference_lines(&self) -> &[dreport_core::models::ChartReferenceLine];
|
||||
fn show_vertical_grid(&self) -> bool;
|
||||
fn vertical_grid_color(&self) -> Option<&str>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -314,6 +331,18 @@ impl ChartDataSource for crate::data_resolve::ResolvedChartData {
|
||||
fn line_width(&self) -> Option<f64> {
|
||||
self.style.line_width
|
||||
}
|
||||
fn curve_type(&self) -> Option<&str> {
|
||||
self.style.curve_type.as_deref()
|
||||
}
|
||||
fn reference_lines(&self) -> &[dreport_core::models::ChartReferenceLine] {
|
||||
self.axis.as_ref().map_or(&[], |a| &a.reference_lines)
|
||||
}
|
||||
fn show_vertical_grid(&self) -> bool {
|
||||
self.axis.as_ref().and_then(|a| a.show_vertical_grid).unwrap_or(true)
|
||||
}
|
||||
fn vertical_grid_color(&self) -> Option<&str> {
|
||||
self.axis.as_ref().and_then(|a| a.vertical_grid_color.as_deref())
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -403,6 +432,18 @@ impl ChartDataSource for crate::ChartRenderData {
|
||||
fn line_width(&self) -> Option<f64> {
|
||||
self.line_width
|
||||
}
|
||||
fn curve_type(&self) -> Option<&str> {
|
||||
self.curve_type.as_deref()
|
||||
}
|
||||
fn reference_lines(&self) -> &[dreport_core::models::ChartReferenceLine] {
|
||||
&self.reference_lines
|
||||
}
|
||||
fn show_vertical_grid(&self) -> bool {
|
||||
self.show_vertical_grid
|
||||
}
|
||||
fn vertical_grid_color(&self) -> Option<&str> {
|
||||
self.vertical_grid_color.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -693,22 +734,14 @@ pub fn compute_x_labels_line(
|
||||
rotate_angle: 0.0,
|
||||
};
|
||||
}
|
||||
let spacing = if n_cats == 1 {
|
||||
pw
|
||||
} else {
|
||||
pw / (n_cats - 1) as f64
|
||||
};
|
||||
let step = pw / n_cats as f64;
|
||||
let max_label_len = categories.iter().map(|c| c.len()).max().unwrap_or(0);
|
||||
let rotate_angle = compute_label_rotation(max_label_len, spacing);
|
||||
let rotate_angle = compute_label_rotation(max_label_len, step);
|
||||
let labels = categories
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ci, cat)| {
|
||||
let x = if n_cats == 1 {
|
||||
px + pw / 2.0
|
||||
} else {
|
||||
px + ci as f64 * pw / (n_cats - 1) as f64
|
||||
};
|
||||
let x = px + step / 2.0 + ci as f64 * step;
|
||||
XLabel {
|
||||
text: cat.clone(),
|
||||
x,
|
||||
@@ -833,6 +866,11 @@ pub fn compute_line_layout(data: &dyn ChartDataSource, cl: &ChartLayout) -> Line
|
||||
let show_labels = data.show_labels();
|
||||
let label_font = data.label_font_size().unwrap_or(2.2);
|
||||
let label_color = data.label_color().unwrap_or("#333").to_string();
|
||||
let smooth = data.curve_type() == Some("smooth");
|
||||
|
||||
// Slot-based positioning: each category gets a slot, point centered in slot
|
||||
// This adds padding on left/right so first/last points don't touch axes
|
||||
let step = if n_cats > 0 { pw / n_cats as f64 } else { pw };
|
||||
|
||||
let series = (0..data.series_count())
|
||||
.map(|si| {
|
||||
@@ -841,11 +879,7 @@ pub fn compute_line_layout(data: &dyn ChartDataSource, cl: &ChartLayout) -> Line
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ci, val)| {
|
||||
let x = if n_cats == 1 {
|
||||
px + pw / 2.0
|
||||
} else {
|
||||
px + ci as f64 * pw / (n_cats - 1) as f64
|
||||
};
|
||||
let x = px + step / 2.0 + ci as f64 * step;
|
||||
let y = py + ph - ((val - min_val) / range) * ph;
|
||||
LinePoint { x, y, value: *val }
|
||||
})
|
||||
@@ -859,6 +893,42 @@ pub fn compute_line_layout(data: &dyn ChartDataSource, cl: &ChartLayout) -> Line
|
||||
|
||||
let x_labels = compute_x_labels_line(data.categories(), px, py + ph, pw);
|
||||
|
||||
// Vertical grid lines at each category
|
||||
let vgrid_color = data.vertical_grid_color().unwrap_or("#E5E7EB").to_string();
|
||||
let mut ref_lines: Vec<RefLineLayout> = if data.show_vertical_grid() {
|
||||
(0..n_cats).map(|ci| {
|
||||
let x = px + step / 2.0 + ci as f64 * step;
|
||||
RefLineLayout {
|
||||
x,
|
||||
y1: py,
|
||||
y2: py + ph,
|
||||
color: vgrid_color.clone(),
|
||||
width: 0.15,
|
||||
dash: false,
|
||||
label: None,
|
||||
}
|
||||
}).collect()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
// Explicit reference lines (overlay on top of grid)
|
||||
for rl in data.reference_lines() {
|
||||
if rl.category_index >= n_cats {
|
||||
continue;
|
||||
}
|
||||
let x = px + step / 2.0 + rl.category_index as f64 * step;
|
||||
ref_lines.push(RefLineLayout {
|
||||
x,
|
||||
y1: py,
|
||||
y2: py + ph,
|
||||
color: rl.color.clone().unwrap_or_else(|| "#9CA3AF".to_string()),
|
||||
width: rl.width.unwrap_or(0.3),
|
||||
dash: rl.dash.unwrap_or(true),
|
||||
label: rl.label.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
LineChartLayout {
|
||||
min_val,
|
||||
max_val,
|
||||
@@ -870,9 +940,11 @@ pub fn compute_line_layout(data: &dyn ChartDataSource, cl: &ChartLayout) -> Line
|
||||
show_labels,
|
||||
label_font,
|
||||
label_color,
|
||||
smooth,
|
||||
x_axis_y: py + ph,
|
||||
x_axis_x1: px,
|
||||
x_axis_x2: px + pw,
|
||||
ref_lines,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -121,14 +121,13 @@ fn render_line(svg: &mut String, data: &ResolvedChartData, cl: &ChartLayout) {
|
||||
// Y axis
|
||||
render_y_axis_svg(svg, &ll.y_axis);
|
||||
|
||||
let mut label_texts = String::new();
|
||||
|
||||
for series_layout in &ll.series {
|
||||
let color = color_at(&cl.palette, series_layout.color_idx);
|
||||
let mut points = String::new();
|
||||
let mut point_circles = String::new();
|
||||
|
||||
for pt in &series_layout.points {
|
||||
write!(points, "{:.2},{:.2} ", pt.x, pt.y).unwrap();
|
||||
|
||||
if ll.show_points {
|
||||
write!(
|
||||
point_circles,
|
||||
@@ -139,19 +138,75 @@ fn render_line(svg: &mut String, data: &ResolvedChartData, cl: &ChartLayout) {
|
||||
}
|
||||
|
||||
if ll.show_labels {
|
||||
svg_text(svg, pt.x, pt.y - 1.5, ll.label_font, &ll.label_color, SvgAnchor::Middle, &format_value(pt.value));
|
||||
svg_text(&mut label_texts, pt.x, pt.y - 1.5, ll.label_font, &ll.label_color, SvgAnchor::Middle, &format_value(pt.value));
|
||||
}
|
||||
}
|
||||
|
||||
write!(
|
||||
svg,
|
||||
r##"<polyline points="{}" fill="none" stroke="{}" stroke-width="{:.2}" stroke-linejoin="round" stroke-linecap="round"/>"##,
|
||||
points.trim(), color, ll.line_width
|
||||
)
|
||||
.unwrap();
|
||||
if ll.smooth && series_layout.points.len() >= 2 {
|
||||
// Catmull-Rom → cubic bezier smooth curve
|
||||
let pts = &series_layout.points;
|
||||
let mut d = format!("M{:.2},{:.2}", pts[0].x, pts[0].y);
|
||||
for i in 0..pts.len() - 1 {
|
||||
let p0 = if i > 0 { &pts[i - 1] } else { &pts[i] };
|
||||
let p1 = &pts[i];
|
||||
let p2 = &pts[i + 1];
|
||||
let p3 = if i + 2 < pts.len() { &pts[i + 2] } else { &pts[i + 1] };
|
||||
|
||||
let cp1x = p1.x + (p2.x - p0.x) / 6.0;
|
||||
let cp1y = p1.y + (p2.y - p0.y) / 6.0;
|
||||
let cp2x = p2.x - (p3.x - p1.x) / 6.0;
|
||||
let cp2y = p2.y - (p3.y - p1.y) / 6.0;
|
||||
|
||||
write!(d, " C{:.2},{:.2} {:.2},{:.2} {:.2},{:.2}",
|
||||
cp1x, cp1y, cp2x, cp2y, p2.x, p2.y
|
||||
).unwrap();
|
||||
}
|
||||
write!(
|
||||
svg,
|
||||
r##"<path d="{}" fill="none" stroke="{}" stroke-width="{:.2}" stroke-linejoin="round" stroke-linecap="round"/>"##,
|
||||
d, color, ll.line_width
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
let mut points = String::new();
|
||||
for pt in &series_layout.points {
|
||||
write!(points, "{:.2},{:.2} ", pt.x, pt.y).unwrap();
|
||||
}
|
||||
write!(
|
||||
svg,
|
||||
r##"<polyline points="{}" fill="none" stroke="{}" stroke-width="{:.2}" stroke-linejoin="round" stroke-linecap="round"/>"##,
|
||||
points.trim(), color, ll.line_width
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
svg.push_str(&point_circles);
|
||||
}
|
||||
|
||||
// Data labels (rendered after lines/points so they appear on top)
|
||||
svg.push_str(&label_texts);
|
||||
|
||||
// Reference lines (vertical)
|
||||
for rl in &ll.ref_lines {
|
||||
if rl.dash {
|
||||
write!(
|
||||
svg,
|
||||
r##"<line x1="{:.2}" y1="{:.2}" x2="{:.2}" y2="{:.2}" stroke="{}" stroke-width="{:.2}" stroke-dasharray="1.5,1"/>"##,
|
||||
rl.x, rl.y1, rl.x, rl.y2, rl.color, rl.width
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
write!(
|
||||
svg,
|
||||
r##"<line x1="{:.2}" y1="{:.2}" x2="{:.2}" y2="{:.2}" stroke="{}" stroke-width="{:.2}"/>"##,
|
||||
rl.x, rl.y1, rl.x, rl.y2, rl.color, rl.width
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
if let Some(ref label) = rl.label {
|
||||
svg_text(svg, rl.x, rl.y1 - 1.0, 2.0, &rl.color, SvgAnchor::Middle, label);
|
||||
}
|
||||
}
|
||||
|
||||
// X axis labels
|
||||
render_x_labels_svg(svg, &ll.x_labels);
|
||||
|
||||
@@ -560,6 +615,9 @@ mod tests {
|
||||
y_label: Some("Revenue".to_string()),
|
||||
show_grid: None,
|
||||
grid_color: None,
|
||||
show_vertical_grid: None,
|
||||
vertical_grid_color: None,
|
||||
reference_lines: vec![],
|
||||
});
|
||||
let svg = render_svg(&data, 100.0, 60.0);
|
||||
|
||||
@@ -575,6 +633,9 @@ mod tests {
|
||||
y_label: Some("Y Label".to_string()),
|
||||
show_grid: None,
|
||||
grid_color: None,
|
||||
show_vertical_grid: None,
|
||||
vertical_grid_color: None,
|
||||
reference_lines: vec![],
|
||||
});
|
||||
let svg = render_svg(&data, 80.0, 80.0);
|
||||
|
||||
|
||||
@@ -161,8 +161,21 @@ pub struct ChartRenderData {
|
||||
// Title align
|
||||
#[serde(default)]
|
||||
pub title_align: Option<String>,
|
||||
// Curve type for line charts
|
||||
#[serde(default)]
|
||||
pub curve_type: Option<String>,
|
||||
// Vertical reference lines
|
||||
#[serde(default)]
|
||||
pub reference_lines: Vec<dreport_core::models::ChartReferenceLine>,
|
||||
// Vertical grid
|
||||
#[serde(default = "default_true")]
|
||||
pub show_vertical_grid: bool,
|
||||
#[serde(default)]
|
||||
pub vertical_grid_color: Option<String>,
|
||||
}
|
||||
|
||||
fn default_true() -> bool { true }
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ChartSeriesData {
|
||||
pub name: String,
|
||||
@@ -340,6 +353,10 @@ impl From<&data_resolve::ResolvedChartData> for ChartRenderData {
|
||||
legend_font_size: cd.legend.as_ref().and_then(|l| l.font_size),
|
||||
x_label: cd.axis.as_ref().and_then(|a| a.x_label.clone()),
|
||||
y_label: cd.axis.as_ref().and_then(|a| a.y_label.clone()),
|
||||
curve_type: cd.style.curve_type.clone(),
|
||||
reference_lines: cd.axis.as_ref().map_or_else(Vec::new, |a| a.reference_lines.clone()),
|
||||
show_vertical_grid: cd.axis.as_ref().and_then(|a| a.show_vertical_grid).unwrap_or(true),
|
||||
vertical_grid_color: cd.axis.as_ref().and_then(|a| a.vertical_grid_color.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1015,11 +1015,33 @@ fn render_chart(
|
||||
}));
|
||||
let path = {
|
||||
let mut pb = PathBuilder::new();
|
||||
for (i, (lx, ly)) in points.iter().enumerate() {
|
||||
if i == 0 {
|
||||
pb.move_to(mm(*lx), mm(*ly));
|
||||
} else {
|
||||
pb.line_to(mm(*lx), mm(*ly));
|
||||
if ll.smooth && points.len() >= 2 {
|
||||
let pts = &series_layout.points;
|
||||
pb.move_to(mm(pts[0].x), mm(pts[0].y));
|
||||
for i in 0..pts.len() - 1 {
|
||||
let p0 = if i > 0 { &pts[i - 1] } else { &pts[i] };
|
||||
let p1 = &pts[i];
|
||||
let p2 = &pts[i + 1];
|
||||
let p3 = if i + 2 < pts.len() { &pts[i + 2] } else { &pts[i + 1] };
|
||||
|
||||
let cp1x = p1.x + (p2.x - p0.x) / 6.0;
|
||||
let cp1y = p1.y + (p2.y - p0.y) / 6.0;
|
||||
let cp2x = p2.x - (p3.x - p1.x) / 6.0;
|
||||
let cp2y = p2.y - (p3.y - p1.y) / 6.0;
|
||||
|
||||
pb.cubic_to(
|
||||
mm(cp1x), mm(cp1y),
|
||||
mm(cp2x), mm(cp2y),
|
||||
mm(p2.x), mm(p2.y),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
for (i, (lx, ly)) in points.iter().enumerate() {
|
||||
if i == 0 {
|
||||
pb.move_to(mm(*lx), mm(*ly));
|
||||
} else {
|
||||
pb.line_to(mm(*lx), mm(*ly));
|
||||
}
|
||||
}
|
||||
}
|
||||
pb.finish()
|
||||
@@ -1055,6 +1077,32 @@ fn render_chart(
|
||||
}
|
||||
}
|
||||
}
|
||||
// Reference lines (vertical)
|
||||
for rl in &ll.ref_lines {
|
||||
let rl_color = parse_color(&rl.color);
|
||||
chart_line_seg(
|
||||
surface,
|
||||
rl.x,
|
||||
rl.y1,
|
||||
rl.x,
|
||||
rl.y2,
|
||||
rl_color,
|
||||
(rl.width * 2.5) as f32,
|
||||
);
|
||||
if let Some(ref label) = rl.label {
|
||||
chart_text(
|
||||
surface,
|
||||
rl.x,
|
||||
rl.y1 - 1.0,
|
||||
label,
|
||||
2.0,
|
||||
&rl.color,
|
||||
ChartTextAlign::Center,
|
||||
fonts,
|
||||
measurer,
|
||||
);
|
||||
}
|
||||
}
|
||||
render_chart_x_labels(surface, &ll.x_labels, fonts, measurer);
|
||||
let ac = parse_color("#9CA3AF");
|
||||
chart_line_seg(
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user