// Moonchart data model types.
// All core option types used for chart configuration.
// ──── Color (value type for performance) ────
///|
/// An RGBA color with components in 0-255 range.
#valtype
pub(all) struct Color {
r : Int
g : Int
b : Int
a : Int
}
///|
/// Construct an opaque color from RGB (alpha = 255).
pub fn Color::rgb(r : Int, g : Int, b : Int) -> Color {
{ r, g, b, a: 255 }
}
///|
/// Construct a color with explicit alpha.
pub fn Color::rgba(r : Int, g : Int, b : Int, a : Int) -> Color {
{ r, g, b, a }
}
///|
/// Convert color to SVG "rgb(r,g,b)" or "rgba(r,g,b,a)" string.
pub fn Color::toSvg(self : Color) -> String {
if self.a == 255 {
"rgb(\{self.r},\{self.g},\{self.b})"
} else {
let aDouble = Double::from_int(self.a) / 255.0
"rgba(\{self.r},\{self.g},\{self.b},\{aDouble})"
}
}
///|
/// Convert color to hex string like "#FF0000".
pub fn Color::toHex(self : Color) -> String {
"#\{hexByte(self.r)}\{hexByte(self.g)}\{hexByte(self.b)}"
}
// Format an Int in 0..255 as two uppercase hex digits.
///|
fn hexByte(n : Int) -> String {
hexDigit(n / 16) + hexDigit(n % 16)
}
// Format a single hex digit 0..15 as uppercase hex char.
///|
fn hexDigit(d : Int) -> String {
if d < 10 {
d.to_string()
} else {
match d {
10 => "A"
11 => "B"
12 => "C"
13 => "D"
14 => "E"
_ => "F"
}
}
}
// ──── Font ────
///|
/// Font configuration for text elements.
pub struct Font {
family : String
size : Double
color : Color
bold : Bool
italic : Bool
}
///|
/// Default font: 12px sans-serif, black, normal weight.
pub fn Font::default() -> Font {
{
family: "sans-serif",
size: 12.0,
color: Color::rgb(0, 0, 0),
bold: false,
italic: false,
}
}
///|
/// Create a title font (16px, bold).
pub fn Font::title() -> Font {
{
family: "sans-serif",
size: 16.0,
color: Color::rgb(0, 0, 0),
bold: true,
italic: false,
}
}
///|
/// Build full SVG text style attributes string.
pub fn Font::svgAttrs(self : Font) -> String {
let base = "font-family='\{self.family}' font-size='\{self.size}' fill='\{self.color.toSvg()}'"
if self.bold && self.italic {
base + " font-weight='bold' font-style='italic'"
} else if self.bold {
base + " font-weight='bold'"
} else if self.italic {
base + " font-style='italic'"
} else {
base
}
}
// ──── Error value ────
///|
/// Represents the error/uncertainty of a data point.
pub(all) enum ErrorValue {
/// Symmetric error: ±value (most common in scientific plots).
Symmetric(Double)
/// Asymmetric error: [lo, hi] range (e.g., confidence intervals).
Asymmetric(Double, Double)
}
///|
/// Get the lower bound of an error value relative to the data point.
pub fn ErrorValue::lo(self : ErrorValue) -> Double {
match self {
Symmetric(v) => -v
Asymmetric(lo, _) => -lo
}
}
///|
/// Get the upper bound of an error value relative to the data point.
pub fn ErrorValue::hi(self : ErrorValue) -> Double {
match self {
Symmetric(v) => v
Asymmetric(_, hi) => hi
}
}
// ──── Data point ────
///|
/// A single data point in a series.
pub struct DataPoint {
/// X value (numeric, interpreted by the x-axis scale).
x : Double
/// Y value.
y : Double
/// Optional Y-axis error bar.
error_y : ErrorValue?
/// Optional X-axis error bar (reserved for future use).
error_x : ErrorValue?
/// Optional text label to display near this point.
label : String?
}
///|
/// Create a simple (x, y) data point with no extras.
pub fn DataPoint::new(x : Double, y : Double) -> DataPoint {
{ x, y, error_y: None, error_x: None, label: None }
}
///|
/// Create a data point with a symmetric Y error bar.
pub fn DataPoint::withErrY(x : Double, y : Double, err : Double) -> DataPoint {
{ x, y, error_y: Some(Symmetric(err)), error_x: None, label: None }
}
///|
/// Create a data point with an asymmetric Y error bar.
pub fn DataPoint::withAsymErrY(
x : Double,
y : Double,
lo : Double,
hi : Double,
) -> DataPoint {
{ x, y, error_y: Some(Asymmetric(lo, hi)), error_x: None, label: None }
}
// ──── Chart type ────
///|
/// The type of chart to render for a series.
pub enum ChartType {
Bar
Line
Scatter
BoxPlot
}
// ──── Point shape (for line/scatter markers) ────
///|
/// Shape of data point markers.
pub(all) enum PointShape {
Circle
Square
Triangle
Diamond
Cross
}
// ──── Line style ────
///|
/// Line dash pattern.
pub(all) enum LineStyle {
Solid
Dashed
Dotted
DashDot
}
// ──── Axis kind ────
///|
/// The type of axis scale.
pub enum AxisKind {
/// Linear numeric scale with min/max range.
Linear(Double, Double)
}
// ──── Axis option ────
///|
/// Configuration for a single axis (X or Y).
pub struct AxisOption {
/// The scale type for this axis.
kind : AxisKind
/// Optional axis label text (e.g., "Time (days)").
label : String?
/// X-axis tick positions (data coordinates). Empty = auto-infer from data.
tickPositions : Array[Double]
/// X-axis tick labels. Must match tickPositions length if set. Empty = auto-generate.
tickLabels : Array[String]
/// Whether to show tick marks. Default: true.
showTick : Bool
/// Desired number of ticks (None = auto). Default: None.
tickCount : Int?
/// Whether to draw grid lines from this axis. Default: false.
gridLine : Bool
/// Tick label font size. Default: 11.
tickFontSize : Double
/// Axis label (title) font size. Default: 13.
labelFontSize : Double
/// Tick label color. Default: dark gray.
tickColor : Color
/// Grid line color. Default: light gray.
gridColor : Color
/// Grid line width. Default: 0.5.
gridWidth : Double
}
///|
/// Create a default linear Y axis [0, 100].
pub fn AxisOption::linearY() -> AxisOption {
{
kind: Linear(0.0, 100.0),
label: None,
tickPositions: [],
tickLabels: [],
showTick: true,
tickCount: None,
gridLine: false,
tickFontSize: 11.0,
labelFontSize: 13.0,
tickColor: Color::rgb(60, 60, 60),
gridColor: Color::rgb(224, 224, 224),
gridWidth: 0.5,
}
}
///|
/// Create a default X axis (auto-infer ticks from data).
pub fn AxisOption::defaultX() -> AxisOption {
{
kind: Linear(0.0, 1.0),
label: None,
tickPositions: [],
tickLabels: [],
showTick: true,
tickCount: None,
gridLine: false,
tickFontSize: 11.0,
labelFontSize: 13.0,
tickColor: Color::rgb(60, 60, 60),
gridColor: Color::rgb(224, 224, 224),
gridWidth: 0.5,
}
}
///|
/// Set the axis label for this axis. Returns a new AxisOption.
pub fn AxisOption::withLabel(self : AxisOption, label : String) -> AxisOption {
{ ..self, label: Some(label) }
}
// ──── Series ────
// ──── Box group (for box plots) ────
///|
/// A group of raw data for box plot computation.
pub(all) struct BoxGroup {
/// X-axis position (data coordinate).
x : Double
/// Group label (shown on X axis tick).
label : String
/// Raw data values.
values : Array[Double]
}
///|
/// Create a box plot data group.
pub fn BoxGroup::new(
x : Double,
label : String,
values : Array[Double],
) -> BoxGroup {
{ x, label, values }
}
// ──── Series ────
///|
/// A data series: one group of data points with a chart type and style.
pub struct Series {
/// Display name (shown in legend).
name : String
/// Chart type for this series.
chartType : ChartType
/// The data points.
data : Array[DataPoint]
/// Override the automatic color assignment.
color : Color?
/// Line width (for Line charts). Default: 2.0.
lineWidth : Double
/// Point marker size (for Line/Scatter). Default: 4.0.
pointSize : Double
/// Point marker shape. Default: Circle.
pointShape : PointShape
/// Line dash style. Default: Solid.
lineStyle : LineStyle
/// Point fill color (None = use series color). Default: None.
pointFill : Color?
/// Bar stroke color (None = no stroke). Default: None.
barStroke : Color?
/// Bar stroke width. Default: 0.
barStrokeWidth : Double
/// Error bar color (None = use series color). Default: Some(black).
errorColor : Color?
/// Error bar line width. Default: 1.0.
errorLineWidth : Double
/// Error bar cap width (total horizontal bar length). Default: 6.0.
errorCapWidth : Double
/// Box plot data groups (for BoxPlot type). Default: empty.
boxGroups : Array[BoxGroup]
/// Whether to show outlier points for box plots. Default: true.
showOutliers : Bool
}
///|
/// Create a bar chart series.
pub fn Series::bar(name : String, data : Array[DataPoint]) -> Series {
{
name,
chartType: Bar,
data,
color: None,
lineWidth: 2.0,
pointSize: 4.0,
pointShape: Circle,
lineStyle: Solid,
pointFill: None,
barStroke: None,
barStrokeWidth: 0.0,
errorColor: Some(Color::rgb(0, 0, 0)),
errorLineWidth: 1.0,
errorCapWidth: 6.0,
boxGroups: [],
showOutliers: true,
}
}
///|
/// Create a line chart series.
pub fn Series::line(name : String, data : Array[DataPoint]) -> Series {
{
name,
chartType: Line,
data,
color: None,
lineWidth: 2.0,
pointSize: 4.0,
pointShape: Circle,
lineStyle: Solid,
pointFill: None,
barStroke: None,
barStrokeWidth: 0.0,
errorColor: Some(Color::rgb(0, 0, 0)),
errorLineWidth: 1.0,
errorCapWidth: 6.0,
boxGroups: [],
showOutliers: true,
}
}
///|
/// Create a box plot series.
pub fn Series::boxplot(name : String, groups : Array[BoxGroup]) -> Series {
{
name,
chartType: BoxPlot,
data: [],
color: None,
lineWidth: 2.0,
pointSize: 4.0,
pointShape: Circle,
lineStyle: Solid,
pointFill: None,
barStroke: None,
barStrokeWidth: 0.0,
errorColor: Some(Color::rgb(0, 0, 0)),
errorLineWidth: 1.0,
errorCapWidth: 6.0,
boxGroups: groups,
showOutliers: true,
}
}
///|
/// Create a scatter chart series.
pub fn Series::scatter(name : String, data : Array[DataPoint]) -> Series {
{
name,
chartType: Scatter,
data,
color: None,
lineWidth: 2.0,
pointSize: 4.0,
pointShape: Circle,
lineStyle: Solid,
pointFill: None,
barStroke: None,
barStrokeWidth: 0.0,
errorColor: Some(Color::rgb(0, 0, 0)),
errorLineWidth: 1.0,
errorCapWidth: 6.0,
boxGroups: [],
showOutliers: true,
}
}
///|
/// Set a custom color for this series.
pub fn Series::withColor(self : Series, c : Color) -> Series {
{ ..self, color: Some(c) }
}
///|
/// Set line width for this series.
pub fn Series::withLineWidth(self : Series, w : Double) -> Series {
{ ..self, lineWidth: w }
}
///|
/// Set point size for this series.
pub fn Series::withPointSize(self : Series, s : Double) -> Series {
{ ..self, pointSize: s }
}
///|
/// Set point shape for this series.
pub fn Series::withPointShape(self : Series, s : PointShape) -> Series {
{ ..self, pointShape: s }
}
///|
/// Set line style (dashed, dotted, etc.).
pub fn Series::withLineStyle(self : Series, s : LineStyle) -> Series {
{ ..self, lineStyle: s }
}
///|
/// Set point fill color.
pub fn Series::withPointFill(self : Series, c : Color) -> Series {
{ ..self, pointFill: Some(c) }
}
///|
/// Set bar stroke color and width.
pub fn Series::withBarStroke(self : Series, c : Color, w : Double) -> Series {
{ ..self, barStroke: Some(c), barStrokeWidth: w }
}
///|
/// Set error bar color (None to inherit series color).
pub fn Series::withErrorColor(self : Series, c : Color?) -> Series {
{ ..self, errorColor: c }
}
///|
/// Set error bar line width.
pub fn Series::withErrorLineWidth(self : Series, w : Double) -> Series {
{ ..self, errorLineWidth: w }
}
///|
/// Set error bar cap width.
pub fn Series::withErrorCapWidth(self : Series, w : Double) -> Series {
{ ..self, errorCapWidth: w }
}
///|
/// Set whether to show outlier points for box plots.
pub fn Series::withShowOutliers(self : Series, show : Bool) -> Series {
{ ..self, showOutliers: show }
}
// ──── Legend position ────
///|
/// Position of the legend within the chart.
pub(all) enum Position {
TopLeft
TopCenter
TopRight
BottomLeft
BottomCenter
BottomRight
Left
Right
}
// ──── Margin (value type) ────
///|
/// Chart margin (space around the plot area).
#valtype
pub(all) struct Margin {
top : Double
right : Double
bottom : Double
left : Double
}
///|
/// Default margin: top=40, right=20, bottom=40, left=50.
pub fn Margin::default() -> Margin {
{ top: 40.0, right: 20.0, bottom: 40.0, left: 50.0 }
}
// ──── Top-level chart option ────
///|
/// Complete chart configuration.
pub struct ChartOption {
/// Canvas width in pixels. Default: 600.
width : Double
/// Canvas height in pixels. Default: 400.
height : Double
/// Margins around the plot area.
margin : Margin
/// Background color. Default: white.
background : Color
/// Chart title (shown centered at the top).
title : String?
/// Title font.
titleFont : Font
/// X-axis configuration.
xAxis : AxisOption
/// Y-axis configuration.
yAxis : AxisOption
/// Data series to plot.
series : Array[Series]
/// Whether to show the legend. Default: true when series > 1.
legend : Bool
/// Legend position. Default: TopRight.
legendPosition : Position
/// Legend font size. Default: 12.
legendFontSize : Double
/// Bar width ratio (0.0~1.0). Fraction of band width occupied by all bars. Default: 0.7.
barWidthRatio : Double
/// Bar gap ratio (0.0~1.0). Gap between bars in a group, as fraction of bar width. Default: 0.0.
barGap : Double
/// Global font family. Default: "sans-serif".
fontFamily : String
}
///|
/// Create a default ChartOption with given axes and series.
pub fn ChartOption::new(
xAxis : AxisOption,
yAxis : AxisOption,
series : Array[Series],
) -> ChartOption {
{
width: 600.0,
height: 400.0,
margin: Margin::default(),
background: Color::rgb(255, 255, 255),
title: None,
titleFont: Font::title(),
xAxis,
yAxis,
series,
legend: series.length() > 1,
legendPosition: TopRight,
legendFontSize: 12.0,
barWidthRatio: 0.7,
barGap: 0.0,
fontFamily: "sans-serif",
}
}