Initial commit before Ralph loop
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
"""
|
||||
Test Plotly interactivity features in the visualization module.
|
||||
|
||||
Verifies that Plotly charts have the expected interactive capabilities:
|
||||
1. Hover templates are properly configured
|
||||
2. Icicle chart settings allow click-to-drill-down navigation
|
||||
3. Layout settings support proper display of interactive features
|
||||
|
||||
Phase 4.7.2: Verify Plotly interactivity (zoom, pan, hover)
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from datetime import datetime
|
||||
|
||||
import plotly.graph_objects as go
|
||||
|
||||
# Import the visualization module
|
||||
try:
|
||||
from visualization.plotly_generator import create_icicle_figure, save_figure_html
|
||||
HAS_VISUALIZATION = True
|
||||
except ImportError:
|
||||
HAS_VISUALIZATION = False
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_chart_data():
|
||||
"""
|
||||
Create sample chart data (ice_df) for testing visualization.
|
||||
|
||||
This mimics the output of prepare_chart_data() from analysis/pathway_analyzer.py
|
||||
"""
|
||||
# Sample hierarchy data: Root -> Trust -> Directory -> Drug
|
||||
data = {
|
||||
'parents': [
|
||||
'', # Root (N&WICS)
|
||||
'N&WICS', # Trust 1
|
||||
'N&WICS', # Trust 2
|
||||
'Trust1', # Directory in Trust1
|
||||
'Trust1', # Another Directory
|
||||
'Trust2', # Directory in Trust2
|
||||
'Trust1/Rheum', # Drug
|
||||
'Trust1/Derm', # Drug
|
||||
'Trust2/Rheum', # Drug
|
||||
],
|
||||
'ids': [
|
||||
'N&WICS',
|
||||
'Trust1',
|
||||
'Trust2',
|
||||
'Trust1/Rheum',
|
||||
'Trust1/Derm',
|
||||
'Trust2/Rheum',
|
||||
'Trust1/Rheum/Adalimumab',
|
||||
'Trust1/Derm/Adalimumab',
|
||||
'Trust2/Rheum/Etanercept',
|
||||
],
|
||||
'labels': [
|
||||
'Norfolk & Waveney ICS',
|
||||
'Manchester University Trust',
|
||||
'Barts Health Trust',
|
||||
'Rheumatology',
|
||||
'Dermatology',
|
||||
'Rheumatology',
|
||||
'Adalimumab',
|
||||
'Adalimumab',
|
||||
'Etanercept',
|
||||
],
|
||||
'value': [50, 30, 20, 20, 10, 20, 20, 10, 20],
|
||||
'colour': [1.0, 0.6, 0.4, 0.4, 0.2, 0.4, 0.4, 0.2, 0.4],
|
||||
'cost': [50000, 30000, 20000, 20000, 10000, 20000, 20000, 10000, 20000],
|
||||
'costpp': [1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000],
|
||||
'cost_pp_pa': [2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000],
|
||||
'First seen': [
|
||||
pd.Timestamp('2023-01-01')] * 9,
|
||||
'Last seen': [
|
||||
pd.Timestamp('2023-12-31')] * 9,
|
||||
'First seen (Parent)': [
|
||||
pd.Timestamp('2023-01-01')] * 9,
|
||||
'Last seen (Parent)': [
|
||||
pd.Timestamp('2023-12-31')] * 9,
|
||||
'average_spacing': ['14 days'] * 9,
|
||||
'avg_days': [pd.Timedelta('180 days')] * 9,
|
||||
}
|
||||
return pd.DataFrame(data)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not HAS_VISUALIZATION, reason="Visualization module not available")
|
||||
class TestPlotlyFigureConfiguration:
|
||||
"""Test that Plotly figures have correct interactive configuration."""
|
||||
|
||||
def test_figure_has_hovertemplate(self, sample_chart_data):
|
||||
"""Verify the icicle chart has a hover template configured."""
|
||||
fig = create_icicle_figure(sample_chart_data, "Test Title")
|
||||
|
||||
# Get the icicle trace
|
||||
assert len(fig.data) > 0, "Figure should have at least one trace"
|
||||
|
||||
icicle_trace = fig.data[0]
|
||||
assert icicle_trace.type == 'icicle', "First trace should be an icicle chart"
|
||||
|
||||
# Verify hovertemplate is set and contains expected placeholders
|
||||
assert icicle_trace.hovertemplate is not None, "Hover template should be configured"
|
||||
assert '%{label}' in icicle_trace.hovertemplate, "Hover should include label"
|
||||
assert '%{customdata' in icicle_trace.hovertemplate, "Hover should include custom data"
|
||||
|
||||
def test_figure_has_texttemplate(self, sample_chart_data):
|
||||
"""Verify the icicle chart has a text template for in-chart text."""
|
||||
fig = create_icicle_figure(sample_chart_data, "Test Title")
|
||||
|
||||
icicle_trace = fig.data[0]
|
||||
|
||||
# Verify texttemplate is set
|
||||
assert icicle_trace.texttemplate is not None, "Text template should be configured"
|
||||
assert '%{label}' in icicle_trace.texttemplate, "Text should include label"
|
||||
|
||||
def test_figure_has_correct_branchvalues(self, sample_chart_data):
|
||||
"""Verify branchvalues is set to 'total' for proper hierarchy summing."""
|
||||
fig = create_icicle_figure(sample_chart_data, "Test Title")
|
||||
|
||||
icicle_trace = fig.data[0]
|
||||
|
||||
# branchvalues should be 'total' for proper hierarchy display
|
||||
assert icicle_trace.branchvalues == 'total', \
|
||||
"branchvalues should be 'total' for hierarchy summation"
|
||||
|
||||
def test_figure_has_maxdepth_for_drilldown(self, sample_chart_data):
|
||||
"""Verify maxdepth is set to allow drill-down navigation."""
|
||||
fig = create_icicle_figure(sample_chart_data, "Test Title")
|
||||
|
||||
icicle_trace = fig.data[0]
|
||||
|
||||
# maxdepth should be set to limit initial view depth
|
||||
# Users can then click to drill into deeper levels
|
||||
assert icicle_trace.maxdepth is not None, "maxdepth should be configured for drill-down"
|
||||
assert icicle_trace.maxdepth >= 2, "maxdepth should be at least 2 to show hierarchy"
|
||||
|
||||
def test_figure_layout_has_hoverlabel(self, sample_chart_data):
|
||||
"""Verify layout has hoverlabel configuration for readable tooltips."""
|
||||
fig = create_icicle_figure(sample_chart_data, "Test Title")
|
||||
|
||||
# Check hoverlabel configuration
|
||||
assert 'hoverlabel' in fig.layout, "Layout should have hoverlabel configuration"
|
||||
# Plotly uses 'font' as a dict with 'size' attribute
|
||||
assert fig.layout.hoverlabel.font is not None, "Hover label font should be configured"
|
||||
assert fig.layout.hoverlabel.font.size is not None, "Hover label font size should be set"
|
||||
assert fig.layout.hoverlabel.font.size >= 12, "Hover label should be readable (>=12px)"
|
||||
|
||||
def test_figure_has_proper_margins(self, sample_chart_data):
|
||||
"""Verify layout has margins configured for proper display."""
|
||||
fig = create_icicle_figure(sample_chart_data, "Test Title")
|
||||
|
||||
# Check margin configuration
|
||||
assert fig.layout.margin is not None, "Margins should be configured"
|
||||
assert fig.layout.margin.t >= 50, "Top margin should have room for title"
|
||||
|
||||
def test_figure_has_title(self, sample_chart_data):
|
||||
"""Verify the figure has a title configured."""
|
||||
fig = create_icicle_figure(sample_chart_data, "Test Analysis")
|
||||
|
||||
assert fig.layout.title is not None, "Figure should have a title"
|
||||
assert "Test Analysis" in fig.layout.title.text, "Title should include custom text"
|
||||
|
||||
def test_figure_has_colorscale(self, sample_chart_data):
|
||||
"""Verify the icicle chart has a colorscale for visual differentiation."""
|
||||
fig = create_icicle_figure(sample_chart_data, "Test Title")
|
||||
|
||||
icicle_trace = fig.data[0]
|
||||
|
||||
# Check marker has colorscale
|
||||
assert icicle_trace.marker is not None, "Marker should be configured"
|
||||
assert icicle_trace.marker.colorscale is not None, "Colorscale should be set"
|
||||
|
||||
|
||||
@pytest.mark.skipif(not HAS_VISUALIZATION, reason="Visualization module not available")
|
||||
class TestPlotlyInteractiveFeatures:
|
||||
"""Test that Plotly figures support expected interactive features."""
|
||||
|
||||
def test_figure_is_interactive_type(self, sample_chart_data):
|
||||
"""Verify the figure is a go.Figure which supports interactivity."""
|
||||
fig = create_icicle_figure(sample_chart_data, "Test Title")
|
||||
|
||||
assert isinstance(fig, go.Figure), "Should return a Plotly Figure object"
|
||||
|
||||
def test_figure_can_be_converted_to_html(self, sample_chart_data, tmp_path):
|
||||
"""Verify the figure can be saved as interactive HTML."""
|
||||
fig = create_icicle_figure(sample_chart_data, "Test Title")
|
||||
|
||||
# Save to temporary file
|
||||
html_path = save_figure_html(fig, str(tmp_path), "test_chart", open_browser=False)
|
||||
|
||||
assert html_path.endswith('.html'), "Should save as HTML file"
|
||||
|
||||
# Verify the HTML file exists and contains Plotly data
|
||||
with open(html_path, 'r', encoding='utf-8') as f:
|
||||
html_content = f.read()
|
||||
|
||||
assert 'plotly' in html_content.lower(), "HTML should contain Plotly"
|
||||
# Interactive HTML should include the plotly.js library
|
||||
assert 'cdn.plot.ly' in html_content or 'plotly-' in html_content, \
|
||||
"HTML should include Plotly.js for interactivity"
|
||||
|
||||
def test_figure_data_includes_ids_for_drilldown(self, sample_chart_data):
|
||||
"""Verify figure data includes ids necessary for click-to-drill navigation."""
|
||||
fig = create_icicle_figure(sample_chart_data, "Test Title")
|
||||
|
||||
icicle_trace = fig.data[0]
|
||||
|
||||
# ids are required for proper drill-down behavior in icicle charts
|
||||
assert icicle_trace.ids is not None, "ids should be provided for drill-down"
|
||||
assert len(icicle_trace.ids) > 0, "ids should not be empty"
|
||||
|
||||
def test_figure_data_includes_parents_for_hierarchy(self, sample_chart_data):
|
||||
"""Verify figure data includes parents for hierarchy navigation."""
|
||||
fig = create_icicle_figure(sample_chart_data, "Test Title")
|
||||
|
||||
icicle_trace = fig.data[0]
|
||||
|
||||
# parents are required for hierarchy structure
|
||||
assert icicle_trace.parents is not None, "parents should be provided"
|
||||
assert len(icicle_trace.parents) > 0, "parents should not be empty"
|
||||
|
||||
def test_figure_customdata_enables_rich_hover(self, sample_chart_data):
|
||||
"""Verify customdata is provided for rich hover information."""
|
||||
fig = create_icicle_figure(sample_chart_data, "Test Title")
|
||||
|
||||
icicle_trace = fig.data[0]
|
||||
|
||||
# customdata enables rich hover templates with additional info
|
||||
assert icicle_trace.customdata is not None, "customdata should be provided"
|
||||
|
||||
# customdata should be a 2D array with multiple columns of data
|
||||
assert len(icicle_trace.customdata) > 0, "customdata should have rows"
|
||||
# Each row should have multiple data points for hover display
|
||||
if hasattr(icicle_trace.customdata[0], '__len__'):
|
||||
assert len(icicle_trace.customdata[0]) >= 5, \
|
||||
"customdata should have multiple columns for rich hover"
|
||||
|
||||
|
||||
@pytest.mark.skipif(not HAS_VISUALIZATION, reason="Visualization module not available")
|
||||
class TestReflexCompatibility:
|
||||
"""Test that figures are compatible with Reflex's rx.plotly() component."""
|
||||
|
||||
def test_figure_to_json_serializable(self, sample_chart_data):
|
||||
"""Verify figure can be serialized to JSON (required for Reflex)."""
|
||||
fig = create_icicle_figure(sample_chart_data, "Test Title")
|
||||
|
||||
# Reflex needs to serialize the figure to JSON for the frontend
|
||||
try:
|
||||
json_data = fig.to_json()
|
||||
assert json_data is not None
|
||||
assert len(json_data) > 0
|
||||
except Exception as e:
|
||||
pytest.fail(f"Figure should be JSON serializable: {e}")
|
||||
|
||||
def test_figure_to_dict(self, sample_chart_data):
|
||||
"""Verify figure can be converted to dict (used by Reflex internally)."""
|
||||
fig = create_icicle_figure(sample_chart_data, "Test Title")
|
||||
|
||||
# Reflex may use to_dict internally
|
||||
fig_dict = fig.to_dict()
|
||||
|
||||
assert 'data' in fig_dict, "Figure dict should have data"
|
||||
assert 'layout' in fig_dict, "Figure dict should have layout"
|
||||
assert len(fig_dict['data']) > 0, "Data should not be empty"
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pytest.main([__file__, '-v'])
|
||||
Reference in New Issue
Block a user