Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Zooming in does not resample when Y-value are identical #275

Open
Joakimden4 opened this issue Nov 27, 2023 · 9 comments
Open

Zooming in does not resample when Y-value are identical #275

Joakimden4 opened this issue Nov 27, 2023 · 9 comments
Assignees
Labels
bug Something isn't working

Comments

@Joakimden4
Copy link

Hi!
First off, thanks a lot for your incredible work on this much-needed resampling functionality!
Also, I'm quite inexperienced with submitting github issues, so please let me know if you need any additional information.

I am trying to apply resampling in a plotly dash app on a go.Scattergl trace that visualizes a time series of around 200k data points with hourly frequency.
The app works fine, but when I zoom in on the graph, the granularity of the data remains very low (e.g. weekly granularity).
I suspect it is because the Y-values in my dataset are identical over many subsequent hours, thus in theory not requiring resampling.
However, I would like the user to be able to hover and click the data points, as this graph is part of a bigger interactive app, which initializes different visuals when the user clicks the data points in the graph.

So in essence I have two questions:

  1. Can you confirm that the missing resampling when zooming in is due to the Y-values being identical over many subsequent hours?
  2. If yes, is there a way to force the resampling, so that the graph shows interactable data points on a higher granularity despite identical Y-values?

Below is the code pertaining to the graph in question within the app.
I have also attached my dataset here data.csv

  • OS: Windows
  • Python version: 3.9.2
  • plotly-resampler environment: e.g.: Dash web app (Chrome)
  • plotly-resampler version: 0.9.1
register_plotly_resampler(mode='auto')

layout = dcc.Graph(
    id='graph_bo_hourly',
    clear_on_unhover=True,
)

df_hourly = pd.read_csv("data.csv", index_col=0)

hourly_fig = go.Figure(
    layout=dict(
        dragmode='pan',
        hovermode='x unified',
        xaxis=dict(
            rangeslider_visible=True,
            rangeselector=dict(
                buttons=list([
                    dict(count=1, label="1 day", step="day", stepmode="backward"),
                    dict(count=1, label="1 month", step="month", stepmode="backward"),
                    dict(count=1, label="1 year", step="year", stepmode="backward"),
                    dict(step="all")
                ])
            ),
        ),
    ),
)
hourly_fig.add_trace(go.Scattergl(x=df_hourly['Date'], y=df_hourly['MWh'], name='Hourly position', mode='lines'))

Thanks in advance!

@Joakimden4 Joakimden4 added the bug Something isn't working label Nov 27, 2023
jonasvdd added a commit that referenced this issue Nov 29, 2023
jonasvdd added a commit that referenced this issue Jan 3, 2024
* 🐐 related to issue #275

* 💨 small fixes

* 💪 update readme fix #276

* 💨

* 🖊️ update readme

* ✏️

* 🧹 formatting

* 🖍️ adding docs

* 🙈 remove duplicate entry from gitignore

---------

Co-authored-by: Jeroen Van Der Donckt <18898740+jvdd@users.noreply.github.com>
Co-authored-by: Jeroen Van Der Donckt <boebievdd@gmail.com>
jonasvdd added a commit that referenced this issue Jan 6, 2024
@jonasvdd
Copy link
Member

jonasvdd commented Jan 6, 2024

Hi @Joakimden4,

Can you verify whether the functionality (and demo posted on #286 resolves your issue)?

jonasvdd added a commit that referenced this issue Jan 8, 2024
* ✨ fix for #275

* 🧹 review code

---------

Co-authored-by: Jeroen Van Der Donckt <boebievdd@gmail.com>
@Joakimden4
Copy link
Author

Joakimden4 commented Jan 8, 2024

Hi @jonasvdd,

The provided example works when it is not combined with dash components.
However, when I wrap the figure in a dcc.Graph component and display it in a dash, it behaves the exact same way as originally reported in this bug.

Am I implementing it wrongly within the dash framework?
I ran this code on the bug/rangeselector branch using the dependencies specified by you:

import dash
from dash import html, dcc
from plotly_resampler import register_plotly_resampler
import plotly.graph_objects as go
import pandas as pd

register_plotly_resampler(mode="figure", create_overview=True, verbose=True)

df_hourly = pd.read_csv("data.csv", index_col=0)

hourly_fig = go.Figure(
    layout=dict(
        dragmode='pan',
        hovermode='x unified',
        xaxis=dict(
            rangeslider_visible=True,
            rangeselector=dict(
                buttons=list([
                    dict(count=1, label="1 day", step="day", stepmode="backward"),
                    dict(count=1, label="1 month", step="month", stepmode="backward"),
                    dict(count=1, label="1 year", step="year", stepmode="backward"),
                    dict(step="all")
                ])
            ),
        ),
    ),
)
hourly_fig.add_trace(go.Scattergl(x=df_hourly['Date'], y=df_hourly['MWh'], name='Hourly position', mode='lines'))

app = dash.Dash(__name__, meta_tags=[{'name':'viewport', 'content':'width=device-width, initial-scale=1.0'}])
app.layout = html.Div(
    dcc.Graph(
        id='graph_bo_hourly',
        figure=hourly_fig
    )
)

app.run(debug=False, host='localhost')

@jonasvdd
Copy link
Member

jonasvdd commented Jan 8, 2024

Hi @Joakimden4,

When you use plotly-resampler from the main-branch, the dash app example below appears to work:
What did I change / how did I make this:

  • You cannot use register_plotly_resampler within dash apps, the function is mainly intended for usage within notebook environments
  • I copied a large part of the code from the xaxis overview dash app folder / xaxis overview file
import dash
from dash import html, dcc, Input, Output, State, no_update
import plotly.graph_objects as go
import pandas as pd

# For plain dash apps you need to use the FigureResampler class
# (the register function is for notebooks only)
from plotly_resampler import FigureResampler, ASSETS_FOLDER

FigureResampler(create_overview=True, verbose=True)
GRAPH_ID = "graph-id"
OVERVIEW_GRAPH_ID = "overview-graph"

# 0. Load the data
df_hourly = pd.read_csv("data.csv", index_col=0)

# 1. Create the figure and add data
# fmt: off
hourly_fig = FigureResampler(
    go.Figure(
        layout=dict(
            dragmode="pan",
            hovermode="x unified",
            xaxis=dict(
                rangeselector=dict(
                    buttons=list( [
                        dict(count=1, label="1 day", step="day", stepmode="backward"),
                        dict(count=1, label="1 month", step="month", stepmode="backward"),
                        dict(count=1, label="1 year", step="year", stepmode="backward",),
                    ])
                ),
            ),
        )
    ),
)
hourly_fig.add_trace(go.Scattergl(x=df_hourly["Date"], y=df_hourly["MWh"], name="Hourly position", mode="lines"))

# 1.1 Create the overview figure
coarse_fig = hourly_fig._create_overview_figure()

# Create the app in which the figure will be displayed
app = dash.Dash(
    __name__,
    meta_tags=[
        {"name": "viewport", "content": "width=device-width, initial-scale=1.0"}
    ],
    assets_folder=ASSETS_FOLDER,
    external_scripts=["https://cdn.jsdelivr.net/npm/lodash/lodash.min.js"],
)
# NOTE: you need to create both a coars and 
app.layout = html.Div(
    children=[
        dcc.Graph(id=GRAPH_ID, figure=hourly_fig),
        dcc.Graph(id=OVERVIEW_GRAPH_ID, figure=coarse_fig),
    ]
)

# -------------------- Callbacks --------------------
# --- Clientside callbacks used to bidirectionally link the overview and main graph ---
app.clientside_callback(
    dash.ClientsideFunction(namespace="clientside", function_name="main_to_coarse"),
    dash.Output(OVERVIEW_GRAPH_ID, "id", allow_duplicate=True),
    dash.Input(GRAPH_ID, "relayoutData"),
    [dash.State(OVERVIEW_GRAPH_ID, "id"), dash.State(GRAPH_ID, "id")],
    prevent_initial_call=True,
)

app.clientside_callback(
    dash.ClientsideFunction(namespace="clientside", function_name="coarse_to_main"),
    dash.Output(GRAPH_ID, "id", allow_duplicate=True),
    dash.Input(OVERVIEW_GRAPH_ID, "selectedData"),
    [dash.State(GRAPH_ID, "id"), dash.State(OVERVIEW_GRAPH_ID, "id")],
    prevent_initial_call=True,
)


# --- FigureResampler update callback ---
# The plotly-resampler callback to update the graph after a relayout event (= zoom/pan)
# As we use the figure again as output, we need to set: allow_duplicate=True
@app.callback(
    Output(GRAPH_ID, "figure", allow_duplicate=True),
    Input(GRAPH_ID, "relayoutData"),
    prevent_initial_call=True,
)
def update_fig(relayoutdata: dict):
    if relayoutdata is None:
        return no_update
    return hourly_fig.construct_update_data_patch(relayoutdata)


# Start the app
app.run(debug=False, host="localhost")

I hope this helps you further.
Kind regards,
Jonas

@Joakimden4
Copy link
Author

Hi @jonasvdd,
Thank you very much for the provided example.
I'd love to try this out in my actual project that uses plotly-resampler as dependency.
When are you planning on releasing this?

@jonasvdd
Copy link
Member

jonasvdd commented Jan 9, 2024

Hi @Joakimden4,

I plan to release a new version somewhere this week.
( I just want to improve the docs of the register_plotly_resampler function)

Kind regards,
Jonas

@Joakimden4
Copy link
Author

Hi @jonasvdd,

That's awesome, thanks a lot for your hard work! :)

jonasvdd added a commit that referenced this issue Jan 12, 2024
jonasvdd added a commit that referenced this issue Jan 14, 2024
* ✨ fix for #275

* 🧹 review code

* ✨ new example

* 🔍 reviewing examples

* 🖍️ docs-fix for #275

* 🔍 review

---------

Co-authored-by: Jeroen Van Der Donckt <boebievdd@gmail.com>
jonasvdd added a commit that referenced this issue Jan 14, 2024
@jonasvdd
Copy link
Member

Version 0.9.2 was released!

Please let me know whether your code works, and if so, you can close this issue! :)

@jonasvdd jonasvdd self-assigned this Jan 15, 2024
@Joakimden4
Copy link
Author

Joakimden4 commented Jan 16, 2024

Hi @jonasvdd,
I've updated to version 0.9.2, but I'm still not able to get it to work.
The issue is that no matter what I try to pass to the construct_update_data_patch method, it returns a dash.no_update event.
It always executes line 1333 of figure_resampler_interface.py.

My use case is to pass the figure attribute of the dcc.Graph element through a callback and then apply the resampling.
The reason for this is that the user can change the data for the figure during runtime of the application, so I cannot set a static figure. However, even if I try rebuilding the figure from scratch within the callback to avoid potential dash callback passing format issues, I still end up with the dash.no_update event.

I've tried with a super simple use case where there is no coarse graph.
If you have any ideas how to solve this it would be much appreciated!

@callback(
    Output('graph_bo_hourly', 'figure', allow_duplicate=True),
    Input('graph_bo_hourly', 'relayoutData'),
    State('graph_bo_hourly', 'figure'),
    prevent_initial_call=True
)
def resample_fig(relayoutdata, fig_state):
    if relayoutdata is None:
        return dash.no_update
    else:
        hourly_fig = FigureResampler(
            go.Figure(
                layout=dict(
                    dragmode='pan',
                    hovermode='x unified',
                    xaxis=dict(
                        rangeselector=dict(
                            buttons=list([
                                dict(count=1, label="1 day", step="day", stepmode="backward"),
                                dict(count=1, label="1 month", step="month", stepmode="backward"),
                                dict(count=1, label="1 year", step="year", stepmode="backward"),
                            ])
                        ),
                    ),
                ),
            )
        )
        x_dates = [datetime.strptime(date, "%Y-%m-%dT%H:%M:%S") for date in fig_state['data'][0]['x']]
        hourly_fig.add_trace(go.Scattergl(x=x_dates, y=fig_state['data'][0]['y'], name='Hourly position', mode='lines'))
        return hourly_fig.construct_update_data_patch(relayoutdata)

@Joakimden4
Copy link
Author

Hi @jonasvdd,
I just realized that I'm continually trying to resample the downsampled dataset, which explains why it's not working.
I will have to rethink my whole approach.

I'll let you know when I've verified whether it's working, once I've fixed my approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants