///|
fn valid_point(point : HitPoint) -> Bool {
finite(point.screen_x) &&
finite(point.screen_y) &&
finite(point.data_x) &&
finite(point.data_y)
}
///|
fn valid_info(info : HitInfo) -> Bool {
valid_point(info.point) &&
finite(info.value) &&
(match info.interval {
Some((start, end)) => finite(start) && finite(end)
None => true
}) &&
(match info.fraction {
Some(value) => finite(value)
None => true
}) &&
(match info.cell {
Some(cell) =>
cell.row >= 0 &&
cell.column >= 0 &&
finite(cell.x_range.0) &&
finite(cell.x_range.1) &&
finite(cell.y_range.0) &&
finite(cell.y_range.1) &&
cell.x_range.0 != cell.x_range.1 &&
cell.y_range.0 != cell.y_range.1
None => true
}) &&
(match info.error {
Some(error) =>
finite(error.x_range.0) &&
finite(error.x_range.1) &&
finite(error.y_range.0) &&
finite(error.y_range.1) &&
error.x_range.0 <= error.x_range.1 &&
error.y_range.0 <= error.y_range.1
None => true
}) &&
(match info.band {
Some(band) => finite(band.lower) && finite(band.upper)
None => true
}) &&
(match info.box {
Some(box) =>
box.group_index >= 0 &&
finite(box.lower_whisker) &&
finite(box.q1) &&
finite(box.median) &&
finite(box.q3) &&
finite(box.upper_whisker) &&
box.lower_whisker <= box.q1 &&
box.q1 <= box.median &&
box.median <= box.q3 &&
box.q3 <= box.upper_whisker &&
(match box.part {
Outlier(value) => finite(value)
_ => true
})
None => true
})
}
///|
fn valid_rect(rect : (Double, Double, Double, Double)) -> Bool {
let (left, top, width, height) = rect
finite(left) &&
finite(top) &&
finite(width) &&
finite(height) &&
width > 0.0 &&
height > 0.0 &&
finite(left + width) &&
finite(top + height)
}
///|
fn rect_contains(
rect : (Double, Double, Double, Double),
x : Double,
y : Double,
) -> Bool {
let (left, top, width, height) = rect
valid_rect(rect) &&
x >= left &&
y >= top &&
x <= left + width &&
y <= top + height
}
///|
// An overlay belongs to its panel; it must not mask another overlapping axes.
fn Drawing::excluded(
self : Drawing,
panel_index : Int32,
x : Double,
y : Double,
) -> Bool {
for (panel, rectangle) in self.exclusions {
if panel == panel_index && rect_contains(rectangle, x, y) {
return true
}
}
false
}
///|
fn valid_shape(shape : HitShape) -> Bool {
match shape {
Point => true
Segment(x0, y0, x1, y1) =>
finite(x0) && finite(y0) && finite(x1) && finite(y1)
Quadrilateral(ax, ay, bx, by, cx, cy, dx, dy) =>
valid_quad((ax, ay), (bx, by), (cx, cy), (dx, dy))
Rectangle(left, top, width, height) =>
valid_rect((left, top, width, height))
Sector(cx, cy, radius, start, sweep) =>
finite(cx) &&
finite(cy) &&
finite(radius) &&
radius > 0.0 &&
finite(start) &&
finite(sweep) &&
sweep > 0.0
AnnularSector(cx, cy, inner, outer, start, sweep) =>
finite(cx) &&
finite(cy) &&
finite(inner) &&
finite(outer) &&
inner >= 0.0 &&
outer > inner &&
finite(start) &&
finite(sweep) &&
sweep > 0.0
}
}
///|
fn degrees_normalized(angle : Double) -> Double {
let wrapped = angle % 360.0
if wrapped < 0.0 {
wrapped + 360.0
} else {
wrapped
}
}
///|
fn shape_contains(shape : HitShape, x : Double, y : Double) -> Bool {
match shape {
Point | Segment(_, _, _, _) => false
Quadrilateral(ax, ay, bx, by, cx, cy, dx, dy) =>
quad_contains((ax, ay), (bx, by), (cx, cy), (dx, dy), x, y)
Rectangle(left, top, width, height) =>
rect_contains((left, top, width, height), x, y)
Sector(cx, cy, radius, start, sweep) => {
let dx = x - cx
let dy = cy - y
if !finite(dx) || !finite(dy) || hit_distance(dx, dy) > radius {
return false
}
// The center belongs to every positive sweep and no angle is needed.
if (dx == 0.0 && dy == 0.0) || sweep >= 360.0 {
return true
}
let angle = @math.atan2(dy, dx) * 180.0 / @math.PI
degrees_normalized(degrees_normalized(angle) - degrees_normalized(start)) <=
sweep
}
AnnularSector(cx, cy, inner, outer, start, sweep) => {
let dx = x - cx
let dy = cy - y
let distance = hit_distance(dx, dy)
if !finite(dx) || !finite(dy) || distance < inner || distance > outer {
return false
}
if (dx == 0.0 && dy == 0.0) || sweep >= 360.0 {
return true
}
let angle = @math.atan2(dy, dx) * 180.0 / @math.PI
let offset = degrees_normalized(
degrees_normalized(angle) - degrees_normalized(start),
)
// atan2 and the degree conversion can put an exact edge a few rounding
// steps outside its interval, wrapping a tiny negative offset to 360.
// Bound this allowance by four binary64 epsilons over one revolution
// (about 3.2e-13 degrees), rather than a visible angular hit radius.
let roundoff = 4.0 * 360.0 * 2.220446049250313e-16
offset <= sweep + roundoff || 360.0 - offset <= roundoff
}
}
}
///|
fn Drawing::target_hit(
self : Drawing,
x : Double,
y : Double,
radius : Double,
scale : Double,
mode : HitMode,
) -> HitPoint? {
if self.targets.is_empty() {
return self.nearest(x, y, radius, scale)
}
if self.width <= 0 ||
self.height <= 0 ||
!finite(x) ||
!finite(y) ||
x < 0.0 ||
y < 0.0 ||
x > self.width.to_double() ||
y > self.height.to_double() ||
!finite(radius) ||
radius < 0.0 ||
!finite(scale) ||
scale <= 0.0 {
return None
}
// The last painted area wins, except for actually hit statistics above it.
for index = self.targets.length() - 1; index >= 0; index = index - 1 {
let target = self.targets[index]
if valid_info(target.info) &&
!self.excluded(target.info.point.panel_index, x, y) &&
valid_shape(target.shape) &&
rect_contains(target.bounds, x, y) &&
shape_contains(target.shape, x, y) {
return match
self.statistical_overlay_hit(index, x, y, radius, scale, mode) {
Some(point) => Some(point)
None => Some(target.info.point)
}
}
}
let mut best : HitPoint? = None
let mut best_distance = radius
for target in self.targets {
if self.excluded(target.info.point.panel_index, x, y) ||
(
mode == XNearest &&
(target.info.kind == Line || target.info.kind == ErrorBar)
) ||
!valid_info(target.info) ||
!valid_shape(target.shape) ||
!rect_contains(target.bounds, x, y) {
continue
}
let point = target.info.point
guard target_distance(target, x, y, scale) is Some(distance) else {
continue
}
if distance <= best_distance && (best is None || distance < best_distance) {
best = Some(point)
best_distance = distance
}
}
if best is Some(_) || mode == Nearest {
return best
}
let mut best_dx = 0.0
let mut best_dy = 0.0
for target in self.targets {
if self.excluded(target.info.point.panel_index, x, y) ||
(target.info.kind != Line && target.info.kind != ErrorBar) ||
target.shape != Point ||
!valid_info(target.info) ||
!rect_contains(target.bounds, x, y) ||
!rect_contains(
target.bounds,
target.info.point.screen_x,
target.info.point.screen_y,
) {
continue
}
let point = target.info.point
let dx = (point.screen_x - x).abs()
let dy = (point.screen_y - y).abs()
if !finite(dx) || !finite(dy) {
continue
}
if best is None || dx < best_dx || (dx == best_dx && dy < best_dy) {
best = Some(point)
best_dx = dx
best_dy = dy
}
}
best
}