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 0000000000..0a1cf539ed Binary files /dev/null and b/rrnl/__pycache__/classes.cpython-312.pyc differ diff --git a/rrnl/__pycache__/setters.cpython-312.pyc b/rrnl/__pycache__/setters.cpython-312.pyc new file mode 100644 index 0000000000..fc9381a72a Binary files /dev/null and b/rrnl/__pycache__/setters.cpython-312.pyc differ diff --git a/rrnl/asthmacontrol.json b/rrnl/asthmacontrol.json new file mode 100644 index 0000000000..a9efaf297d --- /dev/null +++ b/rrnl/asthmacontrol.json @@ -0,0 +1,31 @@ +{ + "title": { + "text": "Level of Asthma Control", + "font": "Arial", + "fontSize": 13, + "color": "black" + }, + "data": { + "categories": [ + {"name": "Controlled", "end": 1.5, "color": "green"}, + {"name": "Not Controlled", "end": 6.0, "color": "red"} + ], + "start": 0 + }, + "separation": { + "color": "black", + "width": 5 + }, + "labels": { + "position": "on" + }, + "tickmarks": { + "tickCount": 12, + "position": "bottom" + }, + "value_indicator": { + "value": 3.2, + "title": "Victor's Score", + "overlap": false + } +} \ No newline at end of file diff --git a/rrnl/classes.py b/rrnl/classes.py new file mode 100644 index 0000000000..cd500ea1aa --- /dev/null +++ b/rrnl/classes.py @@ -0,0 +1,73 @@ +class Title: + def __init__ (self, text: str): + self.text = text + self.align = "center" + self.font = "Arial" + self.fontSize = 14 + self.color = "black" + + def set_align(self, align: str): + self.align = align + + def set_font(self, font: str): + self.font = font + + def set_fontSize(self, fontSize: int): + self.fontSize = fontSize + + def set_color(self, color: str): + self.color = color + +class Data: + def __init__ (self, categories: list): + self.categories = categories + self.start = 0 + + def set_start(self, start: int): + self.start = start + +class CategorySeparation: + def __init__ (self): + self.color = "black" + self.width = 0 + + def set_color(self, color: str): + self.color = color + + def set_width(self, width: int): + self.width = width + +class CategoryLabels: + def __init__ (self): + self.position = "on" + + def set_position(self, position: str): + self.position = position + +class TickMarks: + def __init__ (self): + self.tickCount = 10 + self.position = "bottom" + + def set_tickCount(self, tickCount: int): + self.tickCount = tickCount + + def set_position(self, position: str): + self.position = position + + + +class ValueIndicator: + def __init__ (self, value: float, title: str): + self.value = value + self.title = title + self.overlap = False + + def set_value(self, value: int): + self.value = value + + def set_title(self, title: str): + self.title = title + + def set_overlap(self, overlap: bool): + self.overlap = overlap \ No newline at end of file diff --git a/rrnl/d3.html b/rrnl/d3.html new file mode 100644 index 0000000000..460fd6d6c5 --- /dev/null +++ b/rrnl/d3.html @@ -0,0 +1,98 @@ + + + + + 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