Repository: holoviz/panel

Issue:
Tabulator text_align is not working with Bokeh NumberFormatter
#### ALL software version info
panel 1.3.0, bokeh 3.2.2

#### Description of expected behavior and the observed behavior

When using Bokeh NumberFormatter column gets left aligned no matter what is passed to text_align, without the formatters the alignment is working as expected.

#### Complete, minimal, self-contained example code that reproduces the issue

```
import pandas as pd
import panel as pn
from bokeh.models.widgets.tables import NumberFormatter

pn.extension('tabulator') 

df = pd.DataFrame({
    "float": [1.23, 4.56, 7.89, -1.23, -4.56, -7.89],
    "int": [10, 20, 30, -10, -20, -30],
    "pct": [0.25, 0.50, 0.75, -0.25, -0.50, -0.75]
})

pn.widgets.Tabulator(
    df,
    formatters = {
        'float': NumberFormatter(format='0,0.0'),
        'int': NumberFormatter(format='0,0'),
        'pct': NumberFormatter(format='0%'),
    },
    text_align = {
        'float': 'right',
        'int': 'right',
        'pct': 'right',
    },
)

```

#### Stack traceback and/or browser JavaScript console output

#### Screenshots or screencasts of the bug in action

![image](https://github.com/holoviz/panel/assets/79541294/e264a4c3-6ebe-4cda-b5d9-7aab50367a5b)


- [ ] I may be interested in making a pull request to address this

Links to Bokeh formatters give 404 not found
- Go to https://panel.holoviz.org/reference/widgets/Tabulator.html
- Try the formatter links

![image](https://github.com/holoviz/panel/assets/42288570/58ff2daa-79a8-416e-a83d-743d27e9d276)

- Get 404


![image](https://github.com/holoviz/panel/assets/42288570/483333a0-4c4e-404e-bbc1-f7c7b08cd3b3)



Discussion:
As a temporary work-around you can use: `NumberFormatter(format="0,0.0", text_align="right")`

``` python
import pandas as pd
import panel as pn
from bokeh.models.widgets.tables import NumberFormatter

pn.extension("tabulator")

df = pd.DataFrame(
    {
        "float": [1.23, 4.56, 7.89, -1.23, -4.56, -7.89],
        "int": [10, 20, 30, -10, -20, -30],
        "pct": [0.25, 0.50, 0.75, -0.25, -0.50, -0.75],
    }
)

pn.widgets.Tabulator(
    df,
    formatters={
        "float": NumberFormatter(format="0,0.0", text_align="right"),
        "int": NumberFormatter(format="0,0", text_align="right"),
        "pct": NumberFormatter(format="0%", text_align="right"),
    },
)
```

![image](https://github.com/holoviz/panel/assets/19758978/895f4fad-1c9f-450f-ae6e-acabb86d1653)

Which means we can simply set the `text_align` on the bokeh formatter if provided.
Curious about what should take priority in a case like this:

``` python

pn.widgets.Tabulator(
    df,
    formatters = {
        'float': NumberFormatter(format='0,0.0', text_align='left'),
    },
    text_align = {
        'float': 'right',
    },
)
```
Knew you were going to ask that :)

I'd say we warn if the Formatter `text_align` value is non-default and an explicit text_align is provided and then apply the explicit `text_align` value.


Resolving patch:
diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py
--- a/panel/widgets/tables.py
+++ b/panel/widgets/tables.py
@@ -32,6 +32,7 @@
     BOKEH_JS_NAT, clone_model, datetime_as_utctimestamp, isdatetime, lazy_load,
     styler_update, updating,
 )
+from ..util.warnings import warn
 from .base import Widget
 from .button import Button
 from .input import TextInput
@@ -176,34 +177,18 @@ def _get_column_definitions(self, col_names: List[str], df: pd.DataFrame) -> Lis
             col_kwargs = {}
             kind = data.dtype.kind
             editor: CellEditor
-            formatter: CellFormatter
+            formatter: CellFormatter | None = self.formatters.get(col)
             if kind == 'i':
-                formatter = NumberFormatter(text_align='right')
                 editor = IntEditor()
             elif kind == 'b':
-                formatter = StringFormatter(text_align='center')
                 editor = CheckboxEditor()
             elif kind == 'f':
-                formatter = NumberFormatter(format='0,0.0[00000]', text_align='right')
                 editor = NumberEditor()
             elif isdatetime(data) or kind == 'M':
-                if len(data) and isinstance(data.values[0], dt.date):
-                    date_format = '%Y-%m-%d'
-                else:
-                    date_format = '%Y-%m-%d %H:%M:%S'
-                formatter = DateFormatter(format=date_format, text_align='right')
                 editor = DateEditor()
             else:
-                formatter = StringFormatter()
                 editor = StringEditor()
 
-            if isinstance(self.text_align, str):
-                formatter.text_align = self.text_align
-            elif col in self.text_align:
-                formatter.text_align = self.text_align[col]
-            elif col in self.indexes:
-                formatter.text_align = 'left'
-
             if col in self.editors and not isinstance(self.editors[col], (dict, str)):
                 editor = self.editors[col]
                 if isinstance(editor, CellEditor):
@@ -212,10 +197,43 @@ def _get_column_definitions(self, col_names: List[str], df: pd.DataFrame) -> Lis
             if col in indexes or editor is None:
                 editor = CellEditor()
 
-            if col in self.formatters and not isinstance(self.formatters[col], (dict, str)):
-                formatter = self.formatters[col]
+            if formatter is None or isinstance(formatter, (dict, str)):
+                if kind == 'i':
+                    formatter = NumberFormatter(text_align='right')
+                elif kind == 'b':
+                    formatter = StringFormatter(text_align='center')
+                elif kind == 'f':
+                    formatter = NumberFormatter(format='0,0.0[00000]', text_align='right')
+                elif isdatetime(data) or kind == 'M':
+                    if len(data) and isinstance(data.values[0], dt.date):
+                        date_format = '%Y-%m-%d'
+                    else:
+                        date_format = '%Y-%m-%d %H:%M:%S'
+                    formatter = DateFormatter(format=date_format, text_align='right')
+                else:
+                    formatter = StringFormatter()
+
+                default_text_align = True
+            else:
                 if isinstance(formatter, CellFormatter):
                     formatter = clone_model(formatter)
+                if hasattr(formatter, 'text_align'):
+                    default_text_align = type(formatter).text_align.class_default(formatter) == formatter.text_align
+                else:
+                    default_text_align = True
+
+            if isinstance(self.text_align, str):
+                formatter.text_align = self.text_align
+                if not default_text_align:
+                    msg = f"The 'text_align' in Tabulator.formatters[{col!r}] is overridden by Tabulator.text_align"
+                    warn(msg, RuntimeWarning)
+            elif col in self.text_align:
+                formatter.text_align = self.text_align[col]
+                if not default_text_align:
+                    msg = f"The 'text_align' in Tabulator.formatters[{col!r}] is overridden by Tabulator.text_align[{col!r}]"
+                    warn(msg, RuntimeWarning)
+            elif col in self.indexes:
+                formatter.text_align = 'left'
 
             if isinstance(self.widths, int):
                 col_kwargs['width'] = self.widths


Test patch:
diff --git a/panel/tests/widgets/test_tables.py b/panel/tests/widgets/test_tables.py
--- a/panel/tests/widgets/test_tables.py
+++ b/panel/tests/widgets/test_tables.py
@@ -2113,3 +2113,26 @@ def test_tabulator_hidden_columns_fix():
     table = Tabulator(pd.DataFrame(), show_index=False)
     table.hidden_columns = ["a", "b", "c"]
     assert table.hidden_columns == ["a", "b", "c"]
+
+@pytest.mark.parametrize('align', [{"x": "right"}, "right"], ids=["dict", "str"])
+def test_bokeh_formatter_with_text_align(align):
+    # https://github.com/holoviz/panel/issues/5807
+    data = pd.DataFrame({"x": [1.1, 2.0, 3.47]})
+    formatters = {"x": NumberFormatter(format="0.0")}
+    assert formatters["x"].text_align == "left"  # default
+    model = Tabulator(data, formatters=formatters, text_align=align)
+    columns = model._get_column_definitions("x", data)
+    output = columns[0].formatter.text_align
+    assert output == "right"
+
+@pytest.mark.parametrize('align', [{"x": "right"}, "right"], ids=["dict", "str"])
+def test_bokeh_formatter_with_text_align_conflict(align):
+    # https://github.com/holoviz/panel/issues/5807
+    data = pd.DataFrame({"x": [1.1, 2.0, 3.47]})
+    formatters = {"x": NumberFormatter(format="0.0", text_align="center")}
+    model = Tabulator(data, formatters=formatters, text_align=align)
+    msg = r"The 'text_align' in Tabulator\.formatters\['x'\] is overridden by Tabulator\.text_align"
+    with pytest.warns(RuntimeWarning, match=msg):
+        columns = model._get_column_definitions("x", data)
+    output = columns[0].formatter.text_align
+    assert output == "right"