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

Scattergl points disappear when reaching a certain threshold in size difference #4556

Open
luggie opened this issue Mar 22, 2024 · 5 comments

Comments

@luggie
Copy link

luggie commented Mar 22, 2024

I noticed, that in Scattergl points start to disappear from the graph when resizing and reaching certain thresholds in size difference.

Minimal example:

from dash import Dash, html, dcc, callback
from dash.dependencies import Input, Output
import plotly.graph_objects as go
from plotly.graph_objs import Scattergl
import numpy as np
import pandas as pd


np.random.seed(41)
x = np.random.uniform(-10, 10, 10)
y = np.random.uniform(-10, 10, 10)
sizes = np.random.uniform(0, 1000, 10)

df = pd.DataFrame({
    'x': x,
    'y': y,
    'sizes': sizes
})

app = Dash(__name__)

app.layout = html.Div([
    dcc.Slider(min=13, max=14, value=1, id='slider-sizes', step=0.1),
    dcc.Graph(id='graph', style={"width": "100vw", "height": "80vh"}),
])


def update_marker_sizes(fig, size):
    for trace in fig.data:
        if 'marker' in trace and 'size' in trace.marker:
            trace.marker.size = [s * size for s in trace.marker.size]
    return fig


@callback(
    Output('graph', 'figure'),
    Input('slider-sizes', 'value')
)
def update_graph(size):
    figure = go.Figure(data=Scattergl(x=df["x"], y=df["y"], mode='markers',
                                      marker=dict(size=df['sizes']*size, sizemode='area')))
    print(f"{50*'-'}\nslider size:{size}")
    num_points = sum(
        len(trace['x']) for trace in figure.full_figure_for_development()['data'] if
        len(trace["x"]) > 1)
    print(f"num points in dev data (x): {num_points}")

    minsize, maxsize = float("inf"), float("-inf")
    for trace in figure.data:
        if hasattr(trace, 'marker') and hasattr(trace.marker, 'size'):
            if len(trace.marker.size) > 1:
                print(f"num points in data: {len(trace.marker.size)}")
            minsize = min(minsize, min(trace.marker.size))
            maxsize = max(maxsize, max(trace.marker.size))
    print(f"min: {minsize}, max: {maxsize}, diff: {maxsize - minsize}")

    return figure


if __name__ == '__main__':
    app.run_server(debug=True, port=8000)

The points are still present in the underlying data structure:

Output:

--------------------------------------------------
slider size:13.5
num points in dev data (x): 10
num points in data: 10
min: 1350.4235691765416, max: 10008.250444470288, diff: 8657.826875293747
--------------------------------------------------
slider size:13.6
num points in dev data (x): 10
num points in data: 10
min: 1360.4267067259975, max: 10082.385632947846, diff: 8721.958926221849
--------------------------------------------------
slider size:13.5
num points in dev data (x): 10
num points in data: 10
min: 1350.4235691765416, max: 10008.250444470288, diff: 8657.826875293747
--------------------------------------------------

Visually, this is what happens:
Peek 2024-03-22 10-34

@Coding-with-Adam
Copy link
Contributor

hi @luggie I'm getting a blank screen when I try to run your code. How did you get the graph to populate?

image

@luggie
Copy link
Author

luggie commented Mar 26, 2024

@Coding-with-Adam
the callback should fire when slider-sizes it is loaded into the DOM. I just tried the code again. For me it works with this conda env:

channels:
  - plotly
  - anaconda
  - conda-forge
  - defaults
dependencies:
  - _libgcc_mutex=0.1=conda_forge
  - _openmp_mutex=4.5=2_gnu
  - alsa-lib=1.2.8=h166bdaf_0
  - attr=2.5.1=h166bdaf_1
  - boost=1.78.0=py39h7c9e3ff_4
  - boost-cpp=1.78.0=h75c5d50_1
  - brotli=1.0.9=h5eee18b_7
  - brotli-bin=1.0.9=h5eee18b_7
  - brotli-python=1.0.9=py39h5a03fae_8
  - bzip2=1.0.8=h7f98852_4
  - ca-certificates=2023.12.12=h06a4308_0
  - cachelib=0.9.0=py39h06a4308_0
  - cairo=1.16.0=ha61ee94_1014
  - click=8.1.3=unix_pyhd8ed1ab_2
  - cycler=0.11.0=pyhd3eb1b0_0
  - cython=0.29.28=py39h295c915_0
  - dash=2.9.3=pyhd8ed1ab_0
  - dash-bootstrap-components=1.4.1=pyhd8ed1ab_0
  - dash-daq=0.5.0=pyh9f0ad1d_1
  - dash-extensions=1.0.12=pyhd8ed1ab_0
  - dash-table=5.0.0=pyhd8ed1ab_1
  - dataclass-wizard=0.22.3=pyhd8ed1ab_0
  - dbus=1.13.18=hb2f20db_0
  - defusedxml=0.7.1=pyhd8ed1ab_0
  - dill=0.3.6=pyhd8ed1ab_1
  - editorconfig=0.12.3=pyhd8ed1ab_0
  - et_xmlfile=1.1.0=py39h06a4308_0
  - expat=2.5.0=h27087fc_0
  - fftw=3.3.10=nompi_hf0379b8_106
  - flask=2.2.2=pyhd8ed1ab_0
  - flask-caching=2.0.2=pyhd8ed1ab_0
  - flask-compress=1.13=pyhd8ed1ab_0
  - font-ttf-dejavu-sans-mono=2.37=hd3eb1b0_0
  - font-ttf-inconsolata=2.001=hcb22688_0
  - font-ttf-source-code-pro=2.030=hd3eb1b0_0
  - font-ttf-ubuntu=0.83=h8b1ccd4_0
  - fontconfig=2.14.1=hc2a2eb6_0
  - fonts-anaconda=1=h8fa9717_0
  - fonts-conda-ecosystem=1=hd3eb1b0_0
  - fonttools=4.25.0=pyhd3eb1b0_0
  - freetype=2.12.1=hca18f0e_1
  - gensim=4.2.0=py39h6a678d5_0
  - gettext=0.21.1=h27087fc_0
  - giflib=5.2.1=h7b6447c_0
  - glib=2.74.1=h6239696_0
  - glib-tools=2.74.1=h6239696_0
  - greenlet=2.0.1=py39h5a03fae_0
  - gst-plugins-base=1.21.2=h3e40eee_0
  - gstreamer=1.21.2=hd4edc92_0
  - gstreamer-orc=0.4.33=h166bdaf_0
  - gunicorn=20.1.0=py39h06a4308_0
  - icu=70.1=h27087fc_0
  - importlib-metadata=5.1.0=pyha770c72_0
  - itsdangerous=2.1.2=pyhd8ed1ab_0
  - jack=1.9.21=h583fa2b_2
  - jinja2=3.1.2=pyhd8ed1ab_1
  - joblib=1.2.0=pyhd8ed1ab_0
  - jpeg=9e=h7f8727e_0
  - jsbeautifier=1.14.9=pyhd8ed1ab_0
  - keyutils=1.6.1=h166bdaf_0
  - kiwisolver=1.4.2=py39h295c915_0
  - krb5=1.19.3=h08a2579_0
  - lame=3.100=h7b6447c_0
  - lcms2=2.12=h3be6417_0
  - ld_impl_linux-64=2.39=hcc3a1bd_1
  - libblas=3.9.0=16_linux64_openblas
  - libbrotlicommon=1.0.9=h5eee18b_7
  - libbrotlidec=1.0.9=h5eee18b_7
  - libbrotlienc=1.0.9=h5eee18b_7
  - libcap=2.66=ha37c62d_0
  - libcblas=3.9.0=16_linux64_openblas
  - libclang=15.0.6=default_h2e3cab8_0
  - libclang13=15.0.6=default_h3a83d3e_0
  - libcups=2.3.3=h3e49a29_2
  - libdb=6.2.32=h6a678d5_1
  - libedit=3.1.20210910=h7f8727e_0
  - libevent=2.1.10=h28343ad_4
  - libffi=3.4.2=h7f98852_5
  - libflac=1.4.2=h27087fc_0
  - libgcc-ng=12.2.0=h65d4601_19
  - libgcrypt=1.10.1=h166bdaf_0
  - libgfortran-ng=12.2.0=h69a702a_19
  - libgfortran5=12.2.0=h337968e_19
  - libglib=2.74.1=h7a41b64_0
  - libgomp=12.2.0=h65d4601_19
  - libgpg-error=1.45=hc0c96e0_0
  - libiconv=1.17=h166bdaf_0
  - liblapack=3.9.0=16_linux64_openblas
  - libllvm15=15.0.6=h63197d8_0
  - libnsl=2.0.0=h7f98852_0
  - libogg=1.3.5=h27cfd23_1
  - libopenblas=0.3.21=pthreads_h78a6416_3
  - libopus=1.3.1=h7b6447c_0
  - libpng=1.6.39=h753d276_0
  - libpq=15.1=h67c24c5_1
  - libsndfile=1.1.0=hcb278e6_1
  - libsqlite=3.40.0=h753d276_0
  - libstdcxx-ng=12.2.0=h46fd767_19
  - libsystemd0=252=h2a991cd_0
  - libtiff=4.2.0=h2818925_1
  - libtool=2.4.6=h295c915_1008
  - libudev1=252=h166bdaf_0
  - libuuid=2.32.1=h7f98852_1000
  - libvorbis=1.3.7=h7b6447c_0
  - libwebp=1.2.2=h55f646e_0
  - libwebp-base=1.2.2=h7f8727e_0
  - libxcb=1.13=h7f98852_1004
  - libxkbcommon=1.0.3=he3ba5ed_0
  - libxml2=2.10.3=h7463322_0
  - libzlib=1.2.13=h166bdaf_4
  - lz4-c=1.9.3=h295c915_1
  - markupsafe=2.1.1=py39hb9d737c_2
  - matplotlib=3.5.1=py39h06a4308_1
  - matplotlib-base=3.5.1=py39ha18d171_1
  - more-itertools=9.1.0=pyhd8ed1ab_0
  - mpg123=1.31.1=h27087fc_0
  - munkres=1.1.4=py_0
  - mysql-common=8.0.31=h26416b9_0
  - mysql-libs=8.0.31=hbc51c84_0
  - ncurses=6.3=h27087fc_1
  - nspr=4.35=h27087fc_0
  - nss=3.82=he02c5a1_0
  - numpy=1.23.5=py39h3d75532_0
  - odfpy=1.4.1=py_0
  - openpyxl=3.0.10=py39h5eee18b_0
  - openssl=3.1.0=h0b41bf4_0
  - packaging=21.3=pyhd3eb1b0_0
  - pandarallel=1.6.4=pyhd8ed1ab_0
  - pandas=1.5.2=py39h4661b88_0
  - pcre2=10.37=he7ceb23_1
  - pillow=9.2.0=py39hace64e9_1
  - pip=22.3.1=pyhd8ed1ab_0
  - pixman=0.40.0=h36c2ea0_0
  - plotly=5.14.0=py_0
  - ply=3.11=py39h06a4308_0
  - psutil=5.9.4=py39hb9d737c_0
  - pthread-stubs=0.3=h0ce48e5_1
  - pulseaudio=16.1=h126f2b6_0
  - pycairo=1.23.0=py39h23c5bb2_0
  - pyparsing=3.0.4=pyhd3eb1b0_0
  - pyqt=5.15.7=py39h18e9c17_0
  - pyqt5-sip=12.11.0=py39h5a03fae_0
  - python=3.9.15=hba424b6_0_cpython
  - python-dateutil=2.8.2=pyhd8ed1ab_0
  - python_abi=3.9=3_cp39
  - pytz=2022.6=pyhd8ed1ab_0
  - qt-main=5.15.6=hafeba50_4
  - rdkit=2022.09.1=py39h0179058_1
  - readline=8.1.2=h0f457ee_0
  - reportlab=3.6.12=py39ha99c2b1_2
  - scikit-learn=1.2.0=py39h86b2a18_0
  - scipy=1.9.3=py39hddc5342_2
  - seaborn=0.11.2=pyhd3eb1b0_0
  - setuptools=65.5.1=pyhd8ed1ab_0
  - sip=6.6.2=py39h6a678d5_0
  - six=1.16.0=pyh6c4a22f_0
  - smart_open=5.2.1=py39h06a4308_0
  - sqlalchemy=1.4.45=py39h72bdee0_0
  - tenacity=8.1.0=pyhd8ed1ab_0
  - threadpoolctl=3.1.0=pyh8a188c0_0
  - tk=8.6.12=h27826a3_0
  - toml=0.10.2=pyhd3eb1b0_0
  - tornado=6.1=py39h27cfd23_0
  - tqdm=4.64.0=py39h06a4308_0
  - typing-extensions=4.9.0=py39h06a4308_1
  - typing_extensions=4.9.0=py39h06a4308_1
  - tzdata=2022g=h191b570_0
  - werkzeug=2.2.2=pyhd8ed1ab_0
  - wheel=0.38.4=pyhd8ed1ab_0
  - xcb-util=0.4.0=h166bdaf_0
  - xcb-util-image=0.4.0=h166bdaf_0
  - xcb-util-keysyms=0.4.0=h166bdaf_0
  - xcb-util-renderutil=0.3.9=h166bdaf_0
  - xcb-util-wm=0.4.1=h166bdaf_0
  - xorg-kbproto=1.0.7=h7f98852_1002
  - xorg-libice=1.0.10=h7f98852_0
  - xorg-libsm=1.2.3=hd9c2040_1000
  - xorg-libx11=1.7.2=h7f98852_0
  - xorg-libxau=1.0.9=h7f98852_0
  - xorg-libxdmcp=1.1.3=h7f98852_0
  - xorg-libxext=1.3.4=h7f98852_1
  - xorg-libxrender=0.9.10=h7f98852_1003
  - xorg-renderproto=0.11.1=h7f98852_1002
  - xorg-xextproto=7.3.0=h7f98852_1002
  - xorg-xproto=7.0.31=h7f98852_1007
  - xz=5.2.6=h166bdaf_0
  - zipp=3.11.0=pyhd8ed1ab_0
  - zlib=1.2.13=h166bdaf_4
  - zstd=1.5.2=ha4553b6_0
  - pip:
      - asttokens==2.4.1
      - dash-ag-grid==31.0.1
      - dash-bootstrap-templates==1.1.2
      - dash-core-components==2.0.0
      - dash-draggable==0.1.2
      - dash-html-components==2.0.0
      - dash-split==0.0.4
      - decorator==5.1.1
      - exceptiongroup==1.2.0
      - executing==2.0.1
      - flask-login==0.6.3
      - ipython==8.18.1
      - jedi==0.19.1
      - kaleido==0.2.1
      - matplotlib-inline==0.1.6
      - mol2vec==0.1
      - parso==0.8.3
      - pexpect==4.9.0
      - prompt-toolkit==3.0.43
      - ptyprocess==0.7.0
      - pure-eval==0.2.2
      - pygments==2.17.2
      - pygoslin==2.1.0
      - stack-data==0.6.3
      - traitlets==5.14.1
      - wcwidth==0.2.13

However I created an example where it gets little more clear what happens:

from dash import Dash, html, dcc, callback
from dash.dependencies import Input, Output, State
import plotly.graph_objects as go
from plotly.graph_objs import Scattergl
import numpy as np
import pandas as pd

sizes = np.linspace(start=1, stop=100, num=5)
coords = [c for c in range(5)]

df = pd.DataFrame({
    'x': coords,
    'y': coords,
    'sizes': sizes
})

app = Dash(__name__)

app.layout = html.Div([
    dcc.Slider(min=70, max=300, value=70, id='slider-sizes', step=10),
    dcc.Graph(id='graph', style={"width": "100vw", "height": "80vh"}),
    dcc.Interval(id='interval', interval=200, n_intervals=0)
])


def update_marker_sizes(fig, size):
    for trace in fig.data:
        if 'marker' in trace and 'size' in trace.marker:
            trace.marker.size = [s * size for s in trace.marker.size]
    return fig


@callback(
    Output('graph', 'figure'),
    Output('slider-sizes', 'value'),
    Input('interval', 'n_intervals'),
    State('slider-sizes', 'value'),
)
def update_graph(n_intervals, size):
    figure = go.Figure(
        data=Scattergl(x=df["x"], y=df["y"], mode='markers',
                       marker=dict(size=df['sizes']*size, sizemode='area', sizeref=None)
                       )
    )
    if size == 300:
        size = 70
    else:
        size += 10
    return figure, size


if __name__ == '__main__':
    app.run_server(debug=True, port=8000)

@Coding-with-Adam
Copy link
Contributor

@luggie thank you for sharing the last code snippet. Indeed, when the slider value is equal to 150, the third marker disappears.

image

I wonder if this is related to the callback, the dcc.Graph, or whether this is only a Scattergl issue.

Did you face the same issue when building the Scattergl figure outside of a Dash app?

@luggie
Copy link
Author

luggie commented Apr 2, 2024

@Coding-with-Adam Yes, it also happens when not using a dash app around the figure like so:

import plotly.graph_objects as go
import numpy as np
import pandas as pd

sizes = np.linspace(start=1, stop=100*150, num=5)
coords = [c for c in range(5)]
df = pd.DataFrame({
    'x': coords,
    'y': coords,
    'sizes': sizes
})

scatter = go.Scattergl(
    x=df["x"],
    y=df["y"],
    mode='markers',
    marker=dict(
        size=df['sizes'],
        sizemode='area',
        sizeref=1,
        symbol=1
    )
)

fig = go.Figure(data=scatter)

print(fig)

fig.show()

print(fig):

Figure({
    'data': [{'marker': {'size': array([1.000000e+00, 3.750750e+03, 7.500500e+03, 1.125025e+04, 1.500000e+04]),
                         'sizemode': 'area',
                         'sizeref': 1,
                         'symbol': 1},
              'mode': 'markers',
              'type': 'scattergl',
              'x': array([0, 1, 2, 3, 4]),
              'y': array([0, 1, 2, 3, 4])}],
    'layout': {'template': '...'}
})

Screenshot from 2024-04-11 10-26-11

In this example, I realized that the marker never really disappear but their size is reduced to 0 or near 0 (on the plot) when reaching a certain limit or turn over point until they start growing again with rising marker size.
The same happens in my example with dash.
The underlying data structure on python side is not affected but the way that plotly.js interprets it.

@Coding-with-Adam
Copy link
Contributor

hi @archmoj
Here's the codepen to replicate the issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants