Source code for visualization_toolkit.helpers.dash.components.chart

from typing import Optional

from dash import CeleryManager
from dash import dcc, Input, State, get_app
import yipit_dash_mui_components as ymc

from visualization_toolkit import constants
from visualization_toolkit.helpers.dash.core import resolve_html_id
from visualization_toolkit.helpers.plotly import generate_base_figure
from visualization_toolkit.helpers.plotly.theme import YD_CLASSIC_THEME


# These settings control options displayed in the modebar when hovering over a chart
# Details here: https://plotly.com/python/configuration-options
PLOTLY_CHART_CONFIG = {
    "displaylogo": False,
    "toImageButtonOptions": {"format": "png", "filename": "img_export", "scale": 2},
    "modeBarButtonsToRemove": ["pan", "zoom", "lasso", "select"],
}


[docs] def chart_component( index: str, height: Optional[int] = YD_CLASSIC_THEME["layout"]["height"], graph_config_overrides: Optional[dict] = None, clear_on_unhover: bool = False, ) -> dcc.Loading: """ Returns a charting component for plotly figures with built-in ``dcc.Loader`` to handle the intermediate querying state gracefully. Using this component requires specifying the chart component ID as an index, and this index is used to populate the underlying plotly figure in a separate callback. The loader will use the ``ymc.Skeleton`` component as a placeholder when the chart is not ready to be displayed. When defining callbacks using this component, use pattern-matching callback syntax. The ``index`` will match the supplied ``id`` value. The ``type`` of each subcomponent is the following: * ``chart``: Accesses the ``dcc.Graph`` object that holds the chart visual * ``chart-loader``: Accesses the ``dcc.Loading`` object that managers the loading state Important to note that, it requires that a callback modify the loader component's ``display`` parameter to ``hide``. This will remove the loader and display the underlying plotly figure. :param index: The index of the chart, should be unique to the chart within a Dash app. The index will be used in the subcomponents and referenced in callbacks. :param height: Optionally specify the height of the graph object, will default to the standard theme height if not specified. :param graph_config_overrides: Optionally specify a dictionary of plotly chart configuration options, these will override the base ``PLOTLY_CHART_CONFIG`` set by the library. :param clear_on_unhover: Optionally specify whether the hoverData should be cleared when the mouse is not hovering over it. :return: dcc.Loading component with an empty plotly figure graph as its children. Examples ^^^^^^^^^^^^^ .. code-block:: python :caption: Example creating a chart component that uses the loader initially and later be switched to render a graph via callback. from dash import Output, Input from visualization_toolkit.helpers.dash import chart_component from visualization_toolkit.helpers.plotly import chart app.layout = [ chart_component( index="example-graph", ) ] @app.callback( Output({"type": "chart", "index": "example-chart"}, "figure"), Output({"type": "chart-loader", "index": "example-chart"}, "display"), # Accesses the loader display parameter to hide once graph is ready Input({"type": "chart", "index": "example-chart"}, "id"), running=[ (Output({"type": "chart-loader", "index": "example-chart"}, "display"), "show", "hide"), ], ) def display_graph(_): fig = chart(...) # Custom logic to generate plotly figure return fig, "hide" # "hide" tells the loader to hide and show the graph instead """ graph_config_overrides = graph_config_overrides or {} return dcc.Loading( dcc.Graph( id={"type": "chart", "index": index}, figure=generate_base_figure(height=height), config=PLOTLY_CHART_CONFIG | graph_config_overrides, clear_on_unhover=clear_on_unhover, ), display="show", custom_spinner=[ ymc.Skeleton( variant="rectangular", height=height, sx={"flex": 1}, ), ], id={"type": "chart-loader", "index": index}, )
[docs] def custom_tooltip_callback( tooltip_id: str | dict, chart_id: str | dict, x_offset: int = 100, y_offset: int = 0, x_buffer: int = 100, y_buffer: int = 100, tooltip_min_width: int = 300, tooltip_min_height: int = 300, debug: bool = False, ): """ Enable custom tooltip to hover over a chart where the mouse is located. .. warning:: When exiting a chart, the tooltip will not be visible. But this requires setting the ``clear_on_unhover`` parameter to ``True`` in the ``chart_component`` component. :param tooltip_id: The id of the tooltip component. Tooltip should be a ``ymc.Box`` or equivalent object that has fixed positioning. :param chart_id: The id of the chart component (``dcc.Graph`` object) :param x_offset: The amount in pixels to offset the tooltip from the mouse location horizontally. :param y_offset: The amount in pixels to offset the tooltip from the mouse location vertically. :param x_buffer: The amount in pixels to pad the left and right edges of the chart to prevent tooltip from overflowing. :param y_buffer: The amount in pixels to pad the top and bottom edges of the chart to prevent tooltip from overflowing. :param tooltip_min_width: The minimum width of the tooltip to account for use in positioning. :param tooltip_min_height: The minimum height of the tooltip to account for use in positioning. :return: None Examples ^^^^^^^^^^^^^ .. code-block:: python :caption: Example of a custom tooltip callback from dash import Output, Input, State from visualization_toolkit.helpers.dash import chart_component, custom_tooltip_callback app.layout = [ ymc.Box( id="example-tooltip", children=[], # Important styling to ensure tooltip is hidden but can be positioned on charts dynamically sx={"position": "fixed", "top": 0, "left": 0, "zIndex": 1000}, ), chart_component( index="example-chart", clear_on_unhover=True, ), custom_tooltip_callback( tooltip_id="example-tooltip", chart_id="example-chart", ) ] @app.callback( Output("example-tooltip", "children"), Input("example-tooltip", "id"), Input("example-chart", "hoverData"), State("example-chart", "id"), ) def update_tooltip(_, hover_data, chart_id): if hover_data: # Update the tooltip children based on hover data and other plotly chart information return [] return [] """ app = get_app() template = """ function(trigger, chart_data, chart_id, figure) { // Register a function to get the mouse position to orient the tooltip if (!window.getMousePosition) { window.getMousePosition = document.addEventListener('mousemove', function(event) { window.mouseX = event.clientX; window.mouseY = event.clientY; }); } if (!chart_data) { const tooltip = document.getElementById('{{ parameters.tooltip_id }}'); if (tooltip) { tooltip.style.display = 'none'; } return window.dash_clientside.no_update; } if (chart_data.points.length === 0) { return window.dash_clientside.no_update; } const hovermode = figure.layout.hovermode; const tooltip = document.getElementById('{{ parameters.tooltip_id }}'); const chart = document.getElementById('{{ parameters.chart_id }}'); if (tooltip && chart) { tooltip.style.display = 'block'; var rect = tooltip.getBoundingClientRect(); var chartRect = chart.getBoundingClientRect(); // Take the first point if hovermode is 'closest', // otherwise take point closest to the top of the chart let point; if (hovermode === 'closest') { point = chart_data.points[0]; } else if (hovermode === 'x unified') { point = chart_data.points.reduce((min, point) => point.bbox.y0 < min.bbox.y0 ? point : min ); } else { point = chart_data.points[0]; } var xPos = point.bbox.x1; var yPos = point.bbox.y1; var xMax = Math.max(point.bbox.x0, point.bbox.x1); var xMin = Math.min(point.bbox.x0, point.bbox.x1); var yMax = Math.max(point.bbox.y0, point.bbox.y1); var yMin = Math.min(point.bbox.y0, point.bbox.y1); var finalXPos = window.mouseX + {{ parameters.x_offset }}; var finalYPos = window.mouseY + {{ parameters.y_buffer }} - {{ parameters.y_offset }}; tooltip.style.left = (finalXPos) + 'px'; tooltip.style.top = (finalYPos) + 'px'; var finalRect = tooltip.getBoundingClientRect(); // If tooltip would overflow the right edge, move it to the left of the cursor if ((finalRect.right + {{ parameters.x_offset }}) > Math.min(window.innerWidth, chartRect.right)) { finalXPos = window.mouseX - finalRect.width - {{ parameters.x_offset }} - {{ parameters.x_buffer }}; tooltip.style.left = (finalXPos) + 'px'; finalRect = tooltip.getBoundingClientRect(); } // If tooltip would overflow the left edge, move it to the right of the cursor if ((finalRect.left - {{ parameters.x_offset }}) < 0) { finalXPos = window.mouseX + {{ parameters.x_offset }}; tooltip.style.left = (finalXPos) + 'px'; finalRect = tooltip.getBoundingClientRect(); } // Do check to make sure the tooltip is not overflowing the bottom of the page if (finalRect.bottom + {{ parameters.y_buffer }} > window.innerHeight) { finalYPos = chartRect.top + {{ parameters.y_offset }}; tooltip.style.top = (finalYPos) + 'px'; finalRect = tooltip.getBoundingClientRect(); } else if (finalRect.top <= window.mouseY && finalRect.bottom >= window.mouseY) { if ( (chartRect.top + {{ parameters.y_buffer }}) < window.mouseY ) { finalYPos = chartRect.top + {{ parameters.y_buffer }}; tooltip.style.top = (finalYPos) + 'px'; finalRect = tooltip.getBoundingClientRect(); } else { finalYPos = chartRect.bottom - {{ parameters.y_buffer }}; tooltip.style.top = (finalYPos) + 'px'; finalRect = tooltip.getBoundingClientRect(); } } {% if parameters.debug -%} finalRect = tooltip.getBoundingClientRect(); console.log({x: window.mouseX, y: window.mouseY}); console.log({chart_data: chart_data}); console.log({chartRect: chartRect}); console.log({rect: finalRect}); console.log({point: point}); console.log({finalXPos: finalXPos, finalYPos: finalYPos}); {% endif -%} } else { tooltip.style.display = 'none'; } return window.dash_clientside.no_update; } """ app.clientside_callback( constants.JINJA_ENVIRONMENT.from_string(template).render( parameters={ "tooltip_id": resolve_html_id(tooltip_id), "chart_id": resolve_html_id(chart_id), "x_offset": x_offset, "y_offset": y_offset, "x_buffer": x_buffer, "y_buffer": y_buffer, "tooltip_min_width": tooltip_min_width, "tooltip_min_height": tooltip_min_height, "debug": debug, }, ), Input(tooltip_id, "id"), Input(chart_id, "hoverData"), State(chart_id, "id"), State(chart_id, "figure"), )