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.
- JSONGrapher/JSONRecordCreator.py +2332 -360
- JSONGrapher/styles/layout_styles_library.py +86 -59
- JSONGrapher/styles/trace_styles_collection_library.py +14 -5
- JSONGrapher/version.py +1 -1
- {jsongrapher-4.4.dist-info → jsongrapher-4.6.dist-info}/METADATA +1 -1
- {jsongrapher-4.4.dist-info → jsongrapher-4.6.dist-info}/RECORD +11 -11
- {jsongrapher-4.4.data → jsongrapher-4.6.data}/data/LICENSE.txt +0 -0
- {jsongrapher-4.4.data → jsongrapher-4.6.data}/data/README.md +0 -0
- {jsongrapher-4.4.dist-info → jsongrapher-4.6.dist-info}/LICENSE.txt +0 -0
- {jsongrapher-4.4.dist-info → jsongrapher-4.6.dist-info}/WHEEL +0 -0
- {jsongrapher-4.4.dist-info → jsongrapher-4.6.dist-info}/top_level.txt +0 -0
JSONGrapher/JSONRecordCreator.py
CHANGED
@@ -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
|
-
"""
|
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
|
-
"""
|
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
|
-
"""
|
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
|
-
"""
|
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
|
-
"""
|
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
|
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
|
-
"""
|
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
|
-
|
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
|
-
"""
|
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
|
-
"""
|
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
|
-
"""
|
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
|
-
"""
|
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
|
-
"""
|
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
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
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
|
470
|
-
"""
|
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
|
-
"""
|
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
|
-
"""
|
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
|
-
"""
|
484
|
-
|
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
|
-
|
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
|
-
"""
|
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
|
-
|
1062
|
+
Sets the dash style of the line used in the data_series visualization.
|
510
1063
|
|
511
|
-
|
512
|
-
|
513
|
-
|
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
|
-
|
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
|
-
|
1083
|
+
Converts a transparency value into an opacity setting and applies it to the data_series.
|
526
1084
|
|
527
|
-
|
528
|
-
|
529
|
-
|
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
|
-
:
|
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
|
-
"""
|
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
|
-
"""
|
542
|
-
|
543
|
-
|
544
|
-
|
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
|
-
"""
|
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
|
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
|
-
|
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
|
-
|
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
|
-
"""
|
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
|
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=
|
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
|
-
|
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
|
1448
|
+
if line: #This checks variable is not None, and not empty.
|
735
1449
|
data_series_dict["line"] = line
|
736
|
-
if
|
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
|
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
|
-
|
761
|
-
|
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
|
1536
|
+
if line:
|
798
1537
|
data_series_dict["line"] = line
|
799
|
-
if
|
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
|
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
|
-
|
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
|
862
|
-
|
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
|
-
|
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):
|
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:
|
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,
|
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
|
-
|
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):
|
968
|
-
|
969
|
-
|
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:
|
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
|
1022
|
-
|
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
|
1029
|
-
|
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
|
-
|
1036
|
-
|
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
|
-
|
1043
|
-
|
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
|
-
|
1054
|
-
|
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
|
-
|
1065
|
-
|
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
|
-
|
1117
|
-
|
1118
|
-
|
1119
|
-
|
1120
|
-
|
1121
|
-
|
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
|
-
|
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
|
1148
|
-
|
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
|
-
|
1163
|
-
|
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
|
1167
|
-
|
1168
|
-
|
1169
|
-
|
1170
|
-
|
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
|
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,
|
1222
|
-
|
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
|
-
|
2328
|
+
Constructs and returns a matplotlib figure generated from fig_dict, with optional simulation, preprocessing, and styling.
|
1253
2329
|
|
1254
2330
|
Args:
|
1255
|
-
|
1256
|
-
|
1257
|
-
|
1258
|
-
|
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
|
-
|
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)
|
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(
|
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,
|
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(
|
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
|
-
|
1309
|
-
|
1310
|
-
|
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
|
-
|
1339
|
-
|
1340
|
-
|
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 =
|
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
|
-
|
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
|
-
|
1405
|
-
|
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
|
-
|
1414
|
-
|
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
|
-
|
1423
|
-
|
1424
|
-
|
1425
|
-
|
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
|
-
|
1460
|
-
|
1461
|
-
|
1462
|
-
|
1463
|
-
:
|
1464
|
-
|
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
|
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 (
|
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
|
-
|
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
|
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
|
2807
|
+
units_to_check (str): A string containing the units to validate and potentially singularize.
|
1547
2808
|
|
1548
2809
|
Returns:
|
1549
|
-
tuple:
|
1550
|
-
|
1551
|
-
|
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
|
1628
|
-
|
1629
|
-
|
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
|
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
|
-
|
1636
|
-
|
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
|
1692
|
-
|
1693
|
-
|
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
|
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
|
-
|
1741
|
-
|
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
|
1824
|
-
|
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
|
1828
|
-
y_values (list): List of y
|
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
|
1832
|
-
adjust_edges (bool):
|
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:
|
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
|
-
|
1895
|
-
|
1896
|
-
|
1897
|
-
|
1898
|
-
|
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
|
3283
|
+
def remove_plot_style_from_fig_dict(fig_dict):
|
1956
3284
|
"""
|
1957
|
-
|
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
|
-
:
|
1960
|
-
|
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
|
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
|
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
|
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
|
-
|
2098
|
-
|
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`
|
2102
|
-
|
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:
|
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
|
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
|
2131
|
-
|
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:
|
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
|
-
|
2378
|
-
|
2379
|
-
|
2380
|
-
|
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
|
-
|
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
|
-
|
2407
|
-
|
3785
|
+
Args:
|
3786
|
+
data_series (dict): A dictionary representing a single Plotly-style trace.
|
2408
3787
|
|
2409
|
-
:
|
2410
|
-
|
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
|
-
|
3833
|
+
Creates a and returns a trace_style dictionary by extracting formatting attributes from a single data_series dictionary.
|
2437
3834
|
|
2438
|
-
|
2439
|
-
|
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
|
-
:
|
2455
|
-
|
2456
|
-
|
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
|
-
#
|
3981
|
+
#import an entire trace_styles_collection from .json. THe trace_styles_collection is dict.
|
2545
3982
|
def import_trace_styles_collection(filename):
|
2546
|
-
|
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
|
-
|
2555
|
-
if not isinstance(
|
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 =
|
2559
|
-
if not isinstance(collection_name, str) or collection_name not in
|
2560
|
-
|
2561
|
-
|
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
|
-
|
2592
|
-
|
2593
|
-
|
2594
|
-
|
2595
|
-
|
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
|
-
|
4165
|
+
Removes any layout field formatting from a fig_dict while retaining any essential layout field content.
|
2683
4166
|
|
2684
|
-
|
2685
|
-
|
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
|
4242
|
+
def extract_layout_style_from_fig_dict(fig_dict):
|
2754
4243
|
"""
|
2755
|
-
|
4244
|
+
Extracts a layout_style dictionary from a fig_dict by pulling out the cosmetic formatting layout properties.
|
2756
4245
|
|
2757
|
-
|
2758
|
-
|
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
|
2884
|
-
|
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):
|
2888
|
-
range_dict (dict):
|
2889
|
-
|
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
|
2893
|
-
|
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
|
-
|
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
|
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
|
2939
|
-
skip_equations (bool):
|
2940
|
-
skip_simulations (bool):
|
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
|
2945
|
-
- data_series_ranges (dict): A dictionary
|
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
|
-
-
|
2949
|
-
-
|
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
|
-
###
|
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
|
-
"""
|
3065
|
-
|
3066
|
-
|
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
|
-
"""
|
3084
|
-
|
3085
|
-
|
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
|
-
"""
|
3198
|
-
|
3199
|
-
|
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
|
-
"""
|
3219
|
-
|
3220
|
-
|
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
|
-
"""
|
3275
|
-
|
3276
|
-
|
3277
|
-
|
3278
|
-
|
3279
|
-
|
3280
|
-
|
3281
|
-
|
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
|
-
|
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
|
-
|
3322
|
-
|
3323
|
-
|
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
|
-
|
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
|
-
|
3398
|
-
|
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
|
-
|
3575
|
-
|
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
|
3579
|
-
source_fig_dict (dict): The figure dictionary
|
3580
|
-
parallel_structure (bool, optional): If True,
|
3581
|
-
|
3582
|
-
|
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:
|
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
|
-
|
3630
|
-
|
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
|
3646
|
-
simulate_all_series (bool):
|
3647
|
-
evaluate_all_equations (bool):
|
3648
|
-
adjust_implicit_data_ranges (bool):
|
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:
|
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
|