Skip to content

Commit 327f184

Browse files
authored
Merge pull request #67 from AdityaSavara/offset2d
Offset2d
2 parents 9d37e86 + 8e57927 commit 327f184

File tree

5 files changed

+5750
-8
lines changed

5 files changed

+5750
-8
lines changed

JSONGrapher/JSONRecordCreator.py

Lines changed: 148 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2143,7 +2143,7 @@ def export_plotly_json(self, filename, plot_style = None, update_and_validate=Tr
21432143
return plotly_json_string
21442144

21452145
#simulate all series will simulate any series as needed.
2146-
def get_plotly_fig(self, plot_style=None, update_and_validate=True, simulate_all_series=True, evaluate_all_equations=True, adjust_implicit_data_ranges=True):
2146+
def get_plotly_fig(self, plot_style=None, update_and_validate=True, simulate_all_series=True, evaluate_all_equations=True, adjust_implicit_data_ranges=True, adjust_offset2d=True):
21472147
"""
21482148
Constructs and returns a Plotly figure object based on the current fig_dict with optional preprocessing steps.
21492149
- A deep copy of fig_dict is created to avoid unintended mutation of the source object.
@@ -2177,16 +2177,23 @@ def get_plotly_fig(self, plot_style=None, update_and_validate=True, simulate_all
21772177
self.fig_dict = execute_implicit_data_series_operations(self.fig_dict,
21782178
simulate_all_series=simulate_all_series,
21792179
evaluate_all_equations=evaluate_all_equations,
2180-
adjust_implicit_data_ranges=adjust_implicit_data_ranges)
2180+
adjust_implicit_data_ranges=adjust_implicit_data_ranges,
2181+
adjust_offset2d = False)
21812182
#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.
21822183
original_fig_dict = copy.deepcopy(self.fig_dict)
2184+
#The adjust_offset2d should be on the copy, if requested.
2185+
self.fig_dict = execute_implicit_data_series_operations(self.fig_dict,
2186+
simulate_all_series=False,
2187+
evaluate_all_equations=False,
2188+
adjust_implicit_data_ranges=False,
2189+
adjust_offset2d=adjust_offset2d)
21832190
#before cleaning and validating, we'll apply styles.
21842191
plot_style = parse_plot_style(plot_style=plot_style)
21852192
self.apply_plot_style(plot_style=plot_style)
21862193
#Now we clean out the fields and make a plotly object.
21872194
if update_and_validate == True: #this will do some automatic 'corrections' during the validation.
21882195
self.update_and_validate_JSONGrapher_record(clean_for_plotly=False) #We use the False argument here because the cleaning will be on the next line with beyond default arguments.
2189-
self.fig_dict = clean_json_fig_dict(self.fig_dict, fields_to_update=['simulate', 'custom_units_chevrons', 'equation', 'trace_style', '3d_axes', 'bubble', 'superscripts'])
2196+
self.fig_dict = clean_json_fig_dict(self.fig_dict, fields_to_update=['simulate', 'custom_units_chevrons', 'equation', 'trace_style', '3d_axes', 'bubble', 'superscripts', 'nested_comments', 'extraInformation'])
21902197
fig = pio.from_json(json.dumps(self.fig_dict))
21912198
#restore the original fig_dict.
21922199
self.fig_dict = original_fig_dict
@@ -2312,7 +2319,7 @@ def export():
23122319
#update_and_validate will 'clean' for plotly.
23132320
#In the case of creating a matplotlib figure, this really just means removing excess fields.
23142321
#simulate all series will simulate any series as needed.
2315-
def get_matplotlib_fig(self, plot_style = None, update_and_validate=True, simulate_all_series = True, evaluate_all_equations = True, adjust_implicit_data_ranges=True):
2322+
def get_matplotlib_fig(self, plot_style = None, update_and_validate=True, simulate_all_series = True, evaluate_all_equations = True, adjust_implicit_data_ranges=True, adjust_offset2d=True):
23162323
"""
23172324
Constructs and returns a matplotlib figure generated from fig_dict, with optional simulation, preprocessing, and styling.
23182325
@@ -2340,9 +2347,16 @@ def get_matplotlib_fig(self, plot_style = None, update_and_validate=True, simula
23402347
self.fig_dict = execute_implicit_data_series_operations(self.fig_dict,
23412348
simulate_all_series=simulate_all_series,
23422349
evaluate_all_equations=evaluate_all_equations,
2343-
adjust_implicit_data_ranges=adjust_implicit_data_ranges)
2350+
adjust_implicit_data_ranges=adjust_implicit_data_ranges,
2351+
adjust_offset2d=False)
23442352
#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.
23452353
original_fig_dict = copy.deepcopy(self.fig_dict) #we will get a copy, because otherwise the original fig_dict will be forced to be overwritten.
2354+
#We adjust the offsets only after copying, and adjust that one alone.
2355+
self.fig_dict = execute_implicit_data_series_operations(self.fig_dict,
2356+
simulate_all_series=False,
2357+
evaluate_all_equations=False,
2358+
adjust_implicit_data_ranges=False,
2359+
adjust_offset2d=adjust_offset2d)
23462360
#before cleaning and validating, we'll apply styles.
23472361
plot_style = parse_plot_style(plot_style=plot_style)
23482362
self.apply_plot_style(plot_style=plot_style)
@@ -4974,6 +4988,7 @@ def clean_json_fig_dict(json_fig_dict, fields_to_update=None):
49744988
- "trace_style": Removes internal style/tracetype metadata.
49754989
- "3d_axes": Updates layout and data_series for 3D plotting.
49764990
- "superscripts": Replaces superscript strings in titles and labels.
4991+
- "offset": Removes the offset field from the layout field.
49774992
"""
49784993
if fields_to_update is None: # should not initialize mutable objects in arguments line, so doing here.
49794994
fields_to_update = ["title_field", "extraInformation", "nested_comments"]
@@ -5410,7 +5425,7 @@ def update_implicit_data_series_data(target_fig_dict, source_fig_dict, parallel_
54105425
return updated_fig_dict
54115426

54125427

5413-
def execute_implicit_data_series_operations(fig_dict, simulate_all_series=True, evaluate_all_equations=True, adjust_implicit_data_ranges=True):
5428+
def execute_implicit_data_series_operations(fig_dict, simulate_all_series=True, evaluate_all_equations=True, adjust_implicit_data_ranges=True, adjust_offset2d=False):
54145429
"""
54155430
Processes data_series dicts in a fig_dict, executing simulate/equation-based series as needed, including setting the simulate/equation evaluation ranges as needed,
54165431
then provides the simulated/equation-evaluated data into the original fig_dict without altering original fig_dict implicit ranges.
@@ -5432,7 +5447,6 @@ def execute_implicit_data_series_operations(fig_dict, simulate_all_series=True,
54325447
while preserving their original metadata and x_range_default boundaries.
54335448
"""
54345449
import copy # Import inside function for modularity
5435-
54365450
# Create a copy for processing implicit series separately
54375451
fig_dict_for_implicit = copy.deepcopy(fig_dict)
54385452
#first check if any data_series have an equatinon or simulation field. If not, we'll skip.
@@ -5461,10 +5475,137 @@ def execute_implicit_data_series_operations(fig_dict, simulate_all_series=True,
54615475
fig_dict_for_implicit = evaluate_equations_as_needed_in_fig_dict(fig_dict_for_implicit)
54625476
# Copy results back without overwriting the ranges
54635477
fig_dict = update_implicit_data_series_data(target_fig_dict=fig_dict, source_fig_dict=fig_dict_for_implicit, parallel_structure=True, modify_target_directly=True)
5478+
5479+
if adjust_offset2d: #This should occur after simulations and evaluations because it could rely on them.
5480+
#First check if the layout style is that of an offset2d graph.
5481+
layout_style = fig_dict.get("plot_style", {}).get("layout_style", "")
5482+
if "offset2d" in layout_style:
5483+
#This case is different from others -- we will not modify target directly because we are not doing a merge.
5484+
fig_dict = extract_and_implement_offsets(fig_dict_for_implicit, modify_target_directly = False)
5485+
return fig_dict
5486+
5487+
#Small helper function to find if an offset is a float scalar.
5488+
def is_float_scalar(value):
5489+
try:
5490+
float(value)
5491+
return True
5492+
except (TypeError, ValueError):
5493+
return False
54645494

5495+
def extract_and_implement_offsets(fig_dict, modify_target_directly=False, graphical_dimensionality=2):
5496+
import numpy as np
5497+
#First, extract offsets.
5498+
import copy
5499+
if modify_target_directly == False:
5500+
fig_dict_with_offsets = copy.deepcopy(fig_dict)
5501+
else:
5502+
fig_dict_with_offsets = fig_dict
5503+
#initialize offset_variable_name as the case someone decides to specify one.
5504+
offset_variable_name = ""
5505+
if "offset" in fig_dict["layout"]:
5506+
#This is the easy case, because we don't need to determine the offset.
5507+
offset = fig_dict["layout"]["offset"]
5508+
if is_float_scalar(offset):
5509+
offset = fig_dict["layout"]["offset"]
5510+
elif isinstance(offset,str):#check if is a string, in which case it is a variable name.
5511+
#in this case it is a variable where we will extract it from each dataset.
5512+
offset_variable_name = offset
5513+
else:
5514+
#Else assume the offset is an array like object, of length equal to number of datapoints.
5515+
offset = np.array(offset, dtype=float)
5516+
#Now, implement offsets.
5517+
if graphical_dimensionality == 2:
5518+
current_series_offset = 0 # Initialize total offset
5519+
for data_series_index in range(len(fig_dict["data"])):
5520+
data_series_y_values = np.array(fig_dict["data"][data_series_index]["y"])
5521+
if data_series_index == 0:
5522+
fig_dict_with_offsets["data"][data_series_index]["y"] = list(data_series_y_values)
5523+
else:
5524+
# Determine the current offset
5525+
if offset_variable_name != "":
5526+
incremental_offset = np.array(fig_dict["data"][data_series_index][offset_variable_name], dtype=float)
5527+
else:
5528+
incremental_offset = np.array(offset, dtype=float)
5529+
current_series_offset += incremental_offset # Accumulate the offset
5530+
fig_dict_with_offsets["data"][data_series_index]["y"] = list(data_series_y_values + current_series_offset)
5531+
else:
5532+
#This is the hard case, we need to determine a reasonable offset for the dataseries.
5533+
if graphical_dimensionality == 2:
5534+
fig_dict_with_offsets = determine_and_apply_offset2d_for_fig_dict(fig_dict, modify_target_directly=modify_target_directly)
5535+
return fig_dict_with_offsets
5536+
5537+
#A function that calls helper functions to determine and apply a 2D offset to fig_dict
5538+
def determine_and_apply_offset2d_for_fig_dict(fig_dict, modify_target_directly=False):
5539+
if modify_target_directly == False:
5540+
import copy
5541+
fig_dict = copy.deepcopy(fig_dict)
5542+
#First, extract data into a numpy array like [[x1, y1], [x2, y2], ...]
5543+
all_series_array = extract_all_xy_series_data_from_fig_dict(fig_dict)
5544+
#Then determine and apply a vertical offset. For now, we'll only support using the default
5545+
#argument which is 1.2 times the maximum height in the series.
5546+
#If someone wants to do something different, they can provide their own vertical offset value.
5547+
offset_data_array = apply_vertical_offset2d_for_numpy_arrays_list(all_series_array)
5548+
#Then, put the data back in.
5549+
fig_dict = inject_xy_series_data_into_fig_dict(fig_dict=fig_dict, data_list = offset_data_array)
54655550
return fig_dict
54665551

5552+
def extract_all_xy_series_data_from_fig_dict(fig_dict):
5553+
"""
5554+
Extracts all x and y values from a Plotly figure dictionary into a list of NumPy arrays.
5555+
Each array in the list has shape (n_points, 2), where each row is [x, y] like [[x1, y1], [x2, y2], ...].
5556+
"""
5557+
import numpy as np
5558+
series_list = []
5559+
for data_series in fig_dict.get('data', []):
5560+
x_vals = np.array(data_series.get('x', []))
5561+
y_vals = np.array(data_series.get('y', []))
5562+
#Only keep the xy data if x and y lists are the same length.
5563+
if len(x_vals) == len(y_vals):
5564+
series_list.append(np.stack((x_vals, y_vals), axis=-1))
5565+
return series_list
54675566

5567+
def apply_vertical_offset2d_for_numpy_arrays_list(data_list, offset_multiplier=1.2):
5568+
"""
5569+
Applies vertical offsets to a list of 2D NumPy arrays.
5570+
Each array has shape (n_points, 2), with rows like [[x1, y1], [x2, y2], ...].
5571+
Returns a list of the same structure with adjusted y values per series index.
5572+
"""
5573+
import numpy as np
5574+
spans = [np.max(series[:, 1]) - np.min(series[:, 1]) if len(series) > 0 else 0 for series in data_list]
5575+
base_offset = max(spans) * offset_multiplier if spans else 0
5576+
offset_data_list = []
5577+
for series_index, series_array in enumerate(data_list):
5578+
# Skip empty series but preserve its position in the output
5579+
if len(series_array) == 0:
5580+
offset_data_list.append(series_array)
5581+
continue
5582+
# Ensure float type for numerical stability when applying offsets
5583+
offset_series = np.copy(series_array).astype(float)
5584+
# Apply vertical offset based on series index and base offset
5585+
#print("line 5574, before the addition", offset_series)
5586+
offset_series[:, 1] += series_index * base_offset
5587+
#print("line 5576, after the addition", offset_series)
5588+
# Add the adjusted series to the output list
5589+
offset_data_list.append(offset_series)
5590+
return offset_data_list
5591+
5592+
5593+
5594+
5595+
def inject_xy_series_data_into_fig_dict(fig_dict, data_list):
5596+
"""
5597+
Updates a Plotly figure dictionary in-place by injecting x and y data from a list of NumPy arrays.
5598+
Each array must have shape (n_points, 2), where each row is [x, y] like [[x1, y1], [x2, y2], ...].
5599+
The number of arrays must match the number of traces in the figure.
5600+
"""
5601+
n_traces = len(fig_dict.get('data', []))
5602+
if len(data_list) != n_traces:
5603+
raise ValueError("Mismatch between number of traces and number of data series.")
5604+
for i, trace in enumerate(fig_dict['data']):
5605+
series = data_list[i]
5606+
trace['x'] = series[:, 0].tolist()
5607+
trace['y'] = series[:, 1].tolist()
5608+
return fig_dict
54685609

54695610
### 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###
54705611

examples/README_examples.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,6 @@ example_9_scatter3d_and_mesh3d_plots shows an example of 3D plots (both scatter3
2525

2626
example_10_bubble_plot shows an example of how to create a bubble2d plot and also a bubble3d plot. The bubble size adds an extra dimension to the image.
2727

28-
example_11_local_python_call shows an example where the json record calls a local python function to simulate and populate a dataseries. This feature requires the user's script to call the feature, for security reasons. The JSON record cannot call functions "by itself".
28+
example_11_local_python_call shows an example where the json record calls a local python function to simulate and populate a dataseries. This feature requires the user's script to call the feature, for security reasons. The JSON record cannot call functions "by itself".
29+
30+
example_12_offset_and_series2dTo3d shows an example where a series of 2d spectra are offset to or converted into a 3d image. This can also be done with timeseries, scatter, etc.

0 commit comments

Comments
 (0)