diff --git a/.idea/misc.xml b/.idea/misc.xml
index 574ac89..c223352 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -3,5 +3,5 @@
-
+
\ No newline at end of file
diff --git a/.idea/py-tabulator.iml b/.idea/py-tabulator.iml
index c0c85dc..351d4dd 100644
--- a/.idea/py-tabulator.iml
+++ b/.idea/py-tabulator.iml
@@ -4,7 +4,7 @@
-
+
\ No newline at end of file
diff --git a/.idea/watcherTasks.xml b/.idea/watcherTasks.xml
index a2f3433..c7e1a41 100644
--- a/.idea/watcherTasks.xml
+++ b/.idea/watcherTasks.xml
@@ -2,7 +2,6 @@
-
diff --git a/docs/examples/getting_started/app.py b/docs/examples/getting_started/app.py
index fa1695e..f6725ec 100644
--- a/docs/examples/getting_started/app.py
+++ b/docs/examples/getting_started/app.py
@@ -1,6 +1,8 @@
import pandas as pd
from pytabulator.shiny_bindings import output_tabulator, render_tabulator
from pytabulator.tabulator import Tabulator
+from pytabulator.editors import NumberEditor, StarEditor, ProgressEditor
+from pytabulator.formatters import StarFormatter
from shiny import App, render, ui
app_ui = ui.page_fluid(
@@ -15,7 +17,30 @@ def tabulator():
df = pd.read_csv(
"https://raw.githubusercontent.com/datasciencedojo/datasets/master/titanic.csv"
)
- return Tabulator(df, table_options={"height": 311})
+ """
+ return Tabulator(df, options={"height": 311}).set_column_formatter(
+ "Pclass", "star", {"stars": 3}, hozAlign="center"
+ )
+ """
+ return (
+ Tabulator(df)
+ .set_options(height=311)
+ # .set_column_formatter_star("Pclass", 3)
+ .set_column_formatter("Pclass", StarFormatter(stars=3), hoz_align="center")
+ .set_column_formatter_tick_cross("Survived", hoz_align="center")
+ # .set_column_editor("Fare", "number", dict(min=0, max=10))
+ .set_column_editor_number("Fare", min_=0, max_=5)
+ .set_column_title("Pclass", "PassengerClass")
+ .set_column_editor(["Name", "Sex"], "input", hoz_align="center")
+ .set_column_editor("PassengerId", NumberEditor(min=0, max=1000, step=1))
+ .set_column_editor("Pclass", StarEditor())
+ .set_column_formatter("Fare", "progress")
+ .set_column_editor(
+ "Fare",
+ ProgressEditor(min=0, max=100, element_attributes=dict(title="Hey ho")),
+ hoz_align="left",
+ )
+ )
@render.code
async def txt():
diff --git a/docs/examples/getting_started/shiny_express_all.py b/docs/examples/getting_started/shiny_express_all.py
index 74409b7..ad96325 100644
--- a/docs/examples/getting_started/shiny_express_all.py
+++ b/docs/examples/getting_started/shiny_express_all.py
@@ -77,7 +77,7 @@ def selected_rows():
@render_tabulator
def tabulator():
- return Tabulator(df, table_options).options(
+ return Tabulator(df, table_options).set_options(
editTriggerEvent="dblclick"
) # .options(selectableRows=True)
@@ -136,4 +136,4 @@ async def trigger_get_data():
@reactive.Effect
@reactive.event(input.tabulator_data)
def tabulator_data():
- print(input.tabulator_data()[0])
+ print(input.tabulator_data())
diff --git a/docs/examples/getting_started/shiny_express_all_new_style.py b/docs/examples/getting_started/shiny_express_all_new_style.py
new file mode 100644
index 0000000..bd4332d
--- /dev/null
+++ b/docs/examples/getting_started/shiny_express_all_new_style.py
@@ -0,0 +1,147 @@
+from random import randrange
+
+import pandas as pd
+from pytabulator import TableOptions, Tabulator, TabulatorContext, render_tabulator
+from pytabulator.utils import create_columns
+from pytabulator.formatters import ProgressFormatter, TickCrossFormatter
+from pytabulator.editors import ListEditor, InputEditor, ProgressEditor
+from shiny import reactive, render
+from shiny.express import input, ui
+
+# Fetch data
+#
+df = pd.read_csv(
+ "https://raw.githubusercontent.com/datasciencedojo/datasets/master/titanic.csv"
+)[["PassengerId", "Name", "Pclass", "Sex", "Age", "Fare", "Survived"]]
+
+# Setup
+#
+table_options = TableOptions(
+ columns=create_columns(
+ df,
+ default_filter=True,
+ default_editor=True,
+ updates={
+ "Pclass": {
+ "formatter": "star",
+ "formatterParams": {"stars": 3},
+ "hozAlign": "center",
+ },
+ # "Survived": {"formatter": "tickCross"},
+ # "Fare": {"formatter": "progress", "hozAlign": "left"},
+ },
+ ),
+ height=413,
+ pagination=True,
+ pagination_add_row="table",
+ layout="fitColumns",
+ index="PassengerId",
+ add_row_pos="top",
+ selectable_rows=True,
+ history=True,
+)
+
+# Shiny Express App
+#
+with ui.div(style="padding-top: 0px;"):
+ ui.input_action_button("trigger_download", "Download")
+ ui.input_action_button("add_row", "Add row")
+ ui.input_action_button("delete_selected_rows", "Delete selected rows")
+ ui.input_action_button("undo", "Undo")
+ ui.input_action_button("redo", "Redo")
+ ui.input_action_button("trigger_get_data", "Submit data")
+
+ui.div(
+ ui.input_text("name", "Click on 'Add row' to add the Person to the table."),
+ style="padding-top: 20px;",
+)
+ui.div("Click on a row to print the name of the person.", style="padding: 10px;"),
+
+
+@render.code
+async def txt():
+ print(input.tabulator_row_clicked())
+ return input.tabulator_row_clicked()["Name"]
+
+
+ui.div(
+ "Select multiple rows to print the names of the selected persons.",
+ style="padding: 10px;",
+),
+
+
+@render.code
+def selected_rows():
+ data = input.tabulator_rows_selected()
+ output = [item["Name"] for item in data]
+ return "\n".join(output)
+
+
+@render_tabulator
+def tabulator():
+ return (
+ Tabulator(df, table_options)
+ .set_options(editTriggerEvent="dblclick")
+ .set_column_formatter("Fare", ProgressFormatter(), hoz_align="left")
+ .set_column_formatter("Survived", TickCrossFormatter(), hoz_align="center")
+ .set_column_editor("Sex", ListEditor())
+ .set_column_editor("Name", InputEditor())
+ .set_column_editor("Fare", ProgressEditor(), hoz_align="left")
+ )
+
+
+@reactive.Effect
+@reactive.event(input.trigger_download)
+async def trigger_download():
+ print("download triggered")
+ async with TabulatorContext("tabulator") as table:
+ table.trigger_download("csv")
+
+
+@reactive.Effect
+@reactive.event(input.add_row)
+async def add_row():
+ async with TabulatorContext("tabulator") as table:
+ table.add_row(
+ {
+ "Name": input.name() or "Hans",
+ "Age": randrange(55),
+ "Survived": randrange(2),
+ "PassengerId": randrange(10000, 20000, 1),
+ "SibSp": randrange(9),
+ }
+ )
+
+
+@reactive.Effect
+@reactive.event(input.delete_selected_rows)
+async def delete_selected_rows():
+ async with TabulatorContext("tabulator") as table:
+ table.delete_selected_rows()
+
+
+@reactive.Effect
+@reactive.event(input.undo)
+async def undo():
+ async with TabulatorContext("tabulator") as table:
+ table.undo()
+
+
+@reactive.Effect
+@reactive.event(input.redo)
+async def redo():
+ async with TabulatorContext("tabulator") as table:
+ table.redo()
+
+
+@reactive.Effect
+@reactive.event(input.trigger_get_data)
+async def trigger_get_data():
+ async with TabulatorContext("tabulator") as table:
+ table.trigger_get_data()
+
+
+@reactive.Effect
+@reactive.event(input.tabulator_data)
+def tabulator_data():
+ print(input.tabulator_data())
diff --git a/get-py-bindings.sh b/get-py-bindings.sh
new file mode 100755
index 0000000..e3da277
--- /dev/null
+++ b/get-py-bindings.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+branch=${1:-dev}
+# curl -O https://raw.githubusercontent.com/eodaGmbH/tabulator-bindings/${branch}/r-bindings/rtabulator.js
+curl -o pytabulator/srcjs/pytabulator.js https://raw.githubusercontent.com/eodaGmbH/tabulator-bindings/refs/heads/feature/typescript/py-bindings/pytabulator.js
diff --git a/pytabulator/__init__.py b/pytabulator/__init__.py
index 4b66192..233257d 100644
--- a/pytabulator/__init__.py
+++ b/pytabulator/__init__.py
@@ -1,3 +1,4 @@
+"""
from importlib.metadata import PackageNotFoundError, version
try:
@@ -12,7 +13,8 @@
from ._table_options_dc import TableOptionsDC as TableOptions
# print("dataclass")
-
+"""
+from .tabulator_options import TabulatorOptions as TableOptions
# from ._table_options_pydantic import TableOptionsPydantic as TableOptions
from .shiny_bindings import output_tabulator, render_data_frame, render_tabulator
from .tabulator import Tabulator
diff --git a/pytabulator/_abstracts.py b/pytabulator/_abstracts.py
new file mode 100644
index 0000000..8ef7454
--- /dev/null
+++ b/pytabulator/_abstracts.py
@@ -0,0 +1,8 @@
+from pydantic import BaseModel
+
+from ._utils import as_camel_dict_recursive
+
+
+class MyBaseModel(BaseModel):
+ def to_dict(self) -> dict:
+ return as_camel_dict_recursive(self.model_dump(exclude_none=True))
diff --git a/pytabulator/_utils.py b/pytabulator/_utils.py
index 6cd9876..1e102ca 100644
--- a/pytabulator/_utils.py
+++ b/pytabulator/_utils.py
@@ -15,6 +15,20 @@ def set_theme(stylesheet):
def snake_to_camel_case(snake_str: str) -> str:
return snake_str[0].lower() + snake_str.title()[1:].replace("_", "")
- # return "".join(
- # [item if not i else item.title() for i, item in enumerate(snake_str.split("_"))]
- # )
+
+def as_camel_dict(snake_dict: dict) -> dict:
+ return {snake_to_camel_case(k): v for (k, v) in snake_dict.items() if v is not None}
+
+
+def as_camel_dict_recursive(snake_dict: dict) -> dict:
+ camel_case_dict = {}
+ for k, v in snake_dict.items():
+ if v is not None:
+ camel_key = snake_to_camel_case(k) if "_" in k else k
+
+ if isinstance(v, dict):
+ camel_case_dict[camel_key] = as_camel_dict_recursive(v)
+ else:
+ camel_case_dict[camel_key] = v
+
+ return camel_case_dict
diff --git a/pytabulator/data.py b/pytabulator/data.py
new file mode 100644
index 0000000..40ecbd2
--- /dev/null
+++ b/pytabulator/data.py
@@ -0,0 +1,4 @@
+from pandas import read_csv
+
+def titanic():
+ pass
diff --git a/pytabulator/editors.py b/pytabulator/editors.py
new file mode 100644
index 0000000..593b10a
--- /dev/null
+++ b/pytabulator/editors.py
@@ -0,0 +1,89 @@
+from enum import Enum
+from typing import Literal, Optional
+
+from ._abstracts import MyBaseModel
+
+
+class Editors(Enum):
+ INPUT = "input"
+ TEXTAREA = "textarea"
+ NUMBER = "number"
+ RANGE = "range"
+ TICK_CROSS = "tickCross"
+ STAR = "star"
+ PROGRESS = "progress"
+ LIST = "list"
+
+
+class Editor(MyBaseModel):
+ _name: str = ""
+
+ @property
+ def name(self) -> str:
+ return self._name
+
+
+class InputEditor(Editor):
+ _name: str = Editors.INPUT.value
+
+ search: Optional[bool] = None
+ mask: Optional[str] = None
+ select_contents: Optional[bool] = None
+ element_attributes: Optional[dict] = None
+
+
+class TextareaEditor(Editor):
+ _name: str = Editors.TEXTAREA.value
+
+ mask: Optional[str] = None
+ select_contents: Optional[bool] = None
+ vertical_navigation: Literal["hybrid", "editor", "table"] = None
+ shift_enter_submit: Optional[bool] = None
+
+
+class NumberEditor(Editor):
+ _name: str = Editors.NUMBER.value
+
+ min: Optional[float] = None
+ max: Optional[float] = None
+ step: Optional[float] = None
+ element_attributes: Optional[dict] = None
+ mask: Optional[str] = None
+ select_contents: Optional[bool] = None
+ vertical_navigation: Literal["editor", "table"] = None
+
+
+class RangeEditor(Editor):
+ _name: str = Editors.RANGE.value
+
+ min: Optional[float] = None
+ max: Optional[float] = None
+ step: Optional[float] = None
+ element_attributes: Optional[dict] = None
+
+
+class TickCrossEditor(Editor):
+ _name: str = Editors.TICK_CROSS.value
+
+ true_value: Optional[str] = None
+ false_value: Optional[str] = None
+ element_attributes: Optional[dict] = None
+
+
+class StarEditor(Editor):
+ _name: str = Editors.STAR.value
+
+
+class ProgressEditor(Editor):
+ _name: str = Editors.PROGRESS.value
+
+ min: Optional[float] = None
+ max: Optional[float] = None
+ element_attributes: Optional[dict] = None
+
+
+class ListEditor(Editor):
+ _name: str = Editors.LIST.value
+
+ values: Optional[list] = None
+ values_lookup: Optional[bool] = True
diff --git a/pytabulator/formatters.py b/pytabulator/formatters.py
new file mode 100644
index 0000000..eab2774
--- /dev/null
+++ b/pytabulator/formatters.py
@@ -0,0 +1,41 @@
+from enum import Enum
+from typing import Optional
+
+from pydantic import BaseModel
+
+from ._utils import as_camel_dict_recursive
+
+
+class Formatters(Enum):
+ STAR = "star"
+ PROGRESS = "progress"
+ TICK_CROSS = "tickCross"
+
+
+class Formatter(BaseModel):
+ def to_dict(self) -> dict:
+ return as_camel_dict_recursive(self.model_dump(exclude_none=True))
+
+ @property
+ def name(self) -> str:
+ return ""
+
+
+class StarFormatter(Formatter):
+ stars: Optional[int] = None
+
+ @property
+ def name(self) -> str:
+ return Formatters.STAR.value
+
+
+class ProgressFormatter(Formatter):
+ @property
+ def name(self) -> str:
+ return Formatters.PROGRESS.value
+
+
+class TickCrossFormatter(Formatter):
+ @property
+ def name(self) -> str:
+ return Formatters.TICK_CROSS.value
diff --git a/pytabulator/shiny_bindings.py b/pytabulator/shiny_bindings.py
index c6b212a..07fc315 100644
--- a/pytabulator/shiny_bindings.py
+++ b/pytabulator/shiny_bindings.py
@@ -10,7 +10,8 @@
from ._types import TableOptions
from ._utils import df_to_dict
-from .tabulator import Tabulator, jsonifiable_table_options
+# from .tabulator import Tabulator, jsonifiable_table_options
+from .tabulator import Tabulator
# from . import TableOptions
@@ -32,10 +33,10 @@ def tabulator_dep() -> HTMLDependency:
tabulator_bindings_dep = HTMLDependency(
- "tabulator-bindings",
+ "pytabulator",
"0.1.0",
source={"package": "pytabulator", "subdir": "srcjs"},
- script={"src": "tabulator-bindings.js", "type": "module"},
+ script={"src": "pytabulator.js", "type": "module"},
all_files=False,
)
@@ -95,5 +96,7 @@ async def render(self) -> Jsonifiable:
# return {"values": value.values.tolist(), "columns": value.columns.tolist()}
# TODO: convert with js
data = df_to_dict(df)
- data["options"] = jsonifiable_table_options(self.table_options)
+
+ # TODO: Fix this, func was removed
+ # data["options"] = jsonifiable_table_options(self.table_options)
return data
diff --git a/pytabulator/srcjs/pytabulator.js b/pytabulator/srcjs/pytabulator.js
new file mode 100644
index 0000000..0078270
--- /dev/null
+++ b/pytabulator/srcjs/pytabulator.js
@@ -0,0 +1,129 @@
+"use strict";
+(() => {
+ // built/utils.js
+ function convertToDataFrame(data) {
+ const res = {};
+ if (data.length === 0) {
+ return res;
+ }
+ const keys = Object.keys(data[0]);
+ keys.forEach((key) => res[key] = data.map((item) => item[key]));
+ return res;
+ }
+
+ // built/events.js
+ function addEventListeners(tabulatorWidget) {
+ const table = tabulatorWidget.getTable();
+ const elementId = tabulatorWidget.getElementId();
+ const bindingLang = tabulatorWidget.getBindingLang();
+ console.log("binding lang", bindingLang);
+ table.on("rowClick", function(e, row) {
+ const inputName = `${elementId}_row_clicked`;
+ console.log(inputName, row.getData());
+ Shiny.onInputChange(inputName, row.getData());
+ });
+ table.on("rowClick", (e, row) => {
+ const inputName = bindingLang === "r" ? `${elementId}_data:rtabulator.data` : `${elementId}_data`;
+ const data = table.getSelectedRows().map((row2) => row2.getData());
+ console.log(inputName, data);
+ Shiny.onInputChange(inputName, { data: convertToDataFrame(data) });
+ });
+ table.on("cellEdited", function(cell) {
+ const inputName = `${elementId}_cell_edited`;
+ console.log(inputName, cell.getData());
+ Shiny.onInputChange(inputName, cell.getData());
+ });
+ table.on("dataFiltered", function(filters, rows) {
+ const inputName = bindingLang === "r" ? `${elementId}_data:rtabulator.data` : `${elementId}_data`;
+ const data = rows.map((row) => row.getData());
+ console.log(inputName, data);
+ Shiny.onInputChange(inputName, { data: convertToDataFrame(data) });
+ });
+ }
+
+ // built/widget.js
+ function run_calls(tabulatorWidget, calls) {
+ const table = tabulatorWidget.getTable();
+ const elementId = tabulatorWidget.getElementId();
+ const bindingLang = tabulatorWidget.getBindingLang();
+ console.log("binding lang", bindingLang);
+ calls.forEach(([method_name, options]) => {
+ if (method_name === "getData") {
+ const inputName = bindingLang === "r" ? `${elementId}_data:rtabulator.data` : `${elementId}_data`;
+ console.log("custom call", inputName);
+ Shiny.setInputValue(inputName, { data: convertToDataFrame(table.getData()) }, { priority: "event" });
+ return;
+ }
+ if (method_name === "deleteSelectedRows") {
+ console.log("custom call");
+ const rows = table.getSelectedRows();
+ rows.forEach((row) => {
+ console.log(row.getIndex());
+ table.deleteRow(row.getIndex());
+ });
+ return;
+ }
+ if (method_name === "getSheetData") {
+ const inputName = bindingLang === "r" ? `${elementId}_sheet_data:rtabulator.sheet_data` : `${elementId}_sheet_data`;
+ console.log("custom call", inputName);
+ Shiny.setInputValue(inputName, { data: table.getSheetData() }, { priority: "event" });
+ return;
+ }
+ console.log(method_name, options);
+ table[method_name](...options);
+ });
+ }
+ var TabulatorWidget = class {
+ constructor(container, data, options, bindingOptions) {
+ options.data = data;
+ this._container = container;
+ this._bindingOptions = bindingOptions;
+ console.log("columns", options.columns);
+ if (data !== null && options.columns == null) {
+ options.autoColumns = true;
+ }
+ if (options.spreadsheet && options.spreadsheetData == null) {
+ options.spreadsheetData = [];
+ }
+ this._table = new Tabulator(this._container, options);
+ if (typeof Shiny === "object") {
+ addEventListeners(this);
+ this._addShinyMessageHandler();
+ }
+ }
+ _addShinyMessageHandler() {
+ const messageHandlerName = `tabulator-${this._container.id}`;
+ Shiny.addCustomMessageHandler(messageHandlerName, (payload) => {
+ console.log(payload);
+ run_calls(this, payload.calls);
+ });
+ }
+ getTable() {
+ return this._table;
+ }
+ getElementId() {
+ return this._container.id;
+ }
+ getBindingLang() {
+ return this._bindingOptions.lang;
+ }
+ };
+
+ // built/index-py.js
+ var TabulatorOutputBinding = class extends Shiny.OutputBinding {
+ find(scope) {
+ return scope.find(".shiny-tabulator-output");
+ }
+ renderValue(el, payload) {
+ console.log("payload", payload);
+ const widget = new TabulatorWidget(el, payload.data, payload.options, payload.bindingOptions);
+ const table = widget.getTable();
+ table.on("tableBuilt", function() {
+ if (payload.options.columnUpdates != null) {
+ console.log("column updates", payload.options.columnUpdates);
+ }
+ });
+ }
+ };
+ Shiny.outputBindings.register(new TabulatorOutputBinding(), "shiny-tabulator-output");
+})();
diff --git a/pytabulator/srcjs/tabulator-bindings.js b/pytabulator/srcjs/tabulator-bindings.js
deleted file mode 100644
index 81b0324..0000000
--- a/pytabulator/srcjs/tabulator-bindings.js
+++ /dev/null
@@ -1 +0,0 @@
-(()=>{function c(s,e){s.on("rowClick",function(n,t){let o=`${e.id}_row_clicked`;console.log(o,t.getData()),Shiny.onInputChange(o,t.getData())}),s.on("rowClick",(n,t)=>{let o=`${e.id}_rows_selected`,a=s.getSelectedRows().map(i=>i.getData());console.log(o,a),Shiny.onInputChange(o,a)}),s.on("cellEdited",function(n){let t=`${e.id}_row_edited`;console.log(t,n.getData()),Shiny.onInputChange(t,n.getData())}),s.on("dataFiltered",function(n,t){let o=`${e.id}_data_filtered`,a=t.map(i=>i.getData());console.log(o,a),Shiny.onInputChange(o,a)})}function r(s,e,n){n.forEach(([t,o])=>{if(t==="getData"){console.log("custom call"),Shiny.onInputChange(`${s.id}_data`,e.getData());return}if(t==="deleteSelectedRows"){console.log("custom call"),e.getSelectedRows().forEach(i=>{console.log(i.getIndex()),e.deleteRow(i.getIndex())});return}console.log(t,o),e[t](...o)})}var l=class{constructor(e,n,t){t.data=n,this._container=e,console.log("columns",t.columns),t.columns==null&&(t.autoColumns=!0),this._table=new Tabulator(this._container,t),typeof Shiny=="object"&&(c(this._table,this._container),this._addShinyMessageHandler())}_addShinyMessageHandler(){let e=`tabulator-${this._container.id}`;Shiny.addCustomMessageHandler(e,n=>{console.log(n),r(this._container,this._table,n.calls)})}getTable(){return this._table}};var u=class extends Shiny.OutputBinding{find(e){return e.find(".shiny-tabulator-output")}renderValue(e,n){console.log("payload",n),new l(e,n.data,n.options).getTable().on("tableBuilt",function(){n.options.columnUpdates!=null&&console.log("column updates",n.options.columnUpdates)})}};Shiny.outputBindings.register(new u,"shiny-tabulator-output");})();
diff --git a/pytabulator/tabulator.py b/pytabulator/tabulator.py
index 5245f7e..83e3a8f 100644
--- a/pytabulator/tabulator.py
+++ b/pytabulator/tabulator.py
@@ -1,19 +1,19 @@
from __future__ import annotations
-from pandas import DataFrame
-
-from ._types import TableOptions
-from ._utils import df_to_dict
+from typing import Any
+from pandas import DataFrame
-# TODO: Move somewhere else!?
-def jsonifiable_table_options(
- table_options: TableOptions | dict,
-) -> dict:
- if isinstance(table_options, TableOptions):
- return table_options.to_dict()
+try:
+ from typing_extensions import Self
+except ImportError:
+ from typing import Self
- return table_options
+from ._utils import as_camel_dict_recursive, df_to_dict
+from .editors import Editor
+from .formatters import Formatter
+from .tabulator_options import TabulatorOptions
+from .utils import create_columns
class Tabulator(object):
@@ -21,24 +21,150 @@ class Tabulator(object):
Args:
df (DataFrame): A data frame.
- table_options (TableOptions): Table options.
+ options (TabulatorOptions): Setup options.
"""
def __init__(
self,
df: DataFrame,
- table_options: TableOptions | dict = {},
+ options: TabulatorOptions | dict = TabulatorOptions(),
) -> None:
self.df = df
- # self.table_options = table_options
- self._table_options = jsonifiable_table_options(table_options)
+ self._options = (
+ options
+ if isinstance(options, TabulatorOptions)
+ else TabulatorOptions(**options)
+ )
+ if not self._options.columns:
+ self._options.columns = create_columns(self.df)
+
+ @property
+ def columns(self) -> list[dict]:
+ return self._options.columns
+
+ def _find_column(self, col_name: str) -> tuple:
+ for i, col in enumerate(self.columns):
+ if col["field"] == col_name:
+ return i, col
+
+ return None, None
+
+ # Update single column
+ def _update_column(self, col_name: str, **kwargs: Any) -> Self:
+ i, col = self._find_column(col_name)
+ if col is not None:
+ self._options.columns[i] = col | as_camel_dict_recursive(kwargs)
+
+ return self
+
+ # ----- Column generics -----
+ def update_column(self, col_name: str | list, **kwargs: Any) -> Self:
+ col_names = [col_name] if isinstance(col_name, str) else col_name
+ for col_name in col_names:
+ self._update_column(col_name, **kwargs)
+
+ return self
+
+ def update_column2(self, col_name, formatter: dict | Formatter = None, editor: dict | Editor = None, **kwargs: Any) -> Self:
+ if formatter is not None:
+ self.set_column_formatter(col_name, formatter)
+
+ if editor is not None:
+ self.set_column_editor(col_name, editor)
+
+ self._update_column(col_name, **kwargs)
- def options(self, **kwargs) -> Tabulator:
- self._table_options.update(kwargs)
+ return self
+
+ def set_column_formatter(
+ self,
+ col_name: str | list,
+ formatter: str | Formatter,
+ formatter_params: dict = None,
+ **kwargs: Any,
+ ) -> Self:
+ if isinstance(formatter, Formatter):
+ formatter_name = formatter.name
+ formatter_params = formatter.to_dict()
+ else:
+ formatter_name = formatter
+
+ return self.update_column(
+ col_name,
+ **dict(
+ formatter=formatter_name,
+ formatterParams=formatter_params or dict(),
+ **kwargs,
+ ),
+ )
+
+ def set_column_editor(
+ self,
+ col_name: str | list,
+ editor: str | Editor,
+ editor_params: dict = None,
+ validator: Any = None,
+ **kwargs: Any,
+ ) -> Self:
+ if isinstance(editor, Editor):
+ editor_name = editor.name
+ editor_params = editor.to_dict()
+ else:
+ editor_name = editor
+
+ return self.update_column(
+ col_name,
+ **dict(
+ editor=editor_name,
+ editorParams=editor_params or dict(),
+ validator=validator,
+ **kwargs,
+ ),
+ )
+
+ # ----- Column formatters -----
+ def set_column_formatter_star(
+ self, col_name: str | list, stars: int, **kwargs
+ ) -> Self:
+ formatter_params = dict(stars=stars)
+ self.set_column_formatter(
+ col_name, "star", formatter_params, hozAlign="center", **kwargs
+ )
+ return self
+
+ def set_column_formatter_tick_cross(self, col_name: str | list, **kwargs) -> Self:
+ self.set_column_formatter(col_name, "tickCross", **kwargs)
+ return self
+
+ # ----- Column editor -----
+ def set_column_editor_number(
+ self,
+ col_name: str | list,
+ min_value: float = None,
+ max_value: float = None,
+ step: float = None,
+ validator=None,
+ **kwargs,
+ ) -> Self:
+ editor_params = dict(min=min_value, max=max_value, step=step)
+ return self.set_column_editor(
+ col_name, "number", editor_params, validator, **kwargs
+ )
+
+ # ----- Column headers -----
+ def set_column_title(self, col_name: str, title: str, **kwargs) -> Self:
+ return self.update_column(col_name, title=title, **kwargs)
+
+ # ----- Misc -----
+ def set_options(self, **kwargs) -> Self:
+ self._options = self._options.model_copy(update=kwargs)
return self
def to_dict(self) -> dict:
- data = df_to_dict(self.df)
- # data["options"] = jsonifiable_table_options(self.table_options)
- data["options"] = self._table_options
- return data
+ payload = df_to_dict(self.df)
+ payload["options"] = self._options.to_dict()
+ payload["bindingOptions"] = dict(lang="python")
+ return payload
+
+ def to_html(self):
+ pass
diff --git a/pytabulator/tabulator_options.py b/pytabulator/tabulator_options.py
new file mode 100644
index 0000000..3005fd3
--- /dev/null
+++ b/pytabulator/tabulator_options.py
@@ -0,0 +1,78 @@
+from __future__ import annotations
+
+from typing import Literal, Union, Optional
+
+from pydantic import BaseModel, ConfigDict, Field, field_validator
+
+class TabulatorOptions(BaseModel):
+ """Tabulator options.
+
+ Attributes:
+ index (str, optional): The index of the table. Defaults to `id`.
+ header_visible (bool, optional): Whether to display the header of the table. Defaults to `True`.
+ movable_rows (bool, optional): Whether rows are movable or not. Defaults to `False`.
+ group_by: Columns to group by. Defaults to `None`.
+ height (int, optional): Height in px. Defaults to `300`.
+ pagination (bool, optional): Whether to enable pagination. Defaults to `False`.
+ pagination_counter (str, optional): Whether to display counted rows in footer. Defaults to `rows`.
+ pagination_add_row: Where to add rows when pagination is enabled. Defaults to `page`.
+ selectable_rows: Whether a row is selectable. An integer value sets the maximum number of rows, that can be selected.
+ If set to `highlight`, rows do not change state when clicked. Defaults to `highlight`.
+ columns (list, optional): Columns configuration. Defaults to `None`,
+ which means that the default configuration is used.
+ layout: The layout of the table. Defaults to `fitColumns`.
+ add_row_pos: Where to add rows. Defaults to `bottom`.
+ frozen_rows (int, optional): Number of frozen rows. Defaults to `Ǹone`.
+ row_height: Fixed height of rows. Defaults to `None`.
+ history (bool, optional): Whether to enable history. Must be set if `undo` and `redo` is used. Defaults to `False`.
+
+ Note:
+ See [Tabulator Setup Options](https://tabulator.info/docs/5.5/options) for details.
+ """
+
+ index: str = "id"
+ header_visible: Optional[bool] = Field(True, serialization_alias="headerVisible")
+ movable_rows: Optional[bool] = Field(False, serialization_alias="movableRows")
+ group_by: Union[str, list, None] = Field(None, serialization_alias="groupBy")
+ height: Union[int, str, None] = None
+ pagination: Optional[bool] = False
+ pagination_counter: str = Field("rows", serialization_alias="paginationCounter")
+ pagination_add_row: Literal["page", "table"] = Field(
+ "page", serialization_alias="paginationAddRow"
+ )
+ selectable_rows: Union[str, bool, int] = Field(
+ "highlight", serialization_alias="selectableRows"
+ )
+ columns: Optional[list] = None
+ layout: Literal[
+ "fitData", "fitDataFill", "fitDataStretch", "fitDataTable", "fitColumns"
+ ] = "fitColumns"
+ add_row_pos: Literal["bottom", "top"] = Field(
+ "bottom", serialization_alias="addRowPos"
+ )
+ frozen_rows: Optional[int] = Field(None, serialization_alias="frozenRows")
+ row_height: Optional[int] = Field(None, serialization_alias="rowHeight")
+ resizable_column_fit: Optional[bool] = Field(False, serialization_alias="resizableColumnFit")
+ history: Optional[bool] = False
+
+ # New features to be added in the next release
+ """
+ responsiveLayout: str = "hide"
+ columnDefaults: dict = {"tooltip": True}
+ """
+
+ model_config = ConfigDict(
+ validate_assignment=True,
+ extra="allow",
+ # use_enum_values=True
+ )
+
+ @field_validator("height")
+ def validate_height(cls, v):
+ if isinstance(v, int):
+ return f"{v}px"
+
+ return v
+
+ def to_dict(self) -> dict:
+ return self.model_dump(by_alias=True, exclude_none=True)
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..57472cf
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,7 @@
+import pytest
+import pandas as pd
+
+@pytest.fixture
+def persons():
+ data = [["Peter", 10, 10.5], ["Hans", 12, 13.7]]
+ return pd.DataFrame(data, columns=["Name", "Age", "JustANumber"])
diff --git a/tests/test_table.py b/tests/test_table.py
index 4a368a1..a975cda 100644
--- a/tests/test_table.py
+++ b/tests/test_table.py
@@ -1,12 +1,7 @@
import pytest
from pandas import DataFrame
-from pydantic import BaseModel
from pytabulator import Tabulator
-from pytabulator._table_options_dc import TableOptionsDC as TableOptionsDC
-
-# from pytabulator import TableOptions as TableOptionsPydantic
-from pytabulator._table_options_pydantic import TableOptionsPydantic
-
+from pytabulator.tabulator_options import TabulatorOptions
@pytest.fixture
def df() -> DataFrame:
@@ -14,31 +9,15 @@ def df() -> DataFrame:
return DataFrame(data, columns=["Name", "Age"])
-def test_table_dc(df: DataFrame) -> None:
- # Prepare
- table_options = TableOptionsDC(selectable_rows=3)
-
- # Act
- table = Tabulator(df, table_options=table_options)
- table_dict = table.to_dict()
- print(table_dict)
-
- # Assert
- assert list(table_dict.keys()) == ["schema", "data", "options"]
- assert isinstance(table_dict["options"], dict)
- # assert hasattr(table.table_options, "__dataclass_fields__")
-
-
def test_table_pydantic(df: DataFrame) -> None:
# Prepare
- table_options = TableOptionsPydantic(selectable_rows=3)
+ table_options = TabulatorOptions(selectable_rows=3)
# Act
- table = Tabulator(df, table_options=table_options)
+ table = Tabulator(df, options=table_options)
table_dict = table.to_dict()
print(table_dict)
- # assert isinstance(table.table_options, BaseModel)
- # print("pydantic", type(table.table_options))
- assert list(table_dict.keys()) == ["schema", "data", "options"]
+ # Assert
+ assert list(table_dict.keys()) == ["schema", "data", "options", "bindingOptions"]
assert isinstance(table_dict["options"], dict)
diff --git a/tests/test_table_options.py b/tests/test_table_options.py
index 8f390a9..4dc0ce8 100644
--- a/tests/test_table_options.py
+++ b/tests/test_table_options.py
@@ -1,9 +1,6 @@
import pytest
-# from pytabulator import TableOptions
-from pytabulator._table_options_dc import TableOptionsDC
-from pytabulator._table_options_dc import TableOptionsDC as TableOptionsDC
-from pytabulator._table_options_pydantic import TableOptionsPydantic as TableOptions
+from pytabulator.tabulator_options import TabulatorOptions
@pytest.fixture
@@ -18,17 +15,11 @@ def some_table_options():
def test_table_options(some_table_options):
# Prepare
- table_options_pydantic = TableOptions(**some_table_options)
- print("pydantic", table_options_pydantic)
-
- table_options_dc = TableOptionsDC(**some_table_options)
- print("dc", table_options_dc)
+ table_options = TabulatorOptions(**some_table_options)
# Act
- table_options_pydantic_dict = table_options_pydantic.to_dict()
- table_options_dc_dict = table_options_dc.to_dict()
+ table_options_dict = table_options.to_dict()
# Assert
- assert list(table_options_pydantic_dict.items()).sort(
- key=lambda item: item[0]
- ) == list(table_options_dc_dict.items()).sort(key=lambda item: item[0])
+ print(table_options_dict)
+ assert table_options_dict["movableRows"] == False
diff --git a/tests/test_tabulator_columns.py b/tests/test_tabulator_columns.py
new file mode 100644
index 0000000..5cde48a
--- /dev/null
+++ b/tests/test_tabulator_columns.py
@@ -0,0 +1,17 @@
+from pytabulator import Tabulator
+
+
+def test_tabulator_columns(persons):
+ # print(persons)
+ table = Tabulator(persons)
+
+ print(table.columns)
+
+ col = table._find_column("Name")
+ print(col)
+
+ table = table.update_column("Name", editor = True)
+ print(table.columns)
+
+ table = table.set_column_formatter("Age", "html", hozAlign="center")
+ print(table.columns)