Skip to content
1 change: 1 addition & 0 deletions chartlets.js/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* New (MUI) components
- `DataGrid`
- `Dialog`
- `Table`

## Version 0.1.3 (from 2025/01/28)

Expand Down
76 changes: 76 additions & 0 deletions chartlets.js/packages/lib/src/plugins/mui/Table.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { describe, expect, it } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import { Table } from "@/plugins/mui/Table";
import { createChangeHandler } from "@/plugins/mui/common.test";

describe("Table", () => {
const rows = [
["John", "Doe"],
["Johnie", "Undoe"],
];
const columns = [
{ id: "firstName", label: "First Name" },
{ id: "lastName", label: "Last Name" },
];

it("should render the Table component", () => {
render(
<Table
id="table"
type={"Table"}
rows={rows}
columns={columns}
onChange={() => {}}
/>,
);

const table = screen.getByRole("table");
expect(table).toBeDefined();
columns.forEach((column) => {
expect(screen.getByText(column.label)).toBeInTheDocument();
});
rows.forEach((row, index) => {
expect(screen.getByText(row[index])).toBeInTheDocument();
});
});

it("should not render the Table component when no columns provided", () => {
render(<Table id="table" type={"Table"} rows={rows} onChange={() => {}} />);

const table = screen.queryByRole("table");
expect(table).toBeNull();
});

it("should not render the Table component when no rows provided", () => {
render(<Table id="table" type={"Table"} rows={rows} onChange={() => {}} />);

const table = screen.queryByRole("table");
expect(table).toBeNull();
});

it("should call onChange on row click", () => {
const { recordedEvents, onChange } = createChangeHandler();
render(
<Table
id="table"
type={"Table"}
rows={rows}
columns={columns}
onChange={onChange}
/>,
);

fireEvent.click(screen.getAllByRole("row")[1]);
expect(recordedEvents.length).toEqual(1);
expect(recordedEvents[0]).toEqual({
componentType: "Table",
id: "table",
property: "value",
value: {
firstName: "John",
lastName: "Doe",
},
});
});
});
110 changes: 110 additions & 0 deletions chartlets.js/packages/lib/src/plugins/mui/Table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {
Paper,
Table as MuiTable,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
} from "@mui/material";
import type { ComponentProps, ComponentState } from "@/index";
import type { SxProps } from "@mui/system";

interface TableCellProps {
id: string | number;
size?: "medium" | "small";
align?: "inherit" | "left" | "center" | "right" | "justify";
sx?: SxProps;
}

interface TableColumn extends TableCellProps {
label: string;
}

interface TableState extends ComponentState {
rows?: (string | number | boolean | undefined)[][];
columns?: TableColumn[];
hover?: boolean;
stickyHeader?: boolean;
}

interface TableProps extends ComponentProps, TableState {}

export const Table = ({
type,
id,
style,
rows,
columns,
hover,
stickyHeader,
onChange,
}: TableProps) => {
if (!columns || columns.length === 0) {
return <div>No columns provided.</div>;
}

if (!rows || rows.length === 0) {
return <div>No rows provided.</div>;
}

const handleRowClick = (row: (string | number | boolean | undefined)[]) => {
const rowData = row.reduce(
(acc, cell, cellIndex) => {
const columnId = columns[cellIndex]?.id;
if (columnId) {
acc[columnId] = cell;
}
return acc;
},
{} as Record<string, string | number | boolean | undefined>,
);
if (id) {
onChange({
componentType: type,
id: id,
property: "value",
value: rowData,
});
}
};

return (
<TableContainer component={Paper} sx={style} id={id}>
<MuiTable stickyHeader={stickyHeader}>
<TableHead>
<TableRow>
{columns.map((column) => (
<TableCell
key={column.id}
align={column.align || "inherit"}
size={column.size || "medium"}
>
{column.label}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{rows.map((row, row_index) => (
<TableRow
hover={hover}
key={row_index}
onClick={() => handleRowClick(row)}
>
{row?.map((item, item_index) => (
<TableCell
key={item_index}
align={columns[item_index].align || "inherit"}
size={columns[item_index].size || "medium"}
>
{item}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</MuiTable>
</TableContainer>
);
};
2 changes: 2 additions & 0 deletions chartlets.js/packages/lib/src/plugins/mui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Typography } from "./Typography";
import { Slider } from "./Slider";
import { DataGrid } from "@/plugins/mui/DataGrid";
import { Dialog } from "@/plugins/mui/Dialog";
import { Table } from "@/plugins/mui/Table";

export default function mui(): Plugin {
return {
Expand All @@ -31,6 +32,7 @@ export default function mui(): Plugin {
["Select", Select],
["Slider", Slider],
["Switch", Switch],
["Table", Table],
["Tabs", Tabs],
["Typography", Typography],
],
Expand Down
1 change: 1 addition & 0 deletions chartlets.py/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* New (MUI) components
- `DataGrid`
- `Dialog`
- `Table`


## Version 0.1.3 (from 2025/01/28)
Expand Down
1 change: 1 addition & 0 deletions chartlets.py/chartlets/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from .slider import Slider
from .switch import Switch
from .datagrid import DataGrid
from .table import Table
from .tabs import Tab
from .tabs import Tabs
from .typography import Typography
43 changes: 43 additions & 0 deletions chartlets.py/chartlets/components/table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from dataclasses import dataclass
from typing import Literal, TypedDict, TypeAlias
from chartlets import Component


class TableCellProps(TypedDict, total=False):
"""Represents common properties of a table cell."""

id: str | int | float
"""The unique identifier for the cell."""

size: Literal['medium', 'small'] | str | None
"""The size of the cell."""

align: Literal["inherit", "left", "center", "right", "justify"] | None
"""The alignment of the cell content."""


class TableColumn(TableCellProps):
"""Defines a column in the table."""

label: str
"""The display label for the column header."""


TableRow: TypeAlias = list[list[str | int | float | bool | None]]


@dataclass(frozen=True)
class Table(Component):
"""A basic Table with configurable rows and columns."""

columns: list[TableColumn] | None = None
"""The columns to display in the table."""

rows: TableRow | None = None
"""The rows of data to display in the table."""

hover: bool | None = None
"""A boolean indicating whether to highlight a row when hovered over"""

stickyHeader: bool | None = None
"""A boolean to set the header of the table sticky"""
2 changes: 2 additions & 0 deletions chartlets.py/demo/my_extension/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
from .my_panel_3 import panel as my_panel_3
from .my_panel_4 import panel as my_panel_4
from .my_panel_5 import panel as my_panel_5
from .my_panel_6 import panel as my_panel_6

ext = Extension(__name__)
ext.add(my_panel_1)
ext.add(my_panel_2)
ext.add(my_panel_3)
ext.add(my_panel_4)
ext.add(my_panel_5)
ext.add(my_panel_6)
58 changes: 58 additions & 0 deletions chartlets.py/demo/my_extension/my_panel_6.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from chartlets import Component, Input, Output
from chartlets.components import Box, Typography, Table

from server.context import Context
from server.panel import Panel

from chartlets.components.table import TableColumn, TableRow

panel = Panel(__name__, title="Panel F")


# noinspection PyUnusedLocal
@panel.layout()
def render_panel(
ctx: Context,
) -> Component:
columns: list[TableColumn] = [
{"id": "id", "label": "ID", "sortDirection": "desc"},
{
"id": "firstName",
"label": "First Name",
"align": "left",
"sortDirection": "desc",
},
{"id": "lastName", "label": "Last Name", "align": "center"},
{"id": "age", "label": "Age"},
]

rows: TableRow = [
["1", "John", "Doe", 30],
["2", "Jane", "Smith", 25],
["3", "Peter", "Jones", 40],
]

table = Table(id="table", rows=rows, columns=columns, hover=True)

title_text = Typography(id="title_text", children=["Basic Table"])
info_text = Typography(id="info_text", children=["Click on any row."])

return Box(
style={
"display": "flex",
"flexDirection": "column",
"width": "100%",
"height": "100%",
"gap": "6px",
},
children=[title_text, table, info_text],
)


# noinspection PyUnusedLocal
@panel.callback(Input("table"), Output("info_text", "children"))
def update_info_text(
ctx: Context,
table_row: int,
) -> list[str]:
return [f"The clicked row value is {table_row}."]
38 changes: 38 additions & 0 deletions chartlets.py/tests/components/table_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from chartlets.components.table import TableColumn, Table, TableRow
from tests.component_test import make_base


class TableTest(make_base(Table)):

def test_is_json_serializable_empty(self):
self.assert_is_json_serializable(
self.cls(),
{
"type": "Table",
},
)

columns: list[TableColumn] = [
{"id": "id", "label": "ID"},
{"id": "firstName", "label": "First Name"},
{"id": "lastName", "label": "Last Name"},
{"id": "age", "label": "Age"},
]
rows: TableRow = [
["John", "Doe", 30],
["Jane", "Smith", 25],
["Johnie", "Undoe", 40],
]
hover: bool = True
style = {"background-color": "lightgray", "width": "100%"}

self.assert_is_json_serializable(
self.cls(columns=columns, rows=rows, style=style, hover=hover),
{
"type": "Table",
"columns": columns,
"rows": rows,
"style": style,
"hover": hover,
},
)