@@ -1155,45 +1155,96 @@ def _prepare_sankey_data(
11551155
11561156 return ds , title
11571157
1158- def _prepare_effects_sankey_data (
1158+ def _build_effects_sankey (
11591159 self ,
1160- effect : str ,
11611160 select : SelectType | None ,
1162- ) -> tuple [xr .Dataset , str ]:
1163- """Prepare effects data for Sankey diagram.
1161+ colors : ColorType | None ,
1162+ ** plotly_kwargs : Any ,
1163+ ) -> tuple [go .Figure , xr .Dataset ]:
1164+ """Build Sankey diagram showing contributions from components to effects.
1165+
1166+ Creates a Sankey with:
1167+ - Left side: Components (grouped by type)
1168+ - Right side: Effects (costs, CO2, etc.)
1169+ - Links: Contributions from each component to each effect
11641170
11651171 Args:
1166- effect: Effect name to display (e.g., 'costs', 'CO2').
11671172 select: xarray-style selection.
1173+ colors: Color specification for nodes.
1174+ **plotly_kwargs: Additional Plotly layout arguments.
11681175
11691176 Returns:
1170- Tuple of (Dataset with flow labels as variables, title string ).
1177+ Tuple of (Plotly Figure, Dataset with link data ).
11711178 """
11721179 total_effects = self ._stats .total_effects
1173- if effect not in total_effects :
1174- available = list (total_effects .data_vars )
1175- raise ValueError (f"Effect '{ effect } ' not found. Available effects: { available } " )
11761180
1177- effect_data = total_effects [effect ]
1178- effect_data = _apply_selection (effect_data , select )
1181+ # Collect all links: component -> effect
1182+ nodes : set [str ] = set ()
1183+ links : dict [str , list ] = {'source' : [], 'target' : [], 'value' : [], 'label' : []}
11791184
1180- # Sum over any remaining dimensions (period, scenario)
1181- for dim in ['period' , 'scenario' ]:
1182- if dim in effect_data .dims :
1183- effect_data = effect_data .sum (dim = dim )
1185+ for effect_name in total_effects .data_vars :
1186+ effect_data = total_effects [effect_name ]
1187+ effect_data = _apply_selection (effect_data , select )
11841188
1185- # Extract flow-related contributors and create a Dataset with flow labels as variables
1186- flow_labels = list (self ._fs .flows .keys ())
1187- contributors = effect_data .coords ['contributor' ].values
1189+ # Sum over any remaining dimensions
1190+ for dim in ['period' , 'scenario' ]:
1191+ if dim in effect_data .dims :
1192+ effect_data = effect_data .sum (dim = dim )
1193+
1194+ contributors = effect_data .coords ['contributor' ].values
1195+ components = effect_data .coords ['component' ].values
11881196
1189- result = {}
1190- for flow_label in flow_labels :
1191- if flow_label in contributors :
1192- value = float (effect_data .sel (contributor = flow_label ).values )
1193- if abs (value ) > 1e-6 :
1194- result [flow_label ] = xr .DataArray (value )
1197+ for contributor , component in zip (contributors , components , strict = False ):
1198+ value = float (effect_data .sel (contributor = contributor ).values )
1199+ if not np .isfinite (value ) or abs (value ) < 1e-6 :
1200+ continue
11951201
1196- return xr .Dataset (result ), f'{ effect .capitalize ()} per Flow'
1202+ # Use component as source node, effect as target
1203+ source = str (component )
1204+ target = f'[{ effect_name } ]' # Bracket notation to distinguish effects
1205+
1206+ nodes .add (source )
1207+ nodes .add (target )
1208+ links ['source' ].append (source )
1209+ links ['target' ].append (target )
1210+ links ['value' ].append (abs (value ))
1211+ links ['label' ].append (f'{ contributor } → { effect_name } : { value :.2f} ' )
1212+
1213+ # Create figure
1214+ node_list = list (nodes )
1215+ node_indices = {n : i for i , n in enumerate (node_list )}
1216+
1217+ color_map = process_colors (colors , node_list )
1218+ node_colors = [color_map [node ] for node in node_list ]
1219+
1220+ fig = go .Figure (
1221+ data = [
1222+ go .Sankey (
1223+ node = dict (
1224+ pad = 15 , thickness = 20 , line = dict (color = 'black' , width = 0.5 ), label = node_list , color = node_colors
1225+ ),
1226+ link = dict (
1227+ source = [node_indices [s ] for s in links ['source' ]],
1228+ target = [node_indices [t ] for t in links ['target' ]],
1229+ value = links ['value' ],
1230+ label = links ['label' ],
1231+ ),
1232+ )
1233+ ]
1234+ )
1235+ fig .update_layout (title = 'Effect Contributions by Component' , ** plotly_kwargs )
1236+
1237+ sankey_ds = xr .Dataset (
1238+ {'value' : ('link' , links ['value' ])},
1239+ coords = {
1240+ 'link' : range (len (links ['value' ])),
1241+ 'source' : ('link' , links ['source' ]),
1242+ 'target' : ('link' , links ['target' ]),
1243+ 'label' : ('link' , links ['label' ]),
1244+ },
1245+ )
1246+
1247+ return fig , sankey_ds
11971248
11981249 def _build_sankey_links (
11991250 self ,
@@ -1283,8 +1334,7 @@ def _create_sankey_figure(
12831334 def sankey (
12841335 self ,
12851336 * ,
1286- mode : Literal ['flow_hours' , 'sizes' , 'peak_flow' ] = 'flow_hours' ,
1287- effect : str | None = None ,
1337+ mode : Literal ['flow_hours' , 'sizes' , 'peak_flow' , 'effects' ] = 'flow_hours' ,
12881338 timestep : int | str | None = None ,
12891339 aggregate : Literal ['sum' , 'mean' ] = 'sum' ,
12901340 select : SelectType | None = None ,
@@ -1299,8 +1349,7 @@ def sankey(
12991349 - 'flow_hours': Energy/material amounts (default)
13001350 - 'sizes': Investment capacities
13011351 - 'peak_flow': Maximum flow rates
1302- effect: Effect name to display instead of mode (e.g., 'costs', 'CO2').
1303- If provided, shows effect contributions per flow. Overrides mode.
1352+ - 'effects': Component contributions to all effects (costs, CO2, etc.)
13041353 timestep: Specific timestep to show, or None for aggregation (flow_hours only).
13051354 aggregate: How to aggregate if timestep is None ('sum' or 'mean', flow_hours only).
13061355 select: xarray-style selection.
@@ -1318,25 +1367,26 @@ def sankey(
13181367 >>> flow_system.statistics.plot.sankey(mode='sizes')
13191368 >>> # Show peak flow rates
13201369 >>> flow_system.statistics.plot.sankey(mode='peak_flow')
1321- >>> # Show costs per flow
1322- >>> flow_system.statistics.plot.sankey(effect='costs')
1323- >>> # Show CO2 emissions per flow
1324- >>> flow_system.statistics.plot.sankey(effect='CO2')
1370+ >>> # Show effect contributions (components -> effects like costs, CO2)
1371+ >>> flow_system.statistics.plot.sankey(mode='effects')
13251372 """
13261373 self ._stats ._require_solution ()
13271374
1328- if effect is not None :
1329- ds , title = self ._prepare_effects_sankey_data ( effect , select )
1375+ if mode == 'effects' :
1376+ fig , sankey_ds = self ._build_effects_sankey ( select , colors , ** plotly_kwargs )
13301377 else :
13311378 ds , title = self ._prepare_sankey_data (mode , timestep , aggregate , select )
1332-
1333- nodes , links = self ._build_sankey_links (ds )
1334- fig = self ._create_sankey_figure (nodes , links , colors , title , ** plotly_kwargs )
1335-
1336- sankey_ds = xr .Dataset (
1337- {'value' : ('link' , links ['value' ])},
1338- coords = {'link' : links ['label' ], 'source' : ('link' , links ['source' ]), 'target' : ('link' , links ['target' ])},
1339- )
1379+ nodes , links = self ._build_sankey_links (ds )
1380+ fig = self ._create_sankey_figure (nodes , links , colors , title , ** plotly_kwargs )
1381+
1382+ sankey_ds = xr .Dataset (
1383+ {'value' : ('link' , links ['value' ])},
1384+ coords = {
1385+ 'link' : links ['label' ],
1386+ 'source' : ('link' , links ['source' ]),
1387+ 'target' : ('link' , links ['target' ]),
1388+ },
1389+ )
13401390
13411391 if show is None :
13421392 show = CONFIG .Plotting .default_show
0 commit comments