///|
/// One cell in a table artist.
pub struct TableCell {
row : Int32
column : Int32
text : String
facecolor : String
edgecolor : String
align : String
visible_edges : String
}
///|
/// A table laid out in normalized axes coordinates.
pub struct Table {
cells : Array[TableCell]
rows : Int32
columns : Int32
x : Double
y : Double
width : Double
height : Double
column_widths : Array[Double]
}
///|
pub fn TableCell::row(self : TableCell) -> Int32 {
self.row
}
///|
pub fn TableCell::column(self : TableCell) -> Int32 {
self.column
}
///|
pub fn TableCell::text(self : TableCell) -> String {
self.text
}
///|
pub fn TableCell::facecolor(self : TableCell) -> String {
self.facecolor
}
///|
pub fn TableCell::edgecolor(self : TableCell) -> String {
self.edgecolor
}
///|
pub fn TableCell::align(self : TableCell) -> String {
self.align
}
///|
pub fn TableCell::visible_edges(self : TableCell) -> String {
self.visible_edges
}
///|
pub fn Table::cells(self : Table) -> Array[TableCell] {
self.cells.copy()
}
///|
pub fn Table::rows(self : Table) -> Int32 {
self.rows
}
///|
pub fn Table::columns(self : Table) -> Int32 {
self.columns
}
///|
pub fn Table::x(self : Table) -> Double {
self.x
}
///|
pub fn Table::y(self : Table) -> Double {
self.y
}
///|
pub fn Table::width(self : Table) -> Double {
self.width
}
///|
pub fn Table::height(self : Table) -> Double {
self.height
}
///|
pub fn Table::column_widths(self : Table) -> Array[Double] {
self.column_widths.copy()
}
///|
pub fn Table::cell(self : Table, row : Int32, column : Int32) -> TableCell? {
for value in self.cells {
if value.row == row && value.column == column {
return Some(value)
}
}
None
}
///|
fn finite_table(value : Double) -> Bool {
!value.is_nan() && !value.is_inf()
}
///|
fn validate_table_alignment(
value : String,
name : String,
) -> Unit raise @core.DataError {
ignore(name)
if value != "left" && value != "center" && value != "right" {
raise @core.DataError::InvalidReference(
message="table {name} must be left, center, or right",
)
}
}
///|
fn validate_table_matrix(
matrix : Array[Array[String]],
name : String,
) -> Int32 raise @core.DataError {
ignore(name)
if matrix.length() == 0 {
return 0
}
let columns = matrix[0].length()
for row in matrix {
if row.length() != columns {
raise @core.DataError::InvalidReference(
message="table {name} rows must have equal length",
)
}
}
columns
}
///|
fn validate_table_colors(
colors : Array[Array[String]],
rows : Int32,
columns : Int32,
) -> Unit raise @core.DataError {
if colors.length() != 0 {
if colors.length() != rows {
raise @core.DataError::InvalidReference(
message="table cell colours must match text rows",
)
}
for row in colors {
if row.length() != columns {
raise @core.DataError::InvalidReference(
message="table cell colours must match text columns",
)
}
}
}
}
///|
fn table_edge_mode(value : String) -> Bool {
value == "closed" ||
value == "open" ||
value == "horizontal" ||
value == "vertical" ||
(
value.length() > 0 &&
value.to_array().all(c => c is ('B' | 'T' | 'L' | 'R'))
)
}
///|
fn table_color(
colors : Array[Array[String]],
row : Int32,
column : Int32,
fallback : String,
) -> String {
if colors.length() == 0 {
fallback
} else if colors[row][column] == "" {
fallback
} else {
colors[row][column]
}
}
///|
fn copy_table(value : Table) -> Table {
let cells = Array(capacity=value.cells.length())
for cell in value.cells {
cells.push(cell)
}
{
cells,
rows: value.rows,
columns: value.columns,
x: value.x,
y: value.y,
width: value.width,
height: value.height,
column_widths: value.column_widths.copy(),
}
}
///|
fn make_table(
cell_text : Array[Array[String]],
cell_colours : Array[Array[String]],
row_labels : Array[String],
row_colours : Array[String],
col_labels : Array[String],
col_colours : Array[String],
cell_loc : String,
row_loc : String,
col_loc : String,
loc : String,
bbox : Array[Double],
edges : String,
col_widths : Array[Double],
) -> Table raise @core.DataError {
validate_table_alignment(cell_loc, "cell_loc")
validate_table_alignment(row_loc, "row_loc")
validate_table_alignment(col_loc, "col_loc")
if !table_edge_mode(edges) {
raise @core.DataError::InvalidReference(
message="table edges must be closed, open, horizontal, vertical, or edge letters",
)
}
let text_columns = validate_table_matrix(cell_text, "cell_text")
let colour_columns = if cell_colours.length() == 0 {
text_columns
} else {
validate_table_matrix(cell_colours, "cell_colours")
}
let columns = Int32::max(text_columns, colour_columns)
let data_rows = Int32::max(cell_text.length(), cell_colours.length())
if cell_text.length() != 0 && cell_colours.length() != 0 {
validate_table_colors(cell_colours, data_rows, columns)
}
if row_colours.length() != 0 && row_colours.length() != row_labels.length() {
raise @core.DataError::InvalidReference(
message="table row colours must match row labels",
)
}
if col_colours.length() != 0 && col_colours.length() != columns {
raise @core.DataError::InvalidReference(
message="table column colours must match columns",
)
}
if col_labels.length() != 0 && col_labels.length() != columns {
raise @core.DataError::InvalidReference(
message="table column labels must match columns",
)
}
if row_labels.length() != 0 && row_labels.length() != data_rows {
raise @core.DataError::InvalidReference(
message="table row labels must match data rows",
)
}
let header_count = if col_labels.length() == 0 { 0 } else { 1 }
let row_label_count = if row_labels.length() == 0 { 0 } else { 1 }
let rows = data_rows + header_count
let total_columns = columns + row_label_count
if rows == 0 || total_columns == 0 {
raise @core.DataError::InvalidReference(
message="table needs at least one cell",
)
}
let actual_widths = if col_widths.length() == 0 {
let result = Array(capacity=total_columns)
for _ in 0.. (0.05, 1.0 - height - 0.02)
"center" | "centre" => (0.05, (1.0 - height) / 2.0)
"bottom" | "lower" => (0.05, 0.02)
_ =>
raise @core.DataError::InvalidReference(
message="table loc must be top, center, or bottom",
)
}
(position.0, position.1, 0.90, height)
} else {
if bbox.length() != 4 {
raise @core.DataError::InvalidReference(
message="table bbox must contain x, y, width, and height",
)
}
for value in bbox {
if !finite_table(value) {
raise @core.DataError::InvalidReference(
message="table bbox must be finite",
)
}
}
if bbox[2] <= 0.0 || bbox[3] <= 0.0 {
raise @core.DataError::InvalidReference(
message="table bbox size must be positive",
)
}
(bbox[0], bbox[1], bbox[2], bbox[3])
}
let cells = Array(capacity=rows * total_columns)
let header_rows = header_count
for row in 0.. Table raise @core.DataError {
self
.axes()
.table(
cell_text~,
cell_colours~,
cell_loc~,
col_widths~,
row_labels~,
row_colours~,
row_loc~,
col_labels~,
col_colours~,
col_loc~,
loc~,
bbox~,
edges~,
)
}
///|
/// Add a table through the axes view.
pub fn Axes::table(
self : Axes,
cell_text~ : Array[Array[String]] = [],
cell_colours~ : Array[Array[String]] = [],
cell_loc~ : String = "right",
col_widths~ : Array[Double] = [],
row_labels~ : Array[String] = [],
row_colours~ : Array[String] = [],
row_loc~ : String = "left",
col_labels~ : Array[String] = [],
col_colours~ : Array[String] = [],
col_loc~ : String = "center",
loc~ : String = "bottom",
bbox~ : Array[Double] = [],
edges~ : String = "closed",
) -> Table raise @core.DataError {
let result = make_table(
cell_text, cell_colours, row_labels, row_colours, col_labels, col_colours, cell_loc,
row_loc, col_loc, loc, bbox, edges, col_widths,
)
self.push_artist(Artist::Table(copy_table(result)))
result
}