Spaces:
Sleeping
Sleeping
Download src/core/chart_builder.py from FajarHidaa/HumanPlusX: direct link, hf CLI and curl.
- Browser
- Download file 9.76 kB
-
https://huggingface.co/spaces/FajarHidaa/HumanPlusX/resolve/main/src/core/chart_builder.py
- Command line
-
hf download hf://spaces/FajarHidaa/HumanPlusX/src/core/chart_builder.py
-
curl -L -o chart_builder.py https://huggingface.co/spaces/FajarHidaa/HumanPlusX/resolve/main/src/core/chart_builder.py
9.76 kB
| """ | |
| Human+ Chart Builder | |
| Plotly gauge charts dan bar charts untuk biomarker visualization. | |
| Semua chart dark-mode native, sesuai Human+ design system. | |
| """ | |
| from typing import Optional | |
| # ββ Color palette (matches ui/theme.py) ββββββββββββββββββββββββββββββ | |
| _COL = { | |
| "bg": "#070710", | |
| "surface": "#0d0d1a", | |
| "elevated": "#131325", | |
| "line": "#1f1f38", | |
| "text": "#f0f0fa", | |
| "muted": "#8b8ba7", | |
| "optimal": "#10b981", | |
| "warning": "#f59e0b", | |
| "danger": "#ef4444", | |
| "violet": "#8b5cf6", | |
| "cyan": "#22d3ee", | |
| } | |
| _STATUS_COLOR = { | |
| "optimal": _COL["optimal"], | |
| "warning": _COL["warning"], | |
| "danger": _COL["danger"], | |
| "neutral": _COL["muted"], | |
| } | |
| def build_gauge_chart( | |
| name: str, | |
| value: float, | |
| unit: str, | |
| status: str, | |
| range_lo: float, | |
| range_hi: float, | |
| optimal_lo: Optional[float] = None, | |
| optimal_hi: Optional[float] = None, | |
| height: int = 220, | |
| ): | |
| """ | |
| Build a single Plotly gauge chart untuk satu biomarker. | |
| Args: | |
| name: biomarker name | |
| value: numeric value | |
| unit: unit string | |
| status: 'optimal'|'warning'|'danger'|'neutral' | |
| range_lo / range_hi: full display range for the gauge | |
| optimal_lo / optimal_hi: target optimal range (green zone) | |
| height: chart height in px | |
| Returns: | |
| plotly Figure object | |
| """ | |
| import plotly.graph_objects as go | |
| color = _STATUS_COLOR.get(status, _COL["muted"]) | |
| # Build green threshold steps | |
| steps = [] | |
| if optimal_lo is not None and optimal_hi is not None: | |
| steps = [ | |
| {"range": [range_lo, optimal_lo], "color": "rgba(239,68,68,0.12)"}, | |
| {"range": [optimal_lo, optimal_hi], "color": "rgba(16,185,129,0.15)"}, | |
| {"range": [optimal_hi, range_hi], "color": "rgba(245,158,11,0.12)"}, | |
| ] | |
| elif optimal_hi is not None: | |
| steps = [ | |
| {"range": [range_lo, optimal_hi], "color": "rgba(16,185,129,0.15)"}, | |
| {"range": [optimal_hi, range_hi], "color": "rgba(245,158,11,0.12)"}, | |
| ] | |
| elif optimal_lo is not None: | |
| steps = [ | |
| {"range": [range_lo, optimal_lo], "color": "rgba(245,158,11,0.12)"}, | |
| {"range": [optimal_lo, range_hi], "color": "rgba(16,185,129,0.15)"}, | |
| ] | |
| fig = go.Figure(go.Indicator( | |
| mode="gauge+number", | |
| value=value, | |
| number={ | |
| "suffix": f" {unit}", | |
| "font": {"size": 20, "color": color, "family": "DM Sans"}, | |
| }, | |
| title={ | |
| "text": name, | |
| "font": {"size": 13, "color": _COL["muted"], "family": "DM Sans"}, | |
| }, | |
| gauge={ | |
| "axis": { | |
| "range": [range_lo, range_hi], | |
| "tickcolor": _COL["line"], | |
| "tickfont": {"color": _COL["muted"], "size": 9, "family": "JetBrains Mono"}, | |
| "nticks": 5, | |
| }, | |
| "bar": {"color": color, "thickness": 0.22}, | |
| "bgcolor": _COL["elevated"], | |
| "borderwidth": 0, | |
| "steps": steps, | |
| "threshold": { | |
| "line": {"color": color, "width": 2}, | |
| "thickness": 0.75, | |
| "value": value, | |
| }, | |
| }, | |
| )) | |
| fig.update_layout( | |
| height=height, | |
| margin=dict(t=30, b=10, l=20, r=20), | |
| paper_bgcolor=_COL["surface"], | |
| plot_bgcolor=_COL["surface"], | |
| font={"family": "DM Sans", "color": _COL["text"]}, | |
| ) | |
| return fig | |
| def build_biomarker_bar_chart(biomarkers: list[dict], height: int = 380) -> "go.Figure": | |
| """ | |
| Build horizontal bar chart untuk semua biomarker sekaligus. | |
| Setiap bar dicolor sesuai status (merah/kuning/hijau). | |
| Args: | |
| biomarkers: list of enriched biomarker dicts dari pdf_processor | |
| height: chart height px | |
| Returns: | |
| plotly Figure | |
| """ | |
| import plotly.graph_objects as go | |
| # Filter yang punya numeric value | |
| plottable = [ | |
| b for b in biomarkers | |
| if b.get("raw_value") is not None and b.get("status") != "neutral" | |
| ] | |
| if not plottable: | |
| return None | |
| # Sort: danger first | |
| priority = {"danger": 0, "warning": 1, "optimal": 2} | |
| plottable = sorted(plottable, key=lambda b: priority.get(b["status"], 3)) | |
| names = [b["name"] for b in plottable] | |
| values = [b["raw_value"] for b in plottable] | |
| colors = [_STATUS_COLOR.get(b["status"], _COL["muted"]) for b in plottable] | |
| statuses = [b["status"].upper() for b in plottable] | |
| units = [b.get("unit", "") for b in plottable] | |
| refs = [b.get("reference", "") for b in plottable] | |
| def _fmt_ref(r, u): | |
| """Tampilkan reference dengan unit jika ada, fallback ke '-' jika kosong.""" | |
| if not r: | |
| return "β" | |
| # Jika reference sudah mengandung unit atau tanda < >, langsung pakai | |
| if any(c in r for c in ["<", ">", "β", "-"]): | |
| return f"{r} {u}".strip() | |
| return f"{r} {u}".strip() | |
| hover_texts = [ | |
| f"<b>{n}</b><br>" | |
| f"Nilai: <b>{v} {u}</b><br>" | |
| f"Human+ target: {_fmt_ref(r, u)}<br>" | |
| f"Status: <b>{s}</b>" | |
| for n, v, u, r, s in zip(names, values, units, refs, statuses) | |
| ] | |
| fig = go.Figure(go.Bar( | |
| x=values, | |
| y=names, | |
| orientation="h", | |
| marker=dict( | |
| color=colors, | |
| line=dict(width=0), | |
| opacity=0.85, | |
| ), | |
| hovertext=hover_texts, | |
| hoverinfo="text", | |
| hoverlabel=dict( | |
| bgcolor=_COL["elevated"], | |
| bordercolor=_COL["line"], | |
| font=dict(family="DM Sans", size=12, color=_COL["text"]), | |
| ), | |
| text=[f"{v} {u}" for v, u in zip(values, units)], | |
| textposition="outside", | |
| textfont=dict( | |
| family="JetBrains Mono", | |
| size=10, | |
| color=_COL["muted"], | |
| ), | |
| )) | |
| dynamic_height = max(height, len(plottable) * 36 + 80) | |
| fig.update_layout( | |
| height=dynamic_height, | |
| margin=dict(t=16, b=16, l=16, r=80), | |
| paper_bgcolor=_COL["bg"], | |
| plot_bgcolor=_COL["bg"], | |
| font={"family": "DM Sans", "color": _COL["text"]}, | |
| xaxis=dict( | |
| showgrid=True, | |
| gridcolor=_COL["line"], | |
| gridwidth=1, | |
| tickfont=dict(family="JetBrains Mono", size=9, color=_COL["muted"]), | |
| zeroline=False, | |
| ), | |
| yaxis=dict( | |
| tickfont=dict(family="DM Sans", size=11, color=_COL["text"]), | |
| categoryorder="array", | |
| categoryarray=list(reversed(names)), | |
| ), | |
| hoverlabel=dict(align="left"), | |
| bargap=0.35, | |
| ) | |
| return fig | |
| def build_status_donut(optimal: int, warning: int, danger: int) -> "go.Figure": | |
| """ | |
| Build a small donut chart untuk summary biomarker status. | |
| Digunakan di sidebar atau summary card. | |
| """ | |
| import plotly.graph_objects as go | |
| labels = ["Optimal", "Sub-Optimal", "Perlu Perhatian"] | |
| values = [optimal, warning, danger] | |
| colors = [_COL["optimal"], _COL["warning"], _COL["danger"]] | |
| # Filter zero values | |
| filtered = [(l, v, c) for l, v, c in zip(labels, values, colors) if v > 0] | |
| if not filtered: | |
| return None | |
| labels_f, values_f, colors_f = zip(*filtered) | |
| fig = go.Figure(go.Pie( | |
| labels=labels_f, | |
| values=values_f, | |
| hole=0.65, | |
| marker=dict( | |
| colors=colors_f, | |
| line=dict(color=_COL["bg"], width=2), | |
| ), | |
| textinfo="none", | |
| hovertemplate="<b>%{label}</b><br>%{value} marker<extra></extra>", | |
| hoverlabel=dict( | |
| bgcolor=_COL["elevated"], | |
| font=dict(family="DM Sans", size=12, color=_COL["text"]), | |
| ), | |
| )) | |
| total = sum(values_f) | |
| fig.update_layout( | |
| height=180, | |
| margin=dict(t=0, b=0, l=0, r=0), | |
| paper_bgcolor="rgba(0,0,0,0)", | |
| plot_bgcolor="rgba(0,0,0,0)", | |
| showlegend=False, | |
| annotations=[{ | |
| "text": f"<b>{total}</b><br><span style='font-size:10px'>Marker</span>", | |
| "x": 0.5, "y": 0.5, | |
| "font_size": 18, | |
| "font_family": "DM Sans", | |
| "font_color": _COL["text"], | |
| "showarrow": False, | |
| }], | |
| ) | |
| return fig | |
| # ββ Gauge config presets (Human+ biomarker ranges) βββββββββββββββββββ | |
| # Format: name_lower β (range_lo, range_hi, optimal_lo, optimal_hi) | |
| GAUGE_PRESETS: dict[str, tuple] = { | |
| "hs-crp": (0, 10, None, 1.0), | |
| "homocysteine": (0, 30, None, 8.0), | |
| "hba1c": (4.0, 8.0, 4.8, 5.2), | |
| "vitamin d": (0, 100, 50, 80), | |
| "testosterone": (0, 1200, 600, None), | |
| "magnesium": (1.0, 3.5, 2.2, None), | |
| "vitamin b12": (0, 1500, 500, None), | |
| "ferritin": (0, 300, 50, 150), | |
| "apob": (0, 150, None, 80), | |
| "triglycerides": (0, 300, None, 90), | |
| "hdl": (0, 120, 60, None), | |
| "tsh": (0, 6, 0.5, 2.0), | |
| "zinc": (0, 200, 90, 120), | |
| "fasting insulin": (0, 30, None, 7.0), | |
| "fasting glucose": (50, 150, 75, 86), | |
| } | |
| def get_gauge_preset(name: str) -> Optional[tuple]: | |
| """ | |
| Return (range_lo, range_hi, optimal_lo, optimal_hi) untuk biomarker. | |
| Returns None jika tidak ada preset. | |
| """ | |
| name_lower = name.lower() | |
| for key, preset in GAUGE_PRESETS.items(): | |
| if key in name_lower or name_lower in key: | |
| return preset | |
| return None | |