finos / perspective

A data visualization and analytics component, especially well-suited for large and/or streaming datasets.
https://perspective.finos.org/
Apache License 2.0
8.49k stars 1.18k forks source link

removing rows on backend table does not replicate to client table #2293

Open cakir-enes opened 1 year ago

cakir-enes commented 1 year ago

Not sure if this is the expected behavior, but following this guide https://perspective.finos.org/docs/server/#clientserver-replicated the replica table on client, which is derived by a view that backed by the backend table does not propagate removes.

Is this the expected behavior?

version 2.3.1, backend : python fastapi

dariosky commented 1 year ago

Hi @cakir-enes - can you provide an example of the code where you're trying to remove records, so we can help you?

nickroci commented 1 year ago

+1 for this, when I noticed it ages ago it could be repoed by creating a table in python, connect web viewer, asyncio.sleep for 30 seconds table.remove([key]) in python. Observe that they key still exists in Web viewer. I'll see if I can get something specific next week.

It seemed to me that the ws protocol couldn't really deal with deletes.

Fyi a workaround is to have a second ws and manually call remove in javascript on the table...

nickroci commented 1 year ago

As promised too long ago, notice that data is updated and added OK and the ws is sending something on delete.

The workaround is to add a second ws and just send messages to your own js code to call delete in javascript...

As a side note this might make a good example for new developers as its quite hard to get started on the whole thing otherwise...

# pip install 'uvicorn[standard] fastapi perspective-python pandas
import asyncio
import logging
import threading
import uvicorn

from fastapi import FastAPI, WebSocket
from fastapi.middleware.cors import CORSMiddleware
from starlette.responses import HTMLResponse

import pandas as pd

from perspective import Table, PerspectiveManager, PerspectiveStarletteHandler

def perspective_thread(manager):
    """Perspective application thread starts its own event loop, and
    adds the table with the name "data_source_one", which will be used
    in the front-end."""
    psp_loop = asyncio.new_event_loop()
    manager.set_loop_callback(psp_loop.call_soon_threadsafe)
    data = pd.DataFrame({
        "index": list(range(100)),
        "float": [i * 1.5 for i in range(100)],
        "bool": [True for i in range(100)],
        "string": [str(i) for i in range(100)]
    })
    table = Table(data, index="index")
    manager.host_table("data_source", table)
    async def deleteme():
        for i in range(100):
            print("DELETEING", i)
            table.remove([i])
            print(table.num_rows())
            print("Adding Junk")
            table.update({'index': [i+100], 'float': [1], "bool": [False], 'string': ['newData?']})
            table.update({'index': [i+99], 'float': [1], "bool": [False], 'string': ['Modified']})
            await asyncio.sleep(10)
    psp_loop.run_until_complete(deleteme())

def make_app():
    manager = PerspectiveManager()

    thread = threading.Thread(target=perspective_thread, args=(manager,))
    thread.daemon = True
    thread.start()

    async def websocket_handler(websocket: WebSocket):
        handler = PerspectiveStarletteHandler(manager=manager, websocket=websocket)
        await handler.run()

    app = FastAPI()
    app.add_api_websocket_route("/websocket", websocket_handler)

    @app.get("/", response_class=HTMLResponse)
    async def read_items():
        html_content = """
        <!DOCTYPE html>
        <html>
        <head>
<script type="module" src="https://cdn.jsdelivr.net/npm/@finos/perspective/dist/cdn/perspective.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/@finos/perspective-viewer/dist/cdn/perspective-viewer.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/@finos/perspective-viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/@finos/perspective-viewer-d3fc/dist/cdn/perspective-viewer-d3fc.js"></script>
<link rel="stylesheet" crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@finos/perspective-viewer/dist/css/pro.css"/>
            <script type="module">
            import perspective from "https://cdn.jsdelivr.net/npm/@finos/perspective/dist/cdn/perspective.js"
                    const websocket = perspective.websocket("ws://localhost:8080/websocket")
                    const worker = perspective.worker();
                    const server_table = await websocket.open_table("data_source");
                    const server_view = await server_table.view();
                    const table = await worker.table(server_view, {
                        index: await server_table.get_index()
                    });
                    document.getElementById('viewer').load(table);
            </script>
        </head>
        <body><perspective-viewer id="viewer" style="width: 500px; height: 500px"></perspective-viewer></body>
        </html>
        """
        return HTMLResponse(content=html_content, status_code=200)

    app.add_middleware(
        CORSMiddleware,
        allow_origins=["*"],
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )
    return app

if __name__ == "__main__":
    app = make_app()
    logging.critical("Listening on http://localhost:8080")
    uvicorn.run(app, host="0.0.0.0", port=8080)