From 70ba04b27da2080e14edeea9ca81075bfa35da3d Mon Sep 17 00:00:00 2001 From: superMass14 Date: Fri, 28 Nov 2025 15:53:40 +0000 Subject: [PATCH 1/3] chore: add demo.ipynb to .gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0a435cc..e322d99 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *__pycache__ -site \ No newline at end of file +site +demo.ipynb \ No newline at end of file From d7403ab2ba27444ecef0ef63b76b708a83f7caad Mon Sep 17 00:00:00 2001 From: superMass14 Date: Fri, 28 Nov 2025 15:53:47 +0000 Subject: [PATCH 2/3] feat: enhance modular_graph to support custom visualization mode --- circular_graph/modular_graph.py | 358 +++++++++++++++++++++++++++++++- 1 file changed, 352 insertions(+), 6 deletions(-) diff --git a/circular_graph/modular_graph.py b/circular_graph/modular_graph.py index 10ebc1f..2d3b390 100644 --- a/circular_graph/modular_graph.py +++ b/circular_graph/modular_graph.py @@ -22,7 +22,7 @@ def __init__( piscines_list: list[str], checkpoints_list: list[str], mandatory_list: list[str], - kind: Literal["classic", "distribution"] = "classic", + kind: Literal["classic", "distribution", "custom"] = "classic", ): """Initialize a modular_graph instance. @@ -36,13 +36,15 @@ def __init__( (for "classic") or to pandas.Series distributions (for "distribution"). - classic : {project : value} - distribution : {project : pd.Series({'min':..,'max':..,'median':..,'q1':..,'q3':..})} + - custom : {project : {'key1': value1, 'key2': value2, ...}} piscines_list (list[str]): List of "piscines". checkpoints_list (list[str]): List of checkpoints. mandatory_list (list[str]): List of project names that should be rendered as mandatory (star) icons. - kind (Literal['classic','distribution'], optional): Visualization mode. + kind (Literal['classic','distribution','custom'], optional): Visualization mode. "classic" treats each content as a single numeric value; "distribution" expects pandas.Series values with statistical keys. + "custom" expects dictionary values with consistent keys. Defaults to "classic". Side effects: @@ -157,6 +159,7 @@ def __init__( self.CURRENT_CENTER = 1000 self.data = data + self.keys = None # Initialize keys attribute if self.kind == "classic": try: self.max_value = max(data.values()) @@ -169,6 +172,26 @@ def __init__( ) except: self.max_value = 0 + elif self.kind == "custom": + # Validate that all values are dictionaries and have the same keys + first_val = next(iter(data.values())) + if not isinstance(first_val, dict): + raise ValueError( + "For kind='custom', all data values must be dictionaries." + ) + self.keys = list(first_val.keys()) + for key, val in data.items(): + if not isinstance(val, dict) or list(val.keys()) != self.keys: + raise ValueError( + f"Inconsistent keys in data for project '{key}'. All dictionaries must have keys: {self.keys}" + ) + + # Set max_value based on the first key + try: + first_key = self.keys[0] + self.max_value = max(v[first_key] for v in data.values()) + except: + self.max_value = 0 self.piscines_list = piscines_list self.checkpoints_list = checkpoints_list self.mandatory_list = mandatory_list @@ -436,7 +459,9 @@ def render_star_icon(self, x, y, fill, width, name, content_name, value): "id": name, "project-name": content_name, "data-tooltip": str(value), - "onpointerenter": show_info_card(self.kind), + "onpointerenter": show_info_card( + self.kind, self.keys if self.kind == "custom" else None + ), "onpointerleave": 'document.getElementById("info_card").style.visibility = "hidden";', }, ) @@ -580,6 +605,15 @@ def render_content( is_sub_content, content_name, ) + case "custom": + self.render_custom_content( + parent_group, + content_item_data, + circle_props_from_parent, + object_attrs, + is_sub_content, + content_name, + ) case _: self.render_classic_content( parent_group, @@ -736,7 +770,9 @@ def render_classic_content( "id": name, "project-name": name if not content_name else content_name, "data-tooltip": str(value), - "onpointerenter": show_info_card(self.kind), + "onpointerenter": show_info_card( + self.kind, self.keys if self.kind == "custom" else None + ), "onpointerleave": 'document.getElementById("info_card").style.visibility = "hidden";', }, ) @@ -918,8 +954,10 @@ def render_distribution_content( "cy": str(y), "id": name, "project-name": name if not content_name else content_name, - "data-tooltip": f"{value.to_json() if isinstance(value, pd.Series) else {}}", - "onpointerenter": show_info_card(self.kind), + "data-tooltip": f"{value.to_json() if isinstance(value, pd.Series) else '{}'}", + "onpointerenter": show_info_card( + self.kind, self.keys if self.kind == "custom" else None + ), "onpointerleave": 'document.getElementById("info_card").style.visibility = "hidden";', }, ) @@ -1552,6 +1590,8 @@ def generate_info_card(self) -> ET2.Element: return self.generate_distribution_info_card() elif self.kind == "classic": return self.generate_classic_info_card() + elif self.kind == "custom": + return self.generate_custom_info_card() else: print(f"Unknown kind '{self.kind}' for info card generation.") return None @@ -1806,3 +1846,309 @@ def show(self): max_val=int(self.max_value), ) display(HTML(self.graph_svg_text)) + + ############################################################################################################################### + ############################################################################################################################### + # component generation function for info card (type=custom) + def generate_custom_info_card(self) -> ET2.Element: + """Create SVG elements for the 'custom' info card. + + The custom info card contains: + - a project title (tspan#project_name_card) + - a separator line (line#separator) + - a placeholder for dynamic stats (text#stat-holder) + + The group is initially hidden and is intended to be populated and + positioned by the JS info-card handlers. + + Returns: + xml.etree.ElementTree.Element: The root element. + """ + card_width = 200 + card_height = 100 # Initial height, will be adjusted by JS + info_card = self.create_element( + "g", + { + "id": "info_card", + "filter": "url(#filter8_d_1_272)", + "style": "visibility: hidden;", + }, + ) + card = self.create_element("g", {"id": "card"}) + info_card.append(card) + card_a = self.create_element( + "rect", + { + "fill": "#66696992", + "height": card_height, + "data-width": card_width, + "id": "card_a", + "rx": "5", + "width": card_width, + "x": "722.5", + "y": "651.5", + }, + ) + card.append(card_a) + card_b = self.create_element( + "rect", + { + "fill": "#21212188", + "height": card_height, + "id": "card_b", + "rx": "4.5", + "stroke": "#656464", + "width": card_width, + "x": "722.5", + "y": "651.5", + }, + ) + card.append(card_b) + text1 = self.create_element( + "text", + { + "fill": "white", + "font-family": "Inter", + "font-size": "20", + "font-weight": "800", + "letter-spacing": "0em", + "style": "white-space: pre", + "xml:space": "preserve", + }, + ) + info_card.append(text1) + project_name_card = self.create_element( + "tspan", + {"id": "project_name_card", "x": "737.535", "y": "666.864"}, + text_content="project name", + ) + text1.append(project_name_card) + + # line separator + line = self.create_element( + "line", + { + "id": "separator", + "x1": "720", + "x2": "920", + "y1": "650", + "y2": "650", + "stroke": "grey", + "stroke-width": "2", + }, + ) + info_card.append(line) + + text2 = self.create_element( + "text", + { + "fill": "#66FFFA", + "id": "stat-holder", + "font-family": "Inter", + "font-size": "18", + "font-weight": "400", + "letter-spacing": "2px", + "style": "white-space: pre; color: #0e0e0e92;", + "xml:space": "preserve", + }, + ) + info_card.append(text2) + + return info_card + + ################################################################################################ + ################################################################################################ + # rendering function for custom content (project -> dictionary) + def render_custom_content( + self, + parent_group, + content_item_data, + circle_props_from_parent, + object_attrs=None, + is_sub_content=False, + content_name=None, + ): + """Render a custom content node (dictionary value per project). + + Args: + parent_group (Element): SVG group to append the content to. + content_item_data (dict|str): Descriptor or name of the content. + circle_props_from_parent (dict): Circle properties (radius, angle, radii offsets). + object_attrs (dict, optional): Extra object attributes. Defaults to None. + is_sub_content (bool, optional): Whether this is a nested sub-content. Defaults to False. + content_name (str, optional): Optional display name override. + + Returns: + None + """ + + if object_attrs is None: + object_attrs = {} + + name = self.get_content_name(content_item_data) + data_value = self.data.get(name, {}) + + # Use the first key's value for color scaling + first_key = self.keys[0] if self.keys else None + value_for_color = data_value.get(first_key, 0) if first_key else 0 + + coords = self.polar_to_cartesian( + self.CURRENT_CENTER, + self.CURRENT_CENTER, + circle_props_from_parent["radius"], + circle_props_from_parent["angle"], + ) + x, y = coords["x"], coords["y"] + + has_sub_contents = isinstance(content_item_data, dict) + + content_group = self.create_element("g", {"id": name}) + parent_group.append(content_group) + + obj_mandatory = name in self.mandatory_list + is_piscine = name in self.piscines_list + + fill_color = ( + self.COLORS["neutral"] + if value_for_color == 0 + else value_to_color(value_for_color, self.max_value, self.gradient_colors) + ) + + icon_radius = ( + self.PISCINE_CONSTANTS["radius"] + if is_piscine + else circle_props_from_parent.get("contentRadius", 4) + ) + + # Serialize dictionary to JSON for tooltip + tooltip_data = json.dumps(data_value) + + if name in self.checkpoints_list: + icon = self.render_checkpoint_icon( + x, + y, + fill_color, + ( + self.CHECKPOINT_CONSTANTS["subContentWidth"] + if is_sub_content + else self.CHECKPOINT_CONSTANTS["width"] + ), + ) + content_group.append(icon) + elif obj_mandatory: + # For mandatory items, we might still want to show custom info. + icon = self.render_star_icon( + x, + y, + fill_color, + ( + self.STAR_CONSTANTS["subContentWidth"] + if is_sub_content + else self.STAR_CONSTANTS["width"] + ), + name, + name if not content_name else content_name, + tooltip_data, + ) + # Override onpointerenter for custom kind + path = icon.find(f"{{http://www.w3.org/2000/svg}}path") + if path is not None: + path.set("onpointerenter", self.generate_custom_card()) + content_group.append(icon) + else: + if is_piscine: + gradient = self.create_element( + "radialGradient", + attributes={ + "id": content_item_data + "_gradient", + "cx": "50%", + "cy": "50%", + "r": "50%", + "fx": "50%", + "fy": "50%", + }, + ) + gradient.append( + self.create_element( + "stop", + attributes={ + "offset": "0%", + "stop-color": fill_color, + "stop-opacity": "1", + }, + ) + ) + gradient.append( + self.create_element( + "stop", + attributes={ + "offset": "40%", + "stop-color": fill_color, + "stop-opacity": "0.5", + }, + ) + ) + gradient.append( + self.create_element( + "stop", + attributes={ + "offset": "100%", + "stop-color": fill_color, + "stop-opacity": "0", + }, + ) + ) + self.defs.append(gradient) + + circle_el = self.create_element( + "circle", + { + "fill": ( + fill_color + if not is_piscine + else f"url(#{content_item_data}_gradient)" + ), + "r": str(icon_radius), + "cx": str(x), + "cy": str(y), + "id": name, + "project-name": name if not content_name else content_name, + "data-tooltip": tooltip_data, + "onpointerenter": self.generate_custom_card(), + "onpointerleave": 'document.getElementById("info_card").style.visibility = "hidden";', + }, + ) + content_group.append(circle_el) + + # Content name text + name_offset = ( + self.PISCINE_CONSTANTS["nameOffset"] + if is_piscine + else circle_props_from_parent.get("contentNameOffset", 34) + ) + if not is_sub_content: + text_el = self.create_element( + "text", + { + "x": str(x), + "y": str(y + name_offset), + "font-size": "12px", + "text-anchor": "middle", + "fill": self.COLORS["neutral"], + "font-family": "IBM Plex Mono", + "style": "text-transform: uppercase;", + }, + text_content=name, + ) + content_group.append(text_el) + + if has_sub_contents: + self.render_sub_contents( + content_group, content_item_data, circle_props_from_parent + ) + + def generate_custom_card(self): + """Generate the JS call for the custom info card.""" + from circular_graph.tools.renderer_utils import show_custom_info_card + + return show_custom_info_card(self.keys if self.keys else []) From 527dd6aaae38848663d13ffb2a863434d64c3206 Mon Sep 17 00:00:00 2001 From: superMass14 Date: Fri, 28 Nov 2025 15:53:57 +0000 Subject: [PATCH 3/3] feat: make info card rendering dynamic by adding support for custom type --- circular_graph/tools/renderer_utils.py | 234 +++++++++++++++++++++---- 1 file changed, 202 insertions(+), 32 deletions(-) diff --git a/circular_graph/tools/renderer_utils.py b/circular_graph/tools/renderer_utils.py index f953735..8e90372 100644 --- a/circular_graph/tools/renderer_utils.py +++ b/circular_graph/tools/renderer_utils.py @@ -2,12 +2,16 @@ # Function to return the appropriate JS function based on the type of info card -def show_info_card(type: Literal["classic", "distribution"] = "classic") -> str: +def show_info_card( + type: Literal["classic", "distribution", "custom"] = "classic", + keys: list[str] | None = None, +) -> str: """Return the appropriate JS function based on the visualization type. Args: - type (Literal["classic", "distribution"], optional): Type of visualization. + type (Literal["classic", "distribution", "custom"], optional): Type of visualization. Defaults to "classic". + keys (list[str] | None, optional): List of keys to display for custom type. Required when type="custom". Returns: str: JavaScript function as a string. @@ -17,6 +21,10 @@ def show_info_card(type: Literal["classic", "distribution"] = "classic") -> str: return show_classic_info_card() elif type == "distribution": return show_distribution_info_card() + elif type == "custom": + if keys is None: + raise ValueError("keys parameter is required when type='custom'") + return show_custom_info_card(keys) else: print("Invalid visualization type") @@ -61,23 +69,32 @@ def show_classic_info_card() -> str: projectText.textContent = projectName; dataText.textContent = dataNumber; - const projectTextWidth = projectText.getBBox().width; // width after rendering - const card_width = parseFloat(cardA.getAttribute("data-width")) + projectTextWidth; - const cardX = x + card_a_x_shift; - const centeredX = cardX + (card_width - projectTextWidth) / 2; + const projectTextWidth = projectText.getBBox().width; + const dataTextWidth = dataText.getBBox().width; + const base_card_width = parseFloat(cardA.getAttribute("data-width")); + + // Calculate max width needed + const maxContentWidth = Math.max(projectTextWidth, dataTextWidth); + const card_width = Math.max(base_card_width, maxContentWidth + 40); + /***********************************************************/ cardA.setAttribute("x", x + card_a_x_shift); cardA.setAttribute("y", y + card_a_y_shift); - cardA.setAttribute("width", card_width + projectTextWidth) // calculation made to adapt on HEX + cardA.setAttribute("width", card_width); cardB.setAttribute("x", x + card_b_x_shift); cardB.setAttribute("y", y + card_b_y_shift); - cardB.setAttribute("width", card_width + projectTextWidth) // calculation made to adpat on HEX + cardB.setAttribute("width", card_width); + /***********************************************************/ - projectText.setAttribute("x", centeredX); + const cardCenterX = x + card_a_x_shift + (card_width / 2); + + projectText.setAttribute("x", cardCenterX); + projectText.setAttribute("text-anchor", "middle"); projectText.setAttribute("y", y + project_text_y_shift); - dataText.setAttribute("x", centeredX); + dataText.setAttribute("x", cardCenterX); + dataText.setAttribute("text-anchor", "middle"); dataText.setAttribute("y", y + data_text_y_shift); infoCard.style.visibility = "visible"; })(this) @@ -131,53 +148,70 @@ def show_distribution_info_card() -> str: projectText.textContent = projectName.toUpperCase(); /*********************************************/ - const projectTextWidth = projectText.getBBox().width; // width after rendering - const card_width = parseFloat(cardA.getAttribute("data-width")) + projectTextWidth + const projectTextWidth = projectText.getBBox().width; + const base_card_width = parseFloat(cardA.getAttribute("data-width")); + + // Calculate max width needed for content + const ids = ["min", "q1", "median", "q3", "max", "outliers", "upperfence", "lowerfence"]; + let maxContentWidth = projectTextWidth; + const stat_holder = document.getElementById('stat-holder'); + + ids.forEach((value) => { + const testText = data && data[value] !== undefined + ? `${value.charAt(0).toUpperCase() + value.slice(1)} : ${data[value]}` + : `${value.charAt(0).toUpperCase() + value.slice(1)} : N/A`; + const tempSpan = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + tempSpan.textContent = testText; + tempSpan.setAttribute("font-family", "Inter"); + tempSpan.setAttribute("font-size", "18"); + stat_holder.appendChild(tempSpan); + const contentWidth = tempSpan.getBBox().width; + if (contentWidth > maxContentWidth) { + maxContentWidth = contentWidth; + } + stat_holder.removeChild(tempSpan); + }); + + const card_width = Math.max(base_card_width, maxContentWidth + 40); const cardX = x + card_a_x_shift; - - /***********************************************************/ const size = document.getElementById('canevas').getAttribute('viewBox').split(' ').slice(2), - total_width = parseFloat(size[0]), //px - total_height = parseFloat(size[1]), //px - limit_width = total_width / (1+1/5), //px - limit_height = total_height / 7, //px + total_width = parseFloat(size[0]), + total_height = parseFloat(size[1]), + limit_width = total_width / (1+1/5), + limit_height = total_height / 7, y_factor = y <= limit_height+ 15 ? -0.15 : 1, project_text_y_factor = y <= limit_height+ 15 ? -0.27 : 1, text_y_factor = y <= limit_height+ 15 ? -0.55 : 1, x_shift = x >= limit_width -15 ? (card_width - card_a_x_shift) * -1 : card_a_x_shift; - const centeredX = x >= limit_width -15 - ? cardX - (card_width - projectTextWidth) - : cardX + (card_width - projectTextWidth) / 2; - cardA.setAttribute("x", x + x_shift); cardA.setAttribute("y", y + (card_a_y_shift * y_factor)); - cardA.setAttribute("width", card_width + projectTextWidth) // calculation made to adapt on HEX + cardA.setAttribute("width", card_width); - cardB.setAttribute("x", x + x_shift); cardB.setAttribute("y", y + (card_b_y_shift * y_factor)); - cardB.setAttribute("width", card_width + projectTextWidth) // calculation made to adapt on HEX + cardB.setAttribute("width", card_width); /***********************************************/ - projectText.setAttribute("x", centeredX); + const cardCenterX = x + x_shift + (card_width / 2); + + projectText.setAttribute("x", cardCenterX); + projectText.setAttribute("text-anchor", "middle"); projectText.setAttribute("y", y + project_text_y_shift * project_text_y_factor); - sep.setAttribute("x1", x + x_shift +5); - sep.setAttribute("x2", x + x_shift + card_width *1.25); + sep.setAttribute("x1", x + x_shift + 5); + sep.setAttribute("x2", x + x_shift + card_width - 5); - sep.setAttribute("y1", y + (text_y_shift * text_y_factor) -30); + sep.setAttribute("y1", y + (text_y_shift * text_y_factor) - 30); sep.setAttribute("y2", y + (text_y_shift * text_y_factor) - 30); /************* text color display + statical values handling **********************************/ - const ids = ["min", "q1", "median", "q3", "max", "outliers", "upperfence", "lowerfence"]; ids.forEach((value, index) => { const el = document.getElementById(value); - const stat_holder = document.getElementById('stat-holder'); el.textContent = data && data[value] !== undefined ? `${value.charAt(0).toUpperCase() + value.slice(1)} : ${data[value]}` : `${value.charAt(0).toUpperCase() + value.slice(1)} : N/A`; @@ -187,7 +221,8 @@ def show_distribution_info_card() -> str: el.setAttribute("fill", "#66FFFA") } - el.setAttribute("x", x + x_shift + text_x_shift); + el.setAttribute("x", cardCenterX); + el.setAttribute("text-anchor", "middle"); el.setAttribute("y", y + (text_y_shift* text_y_factor) + text_y_margin * index); }); @@ -195,3 +230,138 @@ def show_distribution_info_card() -> str: })(this) """ + + +# JS function to display custom informations dynamically (project name -> dictionary) +def show_custom_info_card(keys: list[str]) -> str: + """Return a JavaScript function string to display custom info cards. + + Args: + keys (list[str]): List of keys to display from the data dictionary. + + Returns: + str: JavaScript function as a string. + """ + keys_json = str(keys).replace("'", '"') + + return f""" + (function showInfoCard(el) {{ + el.style.cursor= "pointer"; + const infoCard = document.getElementById("info_card"); + const cardA = document.getElementById("card_a"); + const cardB = document.getElementById("card_b"); + + const projectText = document.getElementById("project_name_card"); + const statHolder = document.getElementById("stat-holder"); + + const datajson = el.getAttribute("data-tooltip") || "{{}}"; + const data = JSON.parse(datajson); + const sep = document.getElementById("separator"); + + /*****************************/ + const card_a_x_shift = 1.0; + const card_a_y_shift = -295.0; + const card_b_x_shift = 1; + const card_b_y_shift = -295.5; + + const project_text_x_shift = 60.465; + const project_text_y_shift = -270.136; + const text_x_shift = 10.65; + const text_y_shift = -215.636; + const text_y_margin = 28; + + /*******************************************/ + const x = parseFloat(el.getAttribute("cx")); + const y = parseFloat(el.getAttribute("cy")); + const projectName = el.getAttribute("project-name") || el.getAttribute("id").toLowerCase(); + + /*********************************************/ + projectText.textContent = projectName.toUpperCase(); + + /*********************************************/ + const projectTextWidth = projectText.getBBox().width; + const base_card_width = parseFloat(cardA.getAttribute("data-width")); + + /***********************************************************/ + const dataKeys = {keys_json}; + const base_height = 100; + const height_per_key = 30; + const total_card_height = base_height + (dataKeys.length * height_per_key); + + // Calculate max width needed for content + let maxContentWidth = projectTextWidth; + dataKeys.forEach((key) => {{ + const testText = key.charAt(0).toUpperCase() + key.slice(1) + " : " + (data[key] !== undefined ? data[key] : "N/A"); + const tempSpan = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + tempSpan.textContent = testText; + tempSpan.setAttribute("font-family", "Inter"); + tempSpan.setAttribute("font-size", "18"); + statHolder.appendChild(tempSpan); + const contentWidth = tempSpan.getBBox().width; + if (contentWidth > maxContentWidth) {{ + maxContentWidth = contentWidth; + }} + statHolder.removeChild(tempSpan); + }}); + + const card_width = Math.max(base_card_width, maxContentWidth + 40); + const cardX = x + card_a_x_shift; + + const size = document.getElementById('canevas').getAttribute('viewBox').split(' ').slice(2), + total_width = parseFloat(size[0]), + total_height = parseFloat(size[1]), + limit_width = total_width / (1+1/5), + limit_height = total_height / 7, + + y_factor = y <= limit_height+ 15 ? -0.15 : 1, + project_text_y_factor = y <= limit_height+ 15 ? -0.27 : 1, + text_y_factor = y <= limit_height+ 15 ? -0.55 : 1, + + x_shift = x >= limit_width -15 ? (card_width - card_a_x_shift) * -1 : card_a_x_shift; + const centeredX = x >= limit_width -15 + ? cardX - (card_width - projectTextWidth) + : cardX + (card_width - projectTextWidth) / 2; + + cardA.setAttribute("height", total_card_height); + cardB.setAttribute("height", total_card_height); + + cardA.setAttribute("x", x + x_shift); + cardA.setAttribute("y", y + (card_a_y_shift * y_factor)); + cardA.setAttribute("width", card_width); + + cardB.setAttribute("x", x + x_shift); + cardB.setAttribute("y", y + (card_b_y_shift * y_factor)); + cardB.setAttribute("width", card_width); + + /***********************************************/ + const cardCenterX = x + x_shift + (card_width / 2); + + projectText.setAttribute("x", cardCenterX); + projectText.setAttribute("text-anchor", "middle"); + projectText.setAttribute("y", y + project_text_y_shift * project_text_y_factor); + + sep.setAttribute("x1", x + x_shift +5); + sep.setAttribute("x2", x + x_shift + card_width - 5); + + sep.setAttribute("y1", y + (text_y_shift * text_y_factor) -30); + sep.setAttribute("y2", y + (text_y_shift * text_y_factor) - 30); + + /************* text color display + statical values handling **********************************/ + + statHolder.textContent = ""; + + dataKeys.forEach((key, index) => {{ + const tspan = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + tspan.textContent = key.charAt(0).toUpperCase() + key.slice(1) + " : " + (data[key] !== undefined ? data[key] : "N/A"); + tspan.setAttribute("x", cardCenterX); + tspan.setAttribute("text-anchor", "middle"); + tspan.setAttribute("y", y + (text_y_shift* text_y_factor) + text_y_margin * index); + tspan.setAttribute("fill", data[key] !== undefined ? "#66FFFA" : "grey"); + + statHolder.appendChild(tspan); + }}); + + infoCard.style.visibility = "visible"; + + }})(this) + """