jsongrapher 4.4__py3-none-any.whl → 4.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,6 +14,29 @@ global_records_list = [] #This list holds onto records as they are added. Index
14
14
  #That takes filenames and adds new JSONGrapher records to a global_records_list
15
15
  #If the all_selected_file_paths and newest_file_name_and_path are [] and [], that means to clear the global_records_list.
16
16
  def add_records_to_global_records_list_and_plot(all_selected_file_paths, newly_added_file_paths, plot_immediately=True):
17
+ """
18
+ Typically adds new JSONGrapher records to a global records list, merging their data into the main record, and also launches the new plot.
19
+
20
+ In the desktop/local version of JSONgrapehr, a global variable with a records list is used to keep track of JSONGrapher records added
21
+ As this is the desktop/local version of JSONGrapher, the inputs to this function are actually file paths,
22
+ and those file paths are used to create JSONGrapher record objects.
23
+ This function takes in the existing file paths of records in the global records list as well as any newly added file paths.
24
+ The function does not take the global records list as an argument but treats it as an implicit argument.
25
+ As records are added, they are stored in thes global records list before being merged.
26
+
27
+ If both input path lists for this function are empty, the global records list is cleared.
28
+ If input paths are received, if no prior records exist, a new record is created and used as the merge base. Otherwise, new records are appended and merged
29
+ into the existing master record. Optionally triggers a plot update and returns a JSON string
30
+ representation of the updated figure.
31
+
32
+ Args:
33
+ all_selected_file_paths (list[str]): All file paths currently selected by the user.
34
+ newly_added_file_paths (list[str]): File paths recently added to the selection.
35
+ plot_immediately (bool, optional): Whether to trigger a plot update after processing. Default is True.
36
+
37
+ Returns:
38
+ list[str]: A list containing a JSON string of the updated figure, suitable for export.
39
+ """
17
40
  #First check if we have received a "clear" condition.
18
41
  if (len(all_selected_file_paths) == 0) and (len(newly_added_file_paths) == 0):
19
42
  global_records_list.clear()
@@ -52,6 +75,22 @@ def add_records_to_global_records_list_and_plot(all_selected_file_paths, newly_a
52
75
  #This ia JSONGrapher specific wrapper function to drag_and_drop_gui create_and_launch.
53
76
  #This launches the python based JSONGrapher GUI.
54
77
  def launch():
78
+ """
79
+ Launches the JSONGrapher graphical user interface.
80
+
81
+ Attempts to import and start the drag-and-drop GUI interface used for selecting files
82
+ and triggering the record addition workflow.
83
+ In the desktop/local version of JSONGrapher, the a global variable is used
84
+ to store each record as is added and merged in.
85
+ This function returns that updated global records list.
86
+ The first index of the global records list will include the merged record.
87
+
88
+ Args:
89
+ No arguments.
90
+
91
+ Returns:
92
+ list[JSONGrapherRecord]: The updated list of global records after GUI interaction.
93
+ """
55
94
  try:
56
95
  import JSONGrapher.drag_and_drop_gui as drag_and_drop_gui
57
96
  except ImportError:
@@ -70,6 +109,18 @@ def launch():
70
109
  # intuitive to create class objects that way, this variable is actually just a reference
71
110
  # so that we don't have to map the arguments.
72
111
  def create_new_JSONGrapherRecord(hints=False):
112
+ """
113
+ Creates and returns a new JSONGrapherRecord instance, representing a JSONGrapher record.
114
+
115
+ Constructs the new record using the JSONGrapherRecord class constructor. If hints are enabled,
116
+ additional annotation fields are pre-populated to guide user input.
117
+
118
+ Args:
119
+ hints (bool, optional): Whether to include hint fields in the new record. Defaults to False.
120
+
121
+ Returns:
122
+ JSONGrapherRecord: A new instance of a JSONGrapher record, optionally populated with hints.
123
+ """
73
124
  #we will create a new record. While we could populate it with the init,
74
125
  #we will use the functions since it makes thsi function a bit easier to follow.
75
126
  new_record = JSONGrapherRecord()
@@ -79,10 +130,40 @@ def create_new_JSONGrapherRecord(hints=False):
79
130
 
80
131
  #This is actually a wrapper around merge_JSONGrapherRecords. Made for convenience.
81
132
  def load_JSONGrapherRecords(recordsList):
133
+ """
134
+ This is actually a wrapper around merge_JSONGrapherRecords. Made for convenience.
135
+ Merges a list of JSONGrapher records into a single combined record.
136
+
137
+ Passes the provided list directly into the merge function, which consolidates
138
+ multiple records into a single, unified structure.
139
+
140
+ Args:
141
+ recordsList (list[JSONGrapherRecord]): A list of JSONGrapher records to merge.
142
+
143
+ Returns:
144
+ JSONGrapherRecord: A single merged record resulting from merging all input records.
145
+ """
82
146
  return merge_JSONGrapherRecords(recordsList)
83
147
 
84
148
  #This is actually a wrapper around merge_JSONGrapherRecords. Made for convenience.
85
149
  def import_JSONGrapherRecords(recordsList):
150
+ """
151
+ This is actually a wrapper around merge_JSONGrapherRecords. Made for convenience.
152
+ Imports and merges multiple JSONGrapher records into a single consolidated record.
153
+
154
+ This works because when merge_JSONGrapherRecords receives a list
155
+ it checks whether each item in the list is a filepath or a record,
156
+ and when it is a filepath the merge_JSONGrapherRecords function
157
+ will automatically import the record from the filepath to make a new record.
158
+
159
+ This function delegates directly to the merge function to unify all records in the provided list.
160
+
161
+ Args:
162
+ recordsList (list[JSONGrapherRecord]): The list of records to merge.
163
+
164
+ Returns:
165
+ JSONGrapherRecord: A single merged record resulting from merging all input records.
166
+ """
86
167
  return merge_JSONGrapherRecords(recordsList)
87
168
 
88
169
  #This is a function for merging JSONGrapher records.
@@ -92,6 +173,24 @@ def import_JSONGrapherRecords(recordsList):
92
173
  #The units used will be that of the first record encountered
93
174
  #if changing this function's arguments, then also change those for load_JSONGrapherRecords and import_JSONGrapherRecords
94
175
  def merge_JSONGrapherRecords(recordsList):
176
+ """
177
+ Merges multiple JSONGrapher records into one, including converting units by scaling data as needed.
178
+
179
+ Accepts a list of records, each of which may be a JSONGrapherRecord instance, JSONGrapher records as dictionaries
180
+ (which are basically JSON objects), or a string which is filepath to a stored JSON file.
181
+ The records list received can be a mix between these different types of ways of providing reords.
182
+
183
+ For each record, the figure dictionary (fig_dict) is extracted and used for merging. Unit labels are compared and,
184
+ if necessary, data values are scaled to match the units of the first record before merging.
185
+ All data series are consolidated into the single single merged record that is returned.
186
+
187
+ Args:
188
+ recordsList (list): A list of records to merge. May include JSONGrapherRecord instances, JSONGrapher records as dictionaries
189
+ (which are basically JSON objects), or a string which is filepath to a stored JSON file.
190
+
191
+ Returns:
192
+ JSONGrapherRecord: A single merged record resulting from merging all input records.
193
+ """
95
194
  if type(recordsList) == type(""):
96
195
  recordsList = [recordsList]
97
196
  import copy
@@ -148,6 +247,22 @@ def merge_JSONGrapherRecords(recordsList):
148
247
  return merged_JSONGrapherRecord
149
248
 
150
249
  def convert_JSONGRapherRecord_data_list_to_class_objects(record):
250
+ """
251
+ Converts the list of data series objects in the 'data' field of a JSONGrapher record into a list of JSONGrapherDataSeries objects.
252
+
253
+ Each data series object is typically a dictionary,
254
+ The function essentially casts dictionaries into JSONGrapherDataSeries objects.
255
+
256
+ Accepts either a JSONGrapherRecord or a standalone figure dictionary. Each entry in the
257
+ list of the 'data' field is replaced with a JSONGrapherDataSeries instance,
258
+ preserving any existing fields in each data series dictionary. The transformed record or fig_dict is returned.
259
+
260
+ Args:
261
+ record ( JSONGrapherRecord | dict ): A record or figure dictionary to transform.
262
+
263
+ Returns:
264
+ JSONGrapherRecord | dict: The updated record or dictionary with casted data series objects.
265
+ """
151
266
  #will also support receiving a fig_dict
152
267
  if isinstance(record, dict):
153
268
  fig_dict_received = True
@@ -177,6 +292,37 @@ def convert_JSONGRapherRecord_data_list_to_class_objects(record):
177
292
  #Could add "tag_characters"='<>' as an optional argument to this and other functions
178
293
  #to make the option of other characters for custom units.
179
294
  def get_units_scaling_ratio(units_string_1, units_string_2):
295
+ """
296
+ Calculate the scaling ratio between two unit strings, returns ratio from units_string_1 / units_string_2.
297
+ Unit strings may include parentheses, division symbols, multiplication symbols, and exponents.
298
+
299
+
300
+ This function computes the multiplicative ratio required to convert from
301
+ `units_string_1` to `units_string_2`. For example, converting from "(((kg)/m))/s" to "(((g)/m))/s"
302
+ yields a scaling ratio of 1000.
303
+
304
+ Unit expressions may include custom units if they are tagged in advance with angle brackets
305
+ like "<umbrella>/m^(-2)".
306
+
307
+ The function uses the `unitpy` library for parsing and unit arithmetic.
308
+
309
+ Reciprocal units with a "1" can cause issues (e.g., "1/bar"), the
310
+ function attempts a fallback conversion in those cases.
311
+ It is recommended to instead use exponents like "bar^(-1)"
312
+
313
+ Args:
314
+ units_string_1 (str): The source units as a string expression.
315
+ units_string_2 (str): The target units as a string expression.
316
+
317
+ Returns:
318
+ float: The numerical ratio to convert values in `units_string_1`
319
+ to `units_string_2`.
320
+
321
+ Raises:
322
+ KeyError: If a required unit is missing from the definition registry.
323
+ ValueError: If the unit format is invalid or conversion fails.
324
+ RuntimeError: For any unexpected errors during conversion.
325
+ """
180
326
  # Ensure both strings are properly encoded in UTF-8
181
327
  units_string_1 = units_string_1.encode("utf-8").decode("utf-8")
182
328
  units_string_2 = units_string_2.encode("utf-8").decode("utf-8")
@@ -227,7 +373,28 @@ def get_units_scaling_ratio(units_string_1, units_string_2):
227
373
  return ratio_only #function returns ratio only. If function is later changed to return more, then units_strings may need further replacements.
228
374
 
229
375
  def return_custom_units_markup(units_string, custom_units_list):
230
- """puts markup around custom units with '<' and '>' """
376
+ """
377
+ Puts markup around custom units with '<' and '>'.
378
+
379
+ This function receives a units_string and a custom_units_list which is a list of strings
380
+ and then puts tagging markup on any custom units within the units_string, 'tagging' them with '<' and '>'
381
+ The units_string may include *, /, ^, and ( ).
382
+
383
+ For example, if the units string was "((umbrella)/m^(-2))"
384
+ And the custom_units_list was ['umbrella'],
385
+ the string returned would be "((<umbrella>)/m^(-2))"
386
+ If the custom_units_list is empty or custom units are not found
387
+ then no change is made to the units string.
388
+
389
+ Args:
390
+ units_string (str): The input string that may contain units (e.g., "10kohm").
391
+ custom_units_list (List[str]): A list of custom unit strings to search for
392
+ and wrap in markup.
393
+
394
+ Returns:
395
+ str: The updated string with custom units wrapped in angle brackets (e.g., "((umbrella)/m^(-2))" ).
396
+ """
397
+
231
398
  sorted_custom_units_list = sorted(custom_units_list, key=len, reverse=True)
232
399
  #the units should be sorted from longest to shortest if not already sorted that way.
233
400
  for custom_unit in sorted_custom_units_list:
@@ -238,6 +405,26 @@ def return_custom_units_markup(units_string, custom_units_list):
238
405
  #However, because unitpy gives unexpected behavior with the microsymbol,
239
406
  #We are actually going to change them from "µm" to "<microfrogm>"
240
407
  def tag_micro_units(units_string):
408
+ """
409
+ Replaces micro symbol-prefixed units with a custom tagged format for internal use.
410
+
411
+ This function scans a unit string for various Unicode representations of the micro symbol
412
+ (e.g., "µ", "μ", "𝜇", "𝝁") followed by standard unit characters, such as "μm".
413
+
414
+ It replaces each match with a placeholder tag that converts the micro symbol
415
+ to the word "microfrog" with pattern "<microfrogX>", such as "μm*s^(-1)" → "<microfrogm>*s^(-1)"
416
+
417
+ The reason to replace the micro symbols is to avoid any incompatibilities with
418
+ functions or packages that would handle the micro symbols incorrectly,
419
+ especially because there are multiple micro symbols. The reason that "microfrog" is used is
420
+ because it is distinct enough for us to convert back to microsymbol later.
421
+
422
+ Args:
423
+ units_string (str): A units string potentially containing µ symbols as prefixes, such as "μm*s^(-1)".
424
+
425
+ Returns:
426
+ str: A modified string with units containing micro symbols replaced by custom units tags containing "microfrog", such as "<microfrogm>*s^(-1)"
427
+ """
241
428
  # Unicode representations of micro symbols:
242
429
  # U+00B5 → µ (Micro Sign)
243
430
  # U+03BC → μ (Greek Small Letter Mu)
@@ -260,6 +447,25 @@ def tag_micro_units(units_string):
260
447
 
261
448
  #We are actually going to change them back to "µm" from "<microfrogm>"
262
449
  def untag_micro_units(units_string):
450
+ """
451
+ Restores standard micro-prefixed units from internal "microfrog" tagged format.
452
+
453
+ This function reverses the transformation applied by `tag_micro_units`, converting
454
+ placeholder tags like "<microfrogF>" back into units with the Greek micro symbol
455
+ (such as, "μF"). This simply returns to having micro symbols in unit strings for display
456
+ and for record exporting, once the algorithmic work is done.
457
+ For example, from "<microfrogm>*s^(-1)" to "μm*s^(-1)".
458
+
459
+ Note that we always use µ which is unicode U+00B5 adnd this may be different
460
+ from the original micro symbol before the algorithm started.
461
+ See tag_micro_units function comments for more information about microsymbols.
462
+
463
+ Args:
464
+ units_string (str): A string potentially containing tagged micro-units, like "<microfrogm>*s^(-1)"
465
+
466
+ Returns:
467
+ str: The string with tags converted back to standard micro-unit notation (such as from "<microfrogm>*s^(-1)" to "μm*s^(-1)" ).
468
+ """
263
469
  if "<microfrog" not in units_string: # Check if any frogified unit exists
264
470
  return units_string
265
471
  import re
@@ -269,6 +475,17 @@ def untag_micro_units(units_string):
269
475
  return re.sub(pattern, r"µ\1", units_string)
270
476
 
271
477
  def add_custom_unit_to_unitpy(unit_string):
478
+ """
479
+ Registers a new custom unit in the UnitPy framework for internal use.
480
+
481
+ This function adds a user-defined unit to the UnitPy system by inserting a BaseUnit
482
+ into the global base dictionary and defining an Entry using a BaseSet. It's designed
483
+ to expand UnitPy's supported units dynamically while preventing duplicate entries
484
+ that could cause runtime issues.
485
+
486
+ Args:
487
+ unit_string (str): The name of the unit to register (such as, "umbrellaArea").
488
+ """
272
489
  import unitpy
273
490
  from unitpy.definitions.entry import Entry
274
491
  #need to put an entry into "bases" because the BaseSet class will pull from that dictionary.
@@ -287,6 +504,19 @@ def add_custom_unit_to_unitpy(unit_string):
287
504
  unitpy.ledger.add_unit(new_entry) #implied return is here. No return needed.
288
505
 
289
506
  def extract_tagged_strings(text):
507
+ """
508
+ Extracts and returns a sorted list of unique substrings found within angle brackets.
509
+
510
+ This function identifies all substrings wrapped in angle brackets (such as "<umbrella>"),
511
+ removes duplicates, and returns them sorted by length in descending order. It's useful
512
+ for parsing tagged text where the tags follow a consistent markup format.
513
+
514
+ Args:
515
+ text (str): A string potentially containing angle-bracketed tags.
516
+
517
+ Returns:
518
+ List[str]: A list of unique tags sorted from longest to shortest.
519
+ """
290
520
  """Extracts tags surrounded by <> from a given string. Used for custom units.
291
521
  returns them as a list sorted from longest to shortest"""
292
522
  import re
@@ -299,6 +529,21 @@ def extract_tagged_strings(text):
299
529
  #It was written by copilot and refined by further prompting of copilot by testing.
300
530
  #The depth is because the function works iteratively and then stops when finished.
301
531
  def convert_inverse_units(expression, depth=100):
532
+ """
533
+ Converts unit reciprocals in string expressions to exponent notation.
534
+
535
+ This function detects reciprocal expressions like "1/m" or nested forms such as
536
+ "1/(1/m)" and converts them into exponent format (such as "m**(-1)"). It processes
537
+ the expression iteratively up to the specified depth to ensure all nested reciprocals
538
+ are resolved. This helps standardize units for parsing or evaluation.
539
+
540
+ Args:
541
+ expression (str): A string containing unit expressions to transform.
542
+ depth (int, optional): Maximum number of recursive replacements. Default is 100.
543
+
544
+ Returns:
545
+ str: A string with all convertible reciprocal units expressed using exponents.
546
+ """
302
547
  import re
303
548
  # Patterns to match valid reciprocals while ignoring multiplied units, so (1/bar)*bar should be handled correctly.
304
549
  patterns = [r"1/\((1/.*?)\)", r"1/([a-zA-Z]+)"]
@@ -316,6 +561,21 @@ def convert_inverse_units(expression, depth=100):
316
561
  #the below function takes in a fig_dict, as well as x and/or y scaling values.
317
562
  #The function then scales the values in the data of the fig_dict and returns the scaled fig_dict.
318
563
  def scale_fig_dict_values(fig_dict, num_to_scale_x_values_by = 1, num_to_scale_y_values_by = 1):
564
+ """
565
+ Scales the 'x' and/or 'y' values in a figure dictionary for rescaling plotted data.
566
+
567
+ This function takes a figure dictionary from JSONGrapher which has the same structure
568
+ as a Plotly figure dictionary, deep-copies it, and applies x/y scaling factors to each data
569
+ series using the helper function `scale_dataseries_dict`.
570
+
571
+ Args:
572
+ fig_dict (dict): A dictionary containing figure data with a "data" key.
573
+ num_to_scale_x_values_by (float, optional): Factor to scale all x-values. Defaults to 1.
574
+ num_to_scale_y_values_by (float, optional): Factor to scale all y-values. Defaults to 1.
575
+
576
+ Returns:
577
+ dict: A new figure dictionary with scaled x and/or y data values.
578
+ """
319
579
  import copy
320
580
  scaled_fig_dict = copy.deepcopy(fig_dict)
321
581
  #iterate across the data objects inside, and change them.
@@ -326,6 +586,24 @@ def scale_fig_dict_values(fig_dict, num_to_scale_x_values_by = 1, num_to_scale_y
326
586
 
327
587
 
328
588
  def scale_dataseries_dict(dataseries_dict, num_to_scale_x_values_by = 1, num_to_scale_y_values_by = 1, num_to_scale_z_values_by = 1):
589
+ """
590
+ Applies scaling factors to x, y, and optionally z data in a data series dictionary.
591
+
592
+ This function updates the numeric values in a data series dictionary by applying
593
+ scaling factors to each axis. It supports scaling for 2D and 3D datasets, and ensures
594
+ that all resulting values are converted to standard Python floats for compatibility
595
+ and serialization.
596
+
597
+ Args:
598
+ dataseries_dict (dict): A dictionary with keys "x", "y", and optionally "z",
599
+ each mapping to a list of numeric values.
600
+ num_to_scale_x_values_by (float, optional): Factor by which to scale x-values. Default is 1.
601
+ num_to_scale_y_values_by (float, optional): Factor by which to scale y-values. Default is 1.
602
+ num_to_scale_z_values_by (float, optional): Factor by which to scale z-values (if present). Default is 1.
603
+
604
+ Returns:
605
+ dict: The updated data series dictionary with scaled numeric values.
606
+ """
329
607
  import numpy as np
330
608
  dataseries = dataseries_dict
331
609
  dataseries["x"] = list(np.array(dataseries["x"], dtype=float)*num_to_scale_x_values_by) #convert to numpy array for multiplication, then back to list.
@@ -345,25 +623,97 @@ def scale_dataseries_dict(dataseries_dict, num_to_scale_x_values_by = 1, num_to_
345
623
  ## This is a special dictionary class that will allow a dictionary
346
624
  ## inside a main class object to be synchronized with the fields within it.
347
625
  class SyncedDict(dict):
348
- """A dictionary that automatically updates instance attributes."""
626
+ """Enables an owner object that is not a dictionary to behave like a dictionary.
627
+ Each SyncedDict instance is a dictionary that automatically updates and synchronizes attributes with the owner object."""
349
628
  def __init__(self, owner):
629
+ """
630
+ Initialize a SyncedDict with an associated owner, where the fields of the owner will be synchronized.
631
+
632
+ This constructor sets up the base dictionary and stores a reference to the owner
633
+ object whose attributes should remain in sync with the dictionary entries.
634
+ This allows a non-dictionary class object (the owner) to behave like a dictionary.
635
+
636
+ Args:
637
+ owner (object): The parent object that holds this dictionary and will mirror its keys as attributes.
638
+
639
+ Returns:
640
+ None
641
+
642
+ """
643
+
350
644
  super().__init__()
351
645
  self.owner = owner # Store reference to the class instance
352
646
  def __setitem__(self, key, value):
353
- """Update both dictionary and instance attribute."""
647
+ """
648
+ Set a key-value pair in the dictionary and update the owner's attribute.
649
+
650
+ Ensures that when a new item is added or updated in the dictionary, the corresponding
651
+ attribute on the owner object reflects the same value.
652
+
653
+ Args:
654
+ key (str): The key to assign.
655
+ value (any): The value to assign to the key and the owner's attribute.
656
+
657
+ Returns:
658
+ None
659
+
660
+
661
+ """
662
+
354
663
  super().__setitem__(key, value) # Set in the dictionary
355
664
  setattr(self.owner, key, value) # Sync with instance attribute
356
665
  def __delitem__(self, key):
666
+ """
667
+ Delete a key from the dictionary and remove its attribute from the owner.
668
+
669
+ Removes both the dictionary entry and the corresponding attribute from the owner,
670
+ maintaining synchronization.
671
+
672
+ Args:
673
+ key (str): The key to delete.
674
+
675
+ Returns:
676
+ None
677
+
678
+ """
679
+
357
680
  super().__delitem__(key) # Remove from dict
358
681
  if hasattr(self.owner, key):
359
682
  delattr(self.owner, key) # Sync removal from instance
360
683
  def pop(self, key, *args):
361
- """Remove item from dictionary and instance attributes."""
684
+ """
685
+ Remove a key from the dictionary and owner's attributes, returning the value.
686
+
687
+ Behaves like the built-in dict.pop(), but also deletes the attribute from the owner
688
+ if it exists.
689
+
690
+ Args:
691
+ key (str): The key to remove.
692
+ *args: Optional fallback value if the key does not exist.
693
+
694
+ Returns:
695
+ any: The value associated with the removed key, or the fallback value if the key did not exist.
696
+ """
697
+
362
698
  value = super().pop(key, *args) # Remove from dictionary
363
699
  if hasattr(self.owner, key):
364
700
  delattr(self.owner, key) # Remove from instance attributes
365
701
  return value
366
702
  def update(self, *args, **kwargs):
703
+ """
704
+ Update the dictionary with new key-value pairs and sync them to the owner's attributes.
705
+
706
+ This method extends the dictionary update logic to ensure that any added or modified
707
+ keys are also set as attributes on the owner object.
708
+
709
+ Args:
710
+ *args: Accepts a dictionary or iterable of key-value pairs.
711
+ **kwargs: Additional keyword pairs to add.
712
+
713
+ Returns:
714
+ None
715
+ """
716
+
367
717
  super().update(*args, **kwargs) # Update dict
368
718
  for key, value in self.items():
369
719
  setattr(self.owner, key, value) # Sync attributes
@@ -371,11 +721,17 @@ class SyncedDict(dict):
371
721
 
372
722
  class JSONGrapherDataSeries(dict): #inherits from dict.
373
723
  def __init__(self, uid="", name="", trace_style="", x=None, y=None, **kwargs):
374
- """Initialize a data series with synced dictionary behavior.
724
+ """
725
+ Initializes a JSONGrapher data_series object which is a dictionary with custom functions and synchronized attribute-style access.
726
+
727
+ This constructor sets default fields such as 'uid', 'name', 'trace_style', 'x', and 'y',
728
+ and allows additional configuration via keyword arguments. The underlying structure behaves
729
+ like both a dictionary and an object with attributes, supporting synced access patterns.
730
+
375
731
  Here are some fields that can be included, with example values.
376
732
 
377
733
  "uid": data_series_dict["uid"] = "123ABC", # (string) a unique identifier
378
- "name": data_series_dict["name"] = "Sample Data Series", # (string) name of the data series
734
+ "name": data_series_dict["name"] = "Sample Data Series", # (string) name of the data_series
379
735
  "trace_style": data_series_dict["trace_style"] = "scatter", # (string) type of trace (e.g., scatter, bar)
380
736
  "x": data_series_dict["x"] = [1, 2, 3, 4, 5], # (list) x-axis values
381
737
  "y": data_series_dict["y"] = [10, 20, 30, 40, 50], # (list) y-axis values
@@ -389,8 +745,25 @@ class JSONGrapherDataSeries(dict): #inherits from dict.
389
745
  "visible": data_series_dict["visible"] = True, # (boolean) whether the trace is visible
390
746
  "hoverinfo": data_series_dict["hoverinfo"] = "x+y", # (string) format for hover display
391
747
  "legend_group": data_series_dict["legend_group"] = None, # (string or None) optional grouping for legend
392
- "text": data_series_dict["text"] = "Data Point Labels", # (string or None) optional text annotations
393
748
 
749
+
750
+ Args:
751
+ uid (str, optional): Unique identifier for the data_series. Defaults to an empty string.
752
+ name (str, optional): Display name of the series. Defaults to an empty string.
753
+ trace_style (str, optional): Type of plot trace (e.g., 'scatter', 'bar'). Defaults to an empty string.
754
+ x (list, optional): X-axis data values. Defaults to an empty list.
755
+ y (list, optional): Y-axis data values. Defaults to an empty list.
756
+ **kwargs: Additional optional plot configuration (e.g., mode, marker, line, opacity).
757
+
758
+ Example Fields Supported via kwargs:
759
+ - mode: Plot mode such as "lines", "markers", or "lines+markers".
760
+ - marker: Dictionary with subfields like "size", "color", and "symbol".
761
+ - line: Dictionary with subfields like "width" and "dash".
762
+ - opacity: Float value for transparency (0 to 1).
763
+ - visible: Boolean to control trace visibility.
764
+ - hoverinfo: String format for hover data.
765
+ - legend_group: Optional grouping label for legends.
766
+ - text: String or list of annotations.
394
767
  """
395
768
  super().__init__() # Initialize as a dictionary
396
769
 
@@ -407,90 +780,261 @@ class JSONGrapherDataSeries(dict): #inherits from dict.
407
780
  self.update(kwargs)
408
781
 
409
782
  def update_while_preserving_old_terms(self, series_dict):
410
- """Update instance attributes from a dictionary. Overwrites existing terms and preserves other old terms."""
783
+ """
784
+ Updates the current data_series dictionary with new values while retaining previously set terms that are not overwritten.
785
+
786
+ This method applies a partial update to the internal dictionary by using the built-in `update()` method.
787
+ Existing keys in `series_dict` will overwrite corresponding keys in the object, while all other existing
788
+ keys and values will be preserved. Attributes on the owning object remain synchronized.
789
+
790
+ Args:
791
+ series_dict (dict): A dictionary containing updated fields for the data_series.
792
+
793
+ Example:
794
+ # Before: {'x': [1, 2], 'color': 'blue'}
795
+ # After update_while_preserving_old_terms({'x': [3, 4]}): {'x': [3, 4], 'color': 'blue'}
796
+ """
411
797
  self.update(series_dict)
412
798
 
413
799
  def get_data_series_dict(self):
414
- """Return the dictionary representation of the trace."""
800
+
801
+ """
802
+ Returns the underlying dictionary representation of the data_series dictionary.
803
+
804
+ This method provides a clean snapshot of the current state of the data_series object
805
+ by converting it into a standard Python dictionary. It is useful for serialization,
806
+ debugging, or passing the data to plotting libraries like Plotly.
807
+
808
+ Returns:
809
+ dict: A dictionary containing all data fields of the series.
810
+ """
415
811
  return dict(self)
416
812
 
417
813
  def set_x_values(self, x_values):
418
- """Update the x-axis values."""
814
+ """
815
+ Updates the x-axis data for the series with a new set of values.
816
+
817
+ This method replaces the current list of x-values in the data_series. If no values are provided
818
+ (i.e., None or empty), it safely defaults to an empty list. The update is synchronized through
819
+ the internal dictionary mechanism for consistency.
820
+
821
+ Args:
822
+ x_values (list): A list of numerical or categorical values to assign to the 'x' axis.
823
+ """
419
824
  self["x"] = list(x_values) if x_values else []
420
825
 
421
826
  def set_y_values(self, y_values):
422
- """Update the y-axis values."""
827
+ """
828
+ Updates the y-axis data for the series with a new set of values.
829
+
830
+ This method replaces the current list of y-values in the data_series. If no values are provided
831
+ (i.e., None or empty), it defaults to an empty list. The assignment ensures consistency with the
832
+ internal dictionary and allows for flexible input formats.
833
+
834
+ Args:
835
+ y_values (list): A list of numerical or categorical values to assign to the 'y' axis.
836
+ """
423
837
  self["y"] = list(y_values) if y_values else []
424
838
 
839
+ def set_z_values(self, z_values):
840
+ """
841
+ Updates the z-axis data for the series with a new set of values.
842
+
843
+ This method replaces the current list of z-values in the data_series. If no values are provided
844
+ (i.e., None or empty), it safely defaults to an empty list. The update is synchronized through
845
+ the internal dictionary mechanism for consistency.
846
+
847
+ Args:
848
+ z_values (list): A list of numerical or categorical values to assign to the 'z' axis.
849
+ """
850
+ self["z"] = list(z_values) if z_values else []
851
+
852
+
425
853
  def set_name(self, name):
426
- """Update the name of the data series."""
854
+ """
855
+ Sets the name of the data_series to the provided value.
856
+
857
+ This method assigns a human-readable identifier or label to the data_series, which is
858
+ typically used for legend display and trace identification in visualizations.
859
+
860
+ Args:
861
+ name (str): The new name or label to assign to the data_series.
862
+ """
427
863
  self["name"] = name
428
864
 
429
865
  def set_uid(self, uid):
430
- """Update the unique identifier (uid) of the data series."""
866
+ """
867
+ Sets or updates the unique identifier (UID) for the data_series.
868
+
869
+ This method assigns a UID to the data_series, which can be used to uniquely identify
870
+ the trace within a larger figure or dataset. UIDs are helpful for referencing, comparing,
871
+ or updating specific traces, especially in dynamic or interactive plotting environments.
872
+
873
+ Args:
874
+ uid (str): A unique identifier string for the data_series.
875
+ """
431
876
  self["uid"] = uid
432
877
 
433
878
  def set_trace_style(self, style):
434
- """Update the trace style (e.g., scatter, scatter_spline, scatter_line, bar)."""
879
+ """
880
+ Updates the trace style of the data_series to control its rendering behavior.
881
+
882
+ This method sets the 'trace_style' field, which typically defines how the data_series
883
+ appears visually in plots (e.g., 'scatter', 'bar', 'scatter_line', 'scatter_spline').
884
+
885
+ Args:
886
+ style (str): A string representing the desired visual trace style for plotting.
887
+ """
435
888
  self["trace_style"] = style
436
889
 
437
890
  def set_marker_symbol(self, symbol):
891
+ """
892
+ Sets the marker symbol for data points by delegating to the internal set_marker_shape method.
893
+
894
+ This method provides a user-friendly way to define the visual marker used for plotting individual
895
+ points on the graph. The symbol parameter is passed directly to set_marker_shape, which handles
896
+ the internal logic for updating the marker settings.
897
+
898
+ Args:
899
+ symbol (str): The symbol to use for markers (e.g., "circle", "square", "diamond", "x", "star").
900
+ """
438
901
  self.set_marker_shape(shape=symbol)
439
902
 
440
903
  def set_marker_shape(self, shape):
441
904
  """
442
- Update the marker shape (symbol).
443
-
444
- Supported marker shapes in Plotly:
445
- - 'circle' (default)
446
- - 'square'
447
- - 'diamond'
448
- - 'cross'
449
- - 'x'
450
- - 'triangle-up'
451
- - 'triangle-down'
452
- - 'triangle-left'
453
- - 'triangle-right'
454
- - 'pentagon'
455
- - 'hexagon'
456
- - 'star'
457
- - 'hexagram'
458
- - 'star-triangle-up'
459
- - 'star-triangle-down'
460
- - 'star-square'
461
- - 'star-diamond'
462
- - 'hourglass'
463
- - 'bowtie'
464
-
465
- :param shape: String representing the desired marker shape.
905
+ Sets the visual symbol used for markers in the data series.
906
+
907
+ This method updates the shape of marker symbols (used in scatter plots, etc.)
908
+ based on supported Plotly marker types. It ensures the internal marker dictionary
909
+ exists and assigns the specified symbol string to the 'symbol' field.
910
+
911
+ Supported Shapes:
912
+ - circle, square, diamond, cross, x
913
+ - triangle-up, triangle-down, triangle-left, triangle-right
914
+ - pentagon, hexagon, star, hexagram
915
+ - star-triangle-up, star-triangle-down, star-square, star-diamond
916
+ - hourglass, bowtie
917
+
918
+ Args:
919
+ shape (str): The name of the marker symbol to use. Must be one of the supported Plotly shapes.
466
920
  """
467
921
  self.setdefault("marker", {})["symbol"] = shape
468
922
 
469
- def add_data_point(self, x_val, y_val):
470
- """Append a new data point to the series."""
923
+ def add_xy_data_point(self, x_val, y_val):
924
+ """
925
+ Adds a new x-y data point to the data_series.
926
+
927
+ This method appends the provided x and y values to their respective lists
928
+ within the internal data structure. It is typically used to incrementally
929
+ build or extend a dataset for plotting or analysis.
930
+
931
+ Args:
932
+ x_val (any): The x-axis value of the data point.
933
+ y_val (any): The y-axis value of the data point.
934
+ """
935
+ self["x"].append(x_val)
936
+ self["y"].append(y_val)
937
+
938
+ def add_xyz_data_point(self, x_val, y_val, z_val):
939
+ """
940
+ Adds a new x-y-z data point to the data_series.
941
+
942
+ This method appends the provided x and y and z values to their respective lists
943
+ within the internal data structure. It is typically used to incrementally
944
+ build or extend a dataset for plotting or analysis.
945
+
946
+ Args:
947
+ x_val (any): The x-axis value of the data point.
948
+ y_val (any): The y-axis value of the data point.
949
+ z_val (any): The z-axis value of the data point.
950
+ """
471
951
  self["x"].append(x_val)
472
952
  self["y"].append(y_val)
953
+ self["z"].append(z_val)
954
+
473
955
 
474
956
  def set_marker_size(self, size):
475
- """Update the marker size."""
957
+ """
958
+ Updates the size of the markers used in the data_series visualization.
959
+
960
+ This method modifies the 'size' field within the 'marker' dictionary of the data_series.
961
+ If the 'marker' dictionary doesn't already exist, it is created. The marker size controls
962
+ the visual prominence of data points in charts like scatter plots.
963
+
964
+ Args:
965
+ size (int or float): The desired marker size, typically a positive number.
966
+ """
476
967
  self.setdefault("marker", {})["size"] = size
477
968
 
478
969
  def set_marker_color(self, color):
479
- """Update the marker color."""
970
+ """
971
+ Sets the color of the markers in the data_series visualization.
972
+
973
+ This method ensures that the 'marker' dictionary exists within the data_series,
974
+ and then updates its 'color' key with the provided value. Marker color can be a
975
+ standard named color (e.g., "blue"), a hex code (e.g., "#1f77b4"), or an RGB/RGBA string.
976
+
977
+ Args:
978
+ color (str): The color to use for the data_series markers.
979
+ """
480
980
  self.setdefault("marker", {})["color"] = color
481
981
 
482
982
  def set_mode(self, mode):
483
- """Update the mode (options: 'lines', 'markers', 'text', 'lines+markers', 'lines+text', 'markers+text', 'lines+markers+text')."""
484
- # Check if 'line' is in the mode but 'lines' is not. Then correct for user if needed.
983
+ """
984
+ Sets the rendering mode for the data_series, correcting common input patterns.
985
+
986
+ This method updates the 'mode' field to control how data points are visually represented
987
+ in plots (e.g., as lines, markers, text, or combinations). If the user accidentally uses
988
+ "line" instead of "lines", it automatically corrects the term to maintain compatibility
989
+ with plotting libraries like Plotly.
990
+
991
+ Supported Modes:
992
+ - 'lines'
993
+ - 'markers'
994
+ - 'text'
995
+ - 'lines+markers'
996
+ - 'lines+text'
997
+ - 'markers+text'
998
+ - 'lines+markers+text'
999
+
1000
+ Args:
1001
+ mode (str): Desired rendering mode. Common typos like 'line' may be corrected.
1002
+ """
485
1003
  if "line" in mode and "lines" not in mode:
486
1004
  mode = mode.replace("line", "lines")
487
1005
  self["mode"] = mode
488
1006
 
489
1007
  def set_annotations(self, text): #just a convenient wrapper.
1008
+ """
1009
+ Sets text annotations for the data_series by delegating to the internal set_text method.
1010
+
1011
+ This is a convenience wrapper that allows assigning label text to individual data points
1012
+ in the series. Annotations can enhance readability and provide contextual information
1013
+ in visualizations such as tooltips or direct text labels on plots.
1014
+
1015
+ Args:
1016
+ text (str or list): Annotation text for the data points. Can be a single string
1017
+ or a list of strings corresponding to each data point.
1018
+ """
490
1019
  self.set_text(text)
491
1020
 
492
1021
  def set_text(self, text):
493
- #text should be a list of strings teh same length as the data series, one string per point.
1022
+
1023
+ """
1024
+ Sets annotation text for each point in the data_series. The a list of text values must be provided, equal to
1025
+ the number of points. If a single string value is provided, it will be repeated to be the same for each point.
1026
+
1027
+ This method allows the user to assign either a single string or a list of strings to annotate
1028
+ each point in the series. If a single string is provided, it is replicated to match the number
1029
+ of data points; otherwise, the provided list is used as-is. Useful for adding tooltips or labels.
1030
+ The number of values received being equal to the number of points is checked by comparing to the x values.
1031
+
1032
+ Args:
1033
+ text (str or list): Annotation text—either a single string (applied to all points) or a list
1034
+ of strings, one for each point in the series.
1035
+ """
1036
+
1037
+ #text should be a list of strings teh same length as the data_series, one string per point.
494
1038
  """Update the annotations with a list of text as long as the number of data points."""
495
1039
  if text == type("string"):
496
1040
  text = [text] * len(self["x"]) # Repeat the text to match x-values length
@@ -500,55 +1044,101 @@ class JSONGrapherDataSeries(dict): #inherits from dict.
500
1044
 
501
1045
 
502
1046
  def set_line_width(self, width):
503
- """Update the line width, should be a number, normally an integer."""
1047
+ """
1048
+ Sets the width of the line used for the trace of the data_series.
1049
+
1050
+ This method ensures that the 'line' dictionary exists within the data_series and then sets
1051
+ the 'width' attribute to the specified value. This affects the thickness of lines in charts
1052
+ like line plots and splines.
1053
+
1054
+ Args:
1055
+ width (int or float): The thickness value for the line. Typically a positive number.
1056
+ """
504
1057
  line = self.setdefault("line", {})
505
1058
  line.setdefault("width", width) # Ensure width is set
506
1059
 
507
1060
  def set_line_dash(self, dash_style):
508
1061
  """
509
- Update the line dash style.
1062
+ Sets the dash style of the line used in the data_series visualization.
510
1063
 
511
- Supported dash styles in Plotly:
512
- - 'solid' (default) Continuous solid line
513
- - 'dot' Dotted line
514
- - 'dash' → Dashed line
515
- - 'longdash' → Longer dashed line
516
- - 'dashdot' → Dash-dot alternating pattern
517
- - 'longdashdot' → Long dash-dot alternating pattern
1064
+ This method modifies the 'dash' attribute inside the 'line' dictionary, which controls
1065
+ the appearance of the line in the chart. It allows for various visual styles, such as
1066
+ dashed, dotted, or solid lines, aligning with the supported Plotly dash patterns.
518
1067
 
519
- :param dash_style: String representing the desired line style.
1068
+ Supported Styles:
1069
+ - 'solid'
1070
+ - 'dot'
1071
+ - 'dash'
1072
+ - 'longdash'
1073
+ - 'dashdot'
1074
+ - 'longdashdot'
1075
+
1076
+ Args:
1077
+ dash_style (str): The desired dash pattern for the line. Must match one of Plotly’s accepted styles.
520
1078
  """
521
1079
  self.setdefault("line", {})["dash"] = dash_style
522
1080
 
523
1081
  def set_transparency(self, transparency_value):
524
1082
  """
525
- Update the transparency level by converting it to opacity.
1083
+ Converts a transparency value into an opacity setting and applies it to the data_series.
526
1084
 
527
- Transparency ranges from:
528
- - 0 (completely opaque) opacity = 1
529
- - 1 (fully transparent) opacity = 0
530
- - Intermediate values adjust partial transparency.
1085
+ This method accepts a transparency value—ranging from 0 (fully opaque) to 1 (fully transparent)—
1086
+ and calculates the corresponding opacity value used by plotting libraries. It inverts the input
1087
+ so that increasing transparency reduces opacity.
531
1088
 
532
- :param transparency_value: Float between 0 and 1, where 0 is opaque and 1 is transparent.
1089
+ Args:
1090
+ transparency_value (float): A decimal between 0 and 1 where:
1091
+ - 0 means fully visible (opacity = 1),
1092
+ - 1 means fully invisible (opacity = 0),
1093
+ - intermediate values create partial see-through effects.
533
1094
  """
534
1095
  self["opacity"] = 1 - transparency_value
535
1096
 
536
1097
  def set_opacity(self, opacity_value):
537
- """Update the opacity level between 0 and 1."""
1098
+ """
1099
+ Sets the opacity level for the data_series.
1100
+
1101
+ This method directly assigns an opacity value to the data_series, which controls the visual
1102
+ transparency of the trace in the plot. An opacity of 1.0 means fully opaque, while 0.0 is fully
1103
+ transparent. Intermediate values allow for layering effects and visual blending.
1104
+
1105
+ Args:
1106
+ opacity_value (float): A number between 0 (transparent) and 1 (opaque) representing the opacity level.
1107
+ """
538
1108
  self["opacity"] = opacity_value
539
1109
 
540
1110
  def set_visible(self, is_visible):
541
- """Update the visibility of the trace.
542
- "True" The trace is fully visible.
543
- "False" → The trace is completely hidden.
544
- "legendonly" The trace is hidden from the plot but still appears in the legend.
545
-
1111
+ """
1112
+ Sets the visibility state of the data_series in the plot.
1113
+
1114
+ This method allows control over how the trace is displayed. It accepts a boolean value
1115
+ or the string "legendonly" to indicate the desired visibility mode. This feature is
1116
+ particularly useful for managing clutter in complex visualizations or toggling traces dynamically.
1117
+
1118
+ Args:
1119
+ is_visible (bool or str):
1120
+ - True → Fully visible in the plot.
1121
+ - False → Hidden entirely.
1122
+ - "legendonly" → Hidden from the plot but shown in the legend.
546
1123
  """
547
1124
 
548
1125
  self["visible"] = is_visible
549
1126
 
550
1127
  def set_hoverinfo(self, hover_format):
551
- """Update hover information format."""
1128
+ """
1129
+ Sets the formatting for hover labels in interactive visualizations.
1130
+
1131
+ This method defines what information appears when the user hovers over data points in the plot.
1132
+ Accepted formats include combinations of "x", "y", "text", "name", etc., joined with "+" symbols.
1133
+
1134
+ Example formats:
1135
+ - "x+y" → Shows x and y values
1136
+ - "x+text" → Shows x value and text annotation
1137
+ - "none" → Disables hover info
1138
+
1139
+ Args:
1140
+ hover_format (str): A string specifying what data to display on hover.
1141
+ """
552
1142
  self["hoverinfo"] = hover_format
553
1143
 
554
1144
 
@@ -564,19 +1154,19 @@ class JSONGrapherRecord:
564
1154
 
565
1155
  Arguments & Attributes (all are optional):
566
1156
  comments (str): Can be used to put in general description or metadata related to the entire record. Can include citation links. Goes into the record's top level comments field.
567
- datatype: The datatype is the experiment type or similar, it is used to assess which records can be compared and which (if any) schema to compare to. Use of single underscores between words is recommended. This ends up being the datatype field of the full JSONGrapher file. Avoid using double underscores '__' in this field unless you have read the manual about hierarchical datatypes. The user can choose to provide a URL to a schema in this field, rather than a dataype name.
1157
+ datatype: The datatype is the experiment type or similar, it is used to assess which records can be compared and which (if any) schema to compare to. Use of single underscores between words is recommended. This ends up being the datatype field of the full JSONGrapher file. Avoid using double underscores '__' in this field unless you have read the manual about hierarchical datatypes. The user can choose to provide a URL to a schema in this field, rather than a dataype name. May have underscore, should not have spaces.
568
1158
  graph_title: Title of the graph or the dataset being represented.
569
1159
  data_objects_list (list): List of data series dictionaries to pre-populate the record. These may contain 'simulate' fields in them to call javascript source code for simulating on the fly.
570
1160
  simulate_as_added: Boolean. True by default. If true, any data series that are added with a simulation field will have an immediate simulation call attempt.
571
- x_data: Single series x data in a list or array-like structure.
572
- y_data: Single series y data in a list or array-like structure.
1161
+ x_data: Single series x data values in a list or array-like structure.
1162
+ y_data: Single series y data values in a list or array-like structure.
573
1163
  x_axis_label_including_units: A string with units provided in parentheses. Use of multiplication "*" and division "/" and parentheses "( )" are allowed within in the units . The dimensions of units can be multiple, such as mol/s. SI units are expected. Custom units must be inside < > and at the beginning. For example, (<frogs>*kg/s) would be permissible. Units should be non-plural (kg instead of kgs) and should be abbreviated (m not meter). Use “^” for exponents. It is recommended to have no numbers in the units other than exponents, and to thus use (bar)^(-1) rather than 1/bar.
574
1164
  y_axis_label_including_units: A string with units provided in parentheses. Use of multiplication "*" and division "/" and parentheses "( )" are allowed within in the units . The dimensions of units can be multiple, such as mol/s. SI units are expected. Custom units must be inside < > and at the beginning. For example, (<frogs>*kg/s) would be permissible. Units should be non-plural (kg instead of kgs) and should be abbreviated (m not meter). Use “^” for exponents. It is recommended to have no numbers in the units other than exponents, and to thus use (bar)^(-1) rather than 1/bar.
575
1165
  layout: A dictionary defining the layout of the graph, including axis titles,
576
1166
  comments, and general formatting options.
577
1167
 
578
1168
  Methods:
579
- add_data_series: Adds a new data series to the record.
1169
+ add_data_series: Adds a new data_series to the record.
580
1170
  add_data_series_as_equation: Adds a new equation to plot, which will be evaluated on the fly.
581
1171
  set_layout_fields: Updates the layout configuration for the graph.
582
1172
  export_to_json_file: Saves the entire record (comments, datatype, data, layout) as a JSON file.
@@ -585,11 +1175,37 @@ class JSONGrapherRecord:
585
1175
 
586
1176
  def __init__(self, comments="", graph_title="", datatype="", data_objects_list = None, simulate_as_added = True, evaluate_equations_as_added = True, x_data=None, y_data=None, x_axis_label_including_units="", y_axis_label_including_units ="", plot_style ="", layout=None, existing_JSONGrapher_record=None):
587
1177
  """
588
- Initialize a JSONGrapherRecord instance with optional attributes or an existing record.
1178
+ Initializes a JSONGrapherRecord object that represents a structured fig_dict for graphing.
1179
+
1180
+ Optionally populates fields from an existing JSONGrapher record and applies immediate processing
1181
+ such as simulation or equation evaluation on data series when applicable.
1182
+
1183
+ Args:
1184
+ comments (str, optional): General description or metadata for the record.
1185
+ graph_title (str, optional): Title for the graph; is put into the layout title field.
1186
+ datatype (str, optional): The datatype is the experiment type or similar, it is used to assess which records can be compared and which (if any) schema to compare to. Use of single underscores between words is recommended. This ends up being the datatype field of the full JSONGrapher file. Avoid using double underscores '__' in this field unless you have read the manual about hierarchical datatypes. The user can choose to provide a URL to a schema in this field, rather than a dataype name. May have underscore, should not have spaces.
1187
+ data_objects_list (list, optional): List of data_series dictionaries to pre-populate the record.
1188
+ simulate_as_added (bool, optional): If True, attempts simulation on data with 'simulate' fields.
1189
+ evaluate_equations_as_added (bool, optional): True by default. If true, any data series that are added with a simulation field will have an immediate simulation call attempt.
1190
+ x_data (list or array-like, optional): x-axis values for a single data series.
1191
+ y_data (list or array-like, optional): y-axis values for a single data series.
1192
+ x_axis_label_including_units (str, optional): x-axis label with units in parentheses. A string with units provided in parentheses. Use of multiplication "*" and division "/" and parentheses "( )" are allowed within in the units . The dimensions of units can be multiple, such as mol/s. SI units are expected. Custom units must be inside < > and at the beginning. For example, (<frogs>*kg/s) would be permissible. Units should be non-plural (kg instead of kgs) and should be abbreviated (m not meter). Use “^” for exponents. It is recommended to have no numbers in the units other than exponents, and to thus use (bar)^(-1) rather than 1/bar.
1193
+ y_axis_label_including_units (str, optional): y-axis label with units in parentheses. A string with units provided in parentheses. Use of multiplication "*" and division "/" and parentheses "( )" are allowed within in the units . The dimensions of units can be multiple, such as mol/s. SI units are expected. Custom units must be inside < > and at the beginning. For example, (<frogs>*kg/s) would be permissible. Units should be non-plural (kg instead of kgs) and should be abbreviated (m not meter). Use “^” for exponents. It is recommended to have no numbers in the units other than exponents, and to thus use (bar)^(-1) rather than 1/bar.
1194
+ plot_style (str, optional): Style applied to the overall plot; stored in fig_dict["plot_style"].
1195
+ layout (dict, optional): layout_style dictionary configuring graph appearance.
1196
+ existing_JSONGrapher_record (dict, optional): Dictionary representing an existing JSONGrapher record
1197
+ to populate the new or current instance.
1198
+
1199
+ Raises:
1200
+ KeyError: If simulation attempts fail due to missing expected keys.
1201
+ Exception: Catches and logs unexpected errors during simulation or equation evaluation.
1202
+
1203
+ Side Effects:
1204
+ - Updates self.fig_dict with layout_style, data_series dictionaries, and metadata.
1205
+ - Applies optional simulation or evaluation to fig_dict.
1206
+ - Initializes a hints_dictionary to guide field-level edits within the fig_dict.
589
1207
 
590
- layout (dict): Layout dictionary to pre-populate the graph configuration.
591
- existing_JSONGrapher_record (dict): Existing JSONGrapher record to populate the instance.
592
- """
1208
+ """
593
1209
  if layout == None: #it's bad to have an empty dictionary or list as a python argument.
594
1210
  layout = {}
595
1211
 
@@ -653,34 +1269,102 @@ class JSONGrapherRecord:
653
1269
  #That is, if someone uses something like Record["comments"]="frog", it will also put that into self.fig_dict
654
1270
 
655
1271
  def __getitem__(self, key):
1272
+ """
1273
+ Retrieves the value associated with the specified key from the fig_dict.
1274
+
1275
+ Args:
1276
+ key: The field name to look up in the fig_dict.
1277
+
1278
+ Returns:
1279
+ The value mapped to the specified key within the fig_dict.
1280
+ """
656
1281
  return self.fig_dict[key] # Direct access
657
1282
 
658
1283
  def __setitem__(self, key, value):
1284
+ """
1285
+ Sets the specified key in the fig_dict to the given value.
1286
+
1287
+ Args:
1288
+ key: The field name to assign or update in the fig_dict.
1289
+ value: The value to associate with the specified key in the fig_dict.
1290
+ """
659
1291
  self.fig_dict[key] = value # Direct modification
660
1292
 
661
1293
  def __delitem__(self, key):
1294
+ """
1295
+ Deletes the specified key and its associated value from the fig_dict.
1296
+
1297
+ Args:
1298
+ key: The field name to remove from the fig_dict.
1299
+ """
662
1300
  del self.fig_dict[key] # Support for deletion
663
1301
 
664
1302
  def __iter__(self):
1303
+ """
1304
+ Returns an iterator over the keys in the fig_dict.
1305
+
1306
+ Returns:
1307
+ An iterator that allows traversal of all top-level keys in fig_dict.
1308
+ """
665
1309
  return iter(self.fig_dict) # Allow iteration
666
1310
 
667
1311
  def __len__(self):
1312
+ """
1313
+ Returns the number of top-level fields (keys) currently stored in the fig_dict.
1314
+
1315
+ Returns:
1316
+ The count of top-level fields (keys) present in the fig_dict.
1317
+ """
668
1318
  return len(self.fig_dict) # Support len()
669
1319
 
670
1320
  def pop(self, key, default=None):
1321
+ """
1322
+ Removes the specified key and returns its value from the fig_dict.
1323
+
1324
+ Args:
1325
+ key: The field name to remove from the fig_dict.
1326
+ default (optional): Value to return if the key is not found.
1327
+
1328
+ Returns:
1329
+ The value previously associated with the key, or the specified default if the key was absent.
1330
+ """
671
1331
  return self.fig_dict.pop(key, default) # Implement pop()
672
1332
 
673
1333
  def keys(self):
1334
+ """
1335
+ Returns a dynamic, read-only view of all top-level keys in the fig_dict.
1336
+
1337
+ Returns:
1338
+ A view object that reflects the current set of keys in the fig_dict.
1339
+ """
674
1340
  return self.fig_dict.keys() # Dictionary-like keys()
675
1341
 
676
1342
  def values(self):
1343
+ """
1344
+ Returns a dynamic, read-only view of all values stored in the fig_dict.
1345
+
1346
+ Returns:
1347
+ A view object that reflects the current set of values in the fig_dict.
1348
+ """
677
1349
  return self.fig_dict.values() # Dictionary-like values()
678
1350
 
679
1351
  def items(self):
1352
+ """
1353
+ Returns a dynamic, read-only view of all key-value pairs in the fig_dict.
1354
+
1355
+ Returns:
1356
+ A view object that reflects the current set of key-value pairs in the fig_dict.
1357
+ """
680
1358
  return self.fig_dict.items() # Dictionary-like items()
681
1359
 
682
1360
  def update(self, *args, **kwargs):
683
- """Updates the dictionary with multiple key-value pairs."""
1361
+ """
1362
+ Updates the fig_dict with new key-value pairs.
1363
+
1364
+ Args:
1365
+ *args: Positional arguments containing mappings or iterable key-value pairs.
1366
+ **kwargs: Arbitrary keyword arguments to be added as key-value pairs in the fig_dict.
1367
+ """
684
1368
  self.fig_dict.update(*args, **kwargs)
685
1369
 
686
1370
 
@@ -689,15 +1373,45 @@ class JSONGrapherRecord:
689
1373
  #this function enables printing the current record.
690
1374
  def __str__(self):
691
1375
  """
692
- Returns a JSON-formatted string of the record with an indent of 4.
1376
+ Returns a JSON-formatted string representation of the fig_dict with 4-space "pretty-print" indentation.
1377
+
1378
+ Returns:
1379
+ str: A readable JSON string of the record's contents.
1380
+
1381
+ Notes:
1382
+ This method does not perform automatic consistency updates or validation.
1383
+ It is recommended to use the syntax RecordObject.print_to_inspect()
1384
+ which will make automatic consistency updates and validation checks to the record before printing.
1385
+
693
1386
  """
694
1387
  print("Warning: Printing directly will return the raw record without some automatic updates. It is recommended to use the syntax RecordObject.print_to_inspect() which will make automatic consistency updates and validation checks to the record before printing.")
695
1388
  return json.dumps(self.fig_dict, indent=4)
696
1389
 
697
1390
 
698
- def add_data_series(self, series_name, x_values=None, y_values=None, simulate=None, simulate_as_added=True, comments="", trace_style="", uid="", line="", extra_fields=None):
1391
+ def add_data_series(self, series_name, x_values=None, y_values=None, simulate=None, simulate_as_added=True, comments="", trace_style=None, uid="", line=None, extra_fields=None):
699
1392
  """
700
- This is the normal way of adding an x,y data series.
1393
+ Adds a new x,y data series to the fig_dict with optional metadata, styling, and simulation support.
1394
+
1395
+ Args:
1396
+ series_name (str): Label for the data series to appear in the graph.
1397
+ x_values (list or array-like, optional): x-axis values. Defaults to an empty list.
1398
+ y_values (list or array-like, optional): y-axis values. Defaults to an empty list.
1399
+ simulate (dict, optional): Dictionary specifying on-the-fly simulation parameters.
1400
+ simulate_as_added (bool): If True, and if the 'simulate' field is present, then attempts to simulate this series immediately upon addition.
1401
+ comments (str): Description or annotations tied to the data series.
1402
+ trace_style (str or dict): trace_style for the data_series (e.g., scatter, line, spline, bar).
1403
+ uid (str): Optional unique ID (e.g., DOI) linked to the series.
1404
+ line (dict): Dictionary of visual line properties like shape or width.
1405
+ extra_fields (dict): A dictionary with custom keys and values to add into the data_series dictionary.
1406
+
1407
+ Returns:
1408
+ dict: The newly constructed data_series dictionary.
1409
+
1410
+ Notes:
1411
+ - There is also an 'implied' return in that the new data_series_dict is added to the JSONGrapher object's fig_dict.
1412
+ - Inputs are converted to lists to ensure consistency with expected format.
1413
+ - Simulation failures are silently ignored to maintain program flow.
1414
+ - The returned object allows extended editing of visual and structural properties.
701
1415
  """
702
1416
  # series_name: Name of the data series.
703
1417
  # x: List of x-axis values. Or similar structure.
@@ -731,9 +1445,9 @@ class JSONGrapherRecord:
731
1445
  data_series_dict["comments"] = comments
732
1446
  if len(uid) > 0:
733
1447
  data_series_dict["uid"] = uid
734
- if len(line) > 0:
1448
+ if line: #This checks variable is not None, and not empty.
735
1449
  data_series_dict["line"] = line
736
- if len(trace_style) > 0:
1450
+ if trace_style: #This checks variable is not None, and not empty.
737
1451
  data_series_dict['trace_style'] = trace_style
738
1452
  #add simulate field if included.
739
1453
  if simulate:
@@ -755,10 +1469,35 @@ class JSONGrapherRecord:
755
1469
  self.fig_dict["data"].append(data_series_dict) #implied return.
756
1470
  return data_series_dict
757
1471
 
758
- def add_data_series_as_equation(self, series_name, graphical_dimensionality, x_values=None, y_values=None, equation_dict=None, evaluate_equations_as_added=True, comments="", trace_style="", uid="", line="", extra_fields=None):
1472
+ def add_data_series_as_simulation(self, series_name, graphical_dimensionality, x_values=None, y_values=None, simulate_dict=None, simulate_as_added=True, comments="", trace_style=None, uid="", line=None, extra_fields=None):
1473
+ print("This feature, add_data_series_as_simulation, is not yet implemented. JSONGrapher did not add the series. For now, use add_data_series to add series with simulate fields.")
1474
+ pass #TODO: fill out the logic needed to create a data_series_as_simulation. Look at the regular add_data_series and the add_data_series_as_equation functions, for comparison. Also, should probably change both this and add_data_seris_as_equation into wrappers around add_data_series.
1475
+
1476
+ def add_data_series_as_equation(self, series_name, graphical_dimensionality, x_values=None, y_values=None, equation_dict=None, evaluate_equations_as_added=True, comments="", trace_style=None, uid="", line=None, extra_fields=None):
759
1477
  """
760
- This is a way to add an equation that would be used to fill an x,y data series.
761
- The equation will be a equation_dict of the json_equationer type
1478
+ Adds a new data series to the fig_dict using an equation instead of raw data points.
1479
+
1480
+ Args:
1481
+ series_name (str): Label for the data series displayed on the graph.
1482
+ graphical_dimensionality (int): Number of geometric dimensions, typically 2 or 3.
1483
+ x_values (list or array-like, optional): Normally should not be passed in for an equation based data_series. Will become an empty list to be populated when the equation is evaluated.
1484
+ y_values (list or array-like, optional): Normally should not be passed in for an equation based data_series. Will become an empty list to be populated when the equation is evaluated.
1485
+ equation_dict (dict, optional): Dictionary defining the equation using json_equationer structure.
1486
+ evaluate_equations_as_added (bool): If True (default), attempts to evaluate the equation immediately after addition.
1487
+ comments (str): Annotation or description associated with this data series.
1488
+ trace_style (str): Visual trace_style for plotting (e.g., scatter, bar, scatter3d, bubble2d, bubble3d).
1489
+ uid (str): Optional unique identifier (e.g., DOI) for the series.
1490
+ line (dict): Dictionary of line formatting options (e.g., shape, width).
1491
+ extra_fields (dict): A dictionary with custom keys and values to add into the data_series dictionary.
1492
+
1493
+ Returns:
1494
+ dict: The constructed data_series dictionary.
1495
+
1496
+ Notes:
1497
+ - There is also an 'implied' return in that the new data_series_dict is added to the JSONGrapher object's fig_dict.
1498
+ - The graphical_dimensionality is embedded in the equation_dict before use.
1499
+ - Evaluation is deferred until after the series is appended to fig_dict to ensure layout-based unit validation.
1500
+ - If evaluation fails, it is silently skipped to maintain runtime flow.
762
1501
  """
763
1502
  # series_name: Name of the data series.
764
1503
  # graphical_dimensionality is the number of geometric dimensions, so should be either 2 or 3.
@@ -794,9 +1533,9 @@ class JSONGrapherRecord:
794
1533
  data_series_dict["comments"] = comments
795
1534
  if len(uid) > 0:
796
1535
  data_series_dict["uid"] = uid
797
- if len(line) > 0:
1536
+ if line:
798
1537
  data_series_dict["line"] = line
799
- if len(trace_style) > 0:
1538
+ if trace_style:
800
1539
  data_series_dict['trace_style'] = trace_style
801
1540
  #add equation field if included.
802
1541
  if equation_dict:
@@ -822,17 +1561,60 @@ class JSONGrapherRecord:
822
1561
  pass
823
1562
 
824
1563
  def change_data_series_name(self, series_index, series_name):
1564
+ """
1565
+ Renames a data series within the fig_dict at the specified index.
1566
+
1567
+ Args:
1568
+ series_index (int): Index of the target data series in the fig_dict["data"] list.
1569
+ series_name (str): New name to assign to the data series.
1570
+
1571
+ Notes:
1572
+ - This updates the 'name' field of the selected data_series dictionary.
1573
+ """
825
1574
  self.fig_dict["data"][series_index]["name"] = series_name
826
1575
 
827
1576
  #this function forces the re-simulation of a particular dataseries.
828
1577
  #The simulator link will be extracted from the record, by default.
829
1578
  def simulate_data_series_by_index(self, data_series_index, simulator_link='', verbose=False):
1579
+ """
1580
+ Forces re-simulation of a specific data_series within the fig_dict using its index, if the data_series dictionary has a 'simulate' field.
1581
+
1582
+ Args:
1583
+ data_series_index (int): Index of the data series in the fig_dict["data"] list to re-simulate.
1584
+ simulator_link (str, optional): Custom path or URL to override the default simulator. If not provided,
1585
+ the link is extracted from the 'simulate' field in the data_series dictionary.
1586
+ verbose (bool): If True, prints performs the simulation with the verbose flag turned on.
1587
+
1588
+ Returns:
1589
+ dict: The updated data_series dictionary reflecting the results of the re-simulation.
1590
+
1591
+ Notes:
1592
+ - There is an 'implied return': fig_dict is replaced in-place with its updated version after simulation.
1593
+ - Useful when recalculating output for a data_series with a defined 'simulate' field.
1594
+ """
830
1595
  self.fig_dict = simulate_specific_data_series_by_index(fig_dict=self.fig_dict, data_series_index=data_series_index, simulator_link=simulator_link, verbose=verbose)
831
1596
  data_series_dict = self.fig_dict["data"][data_series_index] #implied return
832
1597
  return data_series_dict #Extra regular return
833
1598
  #this function returns the current record.
834
1599
 
835
- def evaluate_eqution_of_data_series_by_index(self, series_index, equation_dict = None, verbose=False):
1600
+ def evaluate_equation_of_data_series_by_index(self, series_index, equation_dict = None, verbose=False):
1601
+ """
1602
+ Forces evaluates the equation associated with a data series in the fig_dict by its index.
1603
+
1604
+ Args:
1605
+ series_index (int): Index of the data series within fig_dict["data"] to evaluate the equation for.
1606
+ equation_dict (dict, optional): An equation dictionary to overwrite or assign as the data_series dictionary before evaluation.
1607
+ verbose (bool): If True, passes the verbose flag forward for during the equation evalution.
1608
+
1609
+ Returns:
1610
+ dict: The data_series dictionary updated with evaluated values.
1611
+
1612
+ Notes:
1613
+ - There is also an 'implied' return in that the new data_series_dict is added to the JSONGrapher object's fig_dict.
1614
+ - If equation_dict is provided, it is assigned to the data_series prior to evaluation.
1615
+ - The fig_dict is updated in-place with the evaluation results.
1616
+ - Ensure the series at the given index contains or receives a valid equation structure.
1617
+ """
836
1618
  if equation_dict != None:
837
1619
  self.fig_dict["data"][series_index]["equation"] = equation_dict
838
1620
  data_series_dict = self.fig_dict["data"][series_index]
@@ -842,12 +1624,28 @@ class JSONGrapherRecord:
842
1624
  #this function returns the current record.
843
1625
  def get_record(self):
844
1626
  """
845
- Returns a JSON-dict string of the record
1627
+ Retrieves the full fig_dict representing the current JSONGrapher record.
1628
+
1629
+ Returns:
1630
+ dict: The fig_dict for the current JSONGrapher record.
846
1631
  """
847
1632
  return self.fig_dict
848
1633
  #The update_and_validate function will clean for plotly.
849
1634
  #TODO: the internal recommending "print_to_inspect" function should, by default, exclude printing the full dictionaries of the layout_style and the trace_collection_style.
850
1635
  def print_to_inspect(self, update_and_validate=True, validate=True, clean_for_plotly = True, remove_remaining_hints=False):
1636
+ """
1637
+ Prints the current fig_dict in a human-readable JSON format, with optional consistency checks.
1638
+
1639
+ Args:
1640
+ update_and_validate (bool): If True (default), applies automatic updates and data cleaning before printing.
1641
+ validate (bool): If True (default), runs validation even if updates are skipped.
1642
+ clean_for_plotly (bool): If True (default), adjusts structure for compatibility with rendering tools.
1643
+ remove_remaining_hints (bool): If True, strips hint-related metadata before output.
1644
+
1645
+ Notes:
1646
+ - Recommended over __str__ for reviewing records with validated and updated content.
1647
+ - Future updates may include options to limit verbosity (e.g., omit layout_style and trace_style sections).
1648
+ """
851
1649
  if remove_remaining_hints == True:
852
1650
  self.remove_hints()
853
1651
  if update_and_validate == True: #this will do some automatic 'corrections' during the validation.
@@ -858,8 +1656,11 @@ class JSONGrapherRecord:
858
1656
 
859
1657
  def populate_from_existing_record(self, existing_JSONGrapher_record):
860
1658
  """
861
- Populates attributes from an existing JSONGrapher record.
862
- existing_JSONGrapher_record: A dictionary representing an existing JSONGrapher record.
1659
+ Populates the fig_dict using an existing JSONGrapher record.
1660
+
1661
+ Args:
1662
+ existing_JSONGrapher_record (dict or JSONGrapherRecord): Source record to use for populating the current instance.
1663
+
863
1664
  """
864
1665
  #While we expect a dictionary, if a JSONGrapher ojbect is provided, we will simply pull the dictionary out of that.
865
1666
  if isinstance(existing_JSONGrapher_record, dict):
@@ -874,6 +1675,18 @@ class JSONGrapherRecord:
874
1675
  #the below function takes in existin JSONGrpher record, and merges the data in.
875
1676
  #This requires scaling any data as needed, according to units.
876
1677
  def merge_in_JSONGrapherRecord(self, fig_dict_to_merge_in):
1678
+ """
1679
+ Merges data from another JSONGrapher record into the current fig_dict with appropriate unit scaling.
1680
+ - Extracts x and y axis units from both records and calculates scaling ratios.
1681
+ - Applies uniform scaling to all data_series dictionaries in the incoming record.
1682
+ - Supports string and object-based inputs by auto-converting them into a usable fig_dict format.
1683
+ - New data series are deep-copied and appended to the current fig_dict["data"] list.
1684
+
1685
+ Args:
1686
+ fig_dict_to_merge_in (dict, str, or JSONGrapherRecord): Source record to merge. Can be a fig_dict dictionary,
1687
+ a JSON-formatted string of a fig_dict, or a JSONGrapherRecord object instance.
1688
+
1689
+ """
877
1690
  import copy
878
1691
  fig_dict_to_merge_in = copy.deepcopy(fig_dict_to_merge_in)
879
1692
  if type(fig_dict_to_merge_in) == type({}):
@@ -903,17 +1716,33 @@ class JSONGrapherRecord:
903
1716
  self.fig_dict["data"].extend(scaled_fig_dict["data"])
904
1717
 
905
1718
  def import_from_dict(self, fig_dict):
1719
+ """
1720
+ Imports a complete fig_dict into the current JSONGrapherRecord instance, replacing its contents.
1721
+ - Any current fig_dict is entirely overwritten without merging or validation.
1722
+
1723
+ Args:
1724
+ fig_dict (dict): A JSONGrapher record fig_dict.
1725
+
1726
+ """
906
1727
  self.fig_dict = fig_dict
907
1728
 
908
1729
  def import_from_file(self, record_filename_or_object):
909
1730
  """
910
- Determine the type of file or data and call the appropriate import function.
1731
+ Imports a record from a supported file type or dictionary to create a fig_dict.
1732
+ - Automatically detects file type via extension when a string path is provided.
1733
+ - CSV/TSV content is handled using import_from_csv with delimiter selection.
1734
+ - JSON files and dictionaries are passed directly to import_from_json.
911
1735
 
912
1736
  Args:
913
- record_filename_or_object (str or dict): Filename of the CSV/TSV/JSON file or a dictionary object.
1737
+ record_filename_or_object (str or dict): A file path for a CSV, TSV, or JSON file,
1738
+ or a dictionary representing a JSONGrapher record.
914
1739
 
915
1740
  Returns:
916
- dict: Processed JSON data.
1741
+ dict: The fig_dict created or extracted from the file or provided object.
1742
+
1743
+ Raises:
1744
+ ValueError: If the file extension is unsupported (not .csv, .tsv, or .json).
1745
+
917
1746
  """
918
1747
  import os # Moved inside the function
919
1748
 
@@ -937,13 +1766,30 @@ class JSONGrapherRecord:
937
1766
 
938
1767
  #the json object can be a filename string or can be json object which is actually a dictionary.
939
1768
  def import_from_json(self, json_filename_or_object):
1769
+ """
1770
+ Imports a fig_dict from a JSON-formatted string, file path, or dictionary.
1771
+ - If a string is passed, the method attempts to parse it as a JSON-formatted string.
1772
+ - If parsing fails, it attempts to treat the string as a file path to a JSON file.
1773
+ - If the file isn’t found, it appends ".json" and tries again.
1774
+ - If a dictionary is passed, it is directly assigned to fig_dict.
1775
+ - Includes detailed error feedback if JSON parsing fails, highlighting common issues like
1776
+ improper quote usage or malformed booleans.
1777
+
1778
+ Args:
1779
+ json_filename_or_object (str or dict): A JSON string, a path to a .json file, or a dict
1780
+ representing a valid fig_dict structure.
1781
+
1782
+ Returns:
1783
+ dict: The parsed and loaded fig_dict.
1784
+
1785
+ """
940
1786
  if type(json_filename_or_object) == type(""): #assume it's a json_string or filename_and_path.
941
1787
  try:
942
1788
  record = json.loads(json_filename_or_object) #first check if it's a json string.
943
1789
  except json.JSONDecodeError as e1: # Catch specific exception
944
1790
  try:
945
1791
  import os
946
- #if the filename does not exist, then we'll check if adding ".json" fixes the problem.
1792
+ #if the filename does not exist, check if adding ".json" fixes the problem.
947
1793
  if not os.path.exists(json_filename_or_object):
948
1794
  json_added_filename = json_filename_or_object + ".json"
949
1795
  if os.path.exists(json_added_filename): json_filename_or_object = json_added_filename #only change the filename if the json_filename exists.
@@ -961,15 +1807,20 @@ class JSONGrapherRecord:
961
1807
 
962
1808
  def import_from_csv(self, filename, delimiter=","):
963
1809
  """
964
- Convert CSV file content into a JSON structure for Plotly.
1810
+ Imports a CSV or TSV file and converts its contents into a fig_dict.
1811
+ - The input file must follow a specific format, as of 6/25/25, but this may be made more general in the future.
1812
+ * Lines 1–5 define config metadata (e.g., comments, datatype, axis labels).
1813
+ * Line 6 defines series names.
1814
+ * Data rows begin on line 9.
965
1815
 
966
1816
  Args:
967
- filename (str): Path to the CSV file.
968
- delimiter (str, optional): Delimiter used in CSV. Default is ",".
969
- Use "\\t" for a tab-delimited file.
1817
+ filename (str): File path to the CSV or TSV file. If no extension is provided,
1818
+ ".csv" or ".tsv" is inferred based on the delimiter.
1819
+ delimiter (str, optional): Field separator character. Defaults to ",". Use "\\t" for TSV files.
970
1820
 
971
1821
  Returns:
972
- dict: JSON representation of the CSV data.
1822
+ dict: The created fig_dict.
1823
+
973
1824
  """
974
1825
  import os
975
1826
  # Modify the filename based on the delimiter and existing extension
@@ -1018,29 +1869,48 @@ class JSONGrapherRecord:
1018
1869
 
1019
1870
  def set_datatype(self, datatype):
1020
1871
  """
1021
- Sets the datatype field used as the experiment type or schema identifier.
1022
- datatype (str): The new data type to set.
1872
+ Sets the 'datatype' field within the fig_dict. Used to classify the record, for example by experiment type, and may
1873
+ point to a schema. May be a url.
1874
+
1875
+ Expected to be a string with no spaces, and may be a url. Underscore
1876
+ may be included "_" and double underscore "__" has a special meaning.
1877
+ See manual for information about the use of double underscore.
1878
+
1879
+ Args:
1880
+ datatype (str): The new string to use for the datatype field.
1881
+
1023
1882
  """
1024
1883
  self.fig_dict['datatype'] = datatype
1025
1884
 
1026
1885
  def set_comments(self, comments):
1027
1886
  """
1028
- Updates the comments field for the record.
1029
- str: The updated comments value.
1887
+ Updates the 'comments' field in the fig_dict.
1888
+
1889
+ Args:
1890
+ comments (str): Text to assign to fig_dict["comments"].
1030
1891
  """
1031
1892
  self.fig_dict['comments'] = comments
1032
1893
 
1033
1894
  def set_graph_title(self, graph_title):
1034
1895
  """
1035
- Updates the title of the graph in the layout dictionary.
1036
- graph_title (str): The new title to set for the graph.
1896
+ Sets the main title of the graph. Updates the title field in the fig_dict's layout section.
1897
+
1898
+ Args:
1899
+ graph_title (str): The title text to assign to fig_dict["layout"]["title"]["text"].
1037
1900
  """
1038
1901
  self.fig_dict['layout']['title']['text'] = graph_title
1039
1902
 
1040
1903
  def set_x_axis_label_including_units(self, x_axis_label_including_units, remove_plural_units=True):
1041
1904
  """
1042
- Updates the title of the x-axis in the layout dictionary.
1043
- xaxis_title (str): The new title to set for the x-axis.
1905
+ Sets and validates the axis label in the fig_dict's layout, with optional removal of plural 's' in units.
1906
+ - Utilizes validate_JSONGrapher_axis_label to enforce proper label formatting and unit conventions.
1907
+ - Ensures the layout["xaxis"] structure is initialized safely before assignment.
1908
+ - The layout_style structure is safely initialized if missing.
1909
+
1910
+ Args:
1911
+ x_axis_label_including_units (str): The full axis label text, including units in parentheses (e.g., "Time (s)").
1912
+ remove_plural_units (bool): If True (default), converts plural unit terms to singular during validation.
1913
+
1044
1914
  """
1045
1915
  if "xaxis" not in self.fig_dict['layout'] or not isinstance(self.fig_dict['layout'].get("xaxis"), dict):
1046
1916
  self.fig_dict['layout']["xaxis"] = {} # Initialize x-axis as a dictionary if it doesn't exist.
@@ -1050,8 +1920,15 @@ class JSONGrapherRecord:
1050
1920
 
1051
1921
  def set_y_axis_label_including_units(self, y_axis_label_including_units, remove_plural_units=True):
1052
1922
  """
1053
- Updates the title of the y-axis in the layout dictionary.
1054
- yaxis_title (str): The new title to set for the y-axis.
1923
+ Sets and validates the axis label in the fig_dict's layout, with optional removal of plural 's' in units.
1924
+ - Utilizes validate_JSONGrapher_axis_label to enforce proper label formatting and unit conventions.
1925
+ - Ensures the layout["yaxis"] structure is initialized safely before assignment.
1926
+ - The layout_style structure is safely initialized if missing.
1927
+
1928
+ Args:
1929
+ y_axis_label_including_units (str): The full axis label text, including units in parentheses (e.g., "Time (s)").
1930
+ remove_plural_units (bool): If True (default), converts plural unit terms to singular during validation.
1931
+
1055
1932
  """
1056
1933
  if "yaxis" not in self.fig_dict['layout'] or not isinstance(self.fig_dict['layout'].get("yaxis"), dict):
1057
1934
  self.fig_dict['layout']["yaxis"] = {} # Initialize y-axis as a dictionary if it doesn't exist.
@@ -1061,8 +1938,15 @@ class JSONGrapherRecord:
1061
1938
 
1062
1939
  def set_z_axis_label_including_units(self, z_axis_label_including_units, remove_plural_units=True):
1063
1940
  """
1064
- Updates the title of the z-axis in the layout dictionary.
1065
- zaxis_title (str): The new title to set for the z-axis.
1941
+ Sets and validates the axis label in the fig_dict's layout, with optional removal of plural 's' in units.
1942
+ - Utilizes validate_JSONGrapher_axis_label to enforce proper label formatting and unit conventions.
1943
+ - Ensures the layout["zaxis"] structure is initialized safely before assignment.
1944
+ - The layout_style structure is safely initialized if missing.
1945
+
1946
+ Args:
1947
+ z_axis_label_including_units (str): The full axis label text, including units in parentheses (e.g., "Time (s)").
1948
+ remove_plural_units (bool): If True (default), converts plural unit terms to singular during validation.
1949
+
1066
1950
  """
1067
1951
  if "zaxis" not in self.fig_dict['layout'] or not isinstance(self.fig_dict['layout'].get("zaxis"), dict):
1068
1952
  self.fig_dict['layout']["zaxis"] = {} # Initialize y-axis as a dictionary if it doesn't exist.
@@ -1073,18 +1957,66 @@ class JSONGrapherRecord:
1073
1957
 
1074
1958
  #function to set the min and max of the x axis in plotly way.
1075
1959
  def set_x_axis_range(self, min_value, max_value):
1960
+ """
1961
+ Sets the minimum and maximum range for the axis in the fig_dict's layout.
1962
+
1963
+ Args:
1964
+ min_value (float): Lower limit of the axis range.
1965
+ max_value (float): Upper limit of the axis range.
1966
+
1967
+ """
1076
1968
  self.fig_dict["layout"]["xaxis"][0] = min_value
1077
1969
  self.fig_dict["layout"]["xaxis"][1] = max_value
1970
+
1078
1971
  #function to set the min and max of the y axis in plotly way.
1079
1972
  def set_y_axis_range(self, min_value, max_value):
1973
+ """
1974
+ Sets the minimum and maximum range for the axis in the fig_dict's layout.
1975
+
1976
+ Args:
1977
+ min_value (float): Lower limit of the axis range.
1978
+ max_value (float): Upper limit of the axis range.
1979
+
1980
+ """
1080
1981
  self.fig_dict["layout"]["yaxis"][0] = min_value
1081
1982
  self.fig_dict["layout"]["yaxis"][1] = max_value
1082
1983
 
1984
+ #function to set the min and max of the y axis in plotly way.
1985
+
1986
+ def set_z_axis_range(self, min_value, max_value):
1987
+ """
1988
+ Sets the minimum and maximum range for the axis in the fig_dict's layout.
1989
+
1990
+ Args:
1991
+ min_value (float): Lower limit of the axis range.
1992
+ max_value (float): Upper limit of the axis range.
1993
+
1994
+ """
1995
+ self.fig_dict["layout"]["zaxis"][0] = min_value
1996
+ self.fig_dict["layout"]["zaxis"][1] = max_value
1997
+
1083
1998
  #function to scale the values in the data series by arbitrary amounts.
1084
1999
  def scale_record(self, num_to_scale_x_values_by = 1, num_to_scale_y_values_by = 1):
2000
+ """
2001
+ Scales all x and y values across data series in the fig_dict by specified scalar factors.
2002
+ - Modifies fig_dict in-place by replacing it with the scaled version.
2003
+
2004
+ Args:
2005
+ num_to_scale_x_values_by (float): Scaling factor applied to all x-values. Default is 1 (no change).
2006
+ num_to_scale_y_values_by (float): Scaling factor applied to all y-values. Default is 1 (no change).
2007
+
2008
+ """
1085
2009
  self.fig_dict = scale_fig_dict_values(self.fig_dict, num_to_scale_x_values_by=num_to_scale_x_values_by, num_to_scale_y_values_by=num_to_scale_y_values_by)
1086
2010
 
1087
2011
  def set_layout_fields(self, comments="", graph_title="", x_axis_label_including_units="", y_axis_label_including_units="", x_axis_comments="",y_axis_comments="", remove_plural_units=True):
2012
+ """
2013
+ Scales all x and y values across data series in the fig_dict by specified scalar factors.
2014
+ - Modifies fig_dict in-place by replacing it with the scaled version.
2015
+
2016
+ Args:
2017
+ num_to_scale_x_values_by (float): Scaling factor applied to all x-values. Default is 1 (no change).
2018
+ num_to_scale_y_values_by (float): Scaling factor applied to all y-values. Default is 1 (no change).
2019
+ """
1088
2020
  # comments: General comments about the layout. Allowed by JSONGrapher, but will be removed if converted to a plotly object.
1089
2021
  # graph_title: Title of the graph.
1090
2022
  # xaxis_title: Title of the x-axis, including units.
@@ -1113,13 +2045,25 @@ class JSONGrapherRecord:
1113
2045
  #TODO: need to add an "include_formatting" option
1114
2046
  def export_to_json_file(self, filename, update_and_validate=True, validate=True, simulate_all_series = True, remove_simulate_fields= False, remove_equation_fields= False, remove_remaining_hints=False):
1115
2047
  """
1116
- writes the json to a file
1117
- returns the json as a dictionary.
1118
- update_and_validate function will clean for plotly. One can alternatively only validate.
1119
- optionally simulates all series that have a simulate field (does so by default)
1120
- optionally removes simulate filed from all series that have a simulate field (does not do so by default)
1121
- optionally removes hints before export and return.
2048
+ Exports the current fig_dict to a JSON file with optional simulation, cleaning, and validation.
2049
+ - Ensures compatibility with Plotly and external tools by validating and cleaning metadata.
2050
+ - JSON file is saved using UTF-8 encoding with 4-space indentation.
2051
+
2052
+ Args:
2053
+ filename (str): Destination filename. A '.json' extension will be appended if missing.
2054
+ update_and_validate (bool): If True (default), applies automatic corrections and cleans the fig_dict before export.
2055
+ validate (bool): If True (default), performs validation even without updates.
2056
+ simulate_all_series (bool): If True (default), evaluates all data series containing a 'simulate' field prior to export.
2057
+ remove_simulate_fields (bool): If True, strips out 'simulate' fields from each data series before export.
2058
+ remove_equation_fields (bool): If True, removes 'equation' fields from each series.
2059
+ remove_remaining_hints (bool): If True, deletes developer hints from the record for cleaner output.
2060
+
2061
+ Returns:
2062
+ dict: The fig_dict after all specified operations.
2063
+
1122
2064
  """
2065
+ import copy
2066
+ original_fig_dict = copy.deepcopy(self.fig_dict)
1123
2067
  #if simulate_all_series is true, we'll try to simulate any series that need it, then clean the simulate fields out if requested.
1124
2068
  if simulate_all_series == True:
1125
2069
  self.fig_dict = simulate_as_needed_in_fig_dict(self.fig_dict)
@@ -1142,11 +2086,53 @@ class JSONGrapherRecord:
1142
2086
  #Write to file using UTF-8 encoding.
1143
2087
  with open(filename, 'w', encoding='utf-8') as f:
1144
2088
  json.dump(self.fig_dict, f, indent=4)
1145
- return self.fig_dict
2089
+ modified_fig_dict = self.fig_dict #store the modified fig_dict for return .
2090
+ self.fig_dict = original_fig_dict #restore the original fig_dict.
2091
+ return modified_fig_dict
1146
2092
 
1147
- def export_plotly_json(self, filename, plot_style = None, update_and_validate=True, simulate_all_series=True, evaluate_all_equations=True,adjust_implicit_data_ranges=True):
1148
- fig = self.get_plotly_fig(plot_style=plot_style, update_and_validate=update_and_validate, simulate_all_series=simulate_all_series, evaluate_all_equations=evaluate_all_equations, adjust_implicit_data_ranges=adjust_implicit_data_ranges)
2093
+ def get_plotly_json(self, plot_style = None, update_and_validate=True, simulate_all_series=True, evaluate_all_equations=True,adjust_implicit_data_ranges=True):
2094
+ """
2095
+ Generates a Plotly-compatible JSON from the current fig_dict
2096
+ - Relies on get_plotly_fig() for figure construction and formatting.
2097
+
2098
+ Args:
2099
+ plot_style (dict, optional): plot_style to apply before exporting.
2100
+ update_and_validate (bool): If True (default), cleans and validates the figure before export.
2101
+ simulate_all_series (bool): If True (default), simulates any data series that include a 'simulate' field before exporting.
2102
+ evaluate_all_equations (bool): If True (default), computes outputs for any equation-based series before exporting.
2103
+ adjust_implicit_data_ranges (bool): If True (default), automatically adjusts 'equation' and 'simulate' series axis ranges to the data, for cases that are compatible with that feature.
2104
+
2105
+ Returns:
2106
+ dict: The Plotly-compatible JSON object, a dictionary, which can be directly plotted with plotly.
2107
+
2108
+ """
2109
+ fig = self.get_plotly_fig(plot_style=plot_style,
2110
+ update_and_validate=update_and_validate,
2111
+ simulate_all_series=simulate_all_series,
2112
+ evaluate_all_equations=evaluate_all_equations,
2113
+ adjust_implicit_data_ranges=adjust_implicit_data_ranges)
1149
2114
  plotly_json_string = fig.to_plotly_json()
2115
+ return plotly_json_string
2116
+
2117
+ def export_plotly_json(self, filename, plot_style = None, update_and_validate=True, simulate_all_series=True, evaluate_all_equations=True,adjust_implicit_data_ranges=True):
2118
+ """
2119
+ Generates a Plotly-compatible JSON file from the current fig_dict and exports it to disk.
2120
+ - Relies on get_plotly_fig() for figure construction and formatting.
2121
+ - Exports the result to a UTF-8 encoded file using standard JSON formatting.
2122
+
2123
+ Args:
2124
+ filename (str): Path for the output file. If no ".json" extension is present, it will be added.
2125
+ plot_style (dict, optional): plot_style to apply before exporting.
2126
+ update_and_validate (bool): If True (default), cleans and validates the figure before export.
2127
+ simulate_all_series (bool): If True (default), simulates any data series that include a 'simulate' field before exporting.
2128
+ evaluate_all_equations (bool): If True (default), computes outputs for any equation-based series before exporting.
2129
+ adjust_implicit_data_ranges (bool): If True (default), automatically adjusts 'equation' and 'simulate' series axis ranges to the data, for cases that are compatible with that feature.
2130
+
2131
+ Returns:
2132
+ dict: The Plotly-compatible JSON object, a dictionary, which can be directly plotted with plotly.
2133
+
2134
+ """
2135
+ plotly_json_string = self.get_plotly_json(plot_style=plot_style, update_and_validate=update_and_validate, simulate_all_series=simulate_all_series, evaluate_all_equations=evaluate_all_equations, adjust_implicit_data_ranges=adjust_implicit_data_ranges)
1150
2136
  if len(filename) > 0: #this means we will be writing to file.
1151
2137
  # Check if the filename has an extension and append `.json` if not
1152
2138
  if '.json' not in filename.lower():
@@ -1157,20 +2143,28 @@ class JSONGrapherRecord:
1157
2143
  return plotly_json_string
1158
2144
 
1159
2145
  #simulate all series will simulate any series as needed.
1160
- 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, adjust_arrange2dTo3d=True):
1161
2147
  """
1162
- Generates a Plotly figure from the stored fig_dict, performing simulations and equations as needed.
1163
- By default, it will apply the default still hard coded into jsongrapher.
2148
+ Constructs and returns a Plotly figure object based on the current fig_dict with optional preprocessing steps.
2149
+ - A deep copy of fig_dict is created to avoid unintended mutation of the source object.
2150
+ - Applies plot styles before cleaning and validation.
2151
+ - Removes JSONGrapher specific fields (e.g., 'simulate', 'equation') before handing off to Plotly.
1164
2152
 
1165
2153
  Args:
1166
- plot_style: String or dictionary of style to apply. Use '' to skip applying a style, or provide a list of length two containing both a layout style and a data series style."none" removes all style.
1167
- simulate_all_series (bool): If True, performs simulations for applicable series.
1168
- update_and_validate (bool): If True, applies automatic corrections to fig_dict.
1169
- evaluate_all_equations (bool): If True, evaluates all equation-based series.
1170
- adjust_implicit_data_ranges (bool): If True, modifies ranges for implicit data series.
2154
+ plot_style (str, dict, or list): plot_style dictionary. Use '' to skip styling and 'none' to clear all styles.
2155
+ Also accepts other options:
2156
+ - A dictionary with layout_style and trace_styles_collection (normal case).
2157
+ - A string such as 'default' to use for both layout_style and trace_styles_collection name
2158
+ - A list of length two with layout_style and trace_styles_collection name
2159
+ update_and_validate (bool): If True (default), applies automated corrections and validation.
2160
+ simulate_all_series (bool): If True (default), simulates any data series that include a 'simulate' field before exporting.
2161
+ evaluate_all_equations (bool): If True (default), computes outputs for any equation-based series before exporting.
2162
+ adjust_implicit_data_ranges (bool): If True (default), automatically adjusts 'equation' and 'simulate' series axis ranges to the data, for cases that are compatible with that feature.
1171
2163
 
1172
2164
  Returns:
1173
- plotly Figure: A validated Plotly figure object based on fig_dict.
2165
+ plotly fig: A fully constructed and styled Plotly figure object.
2166
+
2167
+
1174
2168
  """
1175
2169
  if plot_style is None: #should not initialize mutable objects in arguments line, so doing here.
1176
2170
  plot_style = {"layout_style": "", "trace_styles_collection": ""} # Fresh dictionary per function call
@@ -1183,16 +2177,25 @@ class JSONGrapherRecord:
1183
2177
  self.fig_dict = execute_implicit_data_series_operations(self.fig_dict,
1184
2178
  simulate_all_series=simulate_all_series,
1185
2179
  evaluate_all_equations=evaluate_all_equations,
1186
- adjust_implicit_data_ranges=adjust_implicit_data_ranges)
2180
+ adjust_implicit_data_ranges=adjust_implicit_data_ranges,
2181
+ adjust_offset2d = False,
2182
+ adjust_arrange2dTo3d=False)
1187
2183
  #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.
1188
2184
  original_fig_dict = copy.deepcopy(self.fig_dict)
2185
+ #The adjust_offset2d should be on the copy, if requested.
2186
+ self.fig_dict = execute_implicit_data_series_operations(self.fig_dict,
2187
+ simulate_all_series=False,
2188
+ evaluate_all_equations=False,
2189
+ adjust_implicit_data_ranges=False,
2190
+ adjust_offset2d=adjust_offset2d,
2191
+ adjust_arrange2dTo3d=adjust_arrange2dTo3d)
1189
2192
  #before cleaning and validating, we'll apply styles.
1190
2193
  plot_style = parse_plot_style(plot_style=plot_style)
1191
2194
  self.apply_plot_style(plot_style=plot_style)
1192
2195
  #Now we clean out the fields and make a plotly object.
1193
2196
  if update_and_validate == True: #this will do some automatic 'corrections' during the validation.
1194
2197
  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.
1195
- self.fig_dict = clean_json_fig_dict(self.fig_dict, fields_to_update=['simulate', 'custom_units_chevrons', 'equation', 'trace_style', '3d_axes', 'bubble', 'superscripts'])
2198
+ 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'])
1196
2199
  fig = pio.from_json(json.dumps(self.fig_dict))
1197
2200
  #restore the original fig_dict.
1198
2201
  self.fig_dict = original_fig_dict
@@ -1200,30 +2203,103 @@ class JSONGrapherRecord:
1200
2203
 
1201
2204
  #Just a wrapper aroudn plot_with_plotly.
1202
2205
  def plot(self, plot_style = None, update_and_validate=True, simulate_all_series=True, evaluate_all_equations=True, adjust_implicit_data_ranges=True):
2206
+ """
2207
+ Plots the current fig_dict using Plotly with optional preprocessing, simulation, and visual styling.
2208
+ - Acts as a convenience wrapper around plot_with_plotly().
2209
+
2210
+ Args:
2211
+ plot_style (str, dict, or list): plot_style dictionary. Use '' to skip styling and 'none' to clear all styles.
2212
+ Also accepts other options:
2213
+ - A dictionary with layout_style and trace_styles_collection (normal case).
2214
+ - A string such as 'default' to use for both layout_style and trace_styles_collection name
2215
+ - A list of length two with layout_style and trace_styles_collection name
2216
+ update_and_validate (bool): If True (default), applies automated corrections and validation.
2217
+ simulate_all_series (bool): If True (default), simulates any data series that include a 'simulate' field before exporting.
2218
+ evaluate_all_equations (bool): If True (default), computes outputs for any equation-based series before exporting.
2219
+ adjust_implicit_data_ranges (bool): If True (default), automatically adjusts 'equation' and 'simulate' series axis ranges to the data, for cases that are compatible with that feature.
2220
+
2221
+ Returns:
2222
+ plotly fig: A Plotly figure object rendered from the processed fig_dict. However, the main 'real' return is a graph window that pops up.
2223
+
2224
+ """
1203
2225
  if plot_style is None: #should not initialize mutable objects in arguments line, so doing here.
1204
2226
  plot_style = {"layout_style": "", "trace_styles_collection": ""} # Fresh dictionary per function call
1205
2227
  return self.plot_with_plotly(plot_style=plot_style, update_and_validate=update_and_validate, simulate_all_series=simulate_all_series, evaluate_all_equations=evaluate_all_equations, adjust_implicit_data_ranges=adjust_implicit_data_ranges)
1206
2228
 
1207
2229
  #simulate all series will simulate any series as needed. If changing this function's arguments, also change those for self.plot()
1208
- def plot_with_plotly(self, plot_style = None, update_and_validate=True, simulate_all_series=True, evaluate_all_equations=True, adjust_implicit_data_ranges=True):
2230
+ def plot_with_plotly(self, plot_style = None, update_and_validate=True, simulate_all_series=True, evaluate_all_equations=True, adjust_implicit_data_ranges=True, browser=True):
2231
+ """
2232
+ Displays the current fig_dict as an interactive Plotly figure with optional preprocessing and styling.
2233
+ A Plotly figure object rendered from the processed fig_dict. However, the main 'real' return is a graph window that pops up.
2234
+ - Wraps get_plotly_fig() and renders the resulting figure using fig.show().
2235
+ - Safely leaves the internal fig_dict unchanged after rendering.
2236
+
2237
+ Args:
2238
+ plot_style (str, dict, or list): plot_style dictionary. Use '' to skip styling and 'none' to clear all styles.
2239
+ Also accepts other options:
2240
+ - A dictionary with layout_style and trace_styles_collection (normal case).
2241
+ - A string such as 'default' to use for both layout_style and trace_styles_collection name
2242
+ - A list of length two with layout_style and trace_styles_collection name
2243
+ update_and_validate (bool): If True (default), applies automated corrections and validation.
2244
+ simulate_all_series (bool): If True (default), simulates any data series that include a 'simulate' field before exporting.
2245
+ evaluate_all_equations (bool): If True (default), computes outputs for any equation-based series before exporting.
2246
+ adjust_implicit_data_ranges (bool): If True (default), automatically adjusts 'equation' and 'simulate' series axis ranges to the data, for cases that are compatible with that feature.
2247
+
2248
+ Returns:
2249
+ plotly fig: A Plotly figure object rendered from the processed fig_dict. However, the main 'real' return is a graph window that pops up.
2250
+
2251
+ """
2252
+ if browser == True:
2253
+ import plotly.io as pio; pio.renderers.default = "browser"#
1209
2254
  if plot_style is None: #should not initialize mutable objects in arguments line, so doing here.
1210
2255
  plot_style = {"layout_style": "", "trace_styles_collection": ""} # Fresh dictionary per function call
1211
2256
  fig = self.get_plotly_fig(plot_style=plot_style,
1212
- simulate_all_series=simulate_all_series,
1213
2257
  update_and_validate=update_and_validate,
2258
+ simulate_all_series=simulate_all_series,
1214
2259
  evaluate_all_equations=evaluate_all_equations,
1215
2260
  adjust_implicit_data_ranges=adjust_implicit_data_ranges)
1216
2261
  fig.show()
2262
+ return fig
1217
2263
  #No need for fig.close() for plotly figures.
1218
2264
 
1219
2265
 
1220
2266
  #simulate all series will simulate any series as needed.
1221
- def export_to_plotly_png(self, filename, simulate_all_series = True, update_and_validate=True, timeout=10):
1222
- fig = self.get_plotly_fig(simulate_all_series = simulate_all_series, update_and_validate=update_and_validate)
2267
+ def export_to_plotly_png(self, filename, plot_style = None, update_and_validate=True, simulate_all_series=True, evaluate_all_equations=True, adjust_implicit_data_ranges=True, timeout=10):
2268
+ """
2269
+ Exports the current fig_dict as a PNG image file using a Plotly-rendered figure.
2270
+ Notes:
2271
+ - Relies on get_plotly_fig() to construct the Plotly figure.
2272
+ - Uses export_plotly_image_with_timeout() to safely render and export the image without stalling.
2273
+
2274
+ Args:
2275
+ filename (str): The name of the output PNG file. If missing an extension, ".png" will be inferred downstream.
2276
+ simulate_all_series (bool): If True (default), simulates any data series that include a 'simulate' field before exporting.
2277
+ update_and_validate (bool): If True (default), performs automated cleanup and validation before rendering.
2278
+ timeout (int): Max number of seconds allotted to render and export the figure.
2279
+
2280
+ """
2281
+ fig = self.get_plotly_fig(plot_style=plot_style,
2282
+ update_and_validate=update_and_validate,
2283
+ simulate_all_series=simulate_all_series,
2284
+ evaluate_all_equations=evaluate_all_equations,
2285
+ adjust_implicit_data_ranges=adjust_implicit_data_ranges)
1223
2286
  # Save the figure to a file, but use the timeout version.
1224
2287
  self.export_plotly_image_with_timeout(plotly_fig = fig, filename=filename, timeout=timeout)
1225
2288
 
1226
2289
  def export_plotly_image_with_timeout(self, plotly_fig, filename, timeout=10):
2290
+ """
2291
+ Attempts to export a Plotly figure to a PNG file using Kaleido, with timeout protection.
2292
+ - Runs the export in a background daemon thread to ensure timeout safety.
2293
+ - MathJax is disabled to improve speed and compatibility.
2294
+ - If export exceeds the timeout, a warning is printed and no file is saved.
2295
+ - Kaleido must be installed and working; if issues persist, consider using `export_to_matplotlib_png()` as a fallback.
2296
+
2297
+ Args:
2298
+ plotly_fig (plotly.graph_objs._figure.Figure): The Plotly figure to export as an image.
2299
+ filename (str): Target PNG file name. Adds ".png" if not already present.
2300
+ timeout (int): Maximum duration (in seconds) to allow the export before timing out. Default is 10.
2301
+
2302
+ """
1227
2303
  # Ensure filename ends with .png
1228
2304
  if not filename.lower().endswith(".png"):
1229
2305
  filename += ".png"
@@ -1247,18 +2323,24 @@ class JSONGrapherRecord:
1247
2323
  #update_and_validate will 'clean' for plotly.
1248
2324
  #In the case of creating a matplotlib figure, this really just means removing excess fields.
1249
2325
  #simulate all series will simulate any series as needed.
1250
- 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):
2326
+ 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, adjust_arrange2dTo3d=True):
1251
2327
  """
1252
- Generates a matplotlib figure from the stored fig_dict, performing simulations and equations as needed.
2328
+ Constructs and returns a matplotlib figure generated from fig_dict, with optional simulation, preprocessing, and styling.
1253
2329
 
1254
2330
  Args:
1255
- simulate_all_series (bool): If True, performs simulations for applicable series.
1256
- update_and_validate (bool): If True, applies automatic corrections to fig_dict.
1257
- evaluate_all_equations (bool): If True, evaluates all equation-based series.
1258
- adjust_implicit_data_ranges (bool): If True, modifies ranges for implicit data series.
2331
+ plot_style (str, dict, or list): plot_style dictionary. Use '' to skip styling and 'none' to clear all styles.
2332
+ Also accepts other options:
2333
+ - A dictionary with layout_style and trace_styles_collection (normal case).
2334
+ - A string such as 'default' to use for both layout_style and trace_styles_collection name
2335
+ - A list of length two with layout_style and trace_styles_collection name
2336
+ update_and_validate (bool): If True (default), applies automated corrections and validation.
2337
+ simulate_all_series (bool): If True (default), simulates any data series that include a 'simulate' field before exporting.
2338
+ evaluate_all_equations (bool): If True (default), computes outputs for any equation-based series before exporting.
2339
+ adjust_implicit_data_ranges (bool): If True (default), automatically adjusts 'equation' and 'simulate' series axis ranges to the data, for cases that are compatible with that feature.
1259
2340
 
1260
2341
  Returns:
1261
- plotly Figure: A validated matplotlib figure object based on fig_dict.
2342
+ matplotlib fig: A matplotlib figure object based on fig_dict.
2343
+
1262
2344
  """
1263
2345
  if plot_style is None: #should not initialize mutable objects in arguments line, so doing here.
1264
2346
  plot_style = {"layout_style": "", "trace_styles_collection": ""} # Fresh dictionary per function call
@@ -1269,9 +2351,18 @@ class JSONGrapherRecord:
1269
2351
  self.fig_dict = execute_implicit_data_series_operations(self.fig_dict,
1270
2352
  simulate_all_series=simulate_all_series,
1271
2353
  evaluate_all_equations=evaluate_all_equations,
1272
- adjust_implicit_data_ranges=adjust_implicit_data_ranges)
2354
+ adjust_implicit_data_ranges=adjust_implicit_data_ranges,
2355
+ adjust_offset2d = False,
2356
+ adjust_arrange2dTo3d=False)
1273
2357
  #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.
1274
- 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.
2358
+ original_fig_dict = copy.deepcopy(self.fig_dict)
2359
+ #The adjust_offset2d should be on the copy, if requested.
2360
+ self.fig_dict = execute_implicit_data_series_operations(self.fig_dict,
2361
+ simulate_all_series=False,
2362
+ evaluate_all_equations=False,
2363
+ adjust_implicit_data_ranges=False,
2364
+ adjust_offset2d=adjust_offset2d,
2365
+ adjust_arrange2dTo3d=adjust_arrange2dTo3d)
1275
2366
  #before cleaning and validating, we'll apply styles.
1276
2367
  plot_style = parse_plot_style(plot_style=plot_style)
1277
2368
  self.apply_plot_style(plot_style=plot_style)
@@ -1283,31 +2374,60 @@ class JSONGrapherRecord:
1283
2374
  return fig
1284
2375
 
1285
2376
  #simulate all series will simulate any series as needed.
1286
- def plot_with_matplotlib(self, update_and_validate=True, simulate_all_series=True, evaluate_all_equations=True, adjust_implicit_data_ranges=True):
2377
+ def plot_with_matplotlib(self, plot_style=None, update_and_validate=True, simulate_all_series=True, evaluate_all_equations=True, adjust_implicit_data_ranges=True):
2378
+ """
2379
+ Displays the current fig_dict as a matplotlib figure with optional preprocessing and simulation.
2380
+
2381
+ Args:
2382
+ update_and_validate (bool): If True (default), applies automated corrections and validation.
2383
+ simulate_all_series (bool): If True (default), simulates any data series that include a 'simulate' field before exporting.
2384
+ evaluate_all_equations (bool): If True (default), computes outputs for any equation-based series before exporting.
2385
+ adjust_implicit_data_ranges (bool): If True (default), automatically adjusts 'equation' and 'simulate' series axis ranges to the data, for cases that are compatible with that feature.
2386
+
2387
+ """
1287
2388
  import matplotlib.pyplot as plt
1288
- fig = self.get_matplotlib_fig(simulate_all_series=simulate_all_series,
2389
+ fig = self.get_matplotlib_fig(plot_style=plot_style,
1289
2390
  update_and_validate=update_and_validate,
2391
+ simulate_all_series=simulate_all_series,
1290
2392
  evaluate_all_equations=evaluate_all_equations,
1291
2393
  adjust_implicit_data_ranges=adjust_implicit_data_ranges)
1292
2394
  plt.show()
1293
2395
  plt.close(fig) #remove fig from memory.
1294
2396
 
1295
2397
  #simulate all series will simulate any series as needed.
1296
- def export_to_matplotlib_png(self, filename, simulate_all_series = True, update_and_validate=True):
2398
+ def export_to_matplotlib_png(self, filename, plot_style = None, update_and_validate=True, simulate_all_series = True, evaluate_all_equations = True, adjust_implicit_data_ranges=True):
2399
+ """
2400
+ Export the current fig_dict as a PNG image by rendering it with matplotlib.
2401
+ - Calls get_matplotlib_fig() to generate the figure.
2402
+ - Saves the image using matplotlib's savefig().
2403
+ - Automatically closes the figure after saving to free memory.
2404
+
2405
+ Args:
2406
+ filename (str): Output filepath for the image. Adds a ".png" extension if not provided.
2407
+ simulate_all_series (bool): If True (default), simulates any data series that include a 'simulate' field before exporting.
2408
+ update_and_validate (bool): If True (default), performs cleanup and validation before rendering.
2409
+
2410
+ """
1297
2411
  import matplotlib.pyplot as plt
1298
2412
  # Ensure filename ends with .png
1299
2413
  if not filename.lower().endswith(".png"):
1300
2414
  filename += ".png"
1301
- fig = self.get_matplotlib_fig(simulate_all_series = simulate_all_series, update_and_validate=update_and_validate)
2415
+ fig = self.get_matplotlib_fig(plot_style=plot_style,
2416
+ update_and_validate=update_and_validate,
2417
+ simulate_all_series=simulate_all_series,
2418
+ evaluate_all_equations=evaluate_all_equations,
2419
+ adjust_implicit_data_ranges=adjust_implicit_data_ranges)
1302
2420
  # Save the figure to a file
1303
2421
  fig.savefig(filename)
1304
2422
  plt.close(fig) #remove fig from memory.
1305
2423
 
1306
2424
  def add_hints(self):
1307
2425
  """
1308
- Adds hints to fields that are currently empty strings using self.hints_dictionary.
1309
- Dynamically parses hint keys (e.g., "['layout']['xaxis']['title']") to access and update fields in self.fig_dict.
1310
- The hints_dictionary is first populated during creation of the class object in __init__.
2426
+ Populate empty fields in fig_dict with placeholder text from hints_dictionary.
2427
+ - Each key in hints_dictionary represents a dotted path (e.g., "['layout']['xaxis']['title']") pointing to a location within fig_dict.
2428
+ - If the specified field is missing or contains an empty string, the corresponding hint is
2429
+ Though there is no actual return, there is an implied return of self.fig_dict being modfied.
2430
+
1311
2431
  """
1312
2432
  for hint_key, hint_text in self.hints_dictionary.items():
1313
2433
  # Parse the hint_key into a list of keys representing the path in the record.
@@ -1335,9 +2455,14 @@ class JSONGrapherRecord:
1335
2455
 
1336
2456
  def remove_hints(self):
1337
2457
  """
1338
- Removes hints by converting fields back to empty strings if their value matches the hint text in self.hints_dictionary.
1339
- Dynamically parses hint keys (e.g., "['layout']['xaxis']['title']") to access and update fields in self.fig_dict.
1340
- The hints_dictionary is first populated during creation of the class object in __init__.
2458
+ Remove placeholder hint text from fig_dict
2459
+
2460
+ Does so by checking where fields match entries in hints_dictionary.
2461
+ - Each key in hints_dictionary represents a dotted path (e.g., "['layout']['xaxis']['title']") pointing to a location within fig_dict.
2462
+ - If a matching field is found and its value equals the hint text, it is cleared to an empty string.
2463
+ - Traverses nested dictionaries safely using get() to avoid key errors.
2464
+ - Complements add_hints() by cleaning up unused or placeholder entries.
2465
+
1341
2466
  """
1342
2467
  for hint_key, hint_text in self.hints_dictionary.items():
1343
2468
  # Parse the hint_key into a list of keys representing the path in the record.
@@ -1366,6 +2491,16 @@ class JSONGrapherRecord:
1366
2491
  ## Start of section of JSONGRapher class functions related to styles ##
1367
2492
 
1368
2493
  def apply_plot_style(self, plot_style= None):
2494
+ """
2495
+ Apply layout and trace styling configuration to fig_dict and store it in the 'plot_style' field.
2496
+ - Modifies fig_dict in place using apply_plot_style_to_plotly_dict().
2497
+
2498
+ Args:
2499
+ plot_style (str, dict, or list): A style identifier. Can be a string keyword, a dictionary with 'layout_style' and
2500
+ 'trace_styles_collection', or a list containing both. If None, defaults to an empty style dictionary.
2501
+
2502
+ """
2503
+
1369
2504
  #the plot_style can be a string, or a plot_style dictionary {"layout_style":"default", "trace_styles_collection":"default"} or a list of length two with those two items.
1370
2505
  #The plot_style dictionary can include a pair of dictionaries.
1371
2506
  #if apply style is called directly, we will first put the plot_style into the plot_style field
@@ -1374,20 +2509,59 @@ class JSONGrapherRecord:
1374
2509
  plot_style = {"layout_style": "", "trace_styles_collection": ""} # Fresh dictionary per function call
1375
2510
  self.fig_dict['plot_style'] = plot_style
1376
2511
  self.fig_dict = apply_plot_style_to_plotly_dict(self.fig_dict, plot_style=plot_style)
2512
+
1377
2513
  def remove_plot_style(self):
2514
+ """
2515
+ Remove styling information from fig_dict, including the 'plot_style' field and associated formatting.
2516
+ - Calls remove_plot_style_from_fig_dict to strip trace_style and layout_style formatting.
2517
+ """
1378
2518
  self.fig_dict.pop("plot_style") #This line removes the field of plot_style from the fig_dict.
1379
- self.fig_dict = remove_plot_style_from_plotly_dict(self.fig_dict) #This line removes the actual formatting from the fig_dict.
2519
+ self.fig_dict = remove_plot_style_from_fig_dict(self.fig_dict) #This line removes the actual formatting from the fig_dict.
2520
+
1380
2521
  def set_layout_style(self, layout_style):
2522
+ """
2523
+ Set the 'layout_style' field inside fig_dict['plot_style'].
2524
+ - Initializes fig_dict["plot_style"] if it does not already exist.
2525
+
2526
+ Args:
2527
+ layout_style (str or dict): A string or dictionary representing the desired layout_style.
2528
+ """
1381
2529
  if "plot_style" not in self.fig_dict: #create it not present.
1382
2530
  self.fig_dict["plot_style"] = {} # Initialize if missing
1383
2531
  self.fig_dict["plot_style"]["layout_style"] = layout_style
2532
+
1384
2533
  def remove_layout_style_setting(self):
2534
+ """
2535
+ Remove the 'layout_style' entry from fig_dict['plot_style'], if present.
2536
+ """
1385
2537
  if "layout_style" in self.fig_dict["plot_style"]:
1386
2538
  self.fig_dict["plot_style"].pop("layout_style")
2539
+
1387
2540
  def extract_layout_style(self):
1388
- layout_style = extract_layout_style_from_plotly_dict(self.fig_dict)
2541
+ """
2542
+ Extract the layout_style from fig_dict using a helper function.
2543
+ - Calls extract_layout_style_from_fig_dict to retrieve layout style information.
2544
+
2545
+ Returns:
2546
+ str or dict: The extracted layout style, depending on how styles are stored.
2547
+ """
2548
+ layout_style = extract_layout_style_from_fig_dict(self.fig_dict)
1389
2549
  return layout_style
2550
+
1390
2551
  def apply_trace_style_by_index(self, data_series_index, trace_styles_collection='', trace_style=''):
2552
+ """
2553
+ Apply a trace_style to that data_series at a specified index in the fig_dict.
2554
+ - Initializes fig_dict["plot_style"] if missing and defaults to embedded styling when needed.
2555
+
2556
+ Args:
2557
+ data_series_index (int): Index of the target data series within fig_dict["data"].
2558
+ trace_styles_collection (str or dict): Optional named collection or dictionary of trace styles. Checks fig_dict["plot_style"]["trace_styles_collection"] if empty.
2559
+ trace_style (str or dict): The trace_style to apply. Can be a string for a trace_style name to be retrieved from a trace_styles_collection or can be at trace_style dictionary.
2560
+
2561
+ Returns:
2562
+ dict: The updated data_series dictionary with the applied trace_style.
2563
+
2564
+ """
1391
2565
  if trace_styles_collection == '':
1392
2566
  self.fig_dict.setdefault("plot_style",{}) #create the plot_style dictionary if it's not there. Else, return current value.
1393
2567
  trace_styles_collection = self.fig_dict["plot_style"].get("trace_styles_collection", '') #check if there is a trace_styles_collection within it, and use that. If it's not there, then use ''.
@@ -1397,21 +2571,45 @@ class JSONGrapherRecord:
1397
2571
  self.fig_dict["data"][data_series_index] = data_series
1398
2572
  return data_series
1399
2573
  def set_trace_style_one_data_series(self, data_series_index, trace_style):
2574
+ """
2575
+ Sets the trace_style at the data_series with the specified index in fig_dict.
2576
+ - Overwrites any existing trace_style entry for the specified series.
2577
+
2578
+ Args:
2579
+ data_series_index (int): Index of the target data series within fig_dict["data"].
2580
+ trace_style (dict or string): Dictionary or string specifying visual styling for the selected data series.
2581
+
2582
+ Returns:
2583
+ dict: The updated data_series dictionary after setting the trace_style.
2584
+
2585
+ """
1400
2586
  self.fig_dict['data'][data_series_index]["trace_style"] = trace_style
1401
2587
  return self.fig_dict['data'][data_series_index]
1402
2588
  def set_trace_styles_collection(self, trace_styles_collection):
1403
2589
  """
1404
- Sets the plot_style["trace_styles_collection"] field for the all data series.
1405
- options are: scatter, spline, scatter_spline
2590
+ Set the trace_styles_collection field in fig_dict['plot_style']
2591
+ - Initializes fig_dict['plot_style'] if it does not already exist.
2592
+
2593
+ Args:
2594
+ trace_styles_collection (str): Name of the trace_styles_collection to apply.
1406
2595
  """
1407
2596
  self.fig_dict["plot_style"]["trace_styles_collection"] = trace_styles_collection
1408
2597
  def remove_trace_styles_collection_setting(self):
2598
+ """
2599
+ Remove the 'trace_styles_collection' entry from fig_dict['plot_style'], if present.
2600
+ - Deletes only the trace style setting, preserving other style-related fields like layout_style.
2601
+ - has an implied return since it modifies self.fig_dict in place.
2602
+
2603
+ """
1409
2604
  if "trace_styles_collection" in self.fig_dict["plot_style"]:
1410
2605
  self.fig_dict["plot_style"].pop("trace_styles_collection")
1411
2606
  def set_trace_style_all_series(self, trace_style):
1412
2607
  """
1413
- Sets the trace_style field for the all data series.
1414
- options are: scatter, spline, scatter_spline
2608
+ Set all data_series in the fig_dict to have a speicfic trace_style.
2609
+
2610
+ Args:
2611
+ trace_style (dict or str): A dictionary or string naming a trace_style (e.g., 'scatter', 'spline', 'scatter_spline') defining visual properties for traces.
2612
+
1415
2613
  """
1416
2614
  for data_series_index in range(len(self.fig_dict['data'])): #works with array indexing.
1417
2615
  self.set_trace_style_one_data_series(data_series_index, trace_style)
@@ -1419,10 +2617,23 @@ class JSONGrapherRecord:
1419
2617
  indices_of_data_series_to_extract_styles_from=None,
1420
2618
  new_trace_style_names_list=None, extract_colors=False):
1421
2619
  """
1422
- Extracts trace style collection
1423
- :param new_trace_styles_collection_name: str, Name of the new collection.
1424
- :param indices_of_data_series_to_extract_styles_from: list, Indices of series to extract styles from.
1425
- :param new_trace_style_names_list: list, Names for the new trace styles.
2620
+ Extract a collection of trace styles from selected data series and compile them into a named trace_styles_collection.
2621
+ - Defaults to extracting from all series in fig_dict['data'] if no indices are provided.
2622
+ - Generates indices based style names if none are supplied or found.
2623
+ - Ensures that indices and style names are aligned before processing.
2624
+ - Uses extract_trace_style_by_index to retrieve each style definition.
2625
+
2626
+ Args:
2627
+ new_trace_styles_collection_name (str): Name to assign to the new trace_styles_collection. If empty, a placeholder is used.
2628
+ indices_of_data_series_to_extract_styles_from (list of int): Indices of the data series to extract styles from. Defaults to all series.
2629
+ new_trace_style_names_list (list of str): Names to assign to each extracted style. Auto-generated if omitted.
2630
+ extract_colors (bool): If True, includes color attributes in the extracted styles. This is typically not recommended, because a trace_style with colors would prevent automatic coloring of series across a graph.
2631
+
2632
+ Returns:
2633
+ tuple:
2634
+ - str: Name of the created trace_styles_collection.
2635
+ - dict: Dictionary mapping trace_style names to their definitions.
2636
+
1426
2637
  """
1427
2638
  if indices_of_data_series_to_extract_styles_from is None: # should not initialize mutable objects in arguments line, so doing here.
1428
2639
  indices_of_data_series_to_extract_styles_from = []
@@ -1456,12 +2667,22 @@ class JSONGrapherRecord:
1456
2667
  indices_of_data_series_to_extract_styles_from=None,
1457
2668
  new_trace_style_names_list=None, filename='', extract_colors=False):
1458
2669
  """
1459
- Exports trace style collection while ensuring proper handling of mutable default arguments.
1460
-
1461
- :param new_trace_styles_collection_name: str, Name of the new collection.
1462
- :param indices_of_data_series_to_extract_styles_from: list, Indices of series to extract styles from.
1463
- :param new_trace_style_names_list: list, Names for the new trace styles.
1464
- :param filename: str, Name of the file to export to.
2670
+ Extract a set of trace styles from selected data series and write them to a trace_styles_collection file.
2671
+ - Uses extract_trace_styles_collection() to collect the styles.
2672
+ - Saves the styles to file using write_trace_styles_collection_to_file().
2673
+
2674
+ Args:
2675
+ new_trace_styles_collection_name (str): Name of the new style collection. If empty, a placeholder name is used.
2676
+ indices_of_data_series_to_extract_styles_from (list of int): Indices of the data series to extract styles from. Defaults to all.
2677
+ new_trace_style_names_list (list of str): Names to assign to each extracted style. Auto-generated if not provided.
2678
+ filename (str): Name of the output file. If empty, the collection name is used as the filename.
2679
+ extract_colors (bool): If True, includes color attributes in the extracted styles. This is typically not recommended, because a trace_style with colors would prevent automatic coloring of series across a graph.
2680
+
2681
+ Returns:
2682
+ tuple:
2683
+ - str: The final name assigned to the trace_styles_collection.
2684
+ - dict: Dictionary mapping trace_style names to their definitions.
2685
+
1465
2686
  """
1466
2687
  if indices_of_data_series_to_extract_styles_from is None: # should not initialize mutable objects in arguments line, so doing here.
1467
2688
  indices_of_data_series_to_extract_styles_from = []
@@ -1475,9 +2696,34 @@ class JSONGrapherRecord:
1475
2696
  write_trace_styles_collection_to_file(trace_styles_collection=new_trace_styles_collection_dictionary_without_name, trace_styles_collection_name=new_trace_styles_collection_name, filename=filename)
1476
2697
  return new_trace_styles_collection_name, new_trace_styles_collection_dictionary_without_name
1477
2698
  def extract_trace_style_by_index(self, data_series_index, new_trace_style_name='', extract_colors=False):
2699
+ """
2700
+ Extract the trace_style from a specific data series in fig_dict by index.
2701
+
2702
+ Args:
2703
+ data_series_index (int): Index of the data series to extract the trace_style from.
2704
+ new_trace_style_name (str): Optional name to assign to the extracted style. A default is generated if omitted.
2705
+ extract_colors (bool): If True, includes color attributes in the extracted styles. This is typically not recommended, because a trace_style with colors would prevent automatic coloring of series across a graph.
2706
+
2707
+ Returns:
2708
+ dict: Dictionary containing the extracted trace_style, keyed by new_trace_style_name if provided.
2709
+
2710
+ """
1478
2711
  extracted_trace_style = extract_trace_style_by_index(self.fig_dict, data_series_index, new_trace_style_name=new_trace_style_name, extract_colors=extract_colors)
1479
2712
  return extracted_trace_style
1480
2713
  def export_trace_style_by_index(self, data_series_index, new_trace_style_name='', filename='', extract_colors=False):
2714
+ """
2715
+ Extracts the trace style from a specific data series and exports it to a file for reuse.
2716
+
2717
+ Parameters:
2718
+ data_series_index (int): Index of the data series to extract the trace style from.
2719
+ new_trace_style_name (str): Optional name to assign to the extracted style. Auto-generated if omitted.
2720
+ filename (str): File name to export the trace style to. Defaults to the trace style name.
2721
+ extract_colors (bool): If True, includes color attributes in the extracted styles. This is typically not recommended, because a trace_style with colors would prevent automatic coloring of series across a graph.
2722
+
2723
+ Returns:
2724
+ dict: A dictionary containing the exported trace style, keyed by its name.
2725
+
2726
+ """
1481
2727
  extracted_trace_style = extract_trace_style_by_index(self.fig_dict, data_series_index, new_trace_style_name=new_trace_style_name, extract_colors=extract_colors)
1482
2728
  new_trace_style_name = list(extracted_trace_style.keys())[0] #the extracted_trace_style will have a single key which is the style name.
1483
2729
  if filename == '':
@@ -1488,8 +2734,19 @@ class JSONGrapherRecord:
1488
2734
 
1489
2735
  #Make some pointers to external functions, for convenience, so people can use syntax like record.function_name() if desired.
1490
2736
  def validate_JSONGrapher_record(self):
2737
+ """
2738
+ Wrapper method that validates fig_dict using the external validate_JSONGrapher_record function.
2739
+
2740
+ """
1491
2741
  validate_JSONGrapher_record(self)
1492
2742
  def update_and_validate_JSONGrapher_record(self, clean_for_plotly=True):
2743
+ """
2744
+ Trigger validation and optional cleaning of fig_dict using the external update_and_validate_JSONGrapher_record function.
2745
+
2746
+ Args:
2747
+ clean_for_plotly (bool): If True (default), performs cleaning tailored for Plotly compatibility.
2748
+
2749
+ """
1493
2750
  update_and_validate_JSONGrapher_record(self, clean_for_plotly=clean_for_plotly)
1494
2751
 
1495
2752
 
@@ -1500,12 +2757,15 @@ def validate_JSONGrapher_axis_label(label_string, axis_name="", remove_plural_un
1500
2757
  Validates the axis label provided to JSONGrapher.
1501
2758
 
1502
2759
  Args:
1503
- label_string (str): The axis label containing a numeric value and units.
2760
+ label_string (str): The axis label containing label text and units.
1504
2761
  axis_name (str): The name of the axis being validated (e.g., 'x' or 'y').
1505
- remove_plural_units (boolean) : Instructions wil to remove plural units or not. Will remove them in the returned stringif set to True, or will simply provide a warning if set to False.
2762
+ remove_plural_units (bool): Whether to remove plural units. If True, modifies the label by converting units to singular. If False, issues a warning but leaves the units unchanged.
1506
2763
 
1507
2764
  Returns:
1508
- None: Prints warnings if any validation issues are found.
2765
+ tuple: (bool, list, str)
2766
+ - A boolean indicating whether the label passed validation.
2767
+ - A list of warning messages, if any.
2768
+ - The (potentially modified) label string.
1509
2769
  """
1510
2770
  warnings_list = []
1511
2771
  #First check if the label is empty.
@@ -1541,15 +2801,17 @@ def validate_JSONGrapher_axis_label(label_string, axis_name="", remove_plural_un
1541
2801
 
1542
2802
  def units_plural_removal(units_to_check):
1543
2803
  """
1544
- Parses a units string to remove "s" if the string is found as an exact match without an s in the units lists.
2804
+ Parses a units string and removes the trailing "s" if the singular form is recognized.
2805
+
1545
2806
  Args:
1546
- units_to_check (str): A string containing units to check.
2807
+ units_to_check (str): A string containing the units to validate and potentially singularize.
1547
2808
 
1548
2809
  Returns:
1549
- tuple: A tuple of two values
1550
- - "changed" (Boolean): True, or False, where True means the string was changed to remove an "s" at the end.
1551
- - "singularized" (string): The units parsed to be singular, if needed.
2810
+ tuple:
2811
+ - changed (bool): True if the input string was modified to remove a trailing "s"; otherwise, False.
2812
+ - singularized (str): The singular form of the input units, if modified; otherwise, the original string.
1552
2813
  """
2814
+
1553
2815
  # Check if we have the module we need. If not, return with no change.
1554
2816
  try:
1555
2817
  import JSONGrapher.units_list as units_list
@@ -1584,6 +2846,20 @@ def units_plural_removal(units_to_check):
1584
2846
 
1585
2847
 
1586
2848
  def separate_label_text_from_units(label_with_units):
2849
+ """
2850
+ Separates the main label text and the units text from a string. String must contain the main label text followed by units in parentheses, such as "Distance (km).
2851
+
2852
+ Args:
2853
+ label_with_units (str): A label string expected to include units in parentheses, e.g., "Distance (km)".
2854
+
2855
+ Returns:
2856
+ dict: A dictionary with two keys:
2857
+ - "text" (str): The portion of the label before the first opening parenthesis.
2858
+ - "units" (str): The content within the outermost pair of parentheses, or from the first '(' onward if parentheses are unbalanced internally.
2859
+
2860
+ Raises:
2861
+ ValueError: If the number of opening and closing parentheses in the string do not match.
2862
+ """
1587
2863
  # Check for mismatched parentheses
1588
2864
  open_parentheses = label_with_units.count('(')
1589
2865
  close_parentheses = label_with_units.count(')')
@@ -1624,16 +2900,18 @@ def separate_label_text_from_units(label_with_units):
1624
2900
 
1625
2901
  def validate_plotly_data_list(data):
1626
2902
  """
1627
- Validates the entries in a Plotly data array.
1628
- If a dictionary is received, the function will assume you are sending in a single dataseries for validation
1629
- and will put it in a list of one before the validation.
2903
+ Validates the data series dictionaries in a list provided, also accepts a single data series dictionary instead of a list.
2904
+
2905
+ If a single dictionary is passed, it is treated as a one-item list. The function checks each trace for the required
2906
+ structure and fields based on its inferred type (such as 'scatter', 'bar', 'pie', 'heatmap'). Warnings are printed for any issues found.
1630
2907
 
1631
2908
  Args:
1632
- data (list): A list of dictionaries, each representing a Plotly trace.
2909
+ data (list or dict): A list of data series dictionaries (each for one Plotly traces), or a single data series dictionary.
1633
2910
 
1634
2911
  Returns:
1635
- bool: True if all entries are valid, False otherwise.
1636
- list: A list of errors describing why the validation failed.
2912
+ tuple:
2913
+ - bool: True if all traces are valid; False otherwise.
2914
+ - list: A list of warning messages describing why validation failed (if any).
1637
2915
  """
1638
2916
  #check if a dictionary was received. If so, will assume that
1639
2917
  #a single series has been sent, and will put it in a list by itself.
@@ -1688,9 +2966,9 @@ def validate_plotly_data_list(data):
1688
2966
 
1689
2967
  def parse_units(value):
1690
2968
  """
1691
- Parses a numerical value and its associated units from a string. This meant for scientific constants and parameters
1692
- Such as rate constants, gravitational constant, or simiilar.
1693
- This function is not meant for separating the axis label from its units. For that, use separate_label_text_from_units
2969
+ Parses strings containing numerical values aund units in parentheses, and extracts both the numerical value string and the units string.
2970
+ This is intended for scientific measurements, constants, or parameters, including the gravitational constant, rate constants, etc.
2971
+ It is not meant for separating axis labels from units; for that use `separate_label_text_from_units()` instead.
1694
2972
 
1695
2973
  Args:
1696
2974
  value (str): A string containing a numeric value and optional units enclosed in parentheses.
@@ -1699,8 +2977,9 @@ def parse_units(value):
1699
2977
  Returns:
1700
2978
  dict: A dictionary with two keys:
1701
2979
  - "value" (float): The numeric value parsed from the input string.
1702
- - "units" (str): The units parsed from the input string, or an empty string if no units are present.
2980
+ - "units" (str): The extracted units, or an empty string if no units are present.
1703
2981
  """
2982
+
1704
2983
  # Find the position of the first '(' and the last ')'
1705
2984
  start = value.find('(')
1706
2985
  end = value.rfind(')')
@@ -1723,6 +3002,19 @@ def parse_units(value):
1723
3002
  #This function does updating of internal things before validating
1724
3003
  #This is used before printing and returning the JSON record.
1725
3004
  def update_and_validate_JSONGrapher_record(record, clean_for_plotly=True):
3005
+ """
3006
+ Updates internal properties of a JSONGrapher record and validates it.
3007
+
3008
+ This function is typically called before exporting or printing the record to ensure it's clean
3009
+ and meets validation requirements. Optionally cleans the figure dictionary to make it Plotly-compatible.
3010
+
3011
+ Args:
3012
+ record: A JSONGrapher Record object to update and validate.
3013
+ clean_for_plotly (bool): If True, cleans the `fig_dict` to match Plotly export requirements.
3014
+
3015
+ Returns:
3016
+ The updated and validated record object.
3017
+ """
1726
3018
  record.validate_JSONGrapher_record()
1727
3019
  if clean_for_plotly == True:
1728
3020
  record.fig_dict = clean_json_fig_dict(record.fig_dict)
@@ -1737,8 +3029,9 @@ def validate_JSONGrapher_record(record):
1737
3029
  record (dict): The JSONGrapher record to validate.
1738
3030
 
1739
3031
  Returns:
1740
- bool: True if the record is valid, False otherwise.
1741
- list: A list of errors describing any validation issues.
3032
+ tuple:
3033
+ - bool: True if the record is valid; False otherwise.
3034
+ - list: A list of warning messages describing any validation issues.
1742
3035
  """
1743
3036
  warnings_list = []
1744
3037
 
@@ -1820,19 +3113,23 @@ def validate_JSONGrapher_record(record):
1820
3113
 
1821
3114
  def rolling_polynomial_fit(x_values, y_values, window_size=3, degree=2, num_interpolated_points=0, adjust_edges=True):
1822
3115
  """
1823
- Applies a rolling polynomial regression with a specified window size and degree,
1824
- interpolates additional points, and optionally adjusts edge points for smoother transitions.
3116
+ Applies a rolling polynomial regression to the data using a sliding window.
3117
+
3118
+ Fits a polynomial of the specified degree to segments of the input data and optionally interpolates additional
3119
+ points between each segment. Edge behavior can be adjusted for smoother curve boundaries.
1825
3120
 
1826
3121
  Args:
1827
- x_values (list): List of x coordinates.
1828
- y_values (list): List of y coordinates.
1829
- window_size (int): Number of points per rolling fit (default: 3).
1830
- degree (int): Degree of polynomial to fit (default: 2).
1831
- num_interpolated_points (int): Number of interpolated points per segment (default: 3). Set to 0 to only return original points.
1832
- adjust_edges (bool): Whether to adjust edge cases based on window size (default: True).
3122
+ x_values (list): List of x-coordinate values.
3123
+ y_values (list): List of y-coordinate values.
3124
+ window_size (int): Number of data points per rolling fit window (default: 3).
3125
+ degree (int): Degree of the polynomial to fit within each window (default: 2).
3126
+ num_interpolated_points (int): Number of interpolated points between each pair of x-values (default: 0).
3127
+ adjust_edges (bool): If True, expands window size near edges for smoother transitions (default: True).
1833
3128
 
1834
3129
  Returns:
1835
- tuple: (smoothed_x, smoothed_y) lists for plotting.
3130
+ tuple:
3131
+ - smoothed_x (list): List of x-values including interpolated points.
3132
+ - smoothed_y (list): List of corresponding y-values from the polynomial fits.
1836
3133
  """
1837
3134
  import numpy as np
1838
3135
 
@@ -1891,11 +3188,23 @@ def rolling_polynomial_fit(x_values, y_values, window_size=3, degree=2, num_inte
1891
3188
 
1892
3189
  def parse_plot_style(plot_style):
1893
3190
  """
1894
- Parse the given plot style and return a structured dictionary for layout and data series styles.
1895
- If plot_style is missing a layout_style or trace_styles_collection then will set them as an empty string.
1896
-
1897
- :param plot_style: None, str, list of two items, or a dictionary with at least one valid field.
1898
- :return: dict with "layout_style" and "trace_styles_collection", ensuring defaults if missing.
3191
+ Parses the given plot style and returns a structured dictionary for layout and trace styles.
3192
+
3193
+ Accepts a variety of input formats and ensures a dictionary with "layout_style" and
3194
+ "trace_styles_collection" keys is returned. Defaults are applied if fields are missing.
3195
+ Also issues warnings for common key misspellings in dictionary input.
3196
+
3197
+ Args:
3198
+ plot_style (None, str, list, or dict): The style input. Can be:
3199
+ - A dictionary with one or both expected keys
3200
+ - A list of two strings: [layout_style, trace_styles_collection]
3201
+ - A single string to use for both layout and trace styles
3202
+ - None
3203
+
3204
+ Returns:
3205
+ dict: A dictionary with keys:
3206
+ - "layout_style" (str or None)
3207
+ - "trace_styles_collection" (str or None)
1899
3208
  """
1900
3209
  if plot_style is None:
1901
3210
  parsed_plot_style = {"layout_style": None, "trace_styles_collection": None}
@@ -1930,6 +3239,25 @@ def parse_plot_style(plot_style):
1930
3239
  #IMPORTANT: This is the only function that will set a layout_style or trace_styles_collection that is an empty string into 'default'.
1931
3240
  # all other style applying functions (including parse_plot_style) will pass on the empty string or will do nothing if receiving an empty string.
1932
3241
  def apply_plot_style_to_plotly_dict(fig_dict, plot_style=None):
3242
+ """
3243
+ Applies both layout and trace styles to a Plotly figure dictionary based on the provided plot_style.
3244
+
3245
+ Input plot_style can be a dictionary, list, or string. It is internally parsed and converted to a dictionary if needed via `parse_plot_style()`.
3246
+ This is the only style-applying function that substitutes empty strings with "default" styles in the dictionary itself.
3247
+ Having this as the only style-applying function that will convert empty strings to "default" within the dictionary is important for developers to maintain
3248
+ for the algorithmic flow for how styles are applied.
3249
+
3250
+ Args:
3251
+ fig_dict (dict): The Plotly figure dictionary to which styles will be applied.
3252
+ plot_style (str, list, or dict, optional): The style(s) to apply. Acceptable formats:
3253
+ - A single string (applied to both layout and trace styles).
3254
+ - A list of two strings: [layout_style, trace_styles_collection].
3255
+ - A dictionary with "layout_style" and/or "trace_styles_collection" keys.
3256
+ Defaults to {"layout_style": {}, "trace_styles_collection": {}}.
3257
+
3258
+ Returns:
3259
+ dict: The modified Plotly figure dictionary with styles applied.
3260
+ """
1933
3261
  if plot_style is None: # should not initialize mutable objects in arguments line, so doing here.
1934
3262
  plot_style = {"layout_style": {}, "trace_styles_collection": {}} # Fresh dictionary per function call
1935
3263
  #We first parse style_to_apply to get a properly formatted plot_style dictionary of form: {"layout_style":"default", "trace_styles_collection":"default"}
@@ -1952,12 +3280,18 @@ def apply_plot_style_to_plotly_dict(fig_dict, plot_style=None):
1952
3280
  fig_dict = apply_trace_styles_collection_to_plotly_dict(fig_dict=fig_dict,trace_styles_collection=plot_style["trace_styles_collection"])
1953
3281
  return fig_dict
1954
3282
 
1955
- def remove_plot_style_from_plotly_dict(fig_dict):
3283
+ def remove_plot_style_from_fig_dict(fig_dict):
1956
3284
  """
1957
- Remove both layout and data series styles from a Plotly figure dictionary.
3285
+ Removes both layout and trace styles from a Plotly figure dictionary.
3286
+
3287
+ This function strips custom layout and trace styling, resetting the figure
3288
+ to a default format suitable for clean export or re-styling.
1958
3289
 
1959
- :param fig_dict: dict, Plotly style fig_dict
1960
- :return: dict, Updated Plotly style fig_dict with default formatting.
3290
+ Args:
3291
+ fig_dict (dict): The Plotly figure dictionary to clean.
3292
+
3293
+ Returns:
3294
+ dict: The updated figure dictionary with styles removed.
1961
3295
  """
1962
3296
  fig_dict = remove_layout_style_from_plotly_dict(fig_dict)
1963
3297
  fig_dict = remove_trace_styles_collection_from_plotly_dict(fig_dict)
@@ -1966,13 +3300,17 @@ def remove_plot_style_from_plotly_dict(fig_dict):
1966
3300
 
1967
3301
  def convert_JSONGrapher_dict_to_matplotlib_fig(fig_dict):
1968
3302
  """
1969
- Converts a Plotly figure dictionary into a Matplotlib figure without using pio.from_json.
3303
+ Converts a Plotly figure dictionary into a Matplotlib figure without relying on Plotly's `pio.from_json`.
3304
+
3305
+ Currently supports basic conversion of bar and scatter-style traces. For `scatter_spline` and `spline`,
3306
+ a rolling polynomial fit is used as an approximation. Layout metadata such as title and axis labels
3307
+ are also extracted and applied to the Matplotlib figure.
1970
3308
 
1971
3309
  Args:
1972
3310
  fig_dict (dict): A dictionary representing a Plotly figure.
1973
3311
 
1974
3312
  Returns:
1975
- matplotlib.figure.Figure: The corresponding Matplotlib figure.
3313
+ matplotlib.figure.Figure: The corresponding Matplotlib figure object.
1976
3314
  """
1977
3315
  import matplotlib.pyplot as plt
1978
3316
  fig, ax = plt.subplots()
@@ -2043,22 +3381,18 @@ def convert_JSONGrapher_dict_to_matplotlib_fig(fig_dict):
2043
3381
  ax.legend()
2044
3382
  return fig
2045
3383
 
2046
-
2047
- #The below function works, but because it depends on the python plotly package, we avoid using it
2048
- #To decrease the number of dependencies.
2049
3384
  def convert_plotly_dict_to_matplotlib(fig_dict):
2050
3385
  """
2051
3386
  Converts a Plotly figure dictionary into a Matplotlib figure.
2052
3387
 
2053
- Supports: Bar Charts, Scatter Plots, Spline curves using rolling polynomial regression.
2054
-
2055
- This functiony has a dependency on the plotly python package (pip install plotly)
3388
+ Supports basic translation of Bar Charts, Scatter Plots, and Spline curves
3389
+ (the latter simulated using a rolling polynomial fit).
2056
3390
 
2057
3391
  Args:
2058
3392
  fig_dict (dict): A dictionary representing a Plotly figure.
2059
3393
 
2060
3394
  Returns:
2061
- matplotlib.figure.Figure: The corresponding Matplotlib figure.
3395
+ matplotlib.figure.Figure: The created Matplotlib figure object.
2062
3396
  """
2063
3397
  import plotly.io as pio
2064
3398
  import matplotlib.pyplot as plt
@@ -2094,16 +3428,18 @@ def convert_plotly_dict_to_matplotlib(fig_dict):
2094
3428
 
2095
3429
  def apply_trace_styles_collection_to_plotly_dict(fig_dict, trace_styles_collection="", trace_style_to_apply=""):
2096
3430
  """
2097
- Iterates over all traces in the `data` list of a Plotly figure dictionary
2098
- and applies styles to each one.
3431
+ Applies a trace style preset to each trace in a Plotly figure dictionary.
3432
+
3433
+ Iterates over all traces in `fig_dict["data"]` and updates their appearance using the
3434
+ provided `trace_styles_collection`. Also sets/updates the applied trace_styles_collection name in `fig_dict["plot_style"]`.
2099
3435
 
2100
3436
  Args:
2101
- fig_dict (dict): A dictionary containing a `data` field with Plotly traces.
2102
- trace_style_to_apply (str): Optional style preset to apply. Default is "default".
3437
+ fig_dict (dict): A dictionary containing a Plotly-style `data` list of traces.
3438
+ trace_styles_collection (str or dict): A named style collection or a full style definition dictionary.
3439
+ trace_style_to_apply (str): Optional specific trace style to apply to each series (default is "").
2103
3440
 
2104
3441
  Returns:
2105
- dict: Updated Plotly figure dictionary with defaults applied to each trace.
2106
-
3442
+ dict: The updated Plotly figure dictionary with applied trace styles.
2107
3443
  """
2108
3444
  if type(trace_styles_collection) == type("string"):
2109
3445
  trace_styles_collection_name = trace_styles_collection
@@ -2124,14 +3460,28 @@ def apply_trace_styles_collection_to_plotly_dict(fig_dict, trace_styles_collecti
2124
3460
  # compared to how plotly treats 'type' for a data series. So later in the process, when actually plotting with plotly, the 'type' field will get overwritten.
2125
3461
  def apply_trace_style_to_single_data_series(data_series, trace_styles_collection="", trace_style_to_apply=""):
2126
3462
  """
2127
- Applies predefined styles to a single Plotly data series while preserving relevant fields.
3463
+ Applies a predefined or custom trace style to a single data series while preserving other fields.
3464
+
3465
+ This trace_style_to_apply can be passed in as a dictionary or as a string that is a trace_style name to find in a trace_styles_collection
3466
+ The function applies type-specific formatting (e.g., spline, scatterd3d, bubble2d), and conditionally injects colorscale mappings
3467
+ when specified via a trace-style suffix after a double underscore "__" delimeter (e.g., "scatter__viridis").
3468
+ This function also calls helper functions to populate the sizes for bubble2d and bubble3d plots.
2128
3469
 
2129
3470
  Args:
2130
- data_series (dict): A dictionary representing a single Plotly data series.
2131
- trace_style_to_apply (str or dict): Name of the style preset or a custom style dictionary. Default is "default".
3471
+ data_series (dict): A dictionary representing a single data series / trace.
3472
+ trace_styles_collection (str or dict): Name of the trace_styles_collection to use or a full trace_style_styles_collection dictionary. If
3473
+ empty, the 'default' trace_styles_collection will be used.
3474
+ trace_style_to_apply (str or dict): A specific trace_style name to pull from the trace_styles_collection or full style definition to apply. If
3475
+ empty, the function will check `data_series["trace_style"]` before using the 'default' trace_style.
2132
3476
 
2133
3477
  Returns:
2134
- dict: Updated data series with style applied.
3478
+ dict: The updated data series dictionary with applied style formatting.
3479
+
3480
+ Notes:
3481
+ - Trace styles support 2D and 3D formats including "scatter", "scatter3d", "mesh3d", "heatmap", and "bubble".
3482
+ - A style suffix like "__viridis" triggers automatic colorscale assignment for markers, lines, or intensity maps.
3483
+ - If no valid style is found, the function falls back to the first available style in the collection.
3484
+ - None values in color-mapped data are converted to 0 and produce a warning.
2135
3485
  """
2136
3486
  if not isinstance(data_series, dict):
2137
3487
  return data_series # Return unchanged if the data series is invalid.
@@ -2318,6 +3668,24 @@ def apply_trace_style_to_single_data_series(data_series, trace_styles_collection
2318
3668
  return data_series
2319
3669
 
2320
3670
  def prepare_bubble_sizes(data_series):
3671
+ """
3672
+ Prepares a bubble sizes list for a bubble plot based on the fields provided in the data series,
3673
+ then inserts the bubble sizes list into the right field for a bubble plot.
3674
+
3675
+ This function extracts bubble size values from a data_series field based on 'bubble_sizes' or the `z_points` or `z` field,
3676
+ then scales the bubble sizes to a maximum value, `max_bubble_size`, with a default used if not provided by the data_series.
3677
+ The function a also sets the `text` field of each marker point (each bubble) for hover display.
3678
+
3679
+ Args:
3680
+ data_series (dict): A dictionary representing a single data series, with optional
3681
+ fields such as 'bubble_sizes', and 'max_bubble_size'.
3682
+
3683
+ Returns:
3684
+ dict: The updated data series with bubble sizes (marker sizes) and hover text inserted.
3685
+
3686
+ Raises:
3687
+ KeyError: If no valid source of size data is found and/or bubble size scaling cannot proceed.
3688
+ """
2321
3689
  #To make a bubble plot with plotly, we are actually using a 2D plot
2322
3690
  #and are using the z values in a data_series to create the sizes of each point.
2323
3691
  #We also will scale them to some maximum bubble size that is specifed.
@@ -2374,10 +3742,17 @@ def prepare_bubble_sizes(data_series):
2374
3742
  # So the main class function will also be broken into two and/or need to take an optional argument in
2375
3743
  def remove_trace_styles_collection_from_plotly_dict(fig_dict):
2376
3744
  """
2377
- Remove applied data series styles from a Plotly figure dictionary.
2378
-
2379
- :param fig_dict: dict, Plotly style fig_dict
2380
- :return: dict, Updated Plotly style fig_dict with default formatting.
3745
+ Removes trace styles from all data series in a Plotly figure dictionary.
3746
+
3747
+ This function iterates through each trace in `fig_dict["data"]` and strips out applied
3748
+ style formatting—unless the trace's `trace_style` is explicitly set to "none". It also removes
3749
+ the `trace_styles_collection` reference from the figure's `plot_style` metadata.
3750
+
3751
+ Args:
3752
+ fig_dict (dict): A Plotly-formatted figure dictionary.
3753
+
3754
+ Returns:
3755
+ dict: The updated figure dictionary with trace styles removed.
2381
3756
  """
2382
3757
  #will remove formatting from the individual data_series, but will not remove formatting from any that have trace_style of "none".
2383
3758
  if isinstance(fig_dict, dict) and "data" in fig_dict and isinstance(fig_dict["data"], list):
@@ -2401,13 +3776,17 @@ def remove_trace_styles_collection_from_plotly_dict(fig_dict):
2401
3776
 
2402
3777
  def remove_trace_style_from_single_data_series(data_series):
2403
3778
  """
2404
- Remove only formatting fields from a single Plotly data series while preserving all other fields.
3779
+ Removes style-related formatting fields from a single Plotly data series.
3780
+
3781
+ This function strips only visual styling attributes (e.g., marker, line, fill) while preserving all
3782
+ other metadata and custom fields such as equations or simulation details. The result is returned as a
3783
+ `JSONGrapherDataSeries` object with key values preserved.
2405
3784
 
2406
- Note: Since fig_dict data objects may contain custom fields (e.g., "equation", "metadata"),
2407
- this function explicitly removes predefined **formatting** attributes while leaving all other data intact.
3785
+ Args:
3786
+ data_series (dict): A dictionary representing a single Plotly-style trace.
2408
3787
 
2409
- :param data_series: dict, A dictionary representing a single Plotly data series.
2410
- :return: dict, Updated data series with formatting fields removed but key data retained.
3788
+ Returns:
3789
+ JSONGrapherDataSeries: The cleaned data series with formatting fields removed.
2411
3790
  """
2412
3791
 
2413
3792
  if not isinstance(data_series, dict):
@@ -2427,16 +3806,36 @@ def remove_trace_style_from_single_data_series(data_series):
2427
3806
  return new_data_series_object
2428
3807
 
2429
3808
  def extract_trace_style_by_index(fig_dict, data_series_index, new_trace_style_name='', extract_colors=False):
3809
+ """
3810
+ Pulls a data_series dictionary from a fig_dict by the index,
3811
+ then creates a and returns a trace_style dictionary by extracting formatting attributes from that single data_series dictionary.
3812
+
3813
+ This is a wrapper for `extract_trace_style_from_data_series_dict()` and allows optional renaming
3814
+ of the extracted style and optional extraction of color attributes. Extraction of color for the
3815
+ trace_style is not recommended for normal usage, since a color in a trace_style
3816
+ that overrides auto-coloring schemes when multiple series are present.
3817
+
3818
+ Args:
3819
+ fig_dict (dict): A fig_dict with a `data` field containing a list of data_series dictionaries.
3820
+ data_series_index (int): Index of the target data series within the `data` list.
3821
+ new_trace_style_name (str): Optional new name to assign to the extracted trace_style.
3822
+ extract_colors (bool): Whether to include color-related attributes in the extracted trace_style.
3823
+
3824
+ Returns:
3825
+ dict: A dictionary containing the extracted trace style.
3826
+ """
2430
3827
  data_series_dict = fig_dict["data"][data_series_index]
2431
3828
  extracted_trace_style = extract_trace_style_from_data_series_dict(data_series_dict=data_series_dict, new_trace_style_name=new_trace_style_name, extract_colors=extract_colors)
2432
3829
  return extracted_trace_style
2433
3830
 
2434
3831
  def extract_trace_style_from_data_series_dict(data_series_dict, new_trace_style_name='', additional_attributes_to_extract=None, extract_colors=False):
2435
3832
  """
2436
- Extract formatting attributes from a given Plotly data series.
3833
+ Creates a and returns a trace_style dictionary by extracting formatting attributes from a single data_series dictionary.
2437
3834
 
2438
- The function scans the provided `data_series` dictionary and returns a new dictionary
2439
- containing only the predefined formatting fields.
3835
+ This function returns a trace_style dictionary containing only style-format related fields such as line, marker,
3836
+ and text formatting. Color values (e.g., fill, marker color) can optionally be included in the extracted trace_style.
3837
+ Extraction of color for the trace_style is not recommended for normal usage,
3838
+ since a color in a trace_style that overrides auto-coloring schemes when multiple series are present.
2440
3839
 
2441
3840
  Examples of formatting attributes extracted:
2442
3841
  - "type"
@@ -2451,9 +3850,17 @@ def extract_trace_style_from_data_series_dict(data_series_dict, new_trace_style_
2451
3850
  - "textposition"
2452
3851
  - "textfont"
2453
3852
 
2454
- :param data_series_dict: dict, A dictionary representing a single Plotly data series.
2455
- :param trace_style: string, the key name for what user wants to call the trace_style in the style, after extraction.
2456
- :return: dict, A dictionary containing only the formatting attributes.
3853
+ Args:
3854
+ data_series_dict (dict): A data_series dictionary for a single trace.
3855
+ new_trace_style_name (str): Optional name to assign the extracted style. If empty, the value in
3856
+ the `trace_style` field of the existing data_series dict will be used (if it is a string),
3857
+ and if no string is present there then "custom" will be used.
3858
+ additional_attributes_to_extract (list, optional): Additional formatting attributes to extract.
3859
+ extract_colors (bool): If set to True, will also extract color-related values like 'marker.color' and 'fillcolor'. Not recommended for typical trace_style usage.
3860
+
3861
+ Returns:
3862
+ dict: A trace style dictionary with the format {style_name: formatting_attributes}.
3863
+
2457
3864
  """
2458
3865
  if additional_attributes_to_extract is None: #in python, it's not good to make an empty list a default argument.
2459
3866
  additional_attributes_to_extract = []
@@ -2504,6 +3911,20 @@ def extract_trace_style_from_data_series_dict(data_series_dict, new_trace_style_
2504
3911
 
2505
3912
  #export a single trace_style dictionary to .json.
2506
3913
  def write_trace_style_to_file(trace_style_dict, trace_style_name, filename):
3914
+ """
3915
+ Exports a single trace style dictionary to a JSON file.
3916
+
3917
+ Wraps the trace style under a standard JSON structure with a named identifier and writes it to disk.
3918
+ Ensures the filename ends with ".json" for compatibility.
3919
+
3920
+ Args:
3921
+ trace_style_dict (dict): A dictionary defining a single trace style.
3922
+ trace_style_name (str): The name to assign to the trace style within the exported file.
3923
+ filename (str): The target filename for the output JSON (with or without ".json").
3924
+
3925
+ Returns:
3926
+ None
3927
+ """
2507
3928
  # Ensure the filename ends with .json
2508
3929
  if not filename.lower().endswith(".json"):
2509
3930
  filename += ".json"
@@ -2523,6 +3944,22 @@ def write_trace_style_to_file(trace_style_dict, trace_style_name, filename):
2523
3944
 
2524
3945
  #export an entire trace_styles_collection to .json. The trace_styles_collection is dict.
2525
3946
  def write_trace_styles_collection_to_file(trace_styles_collection, trace_styles_collection_name, filename):
3947
+ """
3948
+ Exports a trace_styles_collection dictionary to a JSON file.
3949
+
3950
+ Accepts a trace_styles_collection dictionary and writes it to disk.
3951
+ The trace_styeles_collection could be provided in a containing dictionary.
3952
+ So the function, first checks if the dictionary recieved has a key named "traces_tyles_collection".
3953
+ If that key is present, the function, pulls the traces_style_collection out of that field and uses it.
3954
+
3955
+ Args:
3956
+ trace_styles_collection (dict): trace_styles_collection dictionary to export. Or a container with a trace_styles_collection inside.
3957
+ trace_styles_collection_name (str): The name for the trace_styles_collection to export, so it can later be used in the JSONGrapher styles_library
3958
+ filename (str): filename to write to. The function Automatically appends ".json" if a filename without file extension is provided.
3959
+
3960
+ Returns:
3961
+ None
3962
+ """
2526
3963
  if "trace_styles_collection" in trace_styles_collection: #We may receive a traces_style collection in a container. If so, we pull the traces_style_collection out.
2527
3964
  trace_styles_collection = trace_styles_collection[trace_styles_collection["name"]]
2528
3965
  # Ensure the filename ends with .json
@@ -2541,30 +3978,68 @@ def write_trace_styles_collection_to_file(trace_styles_collection, trace_styles_
2541
3978
 
2542
3979
 
2543
3980
 
2544
- #export an entire trace_styles_collection from .json. THe trace_styles_collection is dict.
3981
+ #import an entire trace_styles_collection from .json. THe trace_styles_collection is dict.
2545
3982
  def import_trace_styles_collection(filename):
2546
- # Ensure the filename ends with .json
3983
+ """
3984
+ Imports a trace_styles_collection dictionary from a JSON file.
3985
+
3986
+ Reads a JSON-formatted file and extracts the trace_styles_collection
3987
+ identified by its embedded name. The function validates structure and
3988
+ ensures the expected format before returning the trace_styles_dictionary.
3989
+ If there is no name in the dictionary, the dictionary is assumed to
3990
+ be a trace_styles_collection dictionary, and the filename is used as the name.
3991
+
3992
+ Args:
3993
+ filename (str): The name of the JSON file to import from. If the extension is
3994
+ missing, ".json" will be appended automatically.
3995
+
3996
+ Returns:
3997
+ dict: A dictionary containing the imported trace_styles_collection, or a trace_styles_collection dict directly.
3998
+
3999
+ Raises:
4000
+ ValueError: If the JSON structure is malformed or the collection name is not found.
4001
+ """
4002
+ import os
4003
+ # Ensure the filename ends with .json. Add that extension if it's not present.
2547
4004
  if not filename.lower().endswith(".json"):
2548
4005
  filename += ".json"
2549
-
2550
4006
  with open(filename, "r", encoding="utf-8") as file: # Specify UTF-8 encoding for compatibility
2551
4007
  data = json.load(file)
2552
4008
 
2553
4009
  # Validate JSON structure
2554
- containing_dict = data.get("trace_styles_collection")
2555
- if not isinstance(containing_dict, dict):
4010
+ dict_from_file = data.get("trace_styles_collection")
4011
+ if not isinstance(dict_from_file, dict):
2556
4012
  raise ValueError("Error: Missing or malformed 'trace_styles_collection'.")
2557
4013
 
2558
- collection_name = containing_dict.get("name")
2559
- if not isinstance(collection_name, str) or collection_name not in containing_dict:
2560
- raise ValueError(f"Error: Expected dictionary '{collection_name}' is missing or malformed.")
2561
- trace_styles_collection = containing_dict[collection_name]
4014
+ collection_name = dict_from_file.get("name") #check if the dictionary has a name field.
4015
+ if not isinstance(collection_name, str) or collection_name not in dict_from_file:
4016
+ # Use filename without extension if there is no name field in the dictionary.
4017
+ collection_name = os.path.splitext(os.path.basename(filename))[0]
4018
+ trace_styles_collection = dict_from_file #Take the dictionary received directly, assume there is no containing dict.
4019
+ else: #This is actually the normal case, that the trace_styles_collection will be in a containing dictionary.
4020
+ trace_styles_collection = dict_from_file[collection_name]
2562
4021
  # Return only the dictionary corresponding to the collection name
2563
4022
  return trace_styles_collection
2564
4023
 
2565
-
2566
- #export an entire trace_styles_collection from .json. THe trace_styles_collection is dict.
4024
+ #import a single trace_styles dict from a .json file.
2567
4025
  def import_trace_style(filename):
4026
+ """
4027
+ Imports a single trace style from a JSON file.
4028
+
4029
+ Reads a JSON-formatted file that contains a `trace_style` block and returns the
4030
+ associated trace style dictionary identified by its embedded name. Validates structure
4031
+ before returning the style.
4032
+
4033
+ Args:
4034
+ filename (str): The name of the JSON file to import. If the extension is missing,
4035
+ ".json" will be appended automatically.
4036
+
4037
+ Returns:
4038
+ dict: A dictionary representing the imported trace_style.
4039
+
4040
+ Raises:
4041
+ ValueError: If the JSON structure is malformed or the expected trace style is missing.
4042
+ """
2568
4043
  # Ensure the filename ends with .json
2569
4044
  if not filename.lower().endswith(".json"):
2570
4045
  filename += ".json"
@@ -2588,11 +4063,19 @@ def import_trace_style(filename):
2588
4063
 
2589
4064
  def apply_layout_style_to_plotly_dict(fig_dict, layout_style_to_apply="default"):
2590
4065
  """
2591
- Apply a predefined style to a Plotly fig_dict while preserving non-cosmetic fields.
2592
-
2593
- :param fig_dict: dict, Plotly style fig_dict
2594
- :param layout_style_to_apply: str, Name of the style or journal, or a style dictionary to apply.
2595
- :return: dict, Updated Plotly style fig_dict.
4066
+ Applies a predefined layout_style to a fig_dict while preserving non-cosmetic layout fields.
4067
+
4068
+ This function allows applying a custom layout_style (specified by name or as a dictionary) to
4069
+ a given fig_dict. It ensures that key non-cosmetic properties (e.g., axis titles, legend titles,
4070
+ annotations, and update button labels) are retained even after the style is applied.
4071
+
4072
+ Args:
4073
+ fig_dict (dict): A figure dictionary in which the specified or provided layout_style will be applied.
4074
+ layout_style_to_apply (str or dict, optional): The name of a layout_style or a layout_style dictionary
4075
+ to apply. Defaults to "default".
4076
+
4077
+ Returns:
4078
+ dict: The updated fig_dict with the applied layout_style and preserved layout elements.
2596
4079
  """
2597
4080
  if type(layout_style_to_apply) == type("string"):
2598
4081
  layout_style_to_apply_name = layout_style_to_apply
@@ -2679,10 +4162,16 @@ def apply_layout_style_to_plotly_dict(fig_dict, layout_style_to_apply="default")
2679
4162
  # So the main class function will also be broken into two and/or need to take an optional argument in
2680
4163
  def remove_layout_style_from_plotly_dict(fig_dict):
2681
4164
  """
2682
- Remove applied layout styles from a Plotly figure dictionary while preserving essential content.
4165
+ Removes any layout field formatting from a fig_dict while retaining any essential layout field content.
2683
4166
 
2684
- :param fig_dict: dict, Plotly style fig_dict
2685
- :return: dict, Updated Plotly style fig_dict with styles removed but key data intact.
4167
+ This function strips formatting aspects in the layout field from a fig_dict, such as font and background
4168
+ settings, while preserving non-cosmetic fields like axis titles, annotation texts, and interactive labels.
4169
+
4170
+ Args:
4171
+ fig_dict (dict): The fig_dict from which layout_style will be removed.
4172
+
4173
+ Returns:
4174
+ dict: The cleaned fig_dict with layout_style fields removed but important layout content retained.
2686
4175
  """
2687
4176
  if "layout" in fig_dict:
2688
4177
  style_keys = ["font", "paper_bgcolor", "plot_bgcolor", "gridcolor", "gridwidth", "tickfont", "linewidth"]
@@ -2750,12 +4239,19 @@ def remove_layout_style_from_plotly_dict(fig_dict):
2750
4239
  fig_dict["plot_style"].pop("layout_style")
2751
4240
  return fig_dict
2752
4241
 
2753
- def extract_layout_style_from_plotly_dict(fig_dict):
4242
+ def extract_layout_style_from_fig_dict(fig_dict):
2754
4243
  """
2755
- Extract a layout style dictionary from a given Plotly JSON object, including background color, grids, and other appearance attributes.
4244
+ Extracts a layout_style dictionary from a fig_dict by pulling out the cosmetic formatting layout properties.
2756
4245
 
2757
- :param fig_dict: dict, Plotly JSON object.
2758
- :return: dict, Extracted style settings.
4246
+ This function pulls visual configuration details—such as fonts, background colors, margins,
4247
+ grid lines, tick styling, and legend positioning—from a given fig_dict to construct a layout_style
4248
+ dictionary that can be reused or applied elsewhere.
4249
+
4250
+ Args:
4251
+ fig_dict (dict): A fig_dict from which layout formatting fields will be extracted.
4252
+
4253
+ Returns:
4254
+ dict: A layout_style dictionary capturing the extracted layout formatting.
2759
4255
  """
2760
4256
 
2761
4257
 
@@ -2880,22 +4376,28 @@ def extract_layout_style_from_plotly_dict(fig_dict):
2880
4376
 
2881
4377
  def update_implicit_data_series_x_ranges(fig_dict, range_dict):
2882
4378
  """
2883
- Updates the x_range_default values for all simulate and equation data series
2884
- in a given figure dictionary using the provided range dictionary.
4379
+ Updates the x_range_default values for any implicit data_series a fig_dict. Specifically,
4380
+ those defined by an 'equation' field or by a 'simulate' feid.
4381
+
4382
+ This function modifies the x_range_default field in each data_series field based on
4383
+ the values in a supplied range_dict. The x_range_default field will only be changed
4384
+ within the "equation" and "simulate" fields of data_series dictionaries.
4385
+ The rest of the fig_dict is unchanged. A new fig_dict is returned.
4386
+ Deep copying ensures the original fig_dict remains unaltered.
4387
+ This is function is primarily used for updating the x and y axis scales where an equation or simulation
4388
+ will be used in order to match the range that other data series span, to create a properly ranged
4389
+ implicit data series production for the final plot.
2885
4390
 
2886
4391
  Args:
2887
- fig_dict (dict): The original figure dictionary containing various data series.
2888
- range_dict (dict): A dictionary with keys "min_x" and "max_x" providing the
2889
- global minimum and maximum x values for updates.
4392
+ fig_dict (dict): A fig_dict containing one or more data_series entries that may have 'simulate' or 'equation' fields.
4393
+ range_dict (dict): Dictionary with optional keys "min_x" and "max_x" specifying global
4394
+ x-axis bounds to apply.
2890
4395
 
2891
4396
  Returns:
2892
- dict: A new figure dictionary with updated x_range_default values for
2893
- equation and simulate series, while keeping other data unchanged.
2894
-
4397
+ dict: A new fig_dict with updated x_range_default values in applicable data_series, within their 'simulate' or 'equation' fields.
4398
+
2895
4399
  Notes:
2896
- - If min_x or max_x in range_dict is None, the function preserves the
2897
- existing x_range_default values instead of overwriting them.
2898
- - Uses deepcopy to ensure modifications do not affect the original fig_dict.
4400
+ If "min_x" or "max_x" in range_dict is None, the existing value for it in the data_series dictionary is preserved.
2899
4401
  """
2900
4402
  import copy # Import inside function to limit scope
2901
4403
 
@@ -2932,22 +4434,32 @@ def update_implicit_data_series_x_ranges(fig_dict, range_dict):
2932
4434
 
2933
4435
  def get_fig_dict_ranges(fig_dict, skip_equations=False, skip_simulations=False):
2934
4436
  """
2935
- Extracts minimum and maximum x/y values from each data_series in a fig_dict, as well as overall min and max for x and y.
4437
+ Extracts the x and y ranges for a fig_dict, returning both overall min/max x/y values and per-series min/max x/y values.
4438
+
4439
+ This function examines each data_series in the fig_dict and computes individual and aggregate
4440
+ x/y range boundaries. It accounts for simulation or equation series that include x-range
4441
+ metadata, as well as raw data series with explicit "x" and "y" lists. Optional arguments
4442
+ allow filtering out simulations or equations from consideration for the overall min/max x/y values,
4443
+ and will append None values for those per-series range max values.
4444
+ This function is for extracting display limits for a plot, not for evaluation limits.
4445
+ That is why equation and simulate fields may have None as their limits.
2936
4446
 
2937
4447
  Args:
2938
- fig_dict (dict): The figure dictionary containing multiple data series.
2939
- skip_equations (bool): If True, equation-based data series are ignored.
2940
- skip_simulations (bool): If True, simulation-based data series are ignored.
4448
+ fig_dict (dict): The fig_dict containing data_series from which ranges will be extracted.
4449
+ skip_equations (bool, optional): True will give a fig_range that excludes the ranges from equation-based data_series. Defaults to False.
4450
+ skip_simulations (bool, optional): True will give a fig_range that excludes the ranges from simulation-based data_series. Defaults to False.
2941
4451
 
2942
4452
  Returns:
2943
- tuple:
2944
- - fig_dict_ranges (dict): A dictionary containing overall min/max x/y values across all valid series.
2945
- - data_series_ranges (dict): A dictionary with individual min/max values for each data series.
4453
+ tuple:
4454
+ - fig_dict_ranges (dict): A dictionary with overall ranges and keys of "min_x", "max_x", "min_y", and "max_y".
4455
+ - data_series_ranges (dict): A dictionary containing per-series range limits in four lists with dictionary keys
4456
+ of "min_x", "max_x", "min_y", and "max_y". The Indices in the list match the data_series indices,
4457
+ with the indices of skipped data_series being populated with None as their values.
4458
+
2946
4459
 
2947
4460
  Notes:
2948
- - Equations and simulations have predefined x-range defaults and limits.
2949
- - If their x-range is absent, individual data series values are used.
2950
- - Ensures empty lists don't trigger errors when computing min/max values.
4461
+ - If x_range_default or x_range_limits are unavailable, raw x/y values are used instead.
4462
+ - The function avoids errors from empty or missing lists by validating content before computing ranges.
2951
4463
  """
2952
4464
  # Initialize final range values to None to ensure assignment
2953
4465
  fig_dict_ranges = {
@@ -3055,15 +4567,35 @@ def get_fig_dict_ranges(fig_dict, skip_equations=False, skip_simulations=False):
3055
4567
  # print("Data Series Values:", data_series_ranges)
3056
4568
  # print("Extreme Values:", fig_dict_ranges)
3057
4569
 
3058
- ### Start of section of code with functions for extracting and updating x and y ranges of data series ###
4570
+ ### End of section of code with functions for extracting and updating x and y ranges of data series ###
3059
4571
 
3060
4572
 
3061
4573
  ### Start section of code with functions for cleaning fig_dicts for plotly compatibility ###
3062
4574
 
3063
4575
  def update_title_field(fig_dict_or_subdict, depth=1, max_depth=10):
3064
- """ This function is intended to make JSONGrapher .json files compatible with the newer plotly recommended title field formatting
3065
- which is necessary to do things like change the font, and also necessary for being able to convert a JSONGrapher json_dict to python plotly figure objects.
3066
- Recursively checks for 'title' fields and converts them to dictionary format. """
4576
+ """
4577
+ Checks 'title' fields in a fig_dict and its sub-dictionary to see if they are strings,
4578
+ and if they are, then converts them into a dictionary format.
4579
+
4580
+ JSONGrapher already makes title fields as dictionaries.
4581
+ This function is to allow JSONGrapher to be take in fields from old plotly records.
4582
+
4583
+ This is a past-compatibilty function. Plotly previously allowed strings for title fields,
4584
+ but now recommends (or on some cases requires) a dictionary with field of text because
4585
+ the dictionary can then include additional formatting.
4586
+
4587
+ This function prepares a JSONGrapher fig_dict for compatibility with updated layout formatting
4588
+ conventions, where titles must be dictionaries with a 'text' key. The function traverses nested dictionaries
4589
+ and lists, transforming any title field that is a plain string into the proper dictionary structure.
4590
+
4591
+ Args:
4592
+ fig_dict_or_subdict (dict): The fig_dict or any sub-dictionary to be checked and updated recursively.
4593
+ depth (int, optional): Current recursive depth, used internally to limit recursion. Defaults to 1.
4594
+ max_depth (int, optional): Maximum allowed recursion depth to avoid infinite loops. Defaults to 10.
4595
+
4596
+ Returns:
4597
+ dict: The updated dictionary with properly formatted title fields.
4598
+ """
3067
4599
  if depth > max_depth or not isinstance(fig_dict_or_subdict, dict):
3068
4600
  return fig_dict_or_subdict
3069
4601
 
@@ -3080,9 +4612,33 @@ def update_title_field(fig_dict_or_subdict, depth=1, max_depth=10):
3080
4612
 
3081
4613
 
3082
4614
  def update_superscripts_strings(fig_dict_or_subdict, depth=1, max_depth=10):
3083
- """ This function is intended to make JSONGrapher .json files compatible with the newer plotly recommended title field formatting
3084
- which is necessary to do things like change the font, and also necessary for being able to convert a JSONGrapher json_dict to python plotly figure objects.
3085
- Recursively checks for 'title' fields and converts them to dictionary format. """
4615
+ """
4616
+ Replaces superscript-like strings in titles and data series names within a fig_dict.
4617
+
4618
+ This function scans through the fig_dict or sub-dictionary recursively, updating all
4619
+ display string content where superscripts are found—such as in titles and legend names
4620
+ so that superscripts of display strings will appear correctly in Plotly figures.
4621
+
4622
+ Some example inputs and outputs:
4623
+ In : x^(2) + y**(-3) = z^(test)
4624
+ Out: x<sup>2</sup> + y<sup>-3</sup> = z<sup>test</sup>
4625
+ In : E = mc**(2)
4626
+ Out: E = mc<sup>2</sup>
4627
+ In : a^(b) + c**(d)
4628
+ Out: a<sup>b</sup> + c<sup>d</sup>
4629
+ In : r^(theta) and s**(x)
4630
+ Out: r<sup>theta</sup> and s<sup>x</sup>
4631
+ In : v^(1) + u^(-1)
4632
+ Out: v<sup>1</sup> + u^(-1)
4633
+
4634
+ Args:
4635
+ fig_dict_or_subdict (dict): A fig_dict or sub-dictionary to process recursively.
4636
+ depth (int, optional): Current recursion level for nested structures. Defaults to 1.
4637
+ max_depth (int, optional): Maximum allowed recursion depth. Defaults to 10.
4638
+
4639
+ Returns:
4640
+ dict: The updated dictionary with superscript replacements applied where needed.
4641
+ """
3086
4642
  if depth > max_depth or not isinstance(fig_dict_or_subdict, dict):
3087
4643
  return fig_dict_or_subdict
3088
4644
 
@@ -3102,6 +4658,28 @@ def update_superscripts_strings(fig_dict_or_subdict, depth=1, max_depth=10):
3102
4658
 
3103
4659
  #The below function was made with the help of copilot.
3104
4660
  def replace_superscripts(input_string):
4661
+ """
4662
+ Takes a string, finds superscripts denoted symbolically (** or ^), and replaces the
4663
+ symbolic superscript syntax with tagged markup syntax (<sup> </sup>)
4664
+
4665
+ Some example inputs and outputs:
4666
+ In : x^(2) + y**(-3) = z^(test)
4667
+ Out: x<sup>2</sup> + y<sup>-3</sup> = z<sup>test</sup>
4668
+ In : E = mc**(2)
4669
+ Out: E = mc<sup>2</sup>
4670
+ In : a^(b) + c**(d)
4671
+ Out: a<sup>b</sup> + c<sup>d</sup>
4672
+ In : r^(theta) and s**(x)
4673
+ Out: r<sup>theta</sup> and s<sup>x</sup>
4674
+ In : v^(1) + u^(-1)
4675
+ Out: v<sup>1</sup> + u^(-1)
4676
+
4677
+ Args:
4678
+ input_string (str): A string possibly including superscripts denoted by ** or ^
4679
+
4680
+ Returns:
4681
+ str: A string with superscript symbolic notation replaced with tagged markup (<sup> </sup>) notation.
4682
+ """
3105
4683
  #Example usage: print(replace_superscripts("x^(2) + y**(-3) = z^(test)"))
3106
4684
  import re
3107
4685
  # Step 1: Wrap superscript expressions in <sup> tags
@@ -3118,6 +4696,22 @@ def replace_superscripts(input_string):
3118
4696
 
3119
4697
 
3120
4698
  def convert_to_3d_layout(layout):
4699
+ """
4700
+ Converts a standard JSONGrapher layout_dict into the format needed for a plotly 3D layout_dict by nesting axis fields under the 'scene' key.
4701
+
4702
+ This function reorganizes xaxis, yaxis, and zaxis fields from a standard JSONGrapher layout_dict
4703
+ into a Plotly 3D layout_dict by moving axes fields into the 'scene' field, as required for
4704
+ the current plotly schema for 3D plots. A deep copy of the layout_dict is used so the original layout_dict remains untouched.
4705
+ This way of doing things is so JSONGrapher layout_dicts are consistent across 2D and 3D plots
4706
+ whereas Plotly does things differently for 3D plots, so this function converts a
4707
+ standard JSONGrapher layout_dict into what is expected for Plotly 3D plots.
4708
+
4709
+ Args:
4710
+ layout (dict): A standard JSONGrapher layout_dict, typically extracted from a fig_dict.
4711
+
4712
+ Returns:
4713
+ dict: A Plotly 3D layout_dict with axes fields moved to 'scene' field.
4714
+ """
3121
4715
  import copy
3122
4716
  # Create a deep copy to avoid modifying the original layout
3123
4717
  new_layout = copy.deepcopy(layout)
@@ -3137,7 +4731,20 @@ def convert_to_3d_layout(layout):
3137
4731
 
3138
4732
  #A bubble plot uses z data, but that data is then
3139
4733
  #moved into the size field and the z field must be removed.
4734
+
3140
4735
  def remove_bubble_fields(fig_dict):
4736
+ """
4737
+ Removes JSONGrapher bubble plot creation fields to make a JSONGrapher fig_dict Plotly compatible.
4738
+
4739
+ This function iterates over data_series entries in the fig_dict and removes 'bubble_size',
4740
+ and 'max_bubble_size' fields from entries marked as bubble plots.
4741
+
4742
+ Args:
4743
+ fig_dict (dict): A fig_dict potentially containing JSONGrapher bubble plot data_series.
4744
+
4745
+ Returns:
4746
+ dict: The updated fig_dict with JSONGrapher bubble plot creation fields removed for Plotly graphing compatibility.
4747
+ """
3141
4748
  #This code will modify the data_series inside the fig_dict, directly.
3142
4749
  bubble_found = False #initialize with false case.
3143
4750
  for data_series in fig_dict["data"]:
@@ -3178,6 +4785,20 @@ def remove_bubble_fields(fig_dict):
3178
4785
  return fig_dict
3179
4786
 
3180
4787
  def update_3d_axes(fig_dict):
4788
+ """
4789
+ Converts a JSONGrapher 3D graph fig_dict to be compatible with Plotly 3D plotting. Modifies layout_dict and data_series dictionaries.
4790
+
4791
+ This function converts the layout of a fig_dict to a Plotly 3D format by nesting axis fields under 'scene',
4792
+ and also adjusts data_series entries as needed, based on their 3D plot types. For scatter3d and mesh3d traces,
4793
+ any 'z_matrix' fields are removed. For surface plots, 'z' is removed if 'z_matrix' is present and
4794
+ a notice is printed indicating the need for further transformation.
4795
+
4796
+ Args:
4797
+ fig_dict (dict): A fig_dict that may contain JSONGrapher format denoting 3D axes and/or trace_style fields.
4798
+
4799
+ Returns:
4800
+ dict: The updated fig_dict prepared for Plotly 3D rendering.
4801
+ """
3181
4802
  if "zaxis" in fig_dict["layout"]:
3182
4803
  fig_dict['layout'] = convert_to_3d_layout(fig_dict['layout'])
3183
4804
  for data_series_index, data_series in enumerate(fig_dict["data"]):
@@ -3194,9 +4815,20 @@ def update_3d_axes(fig_dict):
3194
4815
  return fig_dict
3195
4816
 
3196
4817
  def remove_extra_information_field(fig_dict, depth=1, max_depth=10):
3197
- """ This function is intended to make JSONGrapher .json files compatible with the current plotly format expectations
3198
- and also necessary for being able to convert a JSONGrapher json_dict to python plotly figure objects.
3199
- Recursively checks for 'extraInformation' fields and removes them."""
4818
+ """
4819
+ Recursively removes 'extraInformation' or 'extra_information' fields from a fig_dict for Plotly plotting compatibility.
4820
+
4821
+ This function traverses a fig_dict or sub-dictionary structure to eliminate keys related to extra metadata
4822
+ that are not supported by current Plotly layout format expectations. It supports deeply nested dictionaries and lists.
4823
+
4824
+ Args:
4825
+ fig_dict (dict): A fig_dict or nested sub-dictionary.
4826
+ depth (int, optional): The current recursion depth during traversal. Defaults to 1.
4827
+ max_depth (int, optional): Maximum depth to avoid infinite recursion. Defaults to 10.
4828
+
4829
+ Returns:
4830
+ dict: The updated dictionary with all 'extraInformation' fields removed.
4831
+ """
3200
4832
  if depth > max_depth or not isinstance(fig_dict, dict):
3201
4833
  return fig_dict
3202
4834
 
@@ -3215,9 +4847,21 @@ def remove_extra_information_field(fig_dict, depth=1, max_depth=10):
3215
4847
 
3216
4848
 
3217
4849
  def remove_nested_comments(data, top_level=True):
3218
- """ This function is intended to make JSONGrapher .json files compatible with the current plotly format expectations
3219
- and also necessary for being able to convert a JSONGrapher json_dict to python plotly figure objects.
3220
- Removes 'comments' fields that are not at the top level of the JSON-dict. Starts with 'top_level = True' when dict is first passed in then becomes false after that. """
4850
+ """
4851
+ Removes all nested 'comments' fields from a fig_dict while preserving top-level comments.
4852
+
4853
+ This function recursively traverses a fig_dict or sub-dictionary, removing any 'comments'
4854
+ entries found below the top level. This ensures compatibility with layout formats that
4855
+ do not support metadata fields in nested locations.
4856
+
4857
+ Args:
4858
+ data (dict): The fig_dict or sub-dictionary to process.
4859
+ top_level (bool, optional): Indicates whether the current recursion level is the top level.
4860
+ Defaults to True.
4861
+
4862
+ Returns:
4863
+ dict: The updated dictionary with nested 'comments' fields removed.
4864
+ """
3221
4865
  if not isinstance(data, dict):
3222
4866
  return data
3223
4867
  # Process nested structures
@@ -3234,6 +4878,19 @@ def remove_nested_comments(data, top_level=True):
3234
4878
  return data
3235
4879
 
3236
4880
  def remove_simulate_field(json_fig_dict):
4881
+ """
4882
+ Removes all 'simulate' fields from the data_series in a fig_dict.
4883
+
4884
+ This function iterates through each entry in the fig_dict's 'data' list and deletes
4885
+ the 'simulate' field if it exists. This prepares the fig_dict for use cases where
4886
+ simulation metadata is unnecessary or unsupported.
4887
+
4888
+ Args:
4889
+ json_fig_dict (dict): A fig_dict containing a list of data_series entries.
4890
+
4891
+ Returns:
4892
+ dict: The updated fig_dict with 'simulate' fields removed from each data_series.
4893
+ """
3237
4894
  data_dicts_list = json_fig_dict['data']
3238
4895
  for data_dict in data_dicts_list:
3239
4896
  data_dict.pop('simulate', None) #Some people recommend using pop over if/del as safer. Both ways should work under normal circumstances.
@@ -3241,6 +4898,19 @@ def remove_simulate_field(json_fig_dict):
3241
4898
  return json_fig_dict
3242
4899
 
3243
4900
  def remove_equation_field(json_fig_dict):
4901
+ """
4902
+ Removes all 'equation' fields from the data_series in a fig_dict.
4903
+
4904
+ This function scans through each item in the 'data' list of a fig_dict and deletes the
4905
+ 'equation' field if it is present. This is useful for stripping out symbolic definitions
4906
+ or expression metadata for use cases where the equation metadata is unnecessary or unsupported.
4907
+
4908
+ Args:
4909
+ json_fig_dict (dict): A fig_dict containing a list of data_series entries.
4910
+
4911
+ Returns:
4912
+ dict: The updated fig_dict with 'equation' fields removed from each data_series.
4913
+ """
3244
4914
  data_dicts_list = json_fig_dict['data']
3245
4915
  for data_dict in data_dicts_list:
3246
4916
  data_dict.pop('equation', None) #Some people recommend using pop over if/del as safer. Both ways should work under normal circumstances.
@@ -3248,6 +4918,19 @@ def remove_equation_field(json_fig_dict):
3248
4918
  return json_fig_dict
3249
4919
 
3250
4920
  def remove_trace_style_field(json_fig_dict):
4921
+ """
4922
+ Removes 'trace_style' and 'tracetype' fields from all data_series entries in a fig_dict.
4923
+
4924
+ This function iterates through the 'data' list of a fig_dict and deletes styling hints such as
4925
+ 'trace_style' and 'tracetype' from each data_series. This is useful for stripping out internal
4926
+ metadata before serialization or external use.
4927
+
4928
+ Args:
4929
+ json_fig_dict (dict): A fig_dict containing a list of data_series entries.
4930
+
4931
+ Returns:
4932
+ dict: The updated fig_dict with trace style metadata removed from all data_series.
4933
+ """
3251
4934
  data_dicts_list = json_fig_dict['data']
3252
4935
  for data_dict in data_dicts_list:
3253
4936
  data_dict.pop('trace_style', None) #Some people recommend using pop over if/del as safer. Both ways should work under normal circumstances.
@@ -3256,6 +4939,19 @@ def remove_trace_style_field(json_fig_dict):
3256
4939
  return json_fig_dict
3257
4940
 
3258
4941
  def remove_custom_units_chevrons(json_fig_dict):
4942
+ """
4943
+ Removes angle bracket characters ('<' and '>') from axis title text fields in a fig_dict.
4944
+
4945
+ This function scans the xaxis, yaxis, and zaxis title text strings in the layout of a fig_dict
4946
+ and removes any chevrons that may exist. It is useful for cleaning up units or labels that
4947
+ were enclosed in angle brackets for processing purposes.
4948
+
4949
+ Args:
4950
+ json_fig_dict (dict): A fig_dict containing axis title text to sanitize.
4951
+
4952
+ Returns:
4953
+ dict: The updated fig_dict with angle brackets removed from axis title labels.
4954
+ """
3259
4955
  try:
3260
4956
  json_fig_dict['layout']['xaxis']['title']['text'] = json_fig_dict['layout']['xaxis']['title']['text'].replace('<','').replace('>','')
3261
4957
  except KeyError:
@@ -3271,15 +4967,35 @@ def remove_custom_units_chevrons(json_fig_dict):
3271
4967
  return json_fig_dict
3272
4968
 
3273
4969
  def clean_json_fig_dict(json_fig_dict, fields_to_update=None):
3274
- """ This function is intended to make JSONGrapher .json files compatible with the current plotly format expectations
3275
- and also necessary for being able to convert a JSONGrapher json_dict to python plotly figure objects.
3276
- fields_to_update should be a list.
3277
- This function can also remove the 'simulate' field from data series. However, that is not the default behavior
3278
- because one would not want to do that by mistake before simulation is performed.
3279
- This function can also remove the 'equation' field from data series. However, that is not the default behavior
3280
- because one would not want to do that by mistake before the equation is evaluated.
3281
- The "superscripts" option is not normally used until right before plotting because that will affect unit conversions.
3282
- """
4970
+ """
4971
+ Cleans and updates a fig_dict by applying selected transformations for Plotly compatibility.
4972
+
4973
+ This function allows selective sanitization of a fig_dict by applying a set of transformations
4974
+ defined in fields_to_update. It prepares a JSONGrapher-compatible dictionary for use with
4975
+ Plotly figure objects by modifying or removing fields such as titles, equations, simulation
4976
+ definitions, custom units, and unused styling metadata.
4977
+
4978
+ Args:
4979
+ json_fig_dict (dict): The original fig_dict containing layout and data_series.
4980
+ fields_to_update (list, optional): A list of update operations to apply. Defaults to
4981
+ ["title_field", "extraInformation", "nested_comments"].
4982
+
4983
+ Returns:
4984
+ dict: The cleaned and optionally transformed fig_dict.
4985
+
4986
+ Supported options in fields_to_update:
4987
+ - "title_field": Updates title fields to dictionary format.
4988
+ - "extraInformation": Removes extra metadata fields.
4989
+ - "nested_comments": Removes non-top-level comments.
4990
+ - "simulate": Removes simulate fields from data_series.
4991
+ - "equation": Removes equation fields from data_series.
4992
+ - "custom_units_chevrons": Removes angle brackets in axis titles.
4993
+ - "bubble": Strips bubble-specific fields and removes zaxis.
4994
+ - "trace_style": Removes internal style/tracetype metadata.
4995
+ - "3d_axes": Updates layout and data_series for 3D plotting.
4996
+ - "superscripts": Replaces superscript strings in titles and labels.
4997
+ - "offset": Removes the offset field from the layout field.
4998
+ """
3283
4999
  if fields_to_update is None: # should not initialize mutable objects in arguments line, so doing here.
3284
5000
  fields_to_update = ["title_field", "extraInformation", "nested_comments"]
3285
5001
  fig_dict = json_fig_dict
@@ -3315,12 +5031,11 @@ local_python_functions_dictionary = {} #This is a global variable that works wit
3315
5031
 
3316
5032
  def run_js_simulation(javascript_simulator_url, simulator_input_json_dict, verbose = False):
3317
5033
  """
3318
- Downloads a JavaScript file using its URL, extracts the filename, appends an export statement,
3319
- executes it with Node.js, and parses the output.
5034
+ Runs a JavaScript-based simulation by downloading and executing a js file with a simulate function from a URL.
3320
5035
 
3321
- Parameters:
3322
- javascript_simulator_url (str): URL of the raw JavaScript file to download and execute. Must have a function named simulate.
3323
- simulator_input_json_dict (dict): Input parameters for the JavaScript simulator.
5036
+ This function fetches a JavaScript file containing a simulate function, appends a module export,
5037
+ and invokes it using Node.js with the provided simulation input dictionary. The resulting simulation output,
5038
+ if properly formatted, is returned as parsed JSON.
3324
5039
 
3325
5040
  # Example inputs
3326
5041
  javascript_simulator_url = "https://github.com/AdityaSavara/JSONGrapherExamples/blob/main/ExampleSimulators/Langmuir_Isotherm.js"
@@ -3333,9 +5048,13 @@ def run_js_simulation(javascript_simulator_url, simulator_input_json_dict, verbo
3333
5048
  }
3334
5049
  }
3335
5050
 
5051
+ Args:
5052
+ javascript_simulator_url (str): URL pointing to the raw JavaScript file containing a 'simulate' function.
5053
+ simulator_input_json_dict (dict): Dictionary of inputs to pass to the simulate function.
5054
+ verbose (bool, optional): If True, prints standard output and error streams from Node.js execution. Defaults to False.
3336
5055
 
3337
5056
  Returns:
3338
- dict: Parsed JSON output from the JavaScript simulation, or None if an error occurred.
5057
+ dict or None: Parsed dictionary output from the simulation, or None if an error occurred.
3339
5058
  """
3340
5059
  import requests
3341
5060
  import subprocess
@@ -3394,8 +5113,17 @@ def run_js_simulation(javascript_simulator_url, simulator_input_json_dict, verbo
3394
5113
 
3395
5114
  def convert_to_raw_github_url(url):
3396
5115
  """
3397
- Converts a GitHub file URL to its raw content URL if necessary, preserving the filename.
3398
- This function is really a support function for run_js_simulation
5116
+ Checks for and converts any GitHub file URLs to its raw content URL format for direct access to file contents. Non Github urls are unchanged.
5117
+
5118
+ This utility rewrites standard GitHub URLs (with or without 'blob') into their raw content
5119
+ equivalents on raw.githubusercontent.com. It preserves the full file path and is used as a
5120
+ helper for code that dynamically executes JavaScript files from GitHub.
5121
+
5122
+ Args:
5123
+ url (str): A URL possibly pointing to a GitHub file.
5124
+
5125
+ Returns:
5126
+ str: An unchanged url if not a Github url, or a raw GitHub URL suitable for direct content download.
3399
5127
  """
3400
5128
  from urllib.parse import urlparse
3401
5129
  parsed_url = urlparse(url)
@@ -3428,6 +5156,23 @@ def convert_to_raw_github_url(url):
3428
5156
  #because it returns extra fields that need to be parsed out.
3429
5157
  #and because it does not do unit conversions as needed after the simulation resultss are returned.
3430
5158
  def simulate_data_series(data_series_dict, simulator_link='', verbose=False):
5159
+ """
5160
+ Runs a simulation for a data_series_dict using either a local Python function or a remote JavaScript simulator.
5161
+
5162
+ This function determines which simulator to invoke—based on the provided simulator_link or the data_series_dict["simulate"]["model"]
5163
+ field, and calls the appropriate method to generate simulation results. It can handle both local Python-based
5164
+ simulation functions and compatible remote JavaScript simulate modules. Intended for internal use, this function
5165
+ may return additional fields that require further parsing and does not perform post-simulation unit conversions.
5166
+
5167
+ Args:
5168
+ data_series_dict (dict): Dictionary describing a single data_series, including simulation parameters.
5169
+ simulator_link (str, optional): Either the name of a local Python simulator or a URL to a JS simulator.
5170
+ If not provided, this function extracts the value from data_series_dict["simulate"]["model"] and follows that.
5171
+ verbose (bool, optional): Whether to print raw simulation output and error details. Defaults to False.
5172
+
5173
+ Returns:
5174
+ dict or None: The simulated data_series dictionary or None if an error occurred during execution.
5175
+ """
3431
5176
  if simulator_link == '':
3432
5177
  simulator_link = data_series_dict["simulate"]["model"]
3433
5178
  if simulator_link == "local_python": #this is the local python case.
@@ -3455,6 +5200,23 @@ def simulate_data_series(data_series_dict, simulator_link='', verbose=False):
3455
5200
  #Function that goes through a fig_dict data series and simulates each data series as needed.
3456
5201
  #If the simulated data returned has "x_label" and/or "y_label" with units, those will be used to scale the data, then will be removed.
3457
5202
  def simulate_as_needed_in_fig_dict(fig_dict, simulator_link='', verbose=False):
5203
+ """
5204
+ Iterates through the data_series in a fig_dict and performs simulation for each as needed.
5205
+
5206
+ This function checks each data_series in the fig_dict and applies simulation using either
5207
+ a specified simulator_link or the model defined within each entry. If the simulation result
5208
+ includes 'x_label' or 'y_label' with units, those are used to scale the data before removing
5209
+ the label fields.
5210
+
5211
+ Args:
5212
+ fig_dict (dict): A fig_dict containing a 'data' list with data_series dictionary entries to simulate.
5213
+ simulator_link (str, optional): An override simulator link or label to apply across all series.
5214
+ Defaults to an empty string, in which case this function checks each data_series dictionary to determine the simulator to use.
5215
+ verbose (bool, optional): Whether to log/output updates and warnings during the simulation process. Defaults to False.
5216
+
5217
+ Returns:
5218
+ dict: The updated fig_dict with simulated results entered into those data_series dictionary fields.
5219
+ """
3458
5220
  data_dicts_list = fig_dict['data']
3459
5221
  for data_dict_index in range(len(data_dicts_list)):
3460
5222
  fig_dict = simulate_specific_data_series_by_index(fig_dict, data_dict_index, simulator_link=simulator_link, verbose=verbose)
@@ -3463,6 +5225,24 @@ def simulate_as_needed_in_fig_dict(fig_dict, simulator_link='', verbose=False):
3463
5225
  #Function that takes fig_dict and dataseries index and simulates if needed. Also performs unit conversions as needed.
3464
5226
  #If the simulated data returned has "x_label" and/or "y_label" with units, those will be used to scale the data, then will be removed.
3465
5227
  def simulate_specific_data_series_by_index(fig_dict, data_series_index, simulator_link='', verbose=False):
5228
+ """
5229
+ Simulates a specific data_series within a fig_dict and applies unit scaling if required.
5230
+
5231
+ This function targets a single data_series by index in the fig_dict, performs simulation
5232
+ using either a specified or embedded simulator, and scales the results to match the units
5233
+ found in the fig_dict layout. If x_label or y_label with units are present in the simulation
5234
+ output, corresponding unit conversions are applied to the data and those fields are removed.
5235
+
5236
+ Args:
5237
+ fig_dict (dict): The figure dictionary containing a list of data_series.
5238
+ data_series_index (int): Index of the data_series to simulate and update.
5239
+ simulator_link (str, optional): Path or URL to the simulator to use. If empty,
5240
+ the simulator is inferred from the data_series entry. Defaults to ''.
5241
+ verbose (bool, optional): If True, prints diagnostic details including scaling ratios. Defaults to False.
5242
+
5243
+ Returns:
5244
+ dict: The updated fig_dict with any simulated results entered into the appropriate data_series dictionary.
5245
+ """
3466
5246
  data_dicts_list = fig_dict['data']
3467
5247
  data_dict_index = data_series_index
3468
5248
  data_dict = data_dicts_list[data_dict_index]
@@ -3495,6 +5275,19 @@ def simulate_specific_data_series_by_index(fig_dict, data_series_index, simulato
3495
5275
  return fig_dict
3496
5276
 
3497
5277
  def evaluate_equations_as_needed_in_fig_dict(fig_dict):
5278
+ """
5279
+ Evaluates and updates any equation-based data_series entries in a fig_dict.
5280
+
5281
+ This function scans the fig_dict for data_series entries that contain an 'equation' field.
5282
+ For each such entry, it invokes the appropriate equation evaluation logic to generate
5283
+ x/y data, replacing or augmenting the original data_series with the computed results.
5284
+
5285
+ Args:
5286
+ fig_dict (dict): A figure dictionary potentially containing equation-based data_series.
5287
+
5288
+ Returns:
5289
+ dict: The updated fig_dict with equation data_series evaluated and populated with data.
5290
+ """
3498
5291
  data_dicts_list = fig_dict['data']
3499
5292
  for data_dict_index, data_dict in enumerate(data_dicts_list):
3500
5293
  if 'equation' in data_dict:
@@ -3503,6 +5296,22 @@ def evaluate_equations_as_needed_in_fig_dict(fig_dict):
3503
5296
 
3504
5297
  #TODO: Should add z units ratio scaling here (just to change units when merging records). Should do the same for the simulate_specific_data_series_by_index function.
3505
5298
  def evaluate_equation_for_data_series_by_index(fig_dict, data_series_index, verbose="auto"):
5299
+ """
5300
+ Evaluates a symbolic equation for the data_series at the specified index of the provided fig_dict and performs units conversion / scaling as needed.
5301
+
5302
+ This function targets an indexed data_series entry with an 'equation' field and uses an external
5303
+ equation engine to evaluate and populate x/y/z data values. If axis labels include units, the values
5304
+ are converted/scaled to match the units defined in the fig_dict layout. The function also assigns default
5305
+ trace types based on the dimensionality of the evaluated data.
5306
+
5307
+ Args:
5308
+ fig_dict (dict): A fig_dict containing a list of data_series and corresponding layout metadata.
5309
+ data_series_index (int): The index of the data_series containing the equation to evaluate.
5310
+ verbose (str or bool, optional): Controls verbosity of the evaluation engine. Defaults to "auto".
5311
+
5312
+ Returns:
5313
+ dict: The updated fig_dict with the specified data_series evaluated and replaced with numeric values.
5314
+ """
3506
5315
  try:
3507
5316
  # Attempt to import from the json_equationer package
3508
5317
  import json_equationer.equation_creator as equation_creator
@@ -3571,24 +5380,22 @@ def evaluate_equation_for_data_series_by_index(fig_dict, data_series_index, verb
3571
5380
 
3572
5381
  def update_implicit_data_series_data(target_fig_dict, source_fig_dict, parallel_structure=True, modify_target_directly = False):
3573
5382
  """
3574
- Updates the x and y values of implicit data series (equation/simulate) in target_fig_dict
3575
- using values from the corresponding series in source_fig_dict.
5383
+ Synchronizes x, y, and z data for implicit data series between two fig_dicts.
5384
+
5385
+ This function transfers numerical data values from a source fig_dict into matching entries
5386
+ in a target fig_dict for all data_series that include a 'simulate' or 'equation' block.
5387
+ It supports both parallel updates by index and matching by series name.
3576
5388
 
3577
5389
  Args:
3578
- target_fig_dict (dict): The figure dictionary that needs updated data.
3579
- source_fig_dict (dict): The figure dictionary that provides x and y values.
3580
- parallel_structure (bool, optional): If True, assumes both data lists are the same
3581
- length and updates using zip(). If False,
3582
- matches by name instead. Default is True.
5390
+ target_fig_dict (dict): The figure dictionary to update.
5391
+ source_fig_dict (dict): The source figure dictionary providing updated values.
5392
+ parallel_structure (bool, optional): If True, updates by index order (zip). If False,
5393
+ matches series by their "name" field. Defaults to True.
5394
+ modify_target_directly (bool, optional): If True, modifies target_fig_dict in-place.
5395
+ If False, operates on a deep copy. Defaults to False.
3583
5396
 
3584
5397
  Returns:
3585
- dict: A new figure dictionary with updated x and y values for implicit data series.
3586
-
3587
- Notes:
3588
- - If parallel_structure=True and both lists have the same length, updates use zip().
3589
- - If parallel_structure=False, matching is done by the "name" field.
3590
- - Only updates data series that contain "simulate" or "equation".
3591
- - Ensures deep copying to avoid modifying the original structures.
5398
+ dict: The updated target_fig_dict with new x, y, and optionally z values for matched implicit series.
3592
5399
  """
3593
5400
  if modify_target_directly == False:
3594
5401
  import copy # Import inside function to limit scope
@@ -3624,43 +5431,28 @@ def update_implicit_data_series_data(target_fig_dict, source_fig_dict, parallel_
3624
5431
  return updated_fig_dict
3625
5432
 
3626
5433
 
3627
- def execute_implicit_data_series_operations(fig_dict, simulate_all_series=True, evaluate_all_equations=True, adjust_implicit_data_ranges=True):
5434
+ def execute_implicit_data_series_operations(fig_dict, simulate_all_series=True, evaluate_all_equations=True, adjust_implicit_data_ranges=True, adjust_offset2d=False, adjust_arrange2dTo3d=False):
3628
5435
  """
3629
- This function is designed to be called during creation of a plotly or matplotlib figure creation.
3630
- Processes implicit data series (equation/simulate), adjusting ranges, performing simulations,
3631
- and evaluating equations as needed.
3632
-
3633
- The important thing is that this function creates a "fresh" fig_dict, does some manipulation, then then gets the data from that
3634
- and adds it to the original fig_dict.
3635
- That way the original fig_dict is not changed other than getting the simulated/evaluated data.
3636
-
3637
- The reason the function works this way is that the x_range_default of the implicit data series (equations and simulations)
3638
- are adjusted to match the data in the fig_dict, but we don't want to change the x_range_default of our main record.
3639
- That's why we make a copy for creating simulated/evaluated data from those adjusted ranges, and then put the simulated/evaluated data
3640
- back into the original dict.
5436
+ Processes data_series dicts in a fig_dict, executing simulate/equation-based series as needed, including setting the simulate/equation evaluation ranges as needed,
5437
+ then provides the simulated/equation-evaluated data into the original fig_dict without altering original fig_dict implicit ranges.
3641
5438
 
3642
-
5439
+ This function evaluates and simulates any data_series entries in the fig_dict that use "simulate"
5440
+ or "equation" blocks. It creates a deep copy of the figure to avoid overwriting original range
5441
+ configurations (such as x_range_default). Simulated and evaluated data is extracted from the copy
5442
+ and merged back into the original fig_dict for use in rendering.
3643
5443
 
3644
5444
  Args:
3645
- fig_dict (dict): The figure dictionary containing data series.
3646
- simulate_all_series (bool): If True, performs simulations for applicable series.
3647
- evaluate_all_equations (bool): If True, evaluates all equation-based series.
3648
- adjust_implicit_data_ranges (bool): If True, modifies ranges for implicit data series.
5445
+ fig_dict (dict): The main figure dictionary that may contain implicit (simulate/equation) data_series.
5446
+ simulate_all_series (bool, optional): Whether to simulate all series requiring it. Defaults to True.
5447
+ evaluate_all_equations (bool, optional): Whether to evaluate all equation-based series. Defaults to True.
5448
+ adjust_implicit_data_ranges (bool, optional): Whether to adapt x-axis ranges of implicit series
5449
+ using non-implicit data ranges. Defaults to True.
3649
5450
 
3650
5451
  Returns:
3651
- dict: Updated figure dictionary with processed implicit data series.
3652
-
3653
- Notes:
3654
- - If adjust_implicit_data_ranges=True, retrieves min/max values from regular data series
3655
- (those that are not equations and not simulations) and applies them to implicit data.
3656
- - If simulate_all_series=True, executes simulations for all series that require them
3657
- and transfers the computed data back to fig_dict without copying ranges.
3658
- - If evaluate_all_equations=True, solves equations as needed and transfers results
3659
- back to fig_dict without copying ranges.
3660
- - Uses deepcopy to avoid modifying the original input dictionary.
5452
+ dict: The updated fig_dict with fresh data inserted into simulated or equation-driven series,
5453
+ while preserving their original metadata and x_range_default boundaries.
3661
5454
  """
3662
5455
  import copy # Import inside function for modularity
3663
-
3664
5456
  # Create a copy for processing implicit series separately
3665
5457
  fig_dict_for_implicit = copy.deepcopy(fig_dict)
3666
5458
  #first check if any data_series have an equatinon or simulation field. If not, we'll skip.
@@ -3689,11 +5481,191 @@ def execute_implicit_data_series_operations(fig_dict, simulate_all_series=True,
3689
5481
  fig_dict_for_implicit = evaluate_equations_as_needed_in_fig_dict(fig_dict_for_implicit)
3690
5482
  # Copy results back without overwriting the ranges
3691
5483
  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)
5484
+
5485
+ if adjust_offset2d: #This should occur after simulations and evaluations because it could rely on them.
5486
+ #First check if the layout style is that of an offset2d graph.
5487
+ layout_style = fig_dict.get("plot_style", {}).get("layout_style", "")
5488
+ if "offset2d" in layout_style:
5489
+ #This case is different from others -- we will not modify target directly because we are not doing a merge.
5490
+ fig_dict = extract_and_implement_offsets(fig_dict_for_implicit, modify_target_directly = False)
5491
+ if adjust_arrange2dTo3d: #This should occur after simulations and evaluations because it could rely on them.
5492
+ #First check if the layout style is that of an arrange2dTo3d graph.
5493
+ layout_style = fig_dict.get("plot_style", {}).get("layout_style", "")
5494
+ if "arrange2dTo3d" in layout_style:
5495
+ #This case is different from others -- we will not modify target directly because we are not doing a merge.
5496
+ fig_dict = implement_arrange2dTo3d(fig_dict_for_implicit, modify_target_directly = False)
5497
+
5498
+ return fig_dict
5499
+
5500
+
5501
+ def implement_arrange2dTo3d(fig_dict, modify_target_directly=False):
5502
+ import copy
5503
+ #TODO: add some logic that enables left, right, and vertical axes variables to be determined
5504
+ #TODO: add some logic that enables the axes labels to be moved as needed.
5505
+ scratch_fig_dict = copy.deepcopy(fig_dict) #initialize. This fig_dict will be modified with pre-processing, then drawn from.
5506
+ modified_fig_dict = copy.deepcopy(fig_dict) #initialize.
5507
+ vertical_axis_variable = fig_dict["layout"].get("vertical_axis_variable", {})
5508
+ if len(vertical_axis_variable) == 0:#This means one was not provided, in which case we'll make a sequential graph with default, which makes y into the vertical axis.
5509
+ vertical_axis_variable = 'y'
5510
+ left_axis_variable = fig_dict["layout"].get("left_axis_variable", {})
5511
+ if len(left_axis_variable) == 0:#This means one was not provided, in which case we'll make a sequential graph with default, which makes x into left axis.
5512
+ left_axis_variable = 'x'
5513
+ right_axis_variable = fig_dict["layout"].get("right_axis_variable", {})
5514
+ if len(right_axis_variable) == 0:#This means one was not provided, in which case we'll make an ascending sequence for the right-axis, which we initiate as "ascending_sequence"
5515
+ right_axis_variable = 'data_series_index_vector'
5516
+ #Now we'll populate the ascending sequence into each data_series.
5517
+ for data_series_index in range(len(fig_dict["data"])):
5518
+ length_needed = len(fig_dict["data"][data_series_index]["x"])
5519
+ data_series_index_vector = [data_series_index] * length_needed #this repeats the data_series_index as many times as needed in a list.
5520
+ scratch_fig_dict["data"][data_series_index]["data_series_index_vector"] = data_series_index_vector
5521
+ #Now, need to rearrange the axes labels as needed.
5522
+ # Ensure nested structure for xaxis, yaxis, and zaxis titles exists
5523
+ #For plotly 3D axes: y is left, x is right, and z is up.
5524
+ for axis in ["xaxis", "yaxis", "zaxis"]:
5525
+ modified_fig_dict.setdefault("layout", {}).setdefault(axis, {}).setdefault("title", {})["text"] = ""
5526
+ modified_fig_dict["layout"]["yaxis"]["title"]["text"] = scratch_fig_dict["layout"][str(left_axis_variable)+"axis"]["title"]["text"]
5527
+ if right_axis_variable != "data_series_index_vector":
5528
+ modified_fig_dict["layout"]["xaxis"]["title"]["text"] = scratch_fig_dict["layout"][str(right_axis_variable)+"axis"]["title"]["text"]
5529
+ else: #This means it's sequence of data series.
5530
+ modified_fig_dict["layout"]["xaxis"]["title"]["text"] = "Data Set"
5531
+ modified_fig_dict["layout"]["zaxis"]["title"]["text"] = scratch_fig_dict["layout"][str(vertical_axis_variable)+"axis"]["title"]["text"]
5532
+ #Now, need to rearrange the variables as would be expected, and need to do it for each data series.
5533
+ for data_series_index in range(len(fig_dict["data"])):
5534
+ #We will support two trace_styles: scatter3d and curve3d.
5535
+ if "scatter" in modified_fig_dict["data"][data_series_index]["trace_style"]:
5536
+ modified_fig_dict["data"][data_series_index]["trace_style"] = "scatter3d" #This is currently the only supported trace style. Need to add some logic.
5537
+ else:
5538
+ modified_fig_dict["data"][data_series_index]["trace_style"] = "curve3d" #This is currently the only supported trace style. Need to add some logic.
5539
+ #For plotly 3D axes: y is left, x is right, and z is up.
5540
+ modified_fig_dict["data"][data_series_index]["y"] = scratch_fig_dict["data"][data_series_index][left_axis_variable]
5541
+ modified_fig_dict["data"][data_series_index]["x"] = scratch_fig_dict["data"][data_series_index][right_axis_variable]
5542
+ modified_fig_dict["data"][data_series_index]["z"] = scratch_fig_dict["data"][data_series_index][vertical_axis_variable]
5543
+ return modified_fig_dict
5544
+
5545
+
5546
+ #Small helper function to find if an offset is a float scalar.
5547
+ def is_float_scalar(value):
5548
+ try:
5549
+ float(value)
5550
+ return True
5551
+ except (TypeError, ValueError):
5552
+ return False
5553
+
5554
+ def extract_and_implement_offsets(fig_dict, modify_target_directly=False, graphical_dimensionality=2):
5555
+ import numpy as np
5556
+ #First, extract offsets.
5557
+ import copy
5558
+ if modify_target_directly == False:
5559
+ fig_dict_with_offsets = copy.deepcopy(fig_dict)
5560
+ else:
5561
+ fig_dict_with_offsets = fig_dict
5562
+ #initialize offset_variable_name as the case someone decides to specify one.
5563
+ offset_variable_name = ""
5564
+ if "offset" in fig_dict["layout"]:
5565
+ #This is the easy case, because we don't need to determine the offset.
5566
+ offset = fig_dict["layout"]["offset"]
5567
+ if is_float_scalar(offset):
5568
+ offset = fig_dict["layout"]["offset"]
5569
+ elif isinstance(offset,str):#check if is a string, in which case it is a variable name.
5570
+ #in this case it is a variable where we will extract it from each dataset.
5571
+ offset_variable_name = offset
5572
+ else:
5573
+ #Else assume the offset is an array like object, of length equal to number of datapoints.
5574
+ offset = np.array(offset, dtype=float)
5575
+ #Now, implement offsets.
5576
+ if graphical_dimensionality == 2:
5577
+ current_series_offset = 0 # Initialize total offset
5578
+ for data_series_index in range(len(fig_dict["data"])):
5579
+ data_series_y_values = np.array(fig_dict["data"][data_series_index]["y"])
5580
+ if data_series_index == 0:
5581
+ fig_dict_with_offsets["data"][data_series_index]["y"] = list(data_series_y_values)
5582
+ else:
5583
+ # Determine the current offset
5584
+ if offset_variable_name != "":
5585
+ incremental_offset = np.array(fig_dict["data"][data_series_index][offset_variable_name], dtype=float)
5586
+ else:
5587
+ incremental_offset = np.array(offset, dtype=float)
5588
+ current_series_offset += incremental_offset # Accumulate the offset
5589
+ fig_dict_with_offsets["data"][data_series_index]["y"] = list(data_series_y_values + current_series_offset)
5590
+ else:
5591
+ #This is the hard case, we need to determine a reasonable offset for the dataseries.
5592
+ if graphical_dimensionality == 2:
5593
+ fig_dict_with_offsets = determine_and_apply_offset2d_for_fig_dict(fig_dict, modify_target_directly=modify_target_directly)
5594
+ return fig_dict_with_offsets
3692
5595
 
5596
+ #A function that calls helper functions to determine and apply a 2D offset to fig_dict
5597
+ def determine_and_apply_offset2d_for_fig_dict(fig_dict, modify_target_directly=False):
5598
+ if modify_target_directly == False:
5599
+ import copy
5600
+ fig_dict = copy.deepcopy(fig_dict)
5601
+ #First, extract data into a numpy array like [[x1, y1], [x2, y2], ...]
5602
+ all_series_array = extract_all_xy_series_data_from_fig_dict(fig_dict)
5603
+ #Then determine and apply a vertical offset. For now, we'll only support using the default
5604
+ #argument which is 1.2 times the maximum height in the series.
5605
+ #If someone wants to do something different, they can provide their own vertical offset value.
5606
+ offset_data_array = apply_vertical_offset2d_for_numpy_arrays_list(all_series_array)
5607
+ #Then, put the data back in.
5608
+ fig_dict = inject_xy_series_data_into_fig_dict(fig_dict=fig_dict, data_list = offset_data_array)
3693
5609
  return fig_dict
3694
5610
 
5611
+ def extract_all_xy_series_data_from_fig_dict(fig_dict):
5612
+ """
5613
+ Extracts all x and y values from a Plotly figure dictionary into a list of NumPy arrays.
5614
+ Each array in the list has shape (n_points, 2), where each row is [x, y] like [[x1, y1], [x2, y2], ...].
5615
+ """
5616
+ import numpy as np
5617
+ series_list = []
5618
+ for data_series in fig_dict.get('data', []):
5619
+ x_vals = np.array(data_series.get('x', []))
5620
+ y_vals = np.array(data_series.get('y', []))
5621
+ #Only keep the xy data if x and y lists are the same length.
5622
+ if len(x_vals) == len(y_vals):
5623
+ series_list.append(np.stack((x_vals, y_vals), axis=-1))
5624
+ return series_list
5625
+
5626
+ def apply_vertical_offset2d_for_numpy_arrays_list(data_list, offset_multiplier=1.2):
5627
+ """
5628
+ Applies vertical offsets to a list of 2D NumPy arrays.
5629
+ Each array has shape (n_points, 2), with rows like [[x1, y1], [x2, y2], ...].
5630
+ Returns a list of the same structure with adjusted y values per series index.
5631
+ """
5632
+ import numpy as np
5633
+ spans = [np.max(series[:, 1]) - np.min(series[:, 1]) if len(series) > 0 else 0 for series in data_list]
5634
+ base_offset = max(spans) * offset_multiplier if spans else 0
5635
+ offset_data_list = []
5636
+ for series_index, series_array in enumerate(data_list):
5637
+ # Skip empty series but preserve its position in the output
5638
+ if len(series_array) == 0:
5639
+ offset_data_list.append(series_array)
5640
+ continue
5641
+ # Ensure float type for numerical stability when applying offsets
5642
+ offset_series = np.copy(series_array).astype(float)
5643
+ # Apply vertical offset based on series index and base offset
5644
+ #print("line 5574, before the addition", offset_series)
5645
+ offset_series[:, 1] += series_index * base_offset
5646
+ #print("line 5576, after the addition", offset_series)
5647
+ # Add the adjusted series to the output list
5648
+ offset_data_list.append(offset_series)
5649
+ return offset_data_list
5650
+
5651
+
3695
5652
 
3696
5653
 
5654
+ def inject_xy_series_data_into_fig_dict(fig_dict, data_list):
5655
+ """
5656
+ Updates a Plotly figure dictionary in-place by injecting x and y data from a list of NumPy arrays.
5657
+ Each array must have shape (n_points, 2), where each row is [x, y] like [[x1, y1], [x2, y2], ...].
5658
+ The number of arrays must match the number of traces in the figure.
5659
+ """
5660
+ n_traces = len(fig_dict.get('data', []))
5661
+ if len(data_list) != n_traces:
5662
+ raise ValueError("Mismatch between number of traces and number of data series.")
5663
+ for i, trace in enumerate(fig_dict['data']):
5664
+ series = data_list[i]
5665
+ trace['x'] = series[:, 0].tolist()
5666
+ trace['y'] = series[:, 1].tolist()
5667
+ return fig_dict
5668
+
3697
5669
  ### 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###
3698
5670
 
3699
5671
  # Example Usage