///|
/// Bins returned by `hexbin`.
pub struct Hexbin {
centers : Array[@core.Point]
values : Array[Double]
counts : Array[Int32]
}
///|
/// A user supplied reducer for values collected in one hexbin cell.
pub struct HexbinReducer {
reducer : (Array[Double]) -> Double
}
///|
/// Wrap a callable reducer for use with `hexbin(reduce_fn=...)`.
pub fn hexbin_reducer(reducer : (Array[Double]) -> Double) -> HexbinReducer {
{ reducer, }
}
///|
pub fn HexbinReducer::apply(
self : HexbinReducer,
samples : Array[Double],
) -> Double {
(self.reducer)(samples)
}
///|
pub fn Hexbin::centers(self : Hexbin) -> Array[@core.Point] {
self.centers.copy()
}
///|
pub fn Hexbin::values(self : Hexbin) -> Array[Double] {
self.values.copy()
}
///|
pub fn Hexbin::counts(self : Hexbin) -> Array[Int32] {
self.counts.copy()
}
///|
priv struct HexbinCell {
col : Int32
row : Int32
mut count : Int32
mut sum : Double
samples : Array[Double]
}
///|
fn validate_hexbin_reduce(reduce : String) -> Unit raise @core.DataError {
if reduce != "mean" &&
reduce != "sum" &&
reduce != "min" &&
reduce != "max" &&
reduce != "count" {
raise @core.DataError::InvalidStatistics(
message="hexbin reduce must be mean, sum, min, max, or count",
)
}
}
///|
fn validate_hexbin_bins(bins : String) -> Unit raise @core.DataError {
if bins != "linear" && bins != "log" {
raise @core.DataError::InvalidStatistics(
message="hexbin bins must be linear or log",
)
}
}
///|
fn hexbin_color_norm(
bins : String,
vmin : Double?,
vmax : Double?,
) -> @colors.ColorNorm raise @core.DataError {
if bins == "linear" {
return linear_color_norm(vmin, vmax)
}
let norm = try {
match (vmin, vmax) {
(Some(lower), Some(upper)) => @colors.log_norm(vmin=lower, vmax=upper)
(Some(lower), None) => @colors.log_norm(vmin=lower)
(None, Some(upper)) => @colors.log_norm(vmax=upper)
(None, None) => @colors.log_norm()
}
} catch {
@colors.NormalizeError::InvalidBounds(message~)
| @colors.NormalizeError::InvalidBase(message~)
| @colors.NormalizeError::InvalidValue(message~) =>
raise @core.DataError::InvalidStatistics(message~)
@colors.NormalizeError::NoFiniteData
| @colors.NormalizeError::NotScaled
| @colors.NormalizeError::NotInvertible =>
raise @core.DataError::InvalidStatistics(
message="invalid hexbin logarithmic normalization",
)
}
@colors.log_color_norm(norm)
}
///|
fn reduce_hexbin_cell(
cell : HexbinCell,
has_values : Bool,
reduce : String,
reduce_fn : HexbinReducer?,
) -> Double raise @core.DataError {
match reduce_fn {
Some(reducer) => {
if !has_values {
raise @core.DataError::InvalidStatistics(
message="hexbin reduce_fn requires values",
)
}
let result = reducer.apply(cell.samples.copy())
if !hexbin_finite(result) {
raise @core.DataError::InvalidStatistics(
message="hexbin reduce_fn must return a finite value",
)
}
result
}
None =>
if !has_values || reduce == "count" {
Double::from_int(cell.count)
} else if reduce == "sum" {
cell.sum
} else if reduce == "min" {
let mut result = cell.samples[0]
for value in cell.samples {
result = Double::min(result, value)
}
result
} else if reduce == "max" {
let mut result = cell.samples[0]
for value in cell.samples {
result = Double::max(result, value)
}
result
} else {
cell.sum / Double::from_int(cell.count)
}
}
}
///|
fn hexbin_finite(value : Double) -> Bool {
!value.is_nan() && !value.is_inf()
}
///|
fn hexbin_vertices(
center_x : Double,
center_y : Double,
radius : Double,
x_scale : @scale.Scale,
y_scale : @scale.Scale,
) -> Array[@core.Point] {
let result = Array(capacity=6)
for side in 0..<6 {
let angle = @math.PI / 6.0 + Double::from_int(side) * @math.PI / 3.0
result.push(
@core.point(
x_scale.inverse(center_x + radius * @math.cos(angle)),
y_scale.inverse(center_y + radius * @math.sin(angle)),
),
)
}
result
}
///|
fn hexbin_scale_values(
xs : Array[Double],
ys : Array[Double],
x_scale : @scale.Scale,
y_scale : @scale.Scale,
) -> (Array[Double], Array[Double]) raise @core.DataError {
let scaled_xs = Array(capacity=xs.length())
let scaled_ys = Array(capacity=ys.length())
for index in 0.. Int32? {
for index in 0.. (Double, Double, Double, Double) raise @core.DataError {
if xs.length() == 0 {
raise @core.DataError::InvalidStatistics(
message="hexbin requires at least one point",
)
}
let mut xmin = 0.0
let mut xmax = 0.0
let mut ymin = 0.0
let mut ymax = 0.0
let mut found = false
for index in 0.. (Double, Double, Double, Double)? raise @core.DataError {
if extent.length() == 0 {
return None
}
if extent.length() != 4 {
raise @core.DataError::InvalidStatistics(
message="hexbin extent must contain xmin, xmax, ymin, and ymax",
)
}
let xmin = extent[0]
let xmax = extent[1]
let ymin = extent[2]
let ymax = extent[3]
if !hexbin_finite(xmin) ||
!hexbin_finite(xmax) ||
!hexbin_finite(ymin) ||
!hexbin_finite(ymax) {
raise @core.DataError::InvalidStatistics(
message="hexbin extent must be finite",
)
}
if !x_scale.is_valid(xmin) ||
!x_scale.is_valid(xmax) ||
!y_scale.is_valid(ymin) ||
!y_scale.is_valid(ymax) {
raise @core.DataError::InvalidStatistics(
message="hexbin extent must be valid for the axis scale",
)
}
let first_x = x_scale.transform(xmin)
let second_x = x_scale.transform(xmax)
let first_y = y_scale.transform(ymin)
let second_y = y_scale.transform(ymax)
if first_x == second_x || first_y == second_y {
raise @core.DataError::InvalidStatistics(
message="hexbin extent must span both axes",
)
}
Some(
(
Double::min(first_x, second_x),
Double::max(first_x, second_x),
Double::min(first_y, second_y),
Double::max(first_y, second_y),
),
)
}
///|
fn hexbin_rectangle(
xmin : Double,
xmax : Double,
ymin : Double,
ymax : Double,
x_scale : @scale.Scale,
y_scale : @scale.Scale,
) -> Array[@core.Point] {
[
@core.point(x_scale.inverse(xmin), y_scale.inverse(ymin)),
@core.point(x_scale.inverse(xmax), y_scale.inverse(ymin)),
@core.point(x_scale.inverse(xmax), y_scale.inverse(ymax)),
@core.point(x_scale.inverse(xmin), y_scale.inverse(ymax)),
]
}
///|
fn hexbin_column_count(cells : Array[HexbinCell], column : Int32) -> Int32 {
let mut result = 0
for cell in cells {
if cell.col == column {
result += cell.count
}
}
result
}
///|
fn hexbin_row_count(cells : Array[HexbinCell], row : Int32) -> Int32 {
let mut result = 0
for cell in cells {
if cell.row == row {
result += cell.count
}
}
result
}
///|
fn hexbin_max_row(cells : Array[HexbinCell]) -> Int32 {
let mut result = -1
for cell in cells {
result = Int32::max(result, cell.row)
}
result
}
///|
/// Bin points into a regular hexagonal grid and add filled polygons.
pub fn Figure::hexbin(
self : Figure,
xs : Array[Double],
ys : Array[Double],
values~ : Array[Double] = [],
gridsize~ : Int32 = 20,
mincnt~ : Int32 = 0,
cmap~ : String = "viridis",
vmin? : Double,
vmax? : Double,
color~ : String = "",
label~ : String = "",
reduce~ : String = "mean",
edgecolor~ : String = "",
linewidth~ : Double = 0.0,
bins~ : String = "linear",
extent~ : Array[Double] = [],
marginals~ : Bool = false,
reduce_fn? : HexbinReducer,
) -> Hexbin raise @core.DataError {
match (vmin, vmax) {
(Some(vmin), Some(vmax)) =>
self
.axes()
.hexbin(
xs,
ys,
values~,
gridsize~,
mincnt~,
cmap~,
vmin~,
vmax~,
color~,
label~,
reduce~,
edgecolor~,
linewidth~,
bins~,
extent~,
marginals~,
reduce_fn~,
)
(Some(vmin), None) =>
self
.axes()
.hexbin(
xs,
ys,
values~,
gridsize~,
mincnt~,
cmap~,
vmin~,
color~,
label~,
reduce~,
edgecolor~,
linewidth~,
bins~,
extent~,
marginals~,
reduce_fn~,
)
(None, Some(vmax)) =>
self
.axes()
.hexbin(
xs,
ys,
values~,
gridsize~,
mincnt~,
cmap~,
vmax~,
color~,
label~,
reduce~,
edgecolor~,
linewidth~,
bins~,
extent~,
marginals~,
reduce_fn~,
)
(None, None) =>
self
.axes()
.hexbin(
xs,
ys,
values~,
gridsize~,
mincnt~,
cmap~,
color~,
label~,
reduce~,
edgecolor~,
linewidth~,
bins~,
extent~,
marginals~,
reduce_fn~,
)
}
}
///|
/// Bin points into a regular hexagonal grid and add filled polygons.
pub fn Axes::hexbin(
self : Axes,
xs : Array[Double],
ys : Array[Double],
values~ : Array[Double] = [],
gridsize~ : Int32 = 20,
mincnt~ : Int32 = 0,
cmap~ : String = "viridis",
vmin? : Double,
vmax? : Double,
color~ : String = "",
label~ : String = "",
reduce~ : String = "mean",
edgecolor~ : String = "",
linewidth~ : Double = 0.0,
bins~ : String = "linear",
extent~ : Array[Double] = [],
marginals~ : Bool = false,
reduce_fn~ : HexbinReducer? = None,
) -> Hexbin raise @core.DataError {
if xs.length() != ys.length() {
raise @core.DataError::LengthMismatch(
expected=xs.length(),
actual=ys.length(),
)
}
if values.length() != 0 && values.length() != xs.length() {
raise @core.DataError::LengthMismatch(
expected=xs.length(),
actual=values.length(),
)
}
if gridsize < 1 {
raise @core.DataError::InvalidStatistics(
message="hexbin gridsize must be positive",
)
}
if mincnt < 0 {
raise @core.DataError::InvalidStatistics(
message="hexbin mincnt must be non-negative",
)
}
if linewidth.is_nan() || linewidth.is_inf() || linewidth < 0.0 {
raise @core.DataError::InvalidStatistics(
message="hexbin linewidth must be finite and non-negative",
)
}
validate_hexbin_reduce(reduce)
validate_hexbin_bins(bins)
if reduce_fn is Some(_) && values.length() == 0 {
raise @core.DataError::InvalidStatistics(
message="hexbin reduce_fn requires values",
)
}
for value in values {
if !hexbin_finite(value) {
raise @core.DataError::InvalidStatistics(
message="hexbin values must be finite",
)
}
}
let x_scale = self.x_scale()
let y_scale = self.y_scale()
let (scaled_xs, scaled_ys) = hexbin_scale_values(xs, ys, x_scale, y_scale)
let (xmin, xmax, ymin, ymax) = match
hexbin_extent_bounds(extent, x_scale, y_scale) {
Some(bounds) => bounds
None => hexbin_bounds(scaled_xs, scaled_ys)
}
let diameter = (xmax - xmin) / Double::from_int(gridsize)
let radius = diameter / 2.0
let vertical_step = radius * 1.5
let cells : Array[HexbinCell] = []
let binned_xs : Array[Double] = []
let binned_ys : Array[Double] = []
let binned_indices : Array[Int32] = []
for index in 0.. xmax ||
scaled_ys[index] < ymin ||
scaled_ys[index] > ymax {
continue
}
binned_xs.push(scaled_xs[index])
binned_ys.push(scaled_ys[index])
binned_indices.push(index)
}
for index in 0.. {
cells[cell].count += 1
if values.length() > 0 {
cells[cell].sum += values[source_index]
cells[cell].samples.push(values[source_index])
}
}
None =>
cells.push({
col: column,
row,
count: 1,
sum: if values.length() > 0 {
values[source_index]
} else {
0.0
},
samples: if values.length() > 0 {
[values[source_index]]
} else {
[]
},
})
}
}
let centers : Array[@core.Point] = []
let bin_values : Array[Double] = []
let counts : Array[Int32] = []
for cell in cells {
if cell.count >= mincnt {
let center_x = xmin + radius + Double::from_int(cell.col) * diameter
let offset = if cell.col % 2 == 0 { 0.0 } else { vertical_step / 2.0 }
let center_y = ymin +
radius * 1.5 +
Double::from_int(cell.row) * vertical_step +
offset
let value = reduce_hexbin_cell(
cell,
values.length() > 0,
reduce,
reduce_fn,
)
centers.push(
@core.point(x_scale.inverse(center_x), y_scale.inverse(center_y)),
)
bin_values.push(value)
counts.push(cell.count)
}
}
if bins == "log" {
for value in bin_values {
if value <= 0.0 {
raise @core.DataError::InvalidStatistics(
message="hexbin logarithmic bins require positive values",
)
}
}
}
let mut legend_color = color
let fill_color = if color == "" {
let color_map = resolve_colormap(cmap)
let norm = hexbin_color_norm(bins, vmin, vmax)
norm.autoscale_none(bin_values)
let style_values = Array(capacity=bin_values.length())
for value in bin_values {
if norm.is_valid(value) {
style_values.push(
color_map.sample_extended(mesh_transform(norm, value)).to_css(),
)
} else {
style_values.push("#000000")
}
}
if style_values.length() > 0 {
legend_color = style_values[0]
} else {
legend_color = color_map.sample_extended(0.5).to_css()
}
style_values
} else {
let style_values = Array(capacity=bin_values.length())
for _ in bin_values {
style_values.push(color)
}
style_values
}
for index in 0.. 0 {
let mut maximum_count = 1
for cell in cells {
maximum_count = Int32::max(maximum_count, cell.count)
}
let marginal_height = Double::min((ymax - ymin) * 0.2, vertical_step * 0.75)
let marginal_width = Double::min((xmax - xmin) * 0.2, diameter * 0.75)
let marginal_color = if color == "" { legend_color } else { color }
let marginal_style = @core.style(
if edgecolor == "" {
marginal_color
} else {
edgecolor
},
width=linewidth,
fill=marginal_color,
)
for column in 0.. 0 {
let lower = xmin + Double::from_int(column) * diameter
let upper = lower + diameter
let height = marginal_height *
Double::from_int(count) /
Double::from_int(maximum_count)
self.push_artist(
Artist::Polygon(
hexbin_rectangle(
lower,
upper,
ymin,
ymin + height,
x_scale,
y_scale,
),
marginal_style,
),
)
}
}
let last_row = hexbin_max_row(cells)
for row in 0..<=last_row {
let count = hexbin_row_count(cells, row)
if count > 0 {
let lower = ymin + Double::from_int(row) * vertical_step
let upper = lower + vertical_step
let width = marginal_width *
Double::from_int(count) /
Double::from_int(maximum_count)
self.push_artist(
Artist::Polygon(
hexbin_rectangle(xmax - width, xmax, lower, upper, x_scale, y_scale),
marginal_style,
),
)
}
}
}
self.add_auto_legend(label, legend_color, LegendKind::Patch)
{ centers, values: bin_values, counts, }
}