Open maxmoro opened 3 months ago
I'm trying to edit the data in the data_frame, without re-render the data_frame output, so I can keep the filter/sort the user performed.
Here is a test code, where when pressing the 'click' button, the second column of the second line should become 'xxx'. But it is not working
I'm using the .set_patches_fn
inside the click
event. I know it is not "by-the-book", I'm just wondering if it is possible in some way.
and the code
from palmerpenguins import load_penguins
from shiny import App, render, ui, reactive, Outputs
penguins = load_penguins()
app_ui = ui.page_fluid(
ui.h2("Palmer Penguins"),
ui.input_action_button('click','click'),
ui.output_ui("rows"),
ui.output_data_frame("penguins_df"),
)
def server(input, output, session):
@render.data_frame
def penguins_df():
return render.DataTable(penguins, editable=True,)
@reactive.Effect
@reactive.event(input.click)
def click():
print('click')
def edtx() -> list[render.CellPatch]():
out = ({'row_index': 1, 'column_index': 1, 'value': 'xxx'},)
print(out)
return(out)
penguins_df.set_patches_fn(edtx())
#just testing if it works when editing another cell
@penguins_df.set_patches_fn
def edt(*, patches: list[render.CellPatch]) -> list[render.CellPatch]:
out = ({'row_index': 1, 'column_index': 1, 'value': 'e'},)
return(out)
app = App(app_ui, server)
I've updated your app to use the new patches handler after being clicked.
There were two subtle changes:
penguins_df.set_patches_fn(edtx)
*, patches
parameters to edtx
Final app (shinylive):
from palmerpenguins import load_penguins
from shiny import App, reactive, render, ui
penguins = load_penguins()
app_ui = ui.page_fluid(
ui.h2("Palmer Penguins"),
ui.input_action_button("click", "click"),
ui.output_ui("rows"),
ui.output_data_frame("penguins_df"),
)
def server(input, output, session):
@render.data_frame
def penguins_df():
return render.DataTable(
penguins,
editable=True,
)
@reactive.Effect
@reactive.event(input.click)
def click():
print("click")
def edtx(*, patches) -> list[render.CellPatch]:
print("new!")
out = [
render.CellPatch(({"row_index": 1, "column_index": 1, "value": "xxx"}))
]
print(out)
return out
penguins_df.set_patches_fn(edtx)
# just testing if it works when editing another cell
@penguins_df.set_patches_fn
def edt(*, patches: list[render.CellPatch]) -> list[render.CellPatch]:
print("original")
out = [render.CellPatch(({"row_index": 1, "column_index": 1, "value": "e"}))]
return out
app = App(app_ui, server)
I saw that the original cell location never escaped a saving state. This is being addressed in https://github.com/posit-dev/py-shiny/pull/1529 .
In Shiny-R I often use the dataTableProxy() to manipulate the data shown in the DT, so the view is changed without reloading/regenerating the entire table. Is it possible to do it with the data_frame in Python?
It is definitely a possible feature! I have a sketch of what it could look like here: https://github.com/posit-dev/py-shiny/blob/6611277da634503e82adcdee3aa1dd67d7bf0a87/shiny/render/_data_frame.py#L719-L739
It is currently not implemented as line 738 hints that we need a "send message to the browser" action that is not implemented in the typescript code. It would be similar to how we can update the sort from the server:
Note: This could also be something similar to update_data(self, data: DataFrameLikeT)
, but the required infrastructure code changes would be similar.
In Shiny-R I often use the dataTableProxy() ....
I do not believe a proxy object will be created within py-shiny. However, Python is pass by reference and we can empower our renderers to have extra methods. These extra methods, (e.g. .data_view()
or .update_sort()
or even .update_data()
) should cover the benefits of proxy object.
One open question that I had was "how should the updates be supplied?". Should it be at the cell level or at the "whole data frame" level?
row
, col
, value
info for every cell.Thoughts?
Thank you for your prompt reply. Here are a couple of examples of common use cases I can think of:
In the first case, editing at the cell level is the most efficient and streamlined approach. The second case requires a full table refresh, so resetting the entire data frame is quicker. (reactive on the @render.data_frame) But the user will lose the filters and sort. (even if the new options in 1.0 will help to reset them)
Based on my experience, I would recommend prioritizing cell-level updates . Whole-table refreshes could be a second priority.
One open question that I had was "how should the updates be supplied?". Should it be at the cell level or at the "whole data frame" level?
Cell
- Efficient and precise
- Harder to work with as a user. Must retrieve to
row
,col
,value
info for every cell.Whole data frame
- Inefficient. Will need to send the whole data frame to the browser
- Comfortable to work with as a user. Keeps the interface transaction as data frame in and data frame out
Thoughts?
I do not believe a proxy object will be created within py-shiny. However, Python is pass by reference and we can empower our renderers to have extra methods. These extra methods, (e.g. .data_view() or .update_sort() or even .update_data()) should cover the benefits of proxy object.
I fully agree, I think Python's by-reference approach is very useful and easy to code with. I intuitively built an App where the edit of a cell triggers other cells to change, just using the referenced data set (.data() and .data_view()).
Currently, when a @render.data_frame
function executes these qualities are reset:
I believe not losing these qualities are the root of the issue.
update_cell_value()
would allow us to shim in new values and not reset any of the qualities. (We're in agreement)update_data()
method could update the data and accept the qualities as parameters to opt-in/opt-out of being updated. If the qualities are maintained, then we could require certain data characteristics to be consistent between the old and the new data. Seems reasonable to reset all at once and not individual qualities. If anything, their corresponding update method can be run.Pseudo code
def update_data(self, data: DataFrameLikeT, *, reset: bool | None = None) -> None:
if reset is True:
# everything naturally resets
...
else:
action_type = warning if reset is None else error # error when reset = False
for each quality, display any error messages with the action_type
verify new data and old data have same column types
verify all existing edits and selections are within the bounds of the new data
Send message to browser with `data` and `reset`
and for completeness
def update_cell_value(self, value: TagNode, *, row: int, col: int) -> None:
Add cell patch info to internal cell patches dictionary
It feels off to call the currently set patches method on this submitted value
client_value = maybe_as_cell_html(value)
Send message to browser with `value: client_value`, `row`, and `col`
I agree with your points. Your pseudo-code would be awesome. It would streamline the process (creation vs. editing vs. refresh data), keep it simple to code, and avoid getting lost in the @render reactivity (in R we need to use Isolate to avoid re-triggering the rendered) Thanks!
I think my comment https://github.com/posit-dev/py-shiny/issues/1560#issuecomment-2362093820 probably more applies to this discussion.
This part:
Currently, when a @render.data_frame function executes these qualities are reset:
column filtering column sorting selected rows user edits
I believe not losing these qualities are the root of the issue.
, is also the problem I want to solve.
My use case does not involve changing the data in any cells only controlling what the underlying dataframe in the datagrid component is compared to the original and how it is displayed. I want access to
df
(possibly trivial but still, is it available as .data()
perhaps)df_mod
which includes
2.2 The column sorting, selected rows, and user filters (edits are not important for my use case, but probably is in general)
2.3 External sorting and filtering I want to apply together with 2.2df_mod
anddf_mod
somewhereCurrently, my external state is (this is a sample):
group by
where a selectize input can choose multiple columns and the order of selecting columns mattera reactive sort
where a selectize input can choose multiple columns to sort by and the order of selecting columns matter
So the use case is when choosing external state parameters and changing df_mod
, so that the dataframe re-renders, I lose the user choices inside the dataframe widget. I want to use my external state + the widget's current inputs simultaneously to decide how the new df_mod
should be constructed and rendered. Currently, the user needs to manually re-apply all widget selections as soon as an external input is changed due to re-rendering.
In Shiny-R I often use the
dataTableProxy()
to manipulate the data shown in the DT, so the view is changed without reloading/regenerating the entire table. Is it possible to do it with thedata_frame
in Python?