From edcd246963be434a31cc17e598e36dc80075db1c Mon Sep 17 00:00:00 2001 From: zmedailleu Date: Mon, 16 Mar 2026 17:36:18 -0400 Subject: [PATCH] Added RRNL generation code in its own folder --- rrnl/README.md | 21 ++ rrnl/__pycache__/classes.cpython-312.pyc | Bin 0 -> 4538 bytes rrnl/__pycache__/setters.cpython-312.pyc | Bin 0 -> 2856 bytes rrnl/asthmacontrol.json | 31 ++ rrnl/classes.py | 73 +++++ rrnl/d3.html | 98 +++++++ rrnl/numberline_to_vega.py | 343 +++++++++++++++++++++++ rrnl/plateletcount.json | 32 +++ rrnl/setters.py | 37 +++ rrnl/visualization.svg | 1 + 10 files changed, 636 insertions(+) create mode 100644 rrnl/README.md create mode 100644 rrnl/__pycache__/classes.cpython-312.pyc create mode 100644 rrnl/__pycache__/setters.cpython-312.pyc create mode 100644 rrnl/asthmacontrol.json create mode 100644 rrnl/classes.py create mode 100644 rrnl/d3.html create mode 100644 rrnl/numberline_to_vega.py create mode 100644 rrnl/plateletcount.json create mode 100644 rrnl/setters.py create mode 100644 rrnl/visualization.svg diff --git a/rrnl/README.md b/rrnl/README.md new file mode 100644 index 0000000000..fc3100cdb1 --- /dev/null +++ b/rrnl/README.md @@ -0,0 +1,21 @@ +# Creating RRNLs for Vega + +Using this program, you can easily create your own reference range number lines. + +* RRNL visualizations are created using the `numberline_to_vega.py` program. When running it, make sure that the file for creating your RRNL is in the same directory as this program, and that the files `classes.py` and `setters.py` are there as well. + - make sure that your JSON file for creating your RRNL is valid in the RRNL domain-specific language. Look at one of the included examples, like `plateletcount.json`, to see how these files are formatted. +* When you run the program, you will be prompted to put in the name of a file. Type the name of a file in the directory that you wish to convert to a Vega object. +* The generated `vega.json` file will be a valid Vega object that will generate a RRNL based on the inputted file. You can insert the Vega object into [the Vega Editor](https://vega.github.io/editor) to view and export the RRNL. + - You can also view the RRNL embedded in an HTML file using the generated `vega.html` file. + - If your file is an array of multiple objects in the RRNL domain-specific language, then a separate JSON file will be created for each object, titled `vega1.json`, `vega2.json`, etc. Every RRNL in the file will be included in `vega.html`. +* If you are familiar with Vega, you can edit the outputted Vega object to make any changes to your RRNL that aren't possible with this program. + +## Adding Gradients + +If you wish to add a gradient to your RRNL, you can do so using `d3.html`. + +* Paste the output from `numberline_to_vega.py` into the Vega Editor and click "Export" at the top of the page. +* Click "Download" under "Export to SVG". +* Add the downloaded SVG file to the same directory as `d3.html`. +* In `d3.html`, change the path nme in the line `const svg = await d3.xml("/rrnl/visualization.svg");` to the path to your SVG file. +* Open `d3.html` locally to view your RRNL. \ No newline at end of file diff --git a/rrnl/__pycache__/classes.cpython-312.pyc b/rrnl/__pycache__/classes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0a1cf539ed99ce2006d165955e384b3e6ff5d316 GIT binary patch literal 4538 zcmbVPOK;p%6t+F~ypoxg(7fAb(iW&u2@e$^N~u~}3N3U1r4%WOG;(aG9qNq3wNsc; z7A&&pg09nLmqh#qegRt+k+-b6OFKxBy5fAtb3Nn9q@rVKj?X>!_02ioJ?HwbQmLrW z^Y^*!`kz@v`3DX8=pCiKNy@gODPJg#vZXlcA6neA6x%eiWfGgp<8-naiA`f$mTbAi zmSJ0-Y=y+8v#m(Bk;G;+N6nHgG2z|WU1@(wjcvtMmQ+Vs(j3*ztZI(7r7!7DhIO5E z!!cOTlAd*PZjQ#}NzXe4wiih+G)K(Q$hch(JvY)r_m|L?o5&3_q_l7HhMVf9QiGiz z2Q4*eY1B&QIvHwVCO?i&E$LLHshCDJ%Gz!tbVX!b5p~OZTBd=KvEo_webW#a8yS{Y zzuSoPRlgBNdC0|j%Z&`%_k3Y$ksi3-Dh~~?Z>93$^3@B=-v_P;mRpv6&szS$T?s;8 ztbHr|A6+{PmW8|O3b$dq?+B}L*Cm5!c+0kD1%VqZtT&^4tyXW;!&+64Ygl81#H*kXuhRs=fq4=K z^c0Rfwna#!pJaO!BReSIr$>bt1?xI5teq-q^4K@fPC{2ual}f@lqE&8^j+APHrghMA4@ zYgTAQMcWG9yS}Kq!C)#>c*XRwNM{+!9a{A%un;NUqE}=Dp(R2|5tw>|!tQI8uzhlr zCol$XrGqkahEDB~-SLU;d3l?92E!YCYUO#sJP8AOFO2xyGSXI=!GO8ko}r<=)X0fB zSSk?nBqH6b6AzdMr(rw?0pG{*0ma3eQh;LtmPF_^WAe$hwnDGHe1BLmV{ll3y*I2| z%2LMBT%7`%p@15W?q*3hNY6F1W-c1L8i%#H=&oDB3hREuPRar;2Eu?v?Fxx)s-jd3 zNQ)l_Wg{6);muS9(uXifkUMe8BID<}6W*iqhol%_bS0)dywaLIxPKCB91ZTEu zrJujHR$MO_%(wWwruoJRB+~rGjvD3H{h$uIiVtYd1H76{?~!l{%#Xq&6++%oGZt{7@l+D(>^Eyyl73N0Z4i=KYHjjI9+g(xK7ImP8@@X zb{V}8NB}`MUCbk0oPNT$lhX%DhZ!l->14puzD%MwUAS?vq7NZ1Rw@@O+(dGnB8I}1 zcd~AQidc^HVl!_RqVgTf+i-6*oVsn%CqraBKsz!*d>Pp|#qvo`^#WP?FY%kHa>l7A z$q1Z!uA3QglO~FY#+03&$|J;htT?6QwqQ+>cdT<-adCUtAfFc9D*2?6>+2ppe*5V+^_6a;aL zn#170Jc$E(3dacka3_$hnE}q|^x>mjp5e)pNocC(NMEjwj8)IK!uSw%FMi+9eDNKL zNMG@NPb{K+1_CKRz7XAj262&o!N(+U5zNpxz{Z|JuixpKPp`cq-v@hZbE3~ZIge~V z7q5!7s%AF+xZrCh4lb7pKE}LRABq?Gi}4rDC2@xaT7X7kf5K4bpD85v%LQm8_Ge1!+;fElL;4Tw$5k)@ literal 0 HcmV?d00001 diff --git a/rrnl/__pycache__/setters.cpython-312.pyc b/rrnl/__pycache__/setters.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fc9381a72aae328001bd91eff0b1bbdde33a502d GIT binary patch literal 2856 zcmdT`O-vg{6y9Br|1du>#&!xuxDgbb1gcP)pOWUMkeEbZs)huqqD5=(1`LZA%`QoZ zKysu?y&Cm^d-IVNRpQ8zdoNzCrL{)6^^{u$xAf9CvtA0+AW^l4uC&iH^WJ*$g4IDgw^lxTo+#L5Coj8wB8ypP6V3U(LiC4H^1fG+G@3^md$puo75J;Eg28kpO zNVnt#=~?w9{O>40f+zgt3}3^dKOT5p@E(l8>L#Zj>ek?d4?rOaRBqSS*ZDPmf!d#V z9P0(R_sK_cNoc!ve+P%>0LOdMlH<0lC)wBXyJk3_xHGDvV&YCKVrDVn zA?_)Lg@eH@JqAp zMg`rh1}@~s58zH*{ZQ9Ivw0Y5c7U(*M?#->!gO6&6aJ-e7a%O$DIA8gNkc}|ueitr+a;r(^)mWpi$Sy<8d5e%QOlY-CZ!Hk*wVT zk>{$xaN+qztf&~lEBVK7TSA4$W=mIotcn^B9sF=(&5HIK(cUfZPSA{gQb9x2KqxGm=! z>GQVx5osN_zsx+MT zUsDAxMk(S^#I&qv#J8+!S?Xvas5yx0X2-4Lww{$N$AAE40f5p5A%P=bt!>}e+n!om}L`nDc#7d84LI-SRYbB zb+Zih7!}RVK3j)xz@8YqPE-fmf9R-@5HnD$I90l0bPiaZgGT4z&hVaKc8*lg=uwh7 zj99-FyJp0$?OfiSH)F{P8as_8+B2|zr3izgQSEdNb~$%u^@{hgex%lyu(DOBcR3U1 zlzs$q!J8s2FGOWUp3UZ@EW|YZqO%3|H6|SKsxM@$h|3RYdOV}1j~q+8*aWDXYeqFn z@Ga_Q`%7+@8kQC(4#IcA>Jqhl3IfVZq`Nd#zC3L7+}@owdLHcQ z%tBeonMu`*EaxYyC}g2719g>9x#zkOzhT9Pjrj2HqrDX~K2brbYM{N0+FA0X5? + + + + RRNL with Gradient + + +
+ + + \ No newline at end of file diff --git a/rrnl/numberline_to_vega.py b/rrnl/numberline_to_vega.py new file mode 100644 index 0000000000..ce43715769 --- /dev/null +++ b/rrnl/numberline_to_vega.py @@ -0,0 +1,343 @@ +import json + +#open file based on user input +filename = input("Enter the name of a RRNL file in this directory: ") +try: + file = open(filename) +except: + print("The inputted file could not be found.") + quit() + +file_data = json.load(file) +all_files = [] +current_file_number = 1 +html_objects = [] +value_indicator_present = True + +from classes import * +from setters import * + +#check if user put in just one object or multiple +if isinstance(file_data, dict): + all_files = [file_data] +elif isinstance(file_data, list): + all_files = file_data + +for object_data in all_files: + #set the data from the number line json to python objects + try: + json_width = object_data["width"] + except: + json_width = 300 + try: + json_title = set_title(object_data) + except: + print("A title is required.") + quit() + + try: + json_data = set_data(object_data) + except: + print("Data for the number line is required.") + quit() + + if "separation" in object_data.keys(): + json_category_separation = set_category_separation(object_data) + else: + json_category_separation = CategorySeparation() + + if "labels" in object_data.keys(): + json_labels = set_category_labels(object_data) + else: + json_labels = CategoryLabels() + + if "tickmarks" in object_data.keys(): + json_tick_marks = set_tick_marks(object_data) + else: + json_tick_marks = TickMarks() + + if "value_indicator" in object_data.keys(): + json_value_indicator = set_value_indicator(object_data) + else: + value_indicator_present = False + + + #convert it to a dict + vega_object = {} + + vega_title = {} + + vega_data = [] + vega_data_table = {} + data_values = [] + vega_data_stacked = {} + vega_data_separators = {} + + vega_scales = [] + vega_axes = [] + vega_marks = [] + + + #set title properties + vega_title["text"] = json_title.text + vega_title["align"] = json_title.align + vega_title["font"] = json_title.font + vega_title["fontSize"] = json_title.fontSize + vega_title["color"] = json_title.color + + + #used to move all objects by a certain amount + offset = 0 + indicator_offset = 0 + category_label_y = 0 + + if (json_tick_marks.position == "top"): + if (json_labels.position == "over"): + category_label_y = "0" + indicator_offset = -15 + offset = 30 + elif (json_labels.position == "on"): + category_label_y = "height/2" + indicator_offset = -15 + elif (json_labels.position == "under"): + category_label_y = "height + 10" + elif (json_tick_marks.position == "bottom"): + if (json_labels.position == "over"): + category_label_y = "0" + offset = 10 + elif (json_labels.position == "on"): + category_label_y = "height/2" + elif (json_labels.position == "under"): + category_label_y = "height + 30" + indicator_offset = 20 + + + + #set data values + category_index = 0 #used to set order for categories in number line + last_end_value = 0 + for json_category in json_data.categories: + old_end_value = json_category["end"] + json_category["end"] -= last_end_value + last_end_value = old_end_value + #add new order property to sort categories properly + json_category["order"] = category_index + category_index += 1 + data_values.append(json_category) + vega_data_table["name"] = "table" + vega_data_table["values"] = data_values + + + #shift offset if any labels have more than one space + if (any(" " in value["name"] for value in json_data.categories)): + if (json_labels.position == "over"): + offset += 12 + elif (json_labels.position == "under"): + indicator_offset += 15 + + #create stacked data set + vega_data_stacked["name"] = "stacked" + vega_data_stacked["source"] = "table" + vega_data_stacked["transform"] = [{"type":"stack", "field":"end", "sort":{"field":"order"}, "as":["x0", "x1"]}, + {"type": "formula", "as": "x0", "expr": "datum.order === 0 ? datum.x0 + {} : datum.x0".format(json_data.start)}] + + #create separator data set + vega_data_separators["name"] = "separators" + vega_data_separators["source"] = "stacked" + vega_data_separators["transform"] = [{"type": "filter", "expr": "datum.x1 < data('stacked')[data('stacked').length - 1].x1"}] + + #create scales and axes + vega_scales.append({"name":"xscale", "domain":{"data":"stacked", "field":"x1"}, "range":"width", "domainMin": json_data.start, "zero": False}) + vega_axes.append({"orient":json_tick_marks.position, "scale":"xscale", "offset":offset if json_tick_marks.position == "bottom" else -offset, + "tickCount":json_tick_marks.tickCount}) + + + #initialize all marks + vega_marks_bars = {} + vega_marks_category_labels = {} + vega_marks_separators = {} + vega_marks_arrow_line = {} + vega_marks_arrow_triangle = {} + vega_marks_indicator_text = {} + vega_marks_indicator_number = {} + vega_marks_overlap = {} + + + #create mark to draw each section of RRNL + + #BARS + vega_marks_bars["type"] = "rect" + vega_marks_bars["from"] = {"data":"stacked"} + bars_data = {} + bars_data["height"] = {"value": 30} + bars_data["x"] = {"scale": "xscale", "field": "x0"} + bars_data["x2"] = {"scale": "xscale", "field": "x1"} + bars_data["y"] = {"signal": "{}".format(offset)} + bars_data["fill"] = {"field": "color"} + vega_marks_bars["encode"] = {"enter": bars_data} + + #LABELS + vega_marks_category_labels["type"] = "text" + vega_marks_category_labels["from"] = {"data":"stacked"} + category_labels_data = {} + category_labels_data["height"] = {"value": 30} + #usual height: "signal":"height / 2" + category_labels_data["y"] = {"signal":category_label_y} + category_labels_data["dy"] = {"signal":"(indexof(datum.name, ' ') >= 0) && {} ? -6 : 0".format(str(json_labels.position == "on").lower())} + category_labels_data["align"] = {"value":"center"} + category_labels_data["baseline"] = {"value":"middle"} + category_labels_data["fill"] = {"value":"black"} + if len(json_data.categories) > 4: + category_labels_data["fontSize"] = {"value":9} + else: + category_labels_data["fontSize"] = {"value":12} + category_labels_data["text"] = {"field":"name"} + category_labels_data["lineBreak"] = {"value":" "} + vega_marks_category_labels["encode"] = {"enter": category_labels_data, "update":{"x":{"signal":"scale('xscale', (datum.x0 + datum.x1) / 2)"}}} + + #SEPARATORS + vega_marks_separators["type"] = "rule" + vega_marks_separators["from"] = {"data":"separators"} + separators_data = {} + separators_data["x"] = {"scale":"xscale", "field":"x1"} + separators_data["y"] = {"signal": "{}".format(offset)} + separators_data["y2"] = {"signal":"height + {}".format(offset)} + separators_data["stroke"] = {"value":json_category_separation.color} + separators_data["strokeWidth"] = {"value":json_category_separation.width} + vega_marks_separators["encode"] = {"enter": separators_data} + + if value_indicator_present: + #ARROW LINE + vega_marks_arrow_line["type"] = "rule" + arrow_line_data = {} + arrow_line_data["x"] = {"scale":"xscale", "value":json_value_indicator.value} + arrow_line_data["y"] = {"signal":"height + 25 + {} + {}".format(offset, indicator_offset)} + arrow_line_data["y2"] = {"signal":"height + 70 + {} + {}".format(offset, indicator_offset)} + arrow_line_data["stroke"] = {"value":"black"} + arrow_line_data["strokeWidth"] = {"value":3} + vega_marks_arrow_line["encode"] = {"enter": arrow_line_data} + + #ARROW POINT + vega_marks_arrow_triangle["type"] = "symbol" + arrow_triangle_data = {} + arrow_triangle_data["x"] = {"scale":"xscale", "value":json_value_indicator.value} + arrow_triangle_data["y"] = {"signal":"height + 25 + {} + {}".format(offset, indicator_offset)} + arrow_triangle_data["shape"] = {"value":"triangle-up"} + arrow_triangle_data["fill"] = {"value":"black"} + arrow_triangle_data["size"] = {"value":150} + vega_marks_arrow_triangle["encode"] = {"enter": arrow_triangle_data} + + #VALUE INDICATOR TEXT + if json_value_indicator.title: + vega_marks_indicator_text["type"] = "text" + indicator_text_data = {} + indicator_text_data["x"] = {"scale":"xscale", "value":json_value_indicator.value} + indicator_text_data["y"] = {"signal":"height + 85 + {} + {}".format(offset, indicator_offset)} + indicator_text_data["text"] = {"value":json_value_indicator.title} + indicator_text_data["align"] = {"value":"center"} + indicator_text_data["fill"] = {"value":"black"} + indicator_text_data["fontSize"] = {"value":14} + vega_marks_indicator_text["encode"] = {"enter": indicator_text_data} + + #VALUE INDICATOR VALUE + vega_marks_indicator_number["type"] = "text" + indicator_number_data = {} + indicator_number_data["x"] = {"scale":"xscale", "value":json_value_indicator.value} + if not json_value_indicator.title: + indicator_offset = indicator_offset - 15 + indicator_number_data["y"] = {"signal":"height + 115 + {} + {}".format(offset, indicator_offset)} + indicator_number_data["text"] = {"value":json_value_indicator.value} + indicator_number_data["align"] = {"value":"center"} + indicator_number_data["fill"] = {"value":"black"} + indicator_number_data["fontSize"] = {"value":30} + vega_marks_indicator_number["encode"] = {"enter": indicator_number_data} + + if json_value_indicator.overlap: + vega_marks_overlap["type"] = "rule" + overlap_data = {} + overlap_data["x"] = {"scale":"xscale", "value":json_value_indicator.value} + overlap_data["y"] = {"signal": "{}".format(offset)} + overlap_data["y2"] = {"signal":"height + {}".format(offset)} + overlap_data["stroke"] = {"value":"black"} + overlap_data["strokeWidth"] = {"value":3} + vega_marks_overlap["encode"] = {"enter": overlap_data} + + + + #add all marks to vega_marks + vega_marks.append(vega_marks_bars) + vega_marks.append(vega_marks_category_labels) + vega_marks.append(vega_marks_separators) + if value_indicator_present: + vega_marks.append(vega_marks_arrow_line) + vega_marks.append(vega_marks_arrow_triangle) + if json_value_indicator.title: + vega_marks.append(vega_marks_indicator_text) + vega_marks.append(vega_marks_indicator_number) + if json_value_indicator.overlap: + vega_marks.append(vega_marks_overlap) + + #add all data sets to data_values + vega_data.append(vega_data_table) + vega_data.append(vega_data_stacked) + vega_data.append(vega_data_separators) + + #add all these properties to the vega object + vega_object["width"] = json_width + vega_object["height"] = 30 + vega_object["padding"] = 10 + vega_object["title"] = vega_title + vega_object["data"] = vega_data + vega_object["scales"] = vega_scales + vega_object["axes"] = vega_axes + vega_object["marks"] = vega_marks + + + + #turn the vega_object dict into json that can be used in vega + vega_json = json.dumps(vega_object) + html_objects.append(json.dumps(vega_object, indent=4)) + + if (len(all_files) == 1): + with open("vega.json", mode="w", encoding="utf-8") as vega_file: + json.dump(vega_object, vega_file, indent=4) + else: + with open("vega{}.json".format(current_file_number), mode="w", encoding="utf-8") as vega_file: + json.dump(vega_object, vega_file, indent=4) + current_file_number = current_file_number + 1 + + +#write all json objects to html +html_start = f""" + + + + Vega + + + + + +""" + +html_body = "" + +for index, object in enumerate(html_objects): + html_body += f"""
\n""" + +html_body += """ + + +""" + +object_as_html = html_start + html_body + html_end + +with open("vega.html", "w") as f: + f.write(object_as_html) \ No newline at end of file diff --git a/rrnl/plateletcount.json b/rrnl/plateletcount.json new file mode 100644 index 0000000000..d72812ab5c --- /dev/null +++ b/rrnl/plateletcount.json @@ -0,0 +1,32 @@ +{ + "width": 500, + "title": { + "text": "Platelet Count (Plt) Test Result", + "font": "sans-serif", + "fontSize": 16, + "color": "black" + }, + "data": { + "categories": [ + {"name": "Very Low", "end": 20, "color": "red"}, + {"name": "Low", "end": 100, "color": "orange"}, + {"name": "Borderline Low", "end": 150, "color": "yellow"}, + {"name": "STANDARD RANGE", "end": 400, "color": "green"}, + {"name": "Borderline High", "end": 450, "color": "yellow"}, + {"name": "High", "end": 500, "color": "orange"} + ], + "start": 0 + }, + "labels": { + "position": "over" + }, + "tickmarks": { + "tickCount": 8, + "position": "bottom" + }, + "value_indicator": { + "value": 135, + "title": "Your Result: 135x10^9/L", + "overlap": true + } +} diff --git a/rrnl/setters.py b/rrnl/setters.py new file mode 100644 index 0000000000..2a8fef7d5a --- /dev/null +++ b/rrnl/setters.py @@ -0,0 +1,37 @@ +from classes import * + +def set_title(data): + title = Title(data["title"]["text"]) + title.set_align(data["title"].get("align", "center")) + title.set_font(data["title"].get("font", "Arial")) + title.set_fontSize(data["title"].get("fontSize", 14)) + title.set_color(data["title"].get("color", "black")) + return title + +def set_data(data): + categories = data["data"]["categories"] + data_obj = Data(categories) + data_obj.set_start(data["data"].get("start", 0)) + return data_obj + +def set_category_separation(data): + cat_sep = CategorySeparation() + cat_sep.set_color(data["separation"].get("color", "black")) + cat_sep.set_width(data["separation"].get("width", 0)) + return cat_sep + +def set_category_labels(data): + cat_labels = CategoryLabels() + cat_labels.set_position(data["labels"].get("position", "on")) + return cat_labels + +def set_tick_marks(data): + tick_marks = TickMarks() + tick_marks.set_tickCount(data["tickmarks"].get("tickCount", 10)) + tick_marks.set_position(data["tickmarks"].get("position", "bottom")) + return tick_marks + +def set_value_indicator(data): + value_indicator = ValueIndicator(data["value_indicator"]["value"], data["value_indicator"].get("title", "")) + value_indicator.set_overlap(data["value_indicator"].get("overlap", False)) + return value_indicator \ No newline at end of file diff --git a/rrnl/visualization.svg b/rrnl/visualization.svg new file mode 100644 index 0000000000..0e93387407 --- /dev/null +++ b/rrnl/visualization.svg @@ -0,0 +1 @@ +050100150200250300350400450500VeryLowLowBorderlineLowSTANDARDRANGEBorderlineHighHighYour Result: 135x10^9/L135Platelet Count (Plt) Test Result \ No newline at end of file