///|
/// An annotation whose anchor and text may use display-relative coordinates.
pub struct Annotation {
x : Double
y : Double
text_x : Double
text_y : Double
value : String
xycoords : String
textcoords : String
connector : String
arrow : Bool
arrowcolor : String
linewidth : Double
head_length : Double
head_width : Double
style : @core.Style
}
///|
fn annotation(
x : Double,
y : Double,
text_x : Double,
text_y : Double,
value : String,
xycoords : String,
textcoords : String,
connector : String,
arrow : Bool,
arrowcolor : String,
linewidth : Double,
head_length : Double,
head_width : Double,
style : @core.Style,
) -> Annotation {
{
x,
y,
text_x,
text_y,
value,
xycoords,
textcoords,
connector,
arrow,
arrowcolor,
linewidth,
head_length,
head_width,
style,
}
}
///|
fn annotation_coordinates_supported(value : String) -> Bool {
value == "data" ||
value == "axes fraction" ||
value == "figure fraction" ||
value == "display" ||
value == "offset points" ||
value == "offset pixels"
}
///|
fn annotation_connector_supported(value : String) -> Bool {
value == "straight" || value == "arc3" || value == "angle"
}
///|
fn validate_annotation_number(
value : Double,
axis : String,
) -> Unit raise @core.DataError {
if value.is_nan() || value.is_inf() {
raise @core.DataError::InvalidAnnotation(
message="annotation coordinates must be finite on \{axis}",
)
}
}
///|
fn validate_annotation_style(
linewidth : Double,
head_length : Double,
head_width : Double,
) -> Unit raise @core.DataError {
if linewidth.is_nan() || linewidth.is_inf() || linewidth < 0.0 {
raise @core.DataError::InvalidAnnotation(
message="annotation linewidth must be finite and non-negative",
)
}
if head_length.is_nan() || head_length.is_inf() || head_length < 0.0 {
raise @core.DataError::InvalidAnnotation(
message="arrow head length must be finite and non-negative",
)
}
if head_width.is_nan() || head_width.is_inf() || head_width < 0.0 {
raise @core.DataError::InvalidAnnotation(
message="arrow head width must be finite and non-negative",
)
}
}
///|
fn annotation_arrow_artists(
axes : Axes,
x : Double,
y : Double,
dx : Double,
dy : Double,
color : String,
linewidth : Double,
head_length : Double,
head_width : Double,
) -> Array[Artist] {
let result : Array[Artist] = []
let start = @core.point(x, y)
let end = @core.point(x + dx, y + dy)
let magnitude = (dx * dx + dy * dy).sqrt()
let line_style = @core.style(color, width=linewidth)
if magnitude == 0.0 {
return result
}
let effective_head_length = Double::min(head_length, magnitude)
let unit_x = dx / magnitude
let unit_y = dy / magnitude
let base_x = end.x() - unit_x * effective_head_length
let base_y = end.y() - unit_y * effective_head_length
let base = @core.point(base_x, base_y)
if effective_head_length == 0.0 || head_width == 0.0 {
let artist = Artist::Line([start, end], line_style)
axes.push_artist(artist)
result.push(artist)
} else {
let half_width = head_width / 2.0
let perpendicular_x = -unit_y
let perpendicular_y = unit_x
let left = @core.point(
base_x + perpendicular_x * half_width,
base_y + perpendicular_y * half_width,
)
let right = @core.point(
base_x - perpendicular_x * half_width,
base_y - perpendicular_y * half_width,
)
let line = Artist::Line([start, base], line_style)
let polygon = Artist::Polygon(
[left, end, right],
@core.style(color, width=linewidth, fill=color),
)
axes.push_artist(line)
axes.push_artist(polygon)
result.push(line)
result.push(polygon)
}
result
}
///|
/// Draw one data-coordinate arrow from `(x, y)` by `(dx, dy)`.
pub fn Figure::arrow(
self : Figure,
x : Double,
y : Double,
dx : Double,
dy : Double,
color~ : String = "",
linewidth~ : Double = 1.0,
head_length~ : Double = 0.15,
head_width~ : Double = 0.10,
label~ : String = "",
) -> Unit raise @core.DataError {
self
.axes()
.arrow(x, y, dx, dy, color~, linewidth~, head_length~, head_width~, label~)
}
///|
/// Draw one data-coordinate arrow from `(x, y)` by `(dx, dy)`.
pub fn Axes::arrow(
self : Axes,
x : Double,
y : Double,
dx : Double,
dy : Double,
color~ : String = "",
linewidth~ : Double = 1.0,
head_length~ : Double = 0.15,
head_width~ : Double = 0.10,
label~ : String = "",
) -> Unit raise @core.DataError {
validate_annotation_number(x, "x")
validate_annotation_number(y, "y")
validate_annotation_number(dx, "dx")
validate_annotation_number(dy, "dy")
validate_annotation_style(linewidth, head_length, head_width)
let resolved_color = self.resolve_color(color)
ignore(
annotation_arrow_artists(
self, x, y, dx, dy, resolved_color, linewidth, head_length, head_width,
),
)
self.add_auto_legend(label, resolved_color, LegendKind::Line)
}
///|
/// Add text at a data point, optionally connected by an arrow.
///
/// `offset_x` and `offset_y` place the text relative to `(x, y)` in data
/// coordinates. When `arrow` is true, the arrow points from the text to the
/// annotated data point.
pub fn Figure::annotate(
self : Figure,
value : String,
x : Double,
y : Double,
offset_x~ : Double = 0.0,
offset_y~ : Double = 0.0,
arrow~ : Bool = false,
color~ : String = "#111827",
arrowcolor~ : String = "",
linewidth~ : Double = 1.0,
head_length~ : Double = 0.15,
head_width~ : Double = 0.10,
fontsize~ : Int32 = 12,
xycoords~ : String = "data",
textcoords~ : String = "data",
connector~ : String = "straight",
) -> Unit raise @core.DataError {
self
.axes()
.annotate(
value,
x,
y,
offset_x~,
offset_y~,
arrow~,
color~,
arrowcolor~,
linewidth~,
head_length~,
head_width~,
fontsize~,
xycoords~,
textcoords~,
connector~,
)
}
///|
/// Add text at a data point, optionally connected by an arrow.
pub fn Axes::annotate(
self : Axes,
value : String,
x : Double,
y : Double,
offset_x~ : Double = 0.0,
offset_y~ : Double = 0.0,
arrow~ : Bool = false,
color~ : String = "#111827",
arrowcolor~ : String = "",
linewidth~ : Double = 1.0,
head_length~ : Double = 0.15,
head_width~ : Double = 0.10,
fontsize~ : Int32 = 12,
xycoords~ : String = "data",
textcoords~ : String = "data",
connector~ : String = "straight",
) -> Unit raise @core.DataError {
validate_annotation_number(x, "x")
validate_annotation_number(y, "y")
validate_annotation_number(offset_x, "offset_x")
validate_annotation_number(offset_y, "offset_y")
validate_annotation_style(linewidth, head_length, head_width)
if fontsize < 1 {
raise @core.DataError::InvalidAnnotation(
message="annotation fontsize must be positive",
)
}
if !annotation_coordinates_supported(xycoords) {
raise @core.DataError::InvalidAnnotation(
message="unsupported annotation xycoords",
)
}
if !annotation_coordinates_supported(textcoords) {
raise @core.DataError::InvalidAnnotation(
message="unsupported annotation textcoords",
)
}
if xycoords == "offset points" || xycoords == "offset pixels" {
raise @core.DataError::InvalidAnnotation(
message="annotation xycoords cannot be an offset mode without a base",
)
}
if !annotation_connector_supported(connector) {
raise @core.DataError::InvalidAnnotation(
message="annotation connector must be straight, arc3, or angle",
)
}
let text_x = if textcoords == "offset points" || textcoords == "offset pixels" {
offset_x
} else {
x + offset_x
}
let text_y = if textcoords == "offset points" {
offset_y
} else {
y + offset_y
}
validate_annotation_number(text_x, "text_x")
validate_annotation_number(text_y, "text_y")
if xycoords == "data" && textcoords == "data" && connector == "straight" {
self.push_artist(
Artist::Text(
text_x,
text_y,
value,
@core.style(color, font_size=Double::from_int(fontsize)),
),
)
if arrow {
let resolved_arrowcolor = self.resolve_color(arrowcolor)
ignore(
annotation_arrow_artists(
self,
text_x,
text_y,
x - text_x,
y - text_y,
resolved_arrowcolor,
linewidth,
head_length,
head_width,
),
)
}
} else {
let resolved_arrowcolor = if arrow {
self.resolve_color(arrowcolor)
} else {
""
}
self.push_artist(
Artist::Annotation(
annotation(
x,
y,
text_x,
text_y,
value,
xycoords,
textcoords,
connector,
arrow,
resolved_arrowcolor,
linewidth,
head_length,
head_width,
@core.style(color, font_size=Double::from_int(fontsize)),
),
),
)
}
}