import warnings
from collections import deque

import numpy as np
import holoviews as hv
import holoviews.streams
import pandas as pd
import param

from holoviz_utils.hydrology import TERMINAL

hv.extension("bokeh")


class Sankey(param.Parameterized):

    node_df = param.DataFrame(
        allow_None=False,
        allow_refs=True,
        doc="DataFrame with columns: id, width, n. Optional: color.",
    )
    flow_df = param.DataFrame(
        allow_None=False,
        allow_refs=True,
        doc="DataFrame with columns: source, target, width. Optional: group.",
    )

    align = param.Selector(
        default="left",
        objects=["left", "right", "center"],
        constant=True,
        doc="Alignment of flows within nodes.",
    )
    throughflow_only = param.Boolean(
        default=False,
        constant=True,
        doc="If True, only show residuals for nodes with both inflow and outflow.",
    )
    group_colors = param.Dict(
        default={},
        doc="Color mapping for flow groups. Keys are group names, values are color strings.",
    )
    residual_colors = param.Dict(
        default={"positive": "#4CAF50", "negative": "#F44336"},
        doc="Color mapping for residual signs.",
    )
    node_alpha = param.Number(default=0.8, doc="Alpha for node rectangles.")
    flow_alpha = param.Number(default=0.3, doc="Alpha for flow bands.")
    residual_alpha = param.Number(default=0.6, doc="Alpha for residual rectangles.")

    labeled_ids = param.ListSelector(
        default=[],
        objects=[],
        doc="Node IDs for which to show text labels. Empty list means no labels.",
    )

    node_height = param.Number(
        default=0.3, constant=True, doc="Fixed height of each rectangle."
    )
    level_spacing = param.Number(
        default=1.0, constant=True, doc="Vertical distance between levels."
    )
    node_gap = param.Number(
        default=0.2,
        doc="Horizontal gap between nodes at the same level.",
    )
    n_interpolation = param.Integer(
        default=50, constant=True, doc="Number of interpolation points per curve."
    )

    node_tooltip = param.String(
        default=None,
        allow_None=True,
        doc="HTML template for node hover tooltips. Use @column_name syntax.",
    )
    flow_tooltip = param.String(
        default=None,
        allow_None=True,
        doc="HTML template for flow hover tooltips.",
    )
    residual_tooltip = param.String(
        default=None,
        allow_None=True,
        doc="HTML template for residual hover tooltips.",
    )

    selected_groups = param.ListSelector(
        default=[],
        objects=[],
        doc="Selected flow groups to display. Updated automatically from flow_df 'group' column.",
    )

    selected_ids = param.ListSelector(
        default=[],
        objects=[],
        doc="Selected node IDs to display. Updated automatically from node_df 'id' column.",
    )

    pinned_ids = param.ListSelector(
        default=[],
        objects=[],
        doc="Node IDs that are always visible regardless of selection.",
    )

    rect_df = param.DataFrame(
        default=None,
        allow_None=True,
        doc="Computed rectangle positions for each node (set automatically by plot).",
    )

    view = param.ClassSelector(
        class_=hv.DynamicMap, doc="DynamicMap that reactively updates the Sankey plot."
    )

    def __init__(self, **params):
        # Pre-compute ListSelector objects so param can validate initial values
        flow_df = params.get("flow_df")
        if flow_df is not None and "group" in flow_df.columns:
            self.param["selected_groups"].objects = sorted(
                flow_df["group"].unique().tolist()
            )
        node_df = params.get("node_df")
        # Coerce id params to strings, drop None values (use default [])
        for key in ("selected_ids", "pinned_ids", "labeled_ids"):
            if key in params:
                if params[key] is None:
                    del params[key]
                else:
                    params[key] = [str(x) for x in params[key]]
        if node_df is not None:
            ids = sorted(node_df["id"].astype(str).unique().tolist())
            # Include any explicitly requested ids so param validation passes
            for key in ("selected_ids", "pinned_ids", "labeled_ids"):
                extra = set(params.get(key, []))
                self.param[key].objects = sorted(set(ids) | extra)
        super().__init__(**params)
        self.param.watch(self._coerce_selected_ids, ["selected_ids"])
        self.param.watch(self._compute_rects, ["node_df", "node_gap"])
        self.param.watch(self._on_node_df_changed, ["node_df"])
        self.param.watch(self._on_flow_df_changed, ["flow_df"])
        self._compute_rects()
        self._tap_stream = holoviews.streams.Tap(x=None, y=None)
        self._last_tap_coords = (None, None)
        self.view = hv.DynamicMap(
            self.plot,
            streams=[
                self.param["rect_df"],
                self.param["flow_df"],
                self.param["selected_groups"],
                self.param["selected_ids"],
                self.param["pinned_ids"],
                self.param["group_colors"],
                self.param["node_alpha"],
                self.param["flow_alpha"],
                self.param["residual_colors"],
                self.param["residual_alpha"],
                self.param["labeled_ids"],
                self.param["node_tooltip"],
                self.param["flow_tooltip"],
                self.param["residual_tooltip"],
                self._tap_stream,
            ],
        )
        self._tap_stream.source = self.view

    def _on_node_df_changed(self, *events):
        self._update_ids()

    def _on_flow_df_changed(self, *events):
        self._update_groups()

    def _coerce_selected_ids(self, *events):
        coerced = [str(x) for x in self.selected_ids]
        if coerced != self.selected_ids:
            self.selected_ids = coerced

    def _update_ids(self):
        ids = set(self.node_df["id"].astype(str).unique().tolist())
        # Preserve previously known objects so selections survive
        # transient data updates that lack some nodes.
        for key in ("selected_ids", "pinned_ids", "labeled_ids"):
            existing = set(self.param[key].objects)
            merged = sorted(existing | ids)
            self.param[key].objects = merged
            current = getattr(self, key)
            valid = [x for x in current if x in merged]
            setattr(self, key, valid)

    def _update_groups(self):
        if self.flow_df is not None and "group" in self.flow_df.columns:
            groups = sorted(self.flow_df["group"].unique().tolist())
        else:
            groups = []
        prev = list(self.selected_groups)
        # Preserve previously known objects so selections survive
        # transient data updates that lack some groups.
        existing = set(self.param["selected_groups"].objects)
        merged = sorted(existing | set(groups))
        self.param["selected_groups"].objects = merged
        valid = [g for g in prev if g in merged]
        self.selected_groups = valid

    def _compute_rects(self, *events):
        df = self.node_df.copy()
        df["id"] = df["id"].astype(str)
        rects = []
        for n, group in df.groupby("n"):
            widths = group["width"].values
            total_width = widths.sum() + self.node_gap * (len(widths) - 1)
            x = -total_width / 2
            y0 = n * self.level_spacing
            y1 = y0 + self.node_height
            for _, row in group.iterrows():
                x1 = x + row["width"]
                entry = [x, y0, x1, y1, row["id"], row["width"]]
                if "color" in df.columns:
                    entry.append(row["color"])
                rects.append(entry)
                x = x1 + self.node_gap

        cols = ["x0", "y0", "x1", "y1", "id", "width"]
        if "color" in df.columns:
            cols.append("color")
        self.param.update(rect_df=pd.DataFrame(rects, columns=cols))

    def _plot_nodes(self, rect_df, selected_ids, node_alpha):
        rect_df = rect_df.copy()
        if selected_ids:
            rect_df = rect_df[rect_df["id"].isin(selected_ids)].copy()
        rect_df["alpha"] = node_alpha
        vdims = ["id", "width", "alpha"]
        if "color" in rect_df.columns:
            vdims.append("color")

        rectangles = hv.Rectangles(
            rect_df,
            kdims=["x0", "y0", "x1", "y1"],
            vdims=vdims,
            group="Nodes",
        )

        opts = dict(
            color="color" if "color" in rect_df.columns else "#4A90D9",
            alpha="alpha",
        )
        if self.node_tooltip is not None:
            opts["hover_tooltips"] = self.node_tooltip

        return rectangles.opts(**opts, framewise=True)

    @staticmethod
    def _smoothstep(t):
        return t * t * (3 - 2 * t)

    @staticmethod
    def _make_band(x_src, w_src, y_src, x_dst, w_dst, y_dst, n=50):
        t = np.linspace(0, 1, n)
        s = Sankey._smoothstep(t)

        y_vals = y_src + (y_dst - y_src) * t

        x_center_src = x_src + w_src / 2
        x_center_dst = x_dst + w_dst / 2
        x_center = x_center_src + (x_center_dst - x_center_src) * s
        w = w_src + (w_dst - w_src) * t

        x_left = x_center - w / 2
        x_right = x_center + w / 2

        xs = np.concatenate([x_left, x_right[::-1], x_left[:1]])
        ys = np.concatenate([y_vals, y_vals[::-1], y_vals[:1]])
        return xs, ys

    @staticmethod
    def _compute_slots(flows, rect_df):
        node_center = {
            row["id"]: (row["x0"] + row["x1"]) / 2 for _, row in rect_df.iterrows()
        }

        src_slots = pd.Series(index=flows.index, dtype=int)
        for _, grp in flows.groupby("source"):
            order = grp["target"].map(node_center).argsort()
            src_slots[grp.index] = order.values

        trg_slots = pd.Series(index=flows.index, dtype=int)
        for _, grp in flows.groupby("target"):
            order = grp["source"].map(node_center).argsort()
            trg_slots[grp.index] = order.values

        return src_slots, trg_slots

    def _compute_residuals(self, flows, rect_df):
        out_total = flows.groupby("source")["width"].sum()
        in_total = flows.groupby("target")["width"].sum()

        if self.throughflow_only:
            all_ids = set(out_total.index) & set(in_total.index)
        else:
            all_ids = set(out_total.index) | set(in_total.index)
        rects = []
        for _, row in rect_df.iterrows():
            nid = row["id"]
            if nid not in all_ids:
                continue
            out_w = out_total.get(nid, 0)
            in_w = in_total.get(nid, 0)
            diff = in_w - out_w
            if diff == 0:
                continue
            residual = abs(diff)
            sign = "positive" if diff > 0 else "negative"
            x1 = row["x1"]
            x0 = x1 - residual
            rects.append((x0, row["y0"], x1, row["y1"], nid, sign))

        return pd.DataFrame(rects, columns=["x0", "y0", "x1", "y1", "id", "sign"])

    def _plot_flows(
        self,
        flows,
        rect_df,
        selected_groups,
        selected_ids,
        group_colors,
        flow_alpha,
        residual_colors,
        residual_alpha,
    ):
        flows = flows.copy()
        has_group = "group" in flows.columns
        flows["source"] = flows["source"].astype(str)
        flows["target"] = flows["target"].astype(str)
        if has_group:
            filtered_flows = flows[flows["group"].isin(selected_groups)]
        else:
            filtered_flows = flows
        if (
            "src_slot" not in filtered_flows.columns
            or "trg_slot" not in filtered_flows.columns
        ):
            filtered_flows = filtered_flows.copy()
            filtered_flows["src_slot"], filtered_flows["trg_slot"] = (
                self._compute_slots(filtered_flows, rect_df)
            )

        node_lookup = {
            row["id"]: (row["x0"], row["y0"], row["x1"], row["y1"])
            for _, row in rect_df.iterrows()
        }

        out_total = filtered_flows.groupby("source")["width"].sum()
        in_total = filtered_flows.groupby("target")["width"].sum()

        out_base = {}
        in_base = {}
        for nid, (x0, _, x1, _) in node_lookup.items():
            node_w = x1 - x0
            out_w = out_total.get(nid, 0)
            in_w = in_total.get(nid, 0)
            if self.align == "left":
                out_base[nid] = x0
                in_base[nid] = x0
            elif self.align == "right":
                out_base[nid] = x1 - out_w
                in_base[nid] = x1 - in_w
            else:
                out_base[nid] = x0 + (node_w - out_w) / 2
                in_base[nid] = x0 + (node_w - in_w) / 2

        out_x = {}
        for src_id, grp in filtered_flows.groupby("source"):
            x = out_base[src_id]
            for idx, row in grp.sort_values("src_slot").iterrows():
                out_x[idx] = x
                x += row["width"]

        in_x = {}
        for tgt_id, grp in filtered_flows.groupby("target"):
            x = in_base[tgt_id]
            for idx, row in grp.sort_values("trg_slot").iterrows():
                in_x[idx] = x
                x += row["width"]

        polys = []
        for idx, flow in filtered_flows.iterrows():
            src_rect = node_lookup[flow["source"]]
            tgt_rect = node_lookup[flow["target"]]
            w = flow["width"]

            src_y0, src_y1 = src_rect[1], src_rect[3]
            tgt_y0, tgt_y1 = tgt_rect[1], tgt_rect[3]
            if src_y0 > tgt_y1:
                y_src, y_dst = src_y0, tgt_y1
            else:
                y_src, y_dst = src_y1, tgt_y0

            xs, ys = self._make_band(
                out_x[idx],
                w,
                y_src,
                in_x[idx],
                w,
                y_dst,
                n=self.n_interpolation,
            )
            poly = {
                "x": xs.tolist(),
                "y": ys.tolist(),
                "source": flow["source"],
                "target": flow["target"],
                "width": w,
            }
            if has_group and group_colors:
                poly["color"] = group_colors.get(flow["group"], "#4A90D9")
            if selected_ids:
                if (
                    flow["source"] not in selected_ids
                    and flow["target"] not in selected_ids
                ):
                    continue
            poly["alpha"] = flow_alpha
            if has_group:
                poly["group"] = flow["group"]
            polys.append(poly)

        has_color_effective = has_group and bool(group_colors)
        vdims = ["source", "target", "width", "alpha"]
        if has_color_effective:
            vdims.append("color")
        if has_group:
            vdims.append("group")
        band_opts = dict(
            color="color" if has_color_effective else "#4A90D9",
            alpha="alpha",
        )
        if self.flow_tooltip is not None:
            band_opts["hover_tooltips"] = self.flow_tooltip
        band_plot = hv.Polygons(polys, vdims=vdims).opts(**band_opts, framewise=True)

        residual_df = self._compute_residuals(flows, rect_df)
        residual_plot = None
        if len(residual_df) > 0:
            if selected_ids:
                residual_df = residual_df[residual_df["id"].isin(selected_ids)].copy()
            residual_df["alpha"] = residual_alpha
            residual_plot = hv.Rectangles(
                residual_df,
                kdims=["x0", "y0", "x1", "y1"],
                vdims=["id", "sign", "alpha"],
                group="Residuals",
            )
            res_opts = dict(
                color="sign",
                cmap=residual_colors,
                alpha="alpha",
            )
            if self.residual_tooltip is not None:
                res_opts["hover_tooltips"] = self.residual_tooltip
            residual_plot = residual_plot.opts(**res_opts, framewise=True)

        return band_plot, residual_plot

    def plot(
        self,
        rect_df,
        flow_df,
        selected_groups,
        selected_ids,
        pinned_ids,
        group_colors,
        node_alpha,
        flow_alpha,
        residual_colors,
        residual_alpha,
        labeled_ids,
        node_tooltip,
        flow_tooltip,
        residual_tooltip,
        x=None,
        y=None,
    ):
        """Generate the full Sankey-like diagram as an HoloViews Overlay."""
        if x is not None and y is not None and (x, y) != self._last_tap_coords:
            self._last_tap_coords = (x, y)
            hit = rect_df[
                (rect_df["x0"] <= x)
                & (x <= rect_df["x1"])
                & (rect_df["y0"] <= y)
                & (y <= rect_df["y1"])
            ]
            if hit.empty:
                new_ids = []
            else:
                nid = str(hit.iloc[0]["id"])
                current = list(self.selected_ids)
                if nid in current:
                    current.remove(nid)
                else:
                    current.append(nid)
                new_ids = current

            self.param.update(selected_ids=new_ids)
            selected_ids = self.selected_ids
        # When nodes are selected, show all flow groups for those nodes
        if selected_ids:
            selected_groups = list(self.param["selected_groups"].objects)
        # Visible nodes = selected + pinned; visible flows = selected only
        visible_node_ids = selected_ids
        if selected_ids and pinned_ids:
            visible_node_ids = list(set(selected_ids) | set(pinned_ids))
        elif pinned_ids and not selected_ids:
            visible_node_ids = []
        nodes = self._plot_nodes(rect_df, visible_node_ids, node_alpha)
        bands, residuals = self._plot_flows(
            flow_df,
            rect_df,
            selected_groups,
            selected_ids,
            group_colors,
            flow_alpha,
            residual_colors,
            residual_alpha,
        )
        layers = bands * nodes
        if residuals is not None:
            layers = layers * residuals
        else:
            layers = layers * hv.Rectangles(
                [], kdims=["x0", "y0", "x1", "y1"],
                vdims=["id", "sign", "alpha"], group="Residuals",
            ).opts(framewise=True)
        if labeled_ids:
            label_filter = set(labeled_ids)
            if visible_node_ids:
                label_filter = label_filter & set(visible_node_ids)
            visible_rects = rect_df[rect_df["id"].isin(label_filter)]
            label_df = pd.DataFrame(
                {
                    "x": visible_rects["x0"].values,
                    "y": (visible_rects["y0"].values + visible_rects["y1"].values) / 2,
                    "label": " " + visible_rects["id"].astype(str),
                }
            )
            labels = hv.Labels(label_df, kdims=["x", "y"], vdims=["label"]).opts(framewise=True)
            layers = layers * labels
        else:
            layers = layers * hv.Labels(
                [], kdims=["x", "y"], vdims=["label"],
            ).opts(framewise=True)
        return layers.opts(framewise=True)


def prepare_sankey_data(df, ranges):
    """Convert hydrological data into node_df and flow_df for Sankey.

    Parameters
    ----------
    df : pd.DataFrame
        Columns: SDG6_SB_ID, AETI (mm/day), PCP (mm/day), Q (m³/s)
    ranges : dict
        Mapping basin_id (str) -> [downstream_basin_id, area] or
        [downstream_basin_id, area, basin_name]. When basin_name is
        present, it is used as the node id instead of the numeric id.

    Returns
    -------
    node_df : pd.DataFrame
        Columns: id, width, n, color
    flow_df : pd.DataFrame
        Columns: source, target, width, group
    """

    def _make_empty():
        node_df = pd.DataFrame(
            [{"id": "empty", "width": 1.0, "n": 0, "color": "#cccccc"}]
        )
        flow_df = pd.DataFrame(
            {
                "source": ["empty"] * 3,
                "target": ["empty"] * 3,
                "width": [0.0] * 3,
                "group": ["PCP", "Q", "AETI"],
            }
        )
        return node_df, flow_df

    if df is None:
        return _make_empty()

    if df.empty:
        return _make_empty()

    # Convert PCP and AETI from mm/day to m³/s
    areas = {int(k): v[1] * 1e6 for k, v in ranges.items() if isinstance(v, list) and len(v) >= 2}
    area_m2 = df["SDG6_SB_ID"].map(areas)
    df = df.copy()
    for col in ("PCP", "AETI"):
        df[col] = df[col] * area_m2 / 1e3 / 86400

    basin_ids = set(df["SDG6_SB_ID"].values)
    id_set = {int(b) if not isinstance(b, int) else b for b in basin_ids}

    # Build name lookup: bid -> display name
    name_of = {}
    for src_str, val in ranges.items():
        if isinstance(val, list) and len(val) >= 3 and val[2] is not None:
            name_of[int(src_str)] = str(val[2])

    def _label(bid):
        return name_of.get(bid, str(bid))

    # Build topology
    downstream = {}
    upstream_of = {bid: [] for bid in id_set}
    for src_str, val in ranges.items():
        src = int(src_str)
        if src not in id_set:
            continue
        dst = val[0] if isinstance(val, list) else val
        if dst in TERMINAL or dst not in id_set:
            downstream[src] = None
        else:
            downstream[src] = dst
            upstream_of[dst].append(src)

    # Find outlet (most downstream basin)
    outlets = [bid for bid, ds in downstream.items() if ds is None]
    if len(outlets) != 1:
        warnings.warn(f"Expected exactly one outlet, found: {outlets}")
        return _make_empty()
    outlet = outlets[0]

    # BFS upstream from outlet to assign levels
    levels = {outlet: 1}
    queue = deque([outlet])
    while queue:
        bid = queue.popleft()
        for child in upstream_of.get(bid, []):
            levels[child] = levels[bid] + 1
            queue.append(child)
    max_level = max(levels.values())

    # DFS from outlet for horizontal ordering (minimizes crossings)
    def _dfs(node):
        result = [node]
        for child in sorted(upstream_of.get(node, []), key=str):
            result.extend(_dfs(child))
        return result

    dfs_ordered = _dfs(outlet)

    # Index df by basin id for lookups
    data = df.set_index("SDG6_SB_ID")

    # Build flows
    flows = []
    for bid in id_set:
        pcp = data.loc[bid, "PCP"]
        aeti = data.loc[bid, "AETI"]
        qout = data.loc[bid, "Q"]

        if pcp > 0:
            flows.append(
                {"source": "PCP", "target": _label(bid), "width": pcp, "group": "PCP"}
            )
        if aeti > 0:
            flows.append(
                {"source": _label(bid), "target": "AETI", "width": aeti, "group": "AETI"}
            )
        ds = downstream.get(bid)
        if ds is not None and qout > 0:
            flows.append(
                {"source": _label(bid), "target": _label(ds), "width": qout, "group": "Q"}
            )

    # Outlet Q flows to separate Q node
    outlet_qout = data.loc[outlet, "Q"]
    if outlet_qout > 0:
        flows.append(
            {
                "source": _label(outlet),
                "target": "Q",
                "width": outlet_qout,
                "group": "Q",
            }
        )

    flow_df = pd.DataFrame(flows)

    # Build nodes
    nodes = []

    # Top PCP node
    total_pcp = data["PCP"].sum()
    nodes.append(
        {"id": "PCP", "width": total_pcp, "n": max_level + 1, "color": "#27AE60"}
    )

    # Subbasin nodes in DFS order
    for bid in dfs_ordered:
        ups = upstream_of.get(bid, [])
        qin = sum(data.loc[u, "Q"] for u in ups) if ups else 0.0
        inflows = data.loc[bid, "PCP"] + qin
        outflows = data.loc[bid, "AETI"] + data.loc[bid, "Q"]
        width = max(inflows, outflows)
        nodes.append(
            {"id": _label(bid), "width": width, "n": levels[bid], "color": "#4A90D9"}
        )

    # Bottom nodes at n=0: AETI sink (left), Q (right)
    total_aeti = data["AETI"].sum()
    nodes.append({"id": "AETI", "width": total_aeti, "n": 0, "color": "#E74C3C"})
    if outlet_qout > 0:
        nodes.append({"id": "Q", "width": outlet_qout, "n": 0, "color": "#3498DB"})

    node_df = pd.DataFrame(nodes)
    return node_df, flow_df


if __name__ == "__main__":

    import json
    from pathlib import Path

    #     SDG6_SB_ID         AETI          PCP           QIN          Q  \
    # 0     7050682  3720.565718  6052.018277  20370.005463  22100.104137
    # 1     7050691  6240.431362  8406.313975  18676.460917  20370.005463
    # 2     7050692  4611.967991  6660.629169      0.000000   2133.391747
    # 3     7050693   238.225478   342.858268  16861.522431  16543.069170
    # 4     7050694  3892.050826  5286.425365      0.000000   1518.984412

    #            ΔS
    # 0  690.647662
    # 1  616.608224
    # 2   94.398473
    # 3  470.792225
    # 4   76.296976
    df = (
        pd.read_pickle(
            "/Users/hmcoerver/Library/Mobile Documents/com~apple~CloudDocs/Repositories/holoviz-utils/in_progress/merged_data.pickle"
        )
        .drop("start_datetime", axis=1)
        .groupby("SDG6_SB_ID")
        .mean()
        .reset_index()
    )
    meta = "/Users/hmcoerver/Library/Mobile Documents/com~apple~CloudDocs/Repositories/holoviz-utils/projects/wapor_discharge/fao_sb_ids.json"

    df = df.rename({"QOUT": "Q"}, axis=1)

    #  basin_id   [downstream, area]
    # '1003738': [1003739, 5451],
    # '1003739': [1003736, 3034],
    # '1003740': [1003742, 4117],
    # '1003741': [1003742, 2665],
    # '1003742': [1003746, 3765],
    # '1003743': [1003742, 2555],
    _ranges = json.loads(Path(meta).read_text())

    node_df, flow_df = prepare_sankey_data(df, _ranges)

    sankey = Sankey(
        node_df=node_df,
        flow_df=flow_df,
        group_colors={"PCP": "#27AE60", "AETI": "#E74C3C", "Q": "#3498DB"},
        selected_groups=["Q"],
        selected_ids=None,
        pinned_ids=["PCP", "AETI"],
        labeled_ids=["PCP", "AETI", "Q"],
        throughflow_only=True,
        node_gap=3000,
        residual_alpha=1.0,
        flow_alpha=0.8,
        node_alpha=0.95,
        # node_tooltip="""
        # <div>
        #     <span style="font-size: 14px; font-weight: bold;">@id</span><br>
        #     <span>Width: @width</span>
        # </div>
        # """,
        # flow_tooltip="""
        # <div>
        #     <span style="font-size: 14px; font-weight: bold;">@source → @target</span><br>
        #     <span>Flow: @width</span>
        # </div>
        # """,
        # residual_tooltip="""
        # <div>
        #     <span style="font-size: 14px; font-weight: bold;">@id</span><br>
        #     <span>Residual: @sign</span>
        # </div>
        # """,
    )

    # sankey.view.sizing_mode = "scale_both"

    from holoviz_utils.dataviz import DataPresentation

    # pn.serve(col)
    x = sankey.view.opts(
        hv.opts.Rectangles(
            "Nodes",
            line_color=None,
            tools=["hover"],
            default_tools=[],
            framewise=True,
        ),
        hv.opts.Rectangles(
            "Residuals",
            line_color=None,
            show_legend=False,
            tools=["hover", "wheel_zoom"],
            # active_tools=["wheel_zoom"],
            default_tools=[],
            framewise=True,
        ),
        hv.opts.Polygons(
            line_alpha=0,
            tools=["hover"],
            default_tools=[],
            framewise=True,
        ),
        hv.opts.Labels(
            text_align="left",
            text_baseline="middle",
            text_font_size="9pt",
            text_color="white",
            default_tools=[],
            framewise=True,
        ),
        hv.opts.Overlay(
            height=500,
            default_tools=[],
            tools=[],
            active_tools=["wheel_zoom"],
            hooks=[
                DataPresentation.bokeh_hooks.beige_background,
                DataPresentation.bokeh_hooks.hook_no_yaxis,
                DataPresentation.bokeh_hooks.hook_no_xaxis,
                DataPresentation.bokeh_hooks.hook_transparent_border,
            ],
            framewise=True,
            responsive=True,
        ),
    )
    # hv.save(x, "nodes.html")
# x