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"),
)