// Box plot rendering: box from Q1-Q3, median line, whiskers, optional outliers.
///|
/// Render a box plot series (Q1/Q3 box, median line, whiskers with caps, and outliers) into SVG. Parameters: layout - chart layout config, opt - full chart options, seriesIndex - index of the series to render.
pub fn renderBoxplot(
layout : Layout,
opt : ChartOption,
seriesIndex : Int,
) -> String {
let series = opt.series[seriesIndex]
let color = match series.color {
Some(c) => c
None => seriesColor(seriesIndex)
}
let fill = color.toSvg()
let stroke = Color::rgb(40, 40, 40).toSvg()
let medianStroke = Color::rgb(20, 20, 20).toSvg()
let plot = layout.plot
let mut svg = ""
let groups = series.boxGroups
let n = groups.length()
if n == 0 {
return ""
}
// Box width: 50% of band width
let boxWidth = layout.barWidth
let halfBox = boxWidth / 2.0
let whiskerLineWidth = 1.0
let capWidth = boxWidth * 0.4
for i = 0; i < n; i = i + 1 {
let group = groups[i]
if group.values.length() < 2 {
continue
}
let stats = computeBoxStats(group.values)
// Use bandCenter for box positioning (bars/boxes align to band centers)
let cx = plot.left() +
layout.xScale.bandCenter(group.x) +
layout.dodgeOffsets[seriesIndex]
// Map data values to Y pixels
let yQ1 = plot.bottom() - layout.yScale.map(stats.q1)
let yQ3 = plot.bottom() - layout.yScale.map(stats.q3)
let yMedian = plot.bottom() - layout.yScale.map(stats.median)
let yWhiskerLow = plot.bottom() - layout.yScale.map(stats.whiskerLow)
let yWhiskerHigh = plot.bottom() - layout.yScale.map(stats.whiskerHigh)
let boxY = yQ3 // top of box (Q3 has lower pixel value = higher on screen)
let boxH = yQ1 - yQ3 // box height
// Box rect (Q1 to Q3)
svg = svg +
rect(
x=cx - halfBox,
y=boxY,
w=boxWidth,
h=boxH,
fill~,
stroke~,
strokeWidth=1.0,
) +
"\n"
// Median line
svg = svg +
line(
x1=cx - halfBox,
y1=yMedian,
x2=cx + halfBox,
y2=yMedian,
stroke=medianStroke,
strokeWidth=2.0,
) +
"\n"
// Whisker: vertical line from Q1 to whiskerLow
svg = svg +
line(
x1=cx,
y1=yQ1,
x2=cx,
y2=yWhiskerLow,
stroke~,
strokeWidth=whiskerLineWidth,
) +
"\n"
// Whisker: vertical line from Q3 to whiskerHigh
svg = svg +
line(
x1=cx,
y1=yQ3,
x2=cx,
y2=yWhiskerHigh,
stroke~,
strokeWidth=whiskerLineWidth,
) +
"\n"
// Whisker caps
svg = svg +
line(
x1=cx - capWidth,
y1=yWhiskerLow,
x2=cx + capWidth,
y2=yWhiskerLow,
stroke~,
strokeWidth=whiskerLineWidth,
) +
"\n"
svg = svg +
line(
x1=cx - capWidth,
y1=yWhiskerHigh,
x2=cx + capWidth,
y2=yWhiskerHigh,
stroke~,
strokeWidth=whiskerLineWidth,
) +
"\n"
// Outliers
if series.showOutliers {
for j = 0; j < stats.outliers.length(); j = j + 1 {
let oy = plot.bottom() - layout.yScale.map(stats.outliers[j])
svg = svg +
circle(cx~, cy=oy, r=3.0, fill=stroke, stroke="none", strokeWidth=0.0) +
"\n"
}
}
}
svg
}