mirror of
https://github.com/rio-labs/rio.git
synced 2026-01-08 14:19:47 -06:00
WIP: new table implementation
This commit is contained in:
@@ -2,128 +2,31 @@ import { markEventAsHandled } from '../eventHandling';
|
||||
import { ComponentBase, ComponentState } from './componentBase';
|
||||
|
||||
type TableValue = number | string;
|
||||
type DataFromBackend = { [label: string]: TableValue[] } | TableValue[][];
|
||||
|
||||
type TableDeltaState = ComponentState & {
|
||||
type TableStyle = {
|
||||
left: number;
|
||||
top: number | 'header';
|
||||
width: number;
|
||||
height: number;
|
||||
|
||||
fontWeight?: 'normal' | 'bold';
|
||||
};
|
||||
|
||||
type TableState = ComponentState & {
|
||||
_type_: 'Table-builtin';
|
||||
data?: DataFromBackend;
|
||||
show_row_numbers?: boolean;
|
||||
headers?: string[] | null;
|
||||
data?: TableValue[][];
|
||||
styling?: TableStyle[];
|
||||
};
|
||||
|
||||
// We can receive data either as an object or as a 2d array, but we store it
|
||||
// as an array of Columns
|
||||
type TableState = Omit<Required<TableDeltaState>, 'data'> & {
|
||||
data: TableColumn[];
|
||||
};
|
||||
|
||||
class TableColumn {
|
||||
public name: string;
|
||||
public dataType: 'number' | 'text' | 'empty';
|
||||
public alignment: string;
|
||||
public values: TableValue[];
|
||||
|
||||
constructor(name: string, values: TableValue[]) {
|
||||
this.name = name;
|
||||
this.values = values;
|
||||
this.dataType = this._determineDataType(values);
|
||||
this.alignment = this.dataType === 'number' ? 'right' : 'left';
|
||||
}
|
||||
|
||||
private _determineDataType(
|
||||
values: TableValue[]
|
||||
): 'number' | 'text' | 'empty' {
|
||||
if (values.length === 0) {
|
||||
return 'empty';
|
||||
}
|
||||
|
||||
if (typeof values[0] === 'number') {
|
||||
return 'number';
|
||||
}
|
||||
|
||||
return 'text';
|
||||
}
|
||||
}
|
||||
|
||||
function dataToColumns(data: DataFromBackend): TableColumn[] {
|
||||
let columns: TableColumn[] = [];
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
let numColumns = data.length === 0 ? 0 : data[0].length;
|
||||
|
||||
for (let i = 0; i < numColumns; i++) {
|
||||
let values = data.map((row) => row[i]);
|
||||
columns.push(new TableColumn('', values));
|
||||
}
|
||||
} else {
|
||||
for (let [name, values] of Object.entries(data)) {
|
||||
columns.push(new TableColumn(name, values));
|
||||
}
|
||||
}
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
class SortOrder {
|
||||
private sortOrder: [string, number][] = [];
|
||||
|
||||
add(columnName: string, ascending: boolean): void {
|
||||
this.sortOrder = this.sortOrder.filter((it) => it[0] !== columnName);
|
||||
this.sortOrder.unshift([columnName, ascending ? 1 : -1]);
|
||||
}
|
||||
|
||||
sort(columns: TableColumn[]): void {
|
||||
if (columns.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let valuesByColumnName: { [columnName: string]: TableValue[] } = {};
|
||||
for (let column of columns) {
|
||||
valuesByColumnName[column.name] = column.values;
|
||||
}
|
||||
|
||||
// Perform an argsort
|
||||
function cmp(i: number, j: number): number {
|
||||
for (let [columnName, multiplier] of this.sortOrder) {
|
||||
let values = valuesByColumnName[columnName];
|
||||
if (values === undefined) {
|
||||
// The table's contents must've changed and no longer have a
|
||||
// column with this name
|
||||
continue;
|
||||
}
|
||||
|
||||
let a = values[i];
|
||||
let b = values[j];
|
||||
|
||||
if (a < b) {
|
||||
return -1 * multiplier;
|
||||
} else if (a > b) {
|
||||
return 1 * multiplier;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
let indices = [...columns[0].values.keys()];
|
||||
indices.sort(cmp.bind(this));
|
||||
|
||||
// Now that we have the sorted indices, we can use them to reorder the values
|
||||
for (let column of columns) {
|
||||
column.values = indices.map((i) => column.values[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class TableComponent extends ComponentBase {
|
||||
state: TableState;
|
||||
state: Required<TableState>;
|
||||
|
||||
private tableElement: HTMLElement;
|
||||
|
||||
private sortOrder = new SortOrder();
|
||||
|
||||
private headerCells: HTMLElement[] = [];
|
||||
private rowNumberCells: HTMLElement[] = [];
|
||||
private dataCells: HTMLElement[] = [];
|
||||
private dataWidth: number;
|
||||
private dataHeight: number;
|
||||
|
||||
createElement(): HTMLElement {
|
||||
let element = document.createElement('div');
|
||||
@@ -136,139 +39,128 @@ export class TableComponent extends ComponentBase {
|
||||
}
|
||||
|
||||
updateElement(
|
||||
deltaState: TableDeltaState,
|
||||
deltaState: TableState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
// Content
|
||||
if (deltaState.data !== undefined) {
|
||||
this.state.data = dataToColumns(deltaState.data);
|
||||
console.log(`Headers ${deltaState.headers}`);
|
||||
console.log(`Data ${deltaState.data}`);
|
||||
this.updateContent();
|
||||
}
|
||||
|
||||
this.displayData();
|
||||
// Anything else requires a styling update
|
||||
this.updateStyling();
|
||||
}
|
||||
|
||||
if (deltaState.show_row_numbers ?? this.state.show_row_numbers) {
|
||||
this.showRowNumbers();
|
||||
}
|
||||
/// Removes any previous content and updates the table with the new data.
|
||||
/// Does not apply any sort of styling, not even to the headers or row
|
||||
/// numbers.
|
||||
private updateContent(): void {
|
||||
// Clear the old table
|
||||
this.tableElement.innerHTML = '';
|
||||
|
||||
if (Array.isArray(deltaState.data)) {
|
||||
this.hideColumnHeaders();
|
||||
} else {
|
||||
this.showColumnHeaders();
|
||||
}
|
||||
} else if (
|
||||
deltaState.show_row_numbers !== undefined &&
|
||||
deltaState.show_row_numbers !== this.state.show_row_numbers
|
||||
) {
|
||||
if (deltaState.show_row_numbers) {
|
||||
this.showRowNumbers();
|
||||
} else {
|
||||
this.hideRowNumbers();
|
||||
// If there is no data, this is it
|
||||
if (this.state.data.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dataHeight = this.state.data.length;
|
||||
this.dataWidth = this.state.data[0].length;
|
||||
|
||||
// Update the table's CSS to match the number of rows & columns
|
||||
this.tableElement.style.gridTemplateColumns = `repeat(${
|
||||
this.dataWidth + 1
|
||||
}, auto)`;
|
||||
|
||||
this.tableElement.style.gridTemplateRows = `repeat(${
|
||||
this.dataHeight + 1
|
||||
}, auto)`;
|
||||
|
||||
// Empty top-left corner
|
||||
let itemElement = document.createElement('div');
|
||||
itemElement.classList.add('rio-table-header');
|
||||
itemElement.textContent = '';
|
||||
this.tableElement.appendChild(itemElement);
|
||||
|
||||
// Add the headers
|
||||
let headers: string[];
|
||||
|
||||
if (this.state.headers === null) {
|
||||
headers = new Array(this.dataWidth).fill('');
|
||||
} else {
|
||||
headers = this.state.headers;
|
||||
}
|
||||
|
||||
for (let ii = 0; ii < this.dataWidth; ii++) {
|
||||
let itemElement = document.createElement('div');
|
||||
itemElement.classList.add('rio-table-header');
|
||||
itemElement.textContent = headers[ii];
|
||||
this.tableElement.appendChild(itemElement);
|
||||
}
|
||||
|
||||
// Add the cells
|
||||
for (let data_yy = 0; data_yy < this.dataHeight; data_yy++) {
|
||||
// Row number
|
||||
let itemElement = document.createElement('div');
|
||||
itemElement.classList.add('rio-table-row-number');
|
||||
itemElement.textContent = (data_yy + 1).toString();
|
||||
this.tableElement.appendChild(itemElement);
|
||||
|
||||
// Data value
|
||||
for (let data_xx = 0; data_xx < this.dataWidth; data_xx++) {
|
||||
let itemElement = document.createElement('div');
|
||||
itemElement.classList.add('rio-table-item');
|
||||
itemElement.textContent =
|
||||
this.state.data[data_yy][data_xx].toString();
|
||||
this.tableElement.appendChild(itemElement);
|
||||
}
|
||||
}
|
||||
|
||||
// Add row-highlighters. These span entire rows and change colors when
|
||||
// hovered
|
||||
for (let ii = 0; ii < this.dataHeight; ii++) {
|
||||
let itemElement = document.createElement('div');
|
||||
itemElement.classList.add('rio-table-row-highlighter');
|
||||
itemElement.style.gridRow = `${ii + 2}`;
|
||||
itemElement.style.gridColumn = `1 / span ${this.dataWidth + 1}`;
|
||||
this.tableElement.appendChild(itemElement);
|
||||
}
|
||||
}
|
||||
|
||||
private displayData(): void {
|
||||
for (let element of this.dataCells) {
|
||||
element.remove();
|
||||
/// Updates the styling of the already populated table.
|
||||
private updateStyling(): void {
|
||||
for (let style of this.state.styling) {
|
||||
this.applySingleStyle(style);
|
||||
}
|
||||
}
|
||||
|
||||
private applySingleStyle(style: TableStyle): void {
|
||||
// Come up with the CSS to apply to the targeted cells
|
||||
let css = {};
|
||||
|
||||
if (style.fontWeight !== undefined) {
|
||||
css['font-weight'] = style.fontWeight;
|
||||
}
|
||||
|
||||
this.dataCells = [];
|
||||
// Find the targeted area
|
||||
let styleLeft = style.left + 1;
|
||||
let styleWidth = style.width;
|
||||
let styleTop = style.top === 'header' ? 0 : style.top + 1;
|
||||
let styleHeight = style.height;
|
||||
|
||||
let columnNr = 2;
|
||||
for (let column of this.state.data) {
|
||||
for (let [rowNr, value] of column.values.entries()) {
|
||||
let cell = document.createElement('span');
|
||||
cell.textContent = `${value}`;
|
||||
cell.style.textAlign = column.alignment;
|
||||
let htmlWidth = this.dataWidth + 1;
|
||||
|
||||
cell.style.gridRow = `${rowNr + 2}`;
|
||||
cell.style.gridColumn = `${columnNr}`;
|
||||
this.tableElement.appendChild(cell);
|
||||
this.dataCells.push(cell);
|
||||
// Apply the CSS to all selected cells
|
||||
for (let yy = styleTop; yy < styleTop + styleHeight; yy++) {
|
||||
for (let xx = styleLeft; xx < styleLeft + styleWidth; xx++) {
|
||||
let index = yy * htmlWidth + xx;
|
||||
let cell = this.tableElement.children[index] as HTMLElement;
|
||||
|
||||
Object.assign(cell.style, css);
|
||||
}
|
||||
|
||||
columnNr++;
|
||||
}
|
||||
}
|
||||
|
||||
private showRowNumbers(): void {
|
||||
this.hideRowNumbers();
|
||||
|
||||
let numRows =
|
||||
this.state.data.length === 0 ? 0 : this.state.data[0].values.length;
|
||||
|
||||
for (let i = 0; i < numRows; i++) {
|
||||
let cell = document.createElement('span');
|
||||
cell.textContent = `${i + 1}.`;
|
||||
cell.style.textAlign = 'right';
|
||||
cell.style.opacity = '0.5';
|
||||
|
||||
cell.style.gridRow = `${i + 2}`;
|
||||
cell.style.gridColumn = '1';
|
||||
this.tableElement.appendChild(cell);
|
||||
|
||||
this.rowNumberCells.push(cell);
|
||||
}
|
||||
}
|
||||
|
||||
private hideRowNumbers(): void {
|
||||
for (let element of this.rowNumberCells) {
|
||||
element.remove();
|
||||
}
|
||||
|
||||
this.rowNumberCells = [];
|
||||
}
|
||||
|
||||
private showColumnHeaders(): void {
|
||||
this.hideColumnHeaders();
|
||||
|
||||
for (let [i, column] of this.state.data.entries()) {
|
||||
let cell = document.createElement('span');
|
||||
cell.classList.add('header');
|
||||
cell.textContent = column.name;
|
||||
cell.style.textAlign = column.alignment;
|
||||
cell.style.opacity = '0.5';
|
||||
|
||||
cell.addEventListener(
|
||||
'click',
|
||||
this.onHeaderClick.bind(this, column.name)
|
||||
);
|
||||
|
||||
cell.style.gridRow = '1';
|
||||
cell.style.gridColumn = `${i + 2}`;
|
||||
this.tableElement.appendChild(cell);
|
||||
|
||||
this.headerCells.push(cell);
|
||||
}
|
||||
}
|
||||
|
||||
private hideColumnHeaders(): void {
|
||||
for (let element of this.headerCells) {
|
||||
element.remove();
|
||||
}
|
||||
|
||||
this.headerCells = [];
|
||||
}
|
||||
|
||||
private onHeaderClick(columnName: string, event: MouseEvent): void {
|
||||
let clickedHeader = event.target as HTMLElement;
|
||||
if (clickedHeader.tagName !== 'SPAN') {
|
||||
clickedHeader = clickedHeader.parentElement!;
|
||||
}
|
||||
|
||||
let ascending = clickedHeader.dataset.sort !== 'ascending';
|
||||
|
||||
// Remove the `data-sort` attribute from all other headers
|
||||
for (let cell of this.headerCells) {
|
||||
delete cell.dataset.sort;
|
||||
}
|
||||
clickedHeader.dataset.sort = ascending ? 'ascending' : 'descending';
|
||||
|
||||
this.sortOrder.add(columnName, ascending);
|
||||
this.sortOrder.sort(this.state.data);
|
||||
this.displayData();
|
||||
|
||||
// Eat the event
|
||||
markEventAsHandled(event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2672,28 +2672,37 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
|
||||
|
||||
display: grid;
|
||||
|
||||
& > * {
|
||||
padding: 0.5rem 0.8rem;
|
||||
.rio-table-header {
|
||||
position: relative;
|
||||
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header::after {
|
||||
content: '▴';
|
||||
display: inline-block;
|
||||
margin-left: 0.3rem;
|
||||
opacity: 0;
|
||||
}
|
||||
.header[data-sort='ascending']::after {
|
||||
content: '▴';
|
||||
opacity: 1;
|
||||
}
|
||||
.header[data-sort='descending']::after {
|
||||
.rio-table-header:hover::after {
|
||||
content: '▾';
|
||||
opacity: 1;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0.5rem;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.rio-table-row-number {
|
||||
opacity: 0.5;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.rio-table-item {
|
||||
}
|
||||
|
||||
& > div {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.rio-table-row-highlighter {
|
||||
padding: 0 !important;
|
||||
height: 0.5rem;
|
||||
background-color: var(--rio-local-bg-active);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ TableValue = int | float | str
|
||||
@dataclass
|
||||
class TableSelection:
|
||||
_left: int
|
||||
_top: int
|
||||
_top: int | Literal["header"]
|
||||
_width: int
|
||||
_height: int
|
||||
|
||||
@@ -58,9 +58,13 @@ class TableSelection:
|
||||
|
||||
|
||||
def _index_to_start_and_extent(
|
||||
index: int | slice,
|
||||
index: int | slice | str,
|
||||
size_in_axis: int,
|
||||
) -> Tuple[int, int]:
|
||||
axis: Literal["x", "y"],
|
||||
) -> Tuple[
|
||||
int | Literal["header"],
|
||||
int,
|
||||
]:
|
||||
"""
|
||||
Given a one-axis `__getitem__` index, returns the start and extent of
|
||||
the slice as non-negative integers.
|
||||
@@ -100,6 +104,10 @@ def _index_to_start_and_extent(
|
||||
f"Table indices should be integers or slices, not {index!r}"
|
||||
)
|
||||
|
||||
# In the y-axis, "header" is a valid index
|
||||
elif axis == "y" and index == "header":
|
||||
return "header", 1
|
||||
|
||||
# Anything else is invalid
|
||||
else:
|
||||
raise ValueError(
|
||||
@@ -131,7 +139,11 @@ def _string_index_to_start_and_extent(
|
||||
index: str | int | slice,
|
||||
column_names: list[str] | None,
|
||||
size_in_axis: int,
|
||||
) -> Tuple[int, int]:
|
||||
axis: Literal["x", "y"],
|
||||
) -> Tuple[
|
||||
int | Literal["header"],
|
||||
int,
|
||||
]:
|
||||
"""
|
||||
Same as `_index_to_start_and_extent`, but with support for string indices.
|
||||
"""
|
||||
@@ -150,15 +162,24 @@ def _string_index_to_start_and_extent(
|
||||
return left, 1
|
||||
|
||||
# Otherwise delegate to the other function
|
||||
return _index_to_start_and_extent(index, size_in_axis)
|
||||
return _index_to_start_and_extent(
|
||||
index,
|
||||
size_in_axis,
|
||||
axis,
|
||||
)
|
||||
|
||||
|
||||
def _indices_to_rectangle(
|
||||
index: str | tuple[int | slice, str | int | slice],
|
||||
index: str | tuple[int | slice | str, str | int | slice],
|
||||
column_names: list[str] | None,
|
||||
data_width: int,
|
||||
data_height: int,
|
||||
) -> tuple[int, int, int, int]:
|
||||
) -> tuple[
|
||||
int,
|
||||
int | Literal["header"],
|
||||
int,
|
||||
int,
|
||||
]:
|
||||
# Get the raw x & y indices
|
||||
if isinstance(index, str):
|
||||
index_y = slice(None)
|
||||
@@ -179,21 +200,25 @@ def _indices_to_rectangle(
|
||||
top, height = _index_to_start_and_extent(
|
||||
index_y,
|
||||
data_height,
|
||||
axis="y",
|
||||
)
|
||||
|
||||
left, width = _string_index_to_start_and_extent(
|
||||
index_x,
|
||||
column_names,
|
||||
data_width,
|
||||
axis="x",
|
||||
)
|
||||
|
||||
assert isinstance(left, int), left # Left can't be "header"
|
||||
|
||||
return left, top, width, height
|
||||
|
||||
|
||||
@final
|
||||
class Table(FundamentalComponent):
|
||||
"""
|
||||
A table of data.
|
||||
Display & input for tabular data.
|
||||
|
||||
Tables are a way to display data in a grid, with rows and columns. They are
|
||||
very useful for displaying data that is naturally tabular, such as
|
||||
@@ -242,11 +267,9 @@ class Table(FundamentalComponent):
|
||||
# All headers, if present
|
||||
_headers: list[str] | None = None
|
||||
|
||||
# All data. This is initialized in `__post_init__`, so most code can rely on
|
||||
# the type hint to be correct, despite the invalid assignment here.
|
||||
#
|
||||
# This is a list of rows.
|
||||
_data: list[list[TableValue]] = None # type: ignore
|
||||
# The data, as a list of rows ("row-major"). This is initialized in
|
||||
# `__post_init__`.
|
||||
_data: list[list[TableValue]] = []
|
||||
|
||||
# All styles applied to the table, in the order they were added.
|
||||
_styling: list[TableSelection] = []
|
||||
@@ -276,17 +299,34 @@ class Table(FundamentalComponent):
|
||||
self._headers = list(data.keys())
|
||||
|
||||
# Verify all columns have the same length
|
||||
lengths = [len(list(column)) for column in self.data.values()]
|
||||
if len(set(lengths)) > 1:
|
||||
columns: list[list[TableValue]] = []
|
||||
column_lengths = set()
|
||||
|
||||
for column in data.values():
|
||||
column = list(column)
|
||||
column_lengths.add(len(column))
|
||||
columns.append(column)
|
||||
|
||||
if len(column_lengths) > 1:
|
||||
raise ValueError("All table columns must have the same length")
|
||||
|
||||
# Black magic to transpose the data
|
||||
self._data = list(map(list, zip(*self.data.values())))
|
||||
|
||||
# Iterable of iterables
|
||||
data = typing.cast(Iterable[Iterable[TableValue]], self.data)
|
||||
self._headers = None
|
||||
self._data = [list(row) for row in data]
|
||||
else:
|
||||
data = typing.cast(Iterable[Iterable[TableValue]], self.data)
|
||||
self._headers = None
|
||||
self._data = []
|
||||
row_lengths = set()
|
||||
|
||||
for row in data:
|
||||
row = list(row)
|
||||
row_lengths.add(len(row))
|
||||
self._data.append(row)
|
||||
|
||||
if len(row_lengths) > 1:
|
||||
raise ValueError("All table rows must have the same length")
|
||||
|
||||
def _custom_serialize(self) -> JsonDoc:
|
||||
return {
|
||||
@@ -321,12 +361,16 @@ class Table(FundamentalComponent):
|
||||
|
||||
def __getitem__(
|
||||
self,
|
||||
index: str | Tuple[int | slice, str | int | slice],
|
||||
index: str
|
||||
| Tuple[
|
||||
int | slice | str,
|
||||
int | slice | str,
|
||||
],
|
||||
) -> TableSelection:
|
||||
# Get the index as a tuple (top, left, height, width)
|
||||
data_height, data_width = self._shape()
|
||||
|
||||
top, left, height, width = _indices_to_rectangle(
|
||||
left, top, width, height = _indices_to_rectangle(
|
||||
index,
|
||||
self._headers,
|
||||
data_width,
|
||||
@@ -335,16 +379,23 @@ class Table(FundamentalComponent):
|
||||
|
||||
# Verify the indices are within bounds
|
||||
right = left + width
|
||||
bottom = top + height
|
||||
out_of_bounds = (left < 0 or left >= data_width) or (
|
||||
right < 0 or right > data_width
|
||||
)
|
||||
|
||||
if (
|
||||
(top < 0 or top >= data_height)
|
||||
or (left < 0 or left >= data_width)
|
||||
or (bottom < 0 or bottom > data_height)
|
||||
or (right < 0 or right > data_width)
|
||||
):
|
||||
if top == "header":
|
||||
assert height == 1, height
|
||||
else:
|
||||
bottom = top + height
|
||||
out_of_bounds = (
|
||||
out_of_bounds
|
||||
or (top < 0 or top >= data_height)
|
||||
or (bottom < 0 or bottom > data_height)
|
||||
)
|
||||
|
||||
if out_of_bounds:
|
||||
raise IndexError(
|
||||
f"Table index out of bounds. You're trying to select [{top}:{bottom}, {left}:{right}] but the table is only {data_height}x{data_width}"
|
||||
f"The table index {index!r} is out of bounds for a table of size {data_height}x{data_width}"
|
||||
)
|
||||
|
||||
# Construct the result
|
||||
|
||||
@@ -145,8 +145,8 @@ def serialize_and_host_component(component: rio.Component) -> JsonDoc:
|
||||
).items():
|
||||
result[name] = serializer(sess, getattr(component, name))
|
||||
|
||||
# Encode any internal additional state. Doing it this late allows the custom
|
||||
# serialization to overwrite automatically generated values.
|
||||
# Encode any internal additional state. Doing it this late allows the
|
||||
# custom serialization to overwrite automatically generated values.
|
||||
result["_type_"] = component._unique_id
|
||||
result.update(component._custom_serialize())
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ Tables support numpy-style 2D indexing. This is rather complex, hence the
|
||||
tests here.
|
||||
"""
|
||||
|
||||
from typing import Any, Type
|
||||
from typing import *
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -230,12 +230,45 @@ make_index = MakeIndex()
|
||||
False,
|
||||
ValueError,
|
||||
),
|
||||
# Indexing into the header
|
||||
(
|
||||
make_index["header", :],
|
||||
False,
|
||||
(0, "header", 10, 1),
|
||||
),
|
||||
(
|
||||
make_index["header", 3:5],
|
||||
False,
|
||||
(3, "header", 2, 1),
|
||||
),
|
||||
(
|
||||
make_index["header", -2:],
|
||||
False,
|
||||
(8, "header", 2, 1),
|
||||
),
|
||||
(
|
||||
make_index["header", :-2],
|
||||
False,
|
||||
(0, "header", 8, 1),
|
||||
),
|
||||
# Indexing into the header, but in the wrong axis
|
||||
(
|
||||
make_index[0, "header"],
|
||||
False,
|
||||
ValueError,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_indices(
|
||||
index: Any,
|
||||
enable_column_names: bool,
|
||||
result_should: tuple[int, int, int, int] | Type[Exception],
|
||||
result_should: tuple[
|
||||
int,
|
||||
int | Literal["header"],
|
||||
int,
|
||||
int,
|
||||
]
|
||||
| Type[Exception],
|
||||
) -> None:
|
||||
if enable_column_names:
|
||||
column_names = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]
|
||||
|
||||
Reference in New Issue
Block a user