@@ -1361,3 +1361,242 @@ def get_midpoint_y(bundled):
13611361 assert mid_y_top - mid_y_bottom > 2.0 , (
13621362 f"Weight effect should create >2.0 difference, got { mid_y_top - mid_y_bottom :.2f} "
13631363 )
1364+
1365+
1366+ class TestCaching :
1367+ """Tests for caching functionality in stat_edgebundle and geom_edgebundle."""
1368+
1369+ def test_stat_edgebundle_cache_hit (self ):
1370+ """Test that identical calls return cached result."""
1371+ from ggplotly .stats .stat_edgebundle import _bundling_cache , clear_bundling_cache
1372+
1373+ clear_bundling_cache ()
1374+
1375+ edges_df = pd .DataFrame ({
1376+ 'x' : [0 , 0 ],
1377+ 'y' : [0 , 1 ],
1378+ 'xend' : [10 , 10 ],
1379+ 'yend' : [0 , 1 ]
1380+ })
1381+
1382+ stat = stat_edgebundle (C = 2 , I = 5 , verbose = False )
1383+
1384+ # First call - should compute and cache
1385+ result1 = stat .compute (edges_df )
1386+ cache_size_after_first = len (_bundling_cache )
1387+ assert cache_size_after_first == 1
1388+
1389+ # Second call with same data - should use cache
1390+ result2 = stat .compute (edges_df )
1391+ cache_size_after_second = len (_bundling_cache )
1392+ assert cache_size_after_second == 1 # No new cache entry
1393+
1394+ # Results should be identical
1395+ pd .testing .assert_frame_equal (result1 , result2 )
1396+
1397+ def test_stat_edgebundle_cache_miss_different_data (self ):
1398+ """Test that different data creates new cache entry."""
1399+ from ggplotly .stats .stat_edgebundle import _bundling_cache , clear_bundling_cache
1400+
1401+ clear_bundling_cache ()
1402+
1403+ edges_df1 = pd .DataFrame ({
1404+ 'x' : [0 , 0 ],
1405+ 'y' : [0 , 1 ],
1406+ 'xend' : [10 , 10 ],
1407+ 'yend' : [0 , 1 ]
1408+ })
1409+
1410+ edges_df2 = pd .DataFrame ({
1411+ 'x' : [0 , 0 ],
1412+ 'y' : [0 , 2 ], # Different y value
1413+ 'xend' : [10 , 10 ],
1414+ 'yend' : [0 , 2 ]
1415+ })
1416+
1417+ stat = stat_edgebundle (C = 2 , I = 5 , verbose = False )
1418+
1419+ stat .compute (edges_df1 )
1420+ assert len (_bundling_cache ) == 1
1421+
1422+ stat .compute (edges_df2 )
1423+ assert len (_bundling_cache ) == 2 # New cache entry
1424+
1425+ def test_stat_edgebundle_cache_miss_different_params (self ):
1426+ """Test that different algorithm parameters create new cache entry."""
1427+ from ggplotly .stats .stat_edgebundle import _bundling_cache , clear_bundling_cache
1428+
1429+ clear_bundling_cache ()
1430+
1431+ edges_df = pd .DataFrame ({
1432+ 'x' : [0 , 0 ],
1433+ 'y' : [0 , 1 ],
1434+ 'xend' : [10 , 10 ],
1435+ 'yend' : [0 , 1 ]
1436+ })
1437+
1438+ stat1 = stat_edgebundle (C = 2 , I = 5 , verbose = False )
1439+ stat2 = stat_edgebundle (C = 3 , I = 5 , verbose = False ) # Different C
1440+
1441+ stat1 .compute (edges_df )
1442+ assert len (_bundling_cache ) == 1
1443+
1444+ stat2 .compute (edges_df )
1445+ assert len (_bundling_cache ) == 2 # New cache entry due to different params
1446+
1447+ def test_stat_edgebundle_cache_miss_different_weights (self ):
1448+ """Test that different weights create new cache entry."""
1449+ from ggplotly .stats .stat_edgebundle import _bundling_cache , clear_bundling_cache
1450+
1451+ clear_bundling_cache ()
1452+
1453+ edges_df = pd .DataFrame ({
1454+ 'x' : [0 , 0 ],
1455+ 'y' : [0 , 1 ],
1456+ 'xend' : [10 , 10 ],
1457+ 'yend' : [0 , 1 ]
1458+ })
1459+
1460+ stat = stat_edgebundle (C = 2 , I = 5 , verbose = False )
1461+
1462+ weights1 = np .array ([1.0 , 1.0 ])
1463+ weights2 = np .array ([10.0 , 1.0 ]) # Different weights
1464+
1465+ stat .compute (edges_df , weights = weights1 )
1466+ assert len (_bundling_cache ) == 1
1467+
1468+ stat .compute (edges_df , weights = weights2 )
1469+ assert len (_bundling_cache ) == 2 # New cache entry
1470+
1471+ def test_stat_edgebundle_cache_hit_with_weights (self ):
1472+ """Test cache hit when same weights are used."""
1473+ from ggplotly .stats .stat_edgebundle import _bundling_cache , clear_bundling_cache
1474+
1475+ clear_bundling_cache ()
1476+
1477+ edges_df = pd .DataFrame ({
1478+ 'x' : [0 , 0 ],
1479+ 'y' : [0 , 1 ],
1480+ 'xend' : [10 , 10 ],
1481+ 'yend' : [0 , 1 ]
1482+ })
1483+
1484+ stat = stat_edgebundle (C = 2 , I = 5 , verbose = False )
1485+ weights = np .array ([5.0 , 10.0 ])
1486+
1487+ result1 = stat .compute (edges_df , weights = weights )
1488+ assert len (_bundling_cache ) == 1
1489+
1490+ result2 = stat .compute (edges_df , weights = weights .copy ()) # Same values
1491+ assert len (_bundling_cache ) == 1 # Cache hit
1492+
1493+ pd .testing .assert_frame_equal (result1 , result2 )
1494+
1495+ def test_clear_bundling_cache (self ):
1496+ """Test that clear_bundling_cache clears the cache."""
1497+ from ggplotly .stats .stat_edgebundle import _bundling_cache , clear_bundling_cache
1498+
1499+ clear_bundling_cache ()
1500+
1501+ edges_df = pd .DataFrame ({
1502+ 'x' : [0 , 0 ],
1503+ 'y' : [0 , 1 ],
1504+ 'xend' : [10 , 10 ],
1505+ 'yend' : [0 , 1 ]
1506+ })
1507+
1508+ stat = stat_edgebundle (C = 2 , I = 5 , verbose = False )
1509+ stat .compute (edges_df )
1510+ assert len (_bundling_cache ) >= 1
1511+
1512+ clear_bundling_cache ()
1513+ assert len (_bundling_cache ) == 0
1514+
1515+ def test_cache_survives_stat_deepcopy (self ):
1516+ """Test that cache persists across deepcopy of stat objects."""
1517+ import copy
1518+ from ggplotly .stats .stat_edgebundle import _bundling_cache , clear_bundling_cache
1519+
1520+ clear_bundling_cache ()
1521+
1522+ edges_df = pd .DataFrame ({
1523+ 'x' : [0 , 0 ],
1524+ 'y' : [0 , 1 ],
1525+ 'xend' : [10 , 10 ],
1526+ 'yend' : [0 , 1 ]
1527+ })
1528+
1529+ stat1 = stat_edgebundle (C = 2 , I = 5 , verbose = False )
1530+ result1 = stat1 .compute (edges_df )
1531+ assert len (_bundling_cache ) == 1
1532+
1533+ # Deep copy the stat (this happens during ggplot processing)
1534+ stat2 = copy .deepcopy (stat1 )
1535+
1536+ # Compute with the copy - should hit cache
1537+ result2 = stat2 .compute (edges_df )
1538+ assert len (_bundling_cache ) == 1 # Still just one entry
1539+
1540+ pd .testing .assert_frame_equal (result1 , result2 )
1541+
1542+ def test_geom_edgebundle_uses_cache (self ):
1543+ """Test that geom_edgebundle uses the stat cache correctly."""
1544+ from ggplotly .stats .stat_edgebundle import _bundling_cache , clear_bundling_cache
1545+
1546+ clear_bundling_cache ()
1547+
1548+ edges_df = pd .DataFrame ({
1549+ 'x' : [0 , 0 ],
1550+ 'y' : [0 , 1 ],
1551+ 'xend' : [10 , 10 ],
1552+ 'yend' : [0 , 1 ]
1553+ })
1554+
1555+ # First plot - should populate cache
1556+ fig1 = (
1557+ ggplot (edges_df , aes (x = 'x' , y = 'y' , xend = 'xend' , yend = 'yend' ))
1558+ + geom_edgebundle (C = 2 , I = 5 , verbose = False , show_highlight = False )
1559+ ).draw ()
1560+ cache_size_after_first = len (_bundling_cache )
1561+ assert cache_size_after_first >= 1
1562+
1563+ # Second plot with same data and params - should use cache
1564+ fig2 = (
1565+ ggplot (edges_df , aes (x = 'x' , y = 'y' , xend = 'xend' , yend = 'yend' ))
1566+ + geom_edgebundle (C = 2 , I = 5 , verbose = False , show_highlight = False )
1567+ ).draw ()
1568+ cache_size_after_second = len (_bundling_cache )
1569+ assert cache_size_after_second == cache_size_after_first
1570+
1571+ def test_cache_key_includes_all_params (self ):
1572+ """Test that cache key includes all relevant algorithm parameters."""
1573+ from ggplotly .stats .stat_edgebundle import _bundling_cache , clear_bundling_cache
1574+
1575+ clear_bundling_cache ()
1576+
1577+ edges_df = pd .DataFrame ({
1578+ 'x' : [0 , 0 ],
1579+ 'y' : [0 , 1 ],
1580+ 'xend' : [10 , 10 ],
1581+ 'yend' : [0 , 1 ]
1582+ })
1583+
1584+ # Create stats with different parameter variations
1585+ param_variations = [
1586+ {'K' : 1.0 , 'E' : 1.0 , 'C' : 2 , 'P' : 1 , 'S' : 0.04 , 'P_rate' : 2 , 'I' : 5 , 'I_rate' : 2 / 3 , 'compatibility_threshold' : 0.6 },
1587+ {'K' : 2.0 , 'E' : 1.0 , 'C' : 2 , 'P' : 1 , 'S' : 0.04 , 'P_rate' : 2 , 'I' : 5 , 'I_rate' : 2 / 3 , 'compatibility_threshold' : 0.6 }, # Different K
1588+ {'K' : 1.0 , 'E' : 2.0 , 'C' : 2 , 'P' : 1 , 'S' : 0.04 , 'P_rate' : 2 , 'I' : 5 , 'I_rate' : 2 / 3 , 'compatibility_threshold' : 0.6 }, # Different E
1589+ {'K' : 1.0 , 'E' : 1.0 , 'C' : 2 , 'P' : 2 , 'S' : 0.04 , 'P_rate' : 2 , 'I' : 5 , 'I_rate' : 2 / 3 , 'compatibility_threshold' : 0.6 }, # Different P
1590+ {'K' : 1.0 , 'E' : 1.0 , 'C' : 2 , 'P' : 1 , 'S' : 0.08 , 'P_rate' : 2 , 'I' : 5 , 'I_rate' : 2 / 3 , 'compatibility_threshold' : 0.6 }, # Different S
1591+ {'K' : 1.0 , 'E' : 1.0 , 'C' : 2 , 'P' : 1 , 'S' : 0.04 , 'P_rate' : 3 , 'I' : 5 , 'I_rate' : 2 / 3 , 'compatibility_threshold' : 0.6 }, # Different P_rate
1592+ {'K' : 1.0 , 'E' : 1.0 , 'C' : 2 , 'P' : 1 , 'S' : 0.04 , 'P_rate' : 2 , 'I' : 10 , 'I_rate' : 2 / 3 , 'compatibility_threshold' : 0.6 }, # Different I
1593+ {'K' : 1.0 , 'E' : 1.0 , 'C' : 2 , 'P' : 1 , 'S' : 0.04 , 'P_rate' : 2 , 'I' : 5 , 'I_rate' : 0.5 , 'compatibility_threshold' : 0.6 }, # Different I_rate
1594+ {'K' : 1.0 , 'E' : 1.0 , 'C' : 2 , 'P' : 1 , 'S' : 0.04 , 'P_rate' : 2 , 'I' : 5 , 'I_rate' : 2 / 3 , 'compatibility_threshold' : 0.8 }, # Different threshold
1595+ ]
1596+
1597+ for params in param_variations :
1598+ stat = stat_edgebundle (verbose = False , ** params )
1599+ stat .compute (edges_df )
1600+
1601+ # Each parameter variation should create a unique cache entry
1602+ assert len (_bundling_cache ) == len (param_variations )
0 commit comments