# DataGrid Formatting ## Implementation Status | Component | Status | Location | |-----------|--------|----------| | **Core Module** | | `src/myfasthtml/core/formatting/` | | Dataclasses (Condition, Style, Formatter, FormatRule) | :white_check_mark: Implemented | `dataclasses.py` | | Style Presets (DaisyUI 5) | :white_check_mark: Implemented | `presets.py` | | Formatter Presets (EUR, USD, etc.) | :white_check_mark: Implemented | `presets.py` | | ConditionEvaluator (12 operators) | :white_check_mark: Implemented | `condition_evaluator.py` | | StyleResolver | :white_check_mark: Implemented | `style_resolver.py` | | FormatterResolver (Number, Date, Boolean, Text, Enum) | :white_check_mark: Implemented | `formatter_resolver.py` | | FormattingEngine (facade + conflict resolution) | :white_check_mark: Implemented | `engine.py` | | **Condition Features** | | | | `col` parameter (row-level conditions) | :white_check_mark: Implemented | | | `row` parameter (column-level conditions) | :x: Not implemented | | | Column reference in value `{"col": "..."}` | :white_check_mark: Implemented | | | **DataGrid Integration** | | | | Integration in `mk_body_cell_content()` | :x: Not implemented | | | DataGridsManager (global presets) | :white_check_mark: Implemented | `DataGridsManager.py` | | **Tests** | | `tests/core/formatting/` | | test_condition_evaluator.py | :white_check_mark: ~45 test cases | | | test_style_resolver.py | :white_check_mark: ~12 test cases | | | test_formatter_resolver.py | :white_check_mark: ~40 test cases | | | test_engine.py | :white_check_mark: ~18 test cases | | --- ## Overview This document describes the formatting capabilities for the DataGrid component. **Formatting applies at three levels:** | Level | Cells Targeted | Condition Evaluated On | |------------|-------------------------|----------------------------------------------| | **Cell** | 1 specific cell | The cell value | | **Row** | All cells in the row | Each cell value (or fixed column with `col`) | | **Column** | All cells in the column | Each cell value (or fixed row with `row`) | --- ## Format Rule Structure A format is a **list** of rules. Each rule is an object: ```json { "condition": {}, "style": {}, "formatter": {} } ``` **Rules:** - `style` and `formatter` can appear alone (unconditional formatting) - `condition` **cannot** appear alone - must be paired with `style` and/or `formatter` - If `condition` is present, the `style`/`formatter` is applied **only if** the condition is met - Rules are evaluated in order; multiple rules can match --- ## Conflict Resolution When multiple rules match the same cell: 1. **Specificity** = number of conditions in the rule 2. **Higher specificity wins** 3. **At equal specificity, last rule wins entirely** (no fusion) ```json [ { "style": { "color": "gray" } }, { "condition": { "operator": "<", "value": 0 }, "style": { "color": "red" } }, { "condition": { "operator": "==", "value": -5 }, "style": { "color": "black" } } ] ``` For `value = -5`: Rule 3 wins (same specificity as rule 2, but defined later). --- ## Condition Structure ### Fields | Field | Type | Default | Required | Description | Status | |------------------|------------------------|---------|----------|---------------------------------------------|--------| | `operator` | string | - | Yes | Comparison operator | :white_check_mark: | | `value` | scalar / list / object | - | Depends | Value to compare against | :white_check_mark: | | `not` | bool | `false` | No | Inverts the condition result | :white_check_mark: (as `negate`) | | `case_sensitive` | bool | `false` | No | Case-sensitive string comparison | :white_check_mark: | | `col` | string | - | No | Reference column (for row-level conditions) | :white_check_mark: | | `row` | int | - | No | Reference row (for column-level conditions) | :x: Not implemented | ### Operators All operators are :white_check_mark: **implemented**. | Operator | Description | Value Required | |--------------|--------------------------|------------------| | `==` | Equal | Yes | | `!=` | Not equal | Yes | | `<` | Less than | Yes | | `<=` | Less than or equal | Yes | | `>` | Greater than | Yes | | `>=` | Greater than or equal | Yes | | `contains` | String contains | Yes | | `startswith` | String starts with | Yes | | `endswith` | String ends with | Yes | | `in` | Value in list | Yes (list) | | `between` | Value between two values | Yes ([min, max]) | | `isempty` | Value is empty/null | No | | `isnotempty` | Value is not empty/null | No | ### Value Types **Literal value:** ```json { "operator": "<", "value": 0 } { "operator": "in", "value": [ "A", "B", "C" ] } ``` **Cell reference (compare with another column):** ```json { "operator": ">", "value": { "col": "budget" } } ``` ### Negation Use the `not` flag instead of separate operators: ```json { "operator": "in", "value": [ "A", "B" ], "not": true } { "operator": "contains", "value": "error", "not": true } ``` ### Case Sensitivity String comparisons are **case-insensitive by default**. ```json { "operator": "==", "value": "Error", "case_sensitive": true } ``` ### Evaluation Behavior | Situation | Behavior | |---------------------------|-----------------------------------| | Cell value is `null` | Condition = `false` | | Referenced cell is `null` | Condition = `false` | | Type mismatch | Condition = `false` (no coercion) | | String operators | Converts value to string first | ### Examples ```json // Row-level: highlight if "status" column == "error" { "col": "status", "operator": "==", "value": "error" } // Column-level: bold if row 0 has value "Total" { "row": 0, "operator": "==", "value": "Total" } // Compare with another column { "operator": ">", "value": { "col": "budget" } } // Negated condition { "operator": "in", "value": [ "draft", "pending" ], "not": true } ``` --- ## Style Structure :white_check_mark: **Fully implemented** in `style_resolver.py` ### Fields | Field | Type | Default | Description | |--------------------|--------|------------|---------------------------------------------------| | `preset` | string | - | Preset name (applied first, can be overridden) | | `background_color` | string | - | Background color (hex, CSS name, or CSS variable) | | `color` | string | - | Text color | | `font_weight` | string | `"normal"` | `"normal"` or `"bold"` | | `font_style` | string | `"normal"` | `"normal"` or `"italic"` | | `font_size` | string | - | Font size (`"12px"`, `"0.9em"`) | | `text_decoration` | string | `"none"` | `"none"`, `"underline"`, `"line-through"` | ### Example ```json { "style": { "preset": "success", "font_weight": "bold" } } ``` ### Default Presets (DaisyUI 5) | Preset | Background | Text | |-------------|--------------------------|----------------------------------| | `primary` | `var(--color-primary)` | `var(--color-primary-content)` | | `secondary` | `var(--color-secondary)` | `var(--color-secondary-content)` | | `accent` | `var(--color-accent)` | `var(--color-accent-content)` | | `neutral` | `var(--color-neutral)` | `var(--color-neutral-content)` | | `info` | `var(--color-info)` | `var(--color-info-content)` | | `success` | `var(--color-success)` | `var(--color-success-content)` | | `warning` | `var(--color-warning)` | `var(--color-warning-content)` | | `error` | `var(--color-error)` | `var(--color-error-content)` | All presets default to `font_weight: "normal"`, `font_style: "normal"`, `text_decoration: "none"`. ### Resolution Logic 1. If `preset` is specified, apply all preset properties 2. Override with any explicit properties **No style fusion:** When multiple rules match, the winning rule's style applies entirely. --- ## Formatter Structure Formatters transform cell values for display without changing the underlying data. ### Usage ```json { "formatter": { "preset": "EUR" } } { "formatter": { "preset": "EUR", "precision": 3 } } ``` ### Error Handling If formatting fails (e.g., non-numeric value for `number` formatter), display `"⚠"`. --- ## Formatter Types All formatter types are :white_check_mark: **implemented** in `formatter_resolver.py`. ### `number` For numbers, currencies, and percentages. | Property | Type | Default | Description | |-----------------|--------|---------|-------------------------------| | `prefix` | string | `""` | Text before value | | `suffix` | string | `""` | Text after value | | `thousands_sep` | string | `""` | Thousands separator | | `decimal_sep` | string | `"."` | Decimal separator | | `precision` | int | `0` | Number of decimal places | | `multiplier` | number | `1` | Multiply value before display | ### `date` For dates and datetimes. | Property | Type | Default | Description | |----------|--------|--------------|-------------------------| | `format` | string | `"%Y-%m-%d"` | strftime format pattern | ### `boolean` For true/false values. | Property | Type | Default | Description | |---------------|--------|-----------|-------------------| | `true_value` | string | `"true"` | Display for true | | `false_value` | string | `"false"` | Display for false | | `null_value` | string | `""` | Display for null | ### `text` For text transformations. | Property | Type | Default | Description | |--------------|--------|---------|----------------------------------------------| | `transform` | string | - | `"uppercase"`, `"lowercase"`, `"capitalize"` | | `max_length` | int | - | Truncate if exceeded | | `ellipsis` | string | `"..."` | Suffix when truncated | ### `enum` For mapping values to display labels. Also used for Select dropdowns. | Property | Type | Default | Description | |---------------|--------|------------------|------------------------------------| | `source` | object | - | Data source (see below) | | `default` | string | `""` | Label for unknown values | | `allow_empty` | bool | `true` | Show empty option in Select | | `empty_label` | string | `"-- Select --"` | Label for empty option | | `order_by` | string | `"source"` | `"source"`, `"display"`, `"value"` | #### Source Types **Static mapping:** :white_check_mark: Implemented ```json { "type": "enum", "source": { "type": "mapping", "value": { "draft": "Brouillon", "pending": "En attente", "approved": "Approuvé" } }, "default": "Inconnu" } ``` **From another DataGrid:** :white_check_mark: Implemented (requires `lookup_resolver` injection) ```json { "type": "enum", "source": { "type": "datagrid", "value": "categories_grid", "value_column": "id", "display_column": "name" } } ``` #### Empty Value Behavior - `allow_empty: true` → Empty option displayed with `empty_label` - `allow_empty: false` → First entry selected by default --- ## Default Formatter Presets ```python formatter_presets = { "EUR": { "type": "number", "suffix": " €", "thousands_sep": " ", "decimal_sep": ",", "precision": 2 }, "USD": { "type": "number", "prefix": "$", "thousands_sep": ",", "decimal_sep": ".", "precision": 2 }, "percentage": { "type": "number", "suffix": "%", "precision": 1, "multiplier": 100 }, "short_date": { "type": "date", "format": "%d/%m/%Y" }, "iso_date": { "type": "date", "format": "%Y-%m-%d" }, "yes_no": { "type": "boolean", "true_value": "Yes", "false_value": "No" } } ``` --- ## Storage Architecture :warning: **Structures exist but integration with formatting engine not implemented** ### Format Storage Location | Level | Storage | Key | Status | |------------|------------------------------|---------|--------| | **Column** | `DataGridColumnState.format` | - | Structure exists | | **Row** | `DataGridRowState.format` | - | Structure exists | | **Cell** | `DatagridState.cell_formats` | Cell ID | Structure exists | ### Cell ID Format ``` tcell_{datagrid_id}-{row_index}-{col_index} ``` --- ## DataGridsManager :white_check_mark: **Implemented** in `src/myfasthtml/controls/DataGridsManager.py` Global presets stored as instance attributes: | Property | Type | Description | Status | |---------------------|--------|-------------------------------------------|--------| | `style_presets` | dict | Style presets (primary, success, etc.) | :white_check_mark: | | `formatter_presets` | dict | Formatter presets (EUR, percentage, etc.) | :white_check_mark: | | `default_locale` | string | Default locale for number/date formatting | :x: Not implemented | **Methods:** | Method | Description | |--------|-------------| | `get_style_presets()` | Get the global style presets | | `get_formatter_presets()` | Get the global formatter presets | | `add_style_preset(name, preset)` | Add or update a style preset | | `add_formatter_preset(name, preset)` | Add or update a formatter preset | | `remove_style_preset(name)` | Remove a style preset | | `remove_formatter_preset(name)` | Remove a formatter preset | **Usage:** ```python # Add custom presets manager.add_style_preset("highlight", {"background-color": "yellow", "color": "black"}) manager.add_formatter_preset("CHF", {"type": "number", "prefix": "CHF ", "precision": 2}) ``` --- ## Future Considerations All items below are :x: **not implemented**. - **`row` parameter for column-level conditions**: Evaluate condition on a specific row - **AND/OR conditions**: Add explicit `and`/`or` operators if `between`/`in` prove insufficient - **Cell references**: Extend to `{"col": "x", "row": 0}` for specific cell and `{"col": "x", "row_offset": -1}` for relative references - **Enum cascade (draft)**: Dependent dropdowns with `depends_on` and `filter_column` ```json { "source": { "type": "datagrid", "value": "cities_grid", "value_column": "id", "display_column": "name", "filter_column": "country_id" }, "depends_on": "country" } ``` - **API source for enum**: `{"type": "api", "value": "https://...", ...}` - **Searchable enum**: For large option lists - **Formatter chaining**: Apply multiple formatters in sequence - **DataGrid integration**: Connect `FormattingEngine` to `DataGrid.mk_body_cell_content()`