@@ -2143,7 +2143,7 @@ def export_plotly_json(self, filename, plot_style = None, update_and_validate=Tr
2143
2143
return plotly_json_string
2144
2144
2145
2145
#simulate all series will simulate any series as needed.
2146
- def get_plotly_fig (self , plot_style = None , update_and_validate = True , simulate_all_series = True , evaluate_all_equations = True , adjust_implicit_data_ranges = True ):
2146
+ def get_plotly_fig (self , plot_style = None , update_and_validate = True , simulate_all_series = True , evaluate_all_equations = True , adjust_implicit_data_ranges = True , adjust_offset2d = True ):
2147
2147
"""
2148
2148
Constructs and returns a Plotly figure object based on the current fig_dict with optional preprocessing steps.
2149
2149
- A deep copy of fig_dict is created to avoid unintended mutation of the source object.
@@ -2177,16 +2177,23 @@ def get_plotly_fig(self, plot_style=None, update_and_validate=True, simulate_all
2177
2177
self .fig_dict = execute_implicit_data_series_operations (self .fig_dict ,
2178
2178
simulate_all_series = simulate_all_series ,
2179
2179
evaluate_all_equations = evaluate_all_equations ,
2180
- adjust_implicit_data_ranges = adjust_implicit_data_ranges )
2180
+ adjust_implicit_data_ranges = adjust_implicit_data_ranges ,
2181
+ adjust_offset2d = False )
2181
2182
#Regardless of implicit data series, we make a fig_dict copy, because we will clean self.fig_dict for creating the new plotting fig object.
2182
2183
original_fig_dict = copy .deepcopy (self .fig_dict )
2184
+ #The adjust_offset2d should be on the copy, if requested.
2185
+ self .fig_dict = execute_implicit_data_series_operations (self .fig_dict ,
2186
+ simulate_all_series = False ,
2187
+ evaluate_all_equations = False ,
2188
+ adjust_implicit_data_ranges = False ,
2189
+ adjust_offset2d = adjust_offset2d )
2183
2190
#before cleaning and validating, we'll apply styles.
2184
2191
plot_style = parse_plot_style (plot_style = plot_style )
2185
2192
self .apply_plot_style (plot_style = plot_style )
2186
2193
#Now we clean out the fields and make a plotly object.
2187
2194
if update_and_validate == True : #this will do some automatic 'corrections' during the validation.
2188
2195
self .update_and_validate_JSONGrapher_record (clean_for_plotly = False ) #We use the False argument here because the cleaning will be on the next line with beyond default arguments.
2189
- self .fig_dict = clean_json_fig_dict (self .fig_dict , fields_to_update = ['simulate' , 'custom_units_chevrons' , 'equation' , 'trace_style' , '3d_axes' , 'bubble' , 'superscripts' ])
2196
+ self .fig_dict = clean_json_fig_dict (self .fig_dict , fields_to_update = ['simulate' , 'custom_units_chevrons' , 'equation' , 'trace_style' , '3d_axes' , 'bubble' , 'superscripts' , 'nested_comments' , 'extraInformation' ])
2190
2197
fig = pio .from_json (json .dumps (self .fig_dict ))
2191
2198
#restore the original fig_dict.
2192
2199
self .fig_dict = original_fig_dict
@@ -2312,7 +2319,7 @@ def export():
2312
2319
#update_and_validate will 'clean' for plotly.
2313
2320
#In the case of creating a matplotlib figure, this really just means removing excess fields.
2314
2321
#simulate all series will simulate any series as needed.
2315
- def get_matplotlib_fig (self , plot_style = None , update_and_validate = True , simulate_all_series = True , evaluate_all_equations = True , adjust_implicit_data_ranges = True ):
2322
+ def get_matplotlib_fig (self , plot_style = None , update_and_validate = True , simulate_all_series = True , evaluate_all_equations = True , adjust_implicit_data_ranges = True , adjust_offset2d = True ):
2316
2323
"""
2317
2324
Constructs and returns a matplotlib figure generated from fig_dict, with optional simulation, preprocessing, and styling.
2318
2325
@@ -2340,9 +2347,16 @@ def get_matplotlib_fig(self, plot_style = None, update_and_validate=True, simula
2340
2347
self .fig_dict = execute_implicit_data_series_operations (self .fig_dict ,
2341
2348
simulate_all_series = simulate_all_series ,
2342
2349
evaluate_all_equations = evaluate_all_equations ,
2343
- adjust_implicit_data_ranges = adjust_implicit_data_ranges )
2350
+ adjust_implicit_data_ranges = adjust_implicit_data_ranges ,
2351
+ adjust_offset2d = False )
2344
2352
#Regardless of implicit data series, we make a fig_dict copy, because we will clean self.fig_dict for creating the new plotting fig object.
2345
2353
original_fig_dict = copy .deepcopy (self .fig_dict ) #we will get a copy, because otherwise the original fig_dict will be forced to be overwritten.
2354
+ #We adjust the offsets only after copying, and adjust that one alone.
2355
+ self .fig_dict = execute_implicit_data_series_operations (self .fig_dict ,
2356
+ simulate_all_series = False ,
2357
+ evaluate_all_equations = False ,
2358
+ adjust_implicit_data_ranges = False ,
2359
+ adjust_offset2d = adjust_offset2d )
2346
2360
#before cleaning and validating, we'll apply styles.
2347
2361
plot_style = parse_plot_style (plot_style = plot_style )
2348
2362
self .apply_plot_style (plot_style = plot_style )
@@ -4974,6 +4988,7 @@ def clean_json_fig_dict(json_fig_dict, fields_to_update=None):
4974
4988
- "trace_style": Removes internal style/tracetype metadata.
4975
4989
- "3d_axes": Updates layout and data_series for 3D plotting.
4976
4990
- "superscripts": Replaces superscript strings in titles and labels.
4991
+ - "offset": Removes the offset field from the layout field.
4977
4992
"""
4978
4993
if fields_to_update is None : # should not initialize mutable objects in arguments line, so doing here.
4979
4994
fields_to_update = ["title_field" , "extraInformation" , "nested_comments" ]
@@ -5410,7 +5425,7 @@ def update_implicit_data_series_data(target_fig_dict, source_fig_dict, parallel_
5410
5425
return updated_fig_dict
5411
5426
5412
5427
5413
- def execute_implicit_data_series_operations (fig_dict , simulate_all_series = True , evaluate_all_equations = True , adjust_implicit_data_ranges = True ):
5428
+ def execute_implicit_data_series_operations (fig_dict , simulate_all_series = True , evaluate_all_equations = True , adjust_implicit_data_ranges = True , adjust_offset2d = False ):
5414
5429
"""
5415
5430
Processes data_series dicts in a fig_dict, executing simulate/equation-based series as needed, including setting the simulate/equation evaluation ranges as needed,
5416
5431
then provides the simulated/equation-evaluated data into the original fig_dict without altering original fig_dict implicit ranges.
@@ -5432,7 +5447,6 @@ def execute_implicit_data_series_operations(fig_dict, simulate_all_series=True,
5432
5447
while preserving their original metadata and x_range_default boundaries.
5433
5448
"""
5434
5449
import copy # Import inside function for modularity
5435
-
5436
5450
# Create a copy for processing implicit series separately
5437
5451
fig_dict_for_implicit = copy .deepcopy (fig_dict )
5438
5452
#first check if any data_series have an equatinon or simulation field. If not, we'll skip.
@@ -5461,10 +5475,137 @@ def execute_implicit_data_series_operations(fig_dict, simulate_all_series=True,
5461
5475
fig_dict_for_implicit = evaluate_equations_as_needed_in_fig_dict (fig_dict_for_implicit )
5462
5476
# Copy results back without overwriting the ranges
5463
5477
fig_dict = update_implicit_data_series_data (target_fig_dict = fig_dict , source_fig_dict = fig_dict_for_implicit , parallel_structure = True , modify_target_directly = True )
5478
+
5479
+ if adjust_offset2d : #This should occur after simulations and evaluations because it could rely on them.
5480
+ #First check if the layout style is that of an offset2d graph.
5481
+ layout_style = fig_dict .get ("plot_style" , {}).get ("layout_style" , "" )
5482
+ if "offset2d" in layout_style :
5483
+ #This case is different from others -- we will not modify target directly because we are not doing a merge.
5484
+ fig_dict = extract_and_implement_offsets (fig_dict_for_implicit , modify_target_directly = False )
5485
+ return fig_dict
5486
+
5487
+ #Small helper function to find if an offset is a float scalar.
5488
+ def is_float_scalar (value ):
5489
+ try :
5490
+ float (value )
5491
+ return True
5492
+ except (TypeError , ValueError ):
5493
+ return False
5464
5494
5495
+ def extract_and_implement_offsets (fig_dict , modify_target_directly = False , graphical_dimensionality = 2 ):
5496
+ import numpy as np
5497
+ #First, extract offsets.
5498
+ import copy
5499
+ if modify_target_directly == False :
5500
+ fig_dict_with_offsets = copy .deepcopy (fig_dict )
5501
+ else :
5502
+ fig_dict_with_offsets = fig_dict
5503
+ #initialize offset_variable_name as the case someone decides to specify one.
5504
+ offset_variable_name = ""
5505
+ if "offset" in fig_dict ["layout" ]:
5506
+ #This is the easy case, because we don't need to determine the offset.
5507
+ offset = fig_dict ["layout" ]["offset" ]
5508
+ if is_float_scalar (offset ):
5509
+ offset = fig_dict ["layout" ]["offset" ]
5510
+ elif isinstance (offset ,str ):#check if is a string, in which case it is a variable name.
5511
+ #in this case it is a variable where we will extract it from each dataset.
5512
+ offset_variable_name = offset
5513
+ else :
5514
+ #Else assume the offset is an array like object, of length equal to number of datapoints.
5515
+ offset = np .array (offset , dtype = float )
5516
+ #Now, implement offsets.
5517
+ if graphical_dimensionality == 2 :
5518
+ current_series_offset = 0 # Initialize total offset
5519
+ for data_series_index in range (len (fig_dict ["data" ])):
5520
+ data_series_y_values = np .array (fig_dict ["data" ][data_series_index ]["y" ])
5521
+ if data_series_index == 0 :
5522
+ fig_dict_with_offsets ["data" ][data_series_index ]["y" ] = list (data_series_y_values )
5523
+ else :
5524
+ # Determine the current offset
5525
+ if offset_variable_name != "" :
5526
+ incremental_offset = np .array (fig_dict ["data" ][data_series_index ][offset_variable_name ], dtype = float )
5527
+ else :
5528
+ incremental_offset = np .array (offset , dtype = float )
5529
+ current_series_offset += incremental_offset # Accumulate the offset
5530
+ fig_dict_with_offsets ["data" ][data_series_index ]["y" ] = list (data_series_y_values + current_series_offset )
5531
+ else :
5532
+ #This is the hard case, we need to determine a reasonable offset for the dataseries.
5533
+ if graphical_dimensionality == 2 :
5534
+ fig_dict_with_offsets = determine_and_apply_offset2d_for_fig_dict (fig_dict , modify_target_directly = modify_target_directly )
5535
+ return fig_dict_with_offsets
5536
+
5537
+ #A function that calls helper functions to determine and apply a 2D offset to fig_dict
5538
+ def determine_and_apply_offset2d_for_fig_dict (fig_dict , modify_target_directly = False ):
5539
+ if modify_target_directly == False :
5540
+ import copy
5541
+ fig_dict = copy .deepcopy (fig_dict )
5542
+ #First, extract data into a numpy array like [[x1, y1], [x2, y2], ...]
5543
+ all_series_array = extract_all_xy_series_data_from_fig_dict (fig_dict )
5544
+ #Then determine and apply a vertical offset. For now, we'll only support using the default
5545
+ #argument which is 1.2 times the maximum height in the series.
5546
+ #If someone wants to do something different, they can provide their own vertical offset value.
5547
+ offset_data_array = apply_vertical_offset2d_for_numpy_arrays_list (all_series_array )
5548
+ #Then, put the data back in.
5549
+ fig_dict = inject_xy_series_data_into_fig_dict (fig_dict = fig_dict , data_list = offset_data_array )
5465
5550
return fig_dict
5466
5551
5552
+ def extract_all_xy_series_data_from_fig_dict (fig_dict ):
5553
+ """
5554
+ Extracts all x and y values from a Plotly figure dictionary into a list of NumPy arrays.
5555
+ Each array in the list has shape (n_points, 2), where each row is [x, y] like [[x1, y1], [x2, y2], ...].
5556
+ """
5557
+ import numpy as np
5558
+ series_list = []
5559
+ for data_series in fig_dict .get ('data' , []):
5560
+ x_vals = np .array (data_series .get ('x' , []))
5561
+ y_vals = np .array (data_series .get ('y' , []))
5562
+ #Only keep the xy data if x and y lists are the same length.
5563
+ if len (x_vals ) == len (y_vals ):
5564
+ series_list .append (np .stack ((x_vals , y_vals ), axis = - 1 ))
5565
+ return series_list
5467
5566
5567
+ def apply_vertical_offset2d_for_numpy_arrays_list (data_list , offset_multiplier = 1.2 ):
5568
+ """
5569
+ Applies vertical offsets to a list of 2D NumPy arrays.
5570
+ Each array has shape (n_points, 2), with rows like [[x1, y1], [x2, y2], ...].
5571
+ Returns a list of the same structure with adjusted y values per series index.
5572
+ """
5573
+ import numpy as np
5574
+ spans = [np .max (series [:, 1 ]) - np .min (series [:, 1 ]) if len (series ) > 0 else 0 for series in data_list ]
5575
+ base_offset = max (spans ) * offset_multiplier if spans else 0
5576
+ offset_data_list = []
5577
+ for series_index , series_array in enumerate (data_list ):
5578
+ # Skip empty series but preserve its position in the output
5579
+ if len (series_array ) == 0 :
5580
+ offset_data_list .append (series_array )
5581
+ continue
5582
+ # Ensure float type for numerical stability when applying offsets
5583
+ offset_series = np .copy (series_array ).astype (float )
5584
+ # Apply vertical offset based on series index and base offset
5585
+ #print("line 5574, before the addition", offset_series)
5586
+ offset_series [:, 1 ] += series_index * base_offset
5587
+ #print("line 5576, after the addition", offset_series)
5588
+ # Add the adjusted series to the output list
5589
+ offset_data_list .append (offset_series )
5590
+ return offset_data_list
5591
+
5592
+
5593
+
5594
+
5595
+ def inject_xy_series_data_into_fig_dict (fig_dict , data_list ):
5596
+ """
5597
+ Updates a Plotly figure dictionary in-place by injecting x and y data from a list of NumPy arrays.
5598
+ Each array must have shape (n_points, 2), where each row is [x, y] like [[x1, y1], [x2, y2], ...].
5599
+ The number of arrays must match the number of traces in the figure.
5600
+ """
5601
+ n_traces = len (fig_dict .get ('data' , []))
5602
+ if len (data_list ) != n_traces :
5603
+ raise ValueError ("Mismatch between number of traces and number of data series." )
5604
+ for i , trace in enumerate (fig_dict ['data' ]):
5605
+ series = data_list [i ]
5606
+ trace ['x' ] = series [:, 0 ].tolist ()
5607
+ trace ['y' ] = series [:, 1 ].tolist ()
5608
+ return fig_dict
5468
5609
5469
5610
### End of section of file that has functions for "simulate" and "equation" fields, to evaluate equations and call external javascript simulators, as well as support functions###
5470
5611
0 commit comments