diff --git a/docs/framework/dataframe.mdx b/docs/framework/dataframe.mdx index af2fa05fd..864cbdfb7 100644 --- a/docs/framework/dataframe.mdx +++ b/docs/framework/dataframe.mdx @@ -3,13 +3,13 @@ title: "Dataframe" --- **writer framework places the dataframe at the core of the application**. This is a great way for modeling a complex and massive data system. -it offers components as `dataframe` and `dataframe editor` to manipulate dataframes. These components allow you to visualize and interact with dataframes. +it offers components as `dataframe` to manipulate dataframes. These components allow you to visualize and interact with dataframes. -| compatibility | dataframe | dataframe editor | -|--------------------|---------------------------------------|-------------------------------| -| `pandas.DataFrame` | x | x | -| `polar.DataFrame` | x | x | -| `list of records` | x (with `EditableDataframe`) | x (with `EditableDataframe`) | +| compatibility | dataframe | +|--------------------|---------------------------------------| +| `pandas.DataFrame` | x | +| `polar.DataFrame` | x | +| `list of records` | x (with `EditableDataframe`) | ### Use a dataframe @@ -44,7 +44,7 @@ wf.init_state({ #### Handle events from a dataframe editor -**The dataframe editor emits events when an action is performed**. You must subscribe to events to integrate changes to the state of the application. +**The dataframe component emits events when an action is performed**. You must subscribe to events to integrate changes to the state of the application. ```python import pandas @@ -55,28 +55,27 @@ wf.init_state({ 'mydf': wf.EditableDataframe(df) }) -# Subscribe this event handler to the `wf-dfeditor-add` event +# Subscribe this event handler to the `wf-dataframe-add` event def on_record_add(state, payload): payload['record']['sales'] = 0 # default value inside the dataframe state['mydf'].record_add(payload) -# Subscribe this event handler to the `wf-dfeditor-update` event +# Subscribe this event handler to the `wf-dataframe-update` event def on_record_change(state, payload): state['mydf'].record_update(payload) -# Subscribe this event handler to the `wf-dfeditor-action` event +# Subscribe this event handler to the `wf-dataframe-action` event def on_record_action(state, payload): """ This event corresponds to a quick action in the drop-down menu to the left of the dataframe. """ + record_index = payload['record_index'] if payload.action == 'remove': - state['mydf'].record_remove(payload) - if payload.action == 'important': - state['mydf'].record(payload.id).update('flag', True) # update the column flag of the dataframe to true, trigger une mutation record_update + state['mydf'].record_remove(payload) if payload.action == 'open': - state['record'] = state['df'].record(payload.id) + state['record'] = state['df'].record(record_index) # dict representation of record ``` #### Alternative to pandas.DataFrame @@ -93,7 +92,6 @@ panda_df = pandas.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]}) polars_df = polars.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]}) list_of_records = [{'a': 1, 'b': 4}, {'a': 2, 'b': 5}, {'a': 3, 'b': 6}] list_of_records = [[1, 4], [2, 5], [3, 6]] -list_of_records = ["a", "b", "c"] wf.init_state({ 'mypandas': wf.EditableDataframe(panda_df), diff --git a/src/writer/core.py b/src/writer/core.py index 229d11879..0d82bd83a 100644 --- a/src/writer/core.py +++ b/src/writer/core.py @@ -1610,10 +1610,20 @@ def match(df: Any) -> bool: """ raise NotImplementedError + @staticmethod + def record(df: Any, record_index: int) -> dict: + """ + This method read a record at the given line and get it back as dictionary + + >>> edf = EditableDataframe(df) + >>> r = edf.record(1) + """ + raise NotImplementedError + @staticmethod def record_add(df: Any, payload: DataframeRecordAdded) -> Any: """ - signature of the methods to be implemented to process wf-dfeditor-add event + signature of the methods to be implemented to process wf-dataframe-add event >>> edf = EditableDataframe(df) >>> edf.record_add({"record": {"a": 1, "b": 2}}) @@ -1623,7 +1633,7 @@ def record_add(df: Any, payload: DataframeRecordAdded) -> Any: @staticmethod def record_update(df: Any, payload: DataframeRecordUpdated) -> Any: """ - signature of the methods to be implemented to process wf-dfeditor-update event + signature of the methods to be implemented to process wf-dataframe-update event >>> edf = EditableDataframe(df) >>> edf.record_update({"record_index": 12, "record": {"a": 1, "b": 2}}) @@ -1633,7 +1643,7 @@ def record_update(df: Any, payload: DataframeRecordUpdated) -> Any: @staticmethod def record_remove(df: Any, payload: DataframeRecordRemoved) -> Any: """ - signature of the methods to be implemented to process wf-dfeditor-remove event + signature of the methods to be implemented to process wf-dataframe-action event >>> edf = EditableDataframe(df) >>> edf.record_remove({"record_index": 12}) @@ -1663,6 +1673,27 @@ def match(df: Any) -> bool: import pandas return True if isinstance(df, pandas.DataFrame) else False + @staticmethod + def record(df: 'pandas.DataFrame', record_index: int) -> dict: + """ + + >>> edf = EditableDataframe(df) + >>> r = edf.record(1) + """ + import pandas + + record = df.iloc[record_index] + if not isinstance(df.index, pandas.RangeIndex): + index_list = df.index.tolist() + record_index_content = index_list[record_index] + if isinstance(record_index_content, tuple): + for i, n in enumerate(df.index.names): + record[n] = record_index_content[i] + else: + record[df.index.names[0]] = record_index_content + + return dict(record) + @staticmethod def record_add(df: 'pandas.DataFrame', payload: DataframeRecordAdded) -> 'pandas.DataFrame': """ @@ -1734,6 +1765,21 @@ def match(df: Any) -> bool: import polars return True if isinstance(df, polars.DataFrame) else False + @staticmethod + def record(df: 'polars.DataFrame', record_index: int) -> dict: + """ + + >>> edf = EditableDataframe(df) + >>> r = edf.record(1) + """ + record = {} + r = df[record_index] + for c in r.columns: + record[c] = df[record_index, c] + + return record + + @staticmethod def record_add(df: 'polars.DataFrame', payload: DataframeRecordAdded) -> 'polars.DataFrame': _assert_record_match_polar_df(df, payload['record']) @@ -1787,6 +1833,17 @@ class RecordListRecordProcessor(DataframeRecordProcessor): def match(df: Any) -> bool: return True if isinstance(df, list) else False + + @staticmethod + def record(df: List[Dict[str, Any]], record_index: int) -> dict: + """ + + >>> edf = EditableDataframe(df) + >>> r = edf.record(1) + """ + r = df[record_index] + return copy.copy(r) + @staticmethod def record_add(df: List[Dict[str, Any]], payload: DataframeRecordAdded) -> List[Dict[str, Any]]: _assert_record_match_list_of_records(df, payload['record']) @@ -1916,6 +1973,18 @@ def pyarrow_table(self) -> pyarrow.Table: pa_table = self.processor.pyarrow_table(self.df) return pa_table + def record(self, record_index: int): + """ + Retrieves a specific record in dictionary form. + + :param record_index: + :return: + """ + assert self.processor is not None + + record = self.processor.record(self.df, record_index) + return record + S = TypeVar("S", bound=WriterState) def new_initial_state(klass: Type[S], raw_state: dict) -> S: diff --git a/tests/backend/test_core.py b/tests/backend/test_core.py index ee1ba0ae9..b0b9aa8cd 100644 --- a/tests/backend/test_core.py +++ b/tests/backend/test_core.py @@ -24,7 +24,8 @@ State, StateSerialiser, StateSerialiserException, - WriterState, import_failure, + WriterState, + import_failure, ) from writer.core_ui import Component from writer.ss_types import WriterEvent @@ -1087,6 +1088,54 @@ def test_editable_dataframe_register_mutation_when_df_is_updated(self) -> None: # Then assert edf.mutated() is True + def test_editable_dataframe_should_read_record_as_dict_based_on_record_index(self) -> None: + df = pandas.DataFrame({ + "name": ["Alice", "Bob", "Charlie"], + "age": [25, 30, 35] + }) + edf = wf.EditableDataframe(df) + + # When + r = edf.record(0) + + # Then + assert r['name'] == 'Alice' + assert r['age'] == 25 + + def test_editable_dataframe_should_read_record_as_dict_based_on_record_index_when_dataframe_has_index(self) -> None: + df = pandas.DataFrame({ + "name": ["Alice", "Bob", "Charlie"], + "age": [25, 30, 35] + }) + df = df.set_index('name') + + edf = wf.EditableDataframe(df) + + # When + r = edf.record(0) + + # Then + assert r['name'] == 'Alice' + assert r['age'] == 25 + + def test_editable_dataframe_should_read_record_as_dict_based_on_record_index_when_dataframe_has_multi_index(self) -> None: + df = pandas.DataFrame({ + "name": ["Alice", "Bob", "Charlie"], + "age": [25, 30, 35], + "city": ["Paris", "London", "New York"] + }) + df = df.set_index(['name', 'city']) + + edf = wf.EditableDataframe(df) + + # When + r = edf.record(0) + + # Then + assert r['name'] == 'Alice' + assert r['age'] == 25 + assert r['city'] == 'Paris' + def test_editable_dataframe_should_process_new_record_into_dataframe(self) -> None: df = pandas.DataFrame({ "name": ["Alice", "Bob", "Charlie"], @@ -1192,6 +1241,20 @@ def test_editable_dataframe_expose_polar_dataframe_in_df_property(self) -> None: assert edf.df is not None assert isinstance(edf.df, polars.DataFrame) + def test_editable_dataframe_should_read_record_from_polar_as_dict_based_on_record_index(self) -> None: + df = polars.DataFrame({ + "name": ["Alice", "Bob", "Charlie"], + "age": [25, 30, 35] + }) + edf = wf.EditableDataframe(df) + + # When + r = edf.record(0) + + # Then + assert r['name'] == 'Alice' + assert r['age'] == 25 + def test_editable_dataframe_should_process_new_record_into_polar_dataframe(self) -> None: df = polars.DataFrame({ "name": ["Alice", "Bob", "Charlie"], @@ -1262,6 +1325,21 @@ def test_editable_dataframe_expose_list_of_records_in_df_property(self) -> None: assert edf.df is not None assert isinstance(edf.df, list) + def test_editable_dataframe_should_read_record_from_list_of_record_as_dict_based_on_record_index(self) -> None: + records = [ + {"name": "Alice", "age": 25}, + {"name": "Bob", "age": 30}, + {"name": "Charlie", "age": 35} + ] + + edf = wf.EditableDataframe(records) + + # When + r = edf.record(0) + + # Then + assert r['name'] == 'Alice' + assert r['age'] == 25 def test_editable_dataframe_should_process_new_record_into_list_of_records(self) -> None: records = [