Skip to content

Commit

Permalink
feat: implement editable dataframe to manage dataframe editor component
Browse files Browse the repository at this point in the history
* feat: implement the method record to read a specific record
  • Loading branch information
FabienArcellier committed Jul 14, 2024
1 parent bdd5e00 commit 27870b2
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 19 deletions.
28 changes: 13 additions & 15 deletions docs/framework/dataframe.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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),
Expand Down
75 changes: 72 additions & 3 deletions src/writer/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}})
Expand All @@ -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}})
Expand All @@ -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})
Expand Down Expand Up @@ -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':
"""
Expand Down Expand Up @@ -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'])
Expand Down Expand Up @@ -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'])
Expand Down Expand Up @@ -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:
Expand Down
80 changes: 79 additions & 1 deletion tests/backend/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"],
Expand Down Expand Up @@ -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"],
Expand Down Expand Up @@ -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 = [
Expand Down

0 comments on commit 27870b2

Please sign in to comment.