VISUAL OF CONNECTION APP
by Code Assistant
SOURCE CODE

<!--BLOGGER / BLOGSPOT COMPATIBLE: FIELD MAPPING GRAPH-->

<script onerror="this.onerror=null;this.src='https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js';" src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js">
</script>

<style>
/* Blogger-safe styles scoped to this widget only */
#field-mapping-widget {
  --ff-sans: Arial, Helvetica, sans-serif;
  --ff-mono: Consolas, "Courier New", monospace;

  --surface: #ffffff;
  --surface-bright: #ffffff;
  --surface-dim: #f2f3f5;
  --surface-container-high: #e6e8eb;

  --on-surface: #202124;
  --on-surface-variant: #656a73;
  --stroke-default: #d9dde3;
  --outline: #727983;

  --primary: #3186ff;
  --accent-container: #dcf1ff;
  --accent-fixed: #3186ff;

  --chart-1: #2575fc;
  --chart-8: #997112;
  --chart-9: #705ec2;

  box-sizing: border-box;
  width: 100%;
  min-width: 0;
  min-height: 280px;
  padding: 12px;

  display: flex;
  flex-direction: column;
  gap: 12px;

  color: var(--on-surface);
  background: var(--surface);
  border: 1px solid #e5e7eb;
  border-radius: 12px;

  font-family: var(--ff-sans);
  font-size: 14px;
  line-height: 1.4;

  position: relative;
  isolation: isolate;
}

#field-mapping-widget *,
#field-mapping-widget *::before,
#field-mapping-widget *::after {
  box-sizing: border-box;
}

/* Optional dark theme */
#field-mapping-widget[data-theme="dark"] {
  --surface: #17191c;
  --surface-bright: #202328;
  --surface-dim: #292d33;
  --surface-container-high: #343941;

  --on-surface: #f1f3f4;
  --on-surface-variant: #b5bac3;
  --stroke-default: #41464f;
  --outline: #a7adb7;

  --primary: #8ab4f8;
  --accent-container: #203b58;
  --accent-fixed: #8ab4f8;

  --chart-1: #6ea8fe;
  --chart-8: #e6bd55;
  --chart-9: #b7a5ff;

  border-color: #41464f;
}

/* Header */
#field-mapping-widget .widget-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}

#field-mapping-widget .header-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

#field-mapping-widget .widget-title {
  margin: 0;
  color: var(--on-surface);
  font-size: 20px;
  font-weight: 700;
  line-height: 1.3;
}

#field-mapping-widget .widget-subtitle {
  margin: 0;
  color: var(--on-surface-variant);
  font-size: 13px;
  line-height: 1.4;
}

/* Filter controls */
#field-mapping-widget .controls-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  flex-wrap: wrap;
}

#field-mapping-widget .chip-group {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
}

#field-mapping-widget .lumi-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;

  padding: 7px 12px;
  min-height: 32px;

  font-size: 13px;
  font-weight: 500;
  color: var(--on-surface);

  background: var(--surface-dim);
  border: 1px solid var(--stroke-default);
  border-radius: 999px;

  cursor: pointer;
  user-select: none;
}

#field-mapping-widget .lumi-chip.is-active {
  color: var(--accent-fixed);
  background: var(--accent-container);
  border-color: var(--primary);
  font-weight: 700;
}

#field-mapping-widget .chip-dot {
  width: 7px;
  height: 7px;
  flex: 0 0 7px;
  border-radius: 50%;
}

/* Mapping viewport */
#field-mapping-widget .mapping-viewport {
  position: relative;
  width: 100%;
  min-width: 0;

  background: var(--surface-bright);
  border: 1px solid var(--stroke-default);
  border-radius: 12px;

  padding: 12px;

  display: flex;
  flex-direction: column;
  gap: 8px;

  overflow: hidden;
}

#field-mapping-widget .column-headers {
  display: flex;
  justify-content: space-between;
  padding: 0 4px;
}

#field-mapping-widget .column-title {
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  color: var(--on-surface-variant);
}

/* Diagram stage */
#field-mapping-widget .diagram-stage {
  position: relative;
  width: 100%;
  height: 280px;
  min-width: 0;

  display: flex;
  justify-content: space-between;
}

#field-mapping-widget .node-column {
  display: flex;
  flex-direction: column;
  justify-content: space-around;

  width: 130px;
  max-width: 38%;
  z-index: 2;
}

/* Source and target nodes */
#field-mapping-widget .node-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 5px;

  min-width: 0;
  height: 38px;
  padding: 0 8px;
  overflow: hidden;

  background: var(--surface-dim);
  border: 1px solid var(--stroke-default);
  border-radius: 8px;

  font-family: var(--ff-mono);
  font-size: 11px;
  color: var(--on-surface);

  cursor: pointer;
  user-select: none;
  position: relative;

  transition:
    background-color 0.2s,
    border-color 0.2s,
    opacity 0.2s;
}

#field-mapping-widget .node-item span:not(.node-handle) {
  overflow-wrap: anywhere;
}

#field-mapping-widget .node-item:hover {
  background: var(--surface-container-high);
  border-color: var(--outline);
}

#field-mapping-widget .node-item.is-selected {
  background: var(--accent-container);
  border-color: var(--primary);
  font-weight: 700;
  box-shadow: 0 0 0 1px var(--primary);
}

#field-mapping-widget .node-item.is-dimmed {
  opacity: 0.35;
}

#field-mapping-widget .node-item.is-hidden {
  display: none;
}

#field-mapping-widget .node-handle {
  width: 8px;
  height: 8px;
  flex: 0 0 8px;
  border-radius: 50%;
  background: var(--outline);
}

#field-mapping-widget .node-item.is-selected .node-handle {
  background: var(--primary);
}

/* SVG connection layer */
#field-mapping-widget .svg-layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 1;
  overflow: visible;
  pointer-events: none;
}

#field-mapping-widget .connection-path {
  fill: none;
  stroke-width: 2.2px;
  pointer-events: stroke;
  cursor: pointer;

  transition:
    stroke-width 0.2s,
    opacity 0.2s;
}

#field-mapping-widget .connection-path:hover {
  stroke-width: 4px !important;
}

#field-mapping-widget .connection-path.is-active {
  stroke-width: 3.5px;
  stroke-dasharray: 6, 3;
  animation: fmw-dash-flow 1s linear infinite;
}

#field-mapping-widget .connection-path.is-dimmed {
  opacity: 0.15 !important;
}

@keyframes fmw-dash-flow {
  from {
    stroke-dashoffset: 18;
  }
  to {
    stroke-dashoffset: 0;
  }
}

/* Information footer */
#field-mapping-widget .info-card {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  flex-wrap: wrap;

  padding: 8px 12px;
  background: var(--surface-dim);
  border-radius: 8px;

  font-size: 12px;
  color: var(--on-surface-variant);
}

#field-mapping-widget .info-title {
  font-weight: 700;
  color: var(--on-surface);
}

#field-mapping-widget .info-metrics {
  font-family: var(--ff-mono);
  white-space: nowrap;
}

/* Floating tooltip */
#field-mapping-tooltip {
  position: fixed;
  z-index: 99999;

  padding: 8px 12px;
  max-width: min(320px, 80vw);

  background: #ffffff;
  color: #202124;
  border: 1px solid #d9dde3;
  border-radius: 8px;

  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);

  pointer-events: none;
  opacity: 0;
  transition: opacity 0.15s;

  font-size: 12px;

  display: flex;
  flex-direction: column;
  gap: 3px;
}

#field-mapping-tooltip .tooltip-header {
  font-weight: 700;
  font-family: Consolas, "Courier New", monospace;
}

#field-mapping-tooltip .tooltip-body {
  color: #656a73;
}

/* Mobile layout */
@media (max-width: 480px) {
  #field-mapping-widget {
    padding: 8px;
    gap: 9px;
  }

  #field-mapping-widget .widget-title {
    font-size: 18px;
  }

  #field-mapping-widget .mapping-viewport {
    padding: 7px;
  }

  #field-mapping-widget .node-column {
    width: 112px;
  }

  #field-mapping-widget .node-item {
    padding: 0 5px;
    font-size: 10px;
  }

  #field-mapping-widget .diagram-stage {
    height: 260px;
  }
}
</style>

<div class="widget-container" id="field-mapping-widget">

  <!--Header-->
  <div class="widget-header">
    <div class="header-text">
      <h2 class="widget-title">Field Mapping Graph</h2>
      <p class="widget-subtitle">
        Interactive link diagram mapping source schema to target fields
      </p>
    </div>
  </div>

  <!--Filter controls-->
  <div class="controls-bar">

    <div class="chip-group" id="filter-chips">

      <div class="lumi-chip is-active" data-filter="all">
        All Fields
      </div>

      <div class="lumi-chip" data-filter="direct">
        <span class="chip-dot" style="background: var(--chart-1);"></span>
        Direct
      </div>

      <div class="lumi-chip" data-filter="transformed">
        <span class="chip-dot" style="background: var(--chart-8);"></span>
        Transformed
      </div>

      <div class="lumi-chip" data-filter="split">
        <span class="chip-dot" style="background: var(--chart-9);"></span>
        Split
      </div>

    </div>

    <div class="chip-group">
      <div class="lumi-chip" data-active="false" id="toggle-unmapped">
        Filter Unmapped
      </div>
    </div>

  </div>

  <!--Main mapping stage-->
  <div class="mapping-viewport" id="mapping-viewport">

    <div class="column-headers">
      <span class="column-title">Source System</span>
      <span class="column-title">Target System</span>
    </div>

    <div class="diagram-stage" id="diagram-stage">

      <!--Source column-->
      <div class="node-column" id="source-col"></div>

      <!--SVG connection overlay-->
      <svg class="svg-layer" id="svg-layer"></svg>

      <!--Target column-->
      <div class="node-column" id="target-col"></div>

    </div>
  </div>

  <!--Details footer-->
  <div class="info-card" id="info-card">

    <div>
      <span id="info-status">
        Click any row node or hover path to isolate schema relationship.
      </span>
    </div>

    <div class="info-metrics" id="info-metrics">
      4 mapped / 3 unmapped
    </div>

  </div>

</div>

<!--Floating tooltip-->
<div class="viz-tooltip" id="field-mapping-tooltip">
  <div class="tooltip-header" id="tt-header">
    SYS1-A100 → DEST-A100
  </div>

  <div class="tooltip-body" id="tt-body">
    Type: Direct Mapping
  </div>
</div>

<script>
document.addEventListener("DOMContentLoaded", function () {

  /* Initial data specification */
  const initialData = {
    source: [
      "SYS1-A100",
      "SYS1-B200",
      "SYS1-C300",
      "SYS1-D400",
      "SYS1-F600"
    ],

    target: [
      "DEST-A100",
      "DEST-B201",
      "DEST-C300",
      "DEST-C301",
      "DEST-E500"
    ],

    connections: [
      {
        from: "SYS1-A100",
        to: "DEST-A100",
        type: "direct",
        desc: "1:1 exact match mapping"
      },
      {
        from: "SYS1-B200",
        to: "DEST-B201",
        type: "transformed",
        desc: "Type converted (String -> Uppercase UUID)"
      },
      {
        from: "SYS1-C300",
        to: "DEST-C300",
        type: "split",
        desc: "Split rule (Prefix part)"
      },
      {
        from: "SYS1-C300",
        to: "DEST-C301",
        type: "split",
        desc: "Split rule (Suffix part)"
      }
    ]
  };

  /* State management */
  const state = {
    selectedNode: null,
    selectedSide: null,
    typeFilter: "all",
    hideUnmapped: false
  };

  const widget = document.getElementById("field-mapping-widget");

  if (!widget) {
    return;
  }

  /* Read CSS variables from this widget */
  function getCssVar(name) {
    if (!name) {
      return "";
    }

    const clean = name
      .replace(/^var\(/, "")
      .replace(/\)$/, "")
      .trim();

    const variableName = clean.startsWith("--")
      ? clean
      : "--" + clean;

    return getComputedStyle(widget)
      .getPropertyValue(variableName)
      .trim() || "";
  }

  /* Connection colors */
  function getTypeColor(type) {
    if (type === "direct") {
      return getCssVar("--chart-1") || "#2575fc";
    }

    if (type === "transformed") {
      return getCssVar("--chart-8") || "#997112";
    }

    if (type === "split") {
      return getCssVar("--chart-9") || "#705ec2";
    }

    return getCssVar("--outline") || "#727983";
  }

  /* Build source and target nodes */
  function buildDOM() {
    const sourceCol = document.getElementById("source-col");
    const targetCol = document.getElementById("target-col");

    if (sourceCol) {
      sourceCol.innerHTML = initialData.source.map(id => `
        <div class="node-item"
             data-id="${id}"
             data-side="source">
          <span>${id}</span>
          <span class="node-handle"></span>
        </div>
      `).join("");
    }

    if (targetCol) {
      targetCol.innerHTML = initialData.target.map(id => `
        <div class="node-item"
             data-id="${id}"
             data-side="target">
          <span class="node-handle"></span>
          <span>${id}</span>
        </div>
      `).join("");
    }

    widget.querySelectorAll(".node-item").forEach(item => {
      item.addEventListener("click", function () {
        const id = item.dataset.id;
        const side = item.dataset.side;

        if (state.selectedNode === id) {
          state.selectedNode = null;
          state.selectedSide = null;
        } else {
          state.selectedNode = id;
          state.selectedSide = side;
        }

        updateViz();
      });
    });
  }

  /* Draw and update the mapping diagram */
  function updateViz() {
    try {
      if (typeof d3 === "undefined") {
        console.error("D3.js failed to load.");
        return;
      }

      const svg = d3.select("#svg-layer");
      const stage = document.getElementById("diagram-stage");

      if (!stage || !svg.node()) {
        return;
      }

      const stageRect = stage.getBoundingClientRect();

      if (!stageRect.width || !stageRect.height) {
        return;
      }

      svg.attr(
        "viewBox",
        `0 0 ${stageRect.width} ${stageRect.height}`
      );

      const mappedSources = new Set(
        initialData.connections.map(c => c.from)
      );

      const mappedTargets = new Set(
        initialData.connections.map(c => c.to)
      );

      /* Filter unmapped nodes and update selection */
      widget.querySelectorAll(".node-item").forEach(node => {
        const id = node.dataset.id;
        const side = node.dataset.side;

        const isMapped = side === "source"
          ? mappedSources.has(id)
          : mappedTargets.has(id);

        if (state.hideUnmapped && !isMapped) {
          node.classList.add("is-hidden");
        } else {
          node.classList.remove("is-hidden");
        }

        if (state.selectedNode === id) {
          node.classList.add("is-selected");
          node.classList.remove("is-dimmed");

        } else if (state.selectedNode) {

          const isConnected = initialData.connections.some(c =>
            (
              state.selectedSide === "source" &&
              c.from === state.selectedNode &&
              c.to === id
            ) ||
            (
              state.selectedSide === "target" &&
              c.to === state.selectedNode &&
              c.from === id
            )
          );

          if (isConnected) {
            node.classList.remove("is-dimmed", "is-selected");
          } else {
            node.classList.add("is-dimmed");
            node.classList.remove("is-selected");
          }

        } else {
          node.classList.remove("is-dimmed", "is-selected");
        }
      });

      /* Apply connection-type filter */
      const activeConnections = initialData.connections.filter(c => {
        return state.typeFilter === "all" ||
          c.type === state.typeFilter;
      });

      /* Calculate SVG path coordinates */
      const pathsData = activeConnections.map(conn => {
        const sourceEl = widget.querySelector(
          `.node-item[data-id="${conn.from}"]`
        );

        const targetEl = widget.querySelector(
          `.node-item[data-id="${conn.to}"]`
        );

        if (
          !sourceEl ||
          !targetEl ||
          sourceEl.classList.contains("is-hidden") ||
          targetEl.classList.contains("is-hidden")
        ) {
          return null;
        }

        const sRect = sourceEl.getBoundingClientRect();
        const tRect = targetEl.getBoundingClientRect();

        const x1 = sRect.right - stageRect.left;
        const y1 = sRect.top + sRect.height / 2 - stageRect.top;

        const x2 = tRect.left - stageRect.left;
        const y2 = tRect.top + tRect.height / 2 - stageRect.top;

        const dx = (x2 - x1) * 0.5;

        const pathD =
          `M ${x1} ${y1} ` +
          `C ${x1 + dx} ${y1}, ` +
          `${x2 - dx} ${y2}, ${x2} ${y2}`;

        const isHighlight = state.selectedNode
          ? (
              (
                state.selectedSide === "source" &&
                conn.from === state.selectedNode
              ) ||
              (
                state.selectedSide === "target" &&
                conn.to === state.selectedNode
              )
            )
          : false;

        const isDimmed = state.selectedNode && !isHighlight;

        return {
          ...conn,
          d: pathD,
          isHighlight,
          isDimmed
        };
      }).filter(Boolean);

      /* Render SVG connection paths */
      const tooltip = d3.select("#field-mapping-tooltip");

      svg.selectAll("path.connection-path")
        .data(pathsData, d => `${d.from}-${d.to}`)
        .join(
          enter => enter.append("path")
            .attr("class", "connection-path")
            .attr("d", d => d.d)
            .attr("stroke", d => getTypeColor(d.type))
            .attr("opacity", 0)
            .call(enter => enter
              .transition()
              .duration(250)
              .attr("opacity", 1)
            ),

          update => update
            .attr("d", d => d.d)
            .attr("stroke", d => getTypeColor(d.type))
            .classed("is-active", d => d.isHighlight)
            .classed("is-dimmed", d => d.isDimmed)
            .attr("opacity", d => d.isDimmed ? 0.15 : 0.85),

          exit => exit
            .transition()
            .duration(200)
            .attr("opacity", 0)
            .remove()
        )

        /* Tooltip appears when hovering over a connection */
        .on("mouseover", (event, d) => {
          const pathEl = event.currentTarget;

          d3.select(pathEl)
            .classed("is-active", true)
            .attr("opacity", 1);

          tooltip.style("opacity", "1");

          document.getElementById("tt-header").textContent =
            `${d.from} → ${d.to}`;

          document.getElementById("tt-body").textContent =
            `Type: ${d.type.toUpperCase()} • ${d.desc}`;

          tooltip
            .style("left", (event.clientX + 14) + "px")
            .style("top", (event.clientY - 12) + "px");
        })

        .on("mousemove", event => {
          tooltip
            .style("left", (event.clientX + 14) + "px")
            .style("top", (event.clientY - 12) + "px");
        })

        .on("mouseout", (event, d) => {
          tooltip.style("opacity", "0");

          const pathEl = event.currentTarget;

          if (!d.isHighlight) {
            d3.select(pathEl)
              .classed("is-active", false)
              .attr(
                "opacity",
                d.isDimmed ? 0.15 : 0.85
              );
          }
        });

      /* Update footer information */
      const infoStatus = document.getElementById("info-status");

      if (state.selectedNode) {
        const conns = initialData.connections.filter(c =>
          c.from === state.selectedNode ||
          c.to === state.selectedNode
        );

        if (conns.length > 0) {
          infoStatus.innerHTML =
            `<span class="info-title">${state.selectedNode}</span> ` +
            `mapped via ${conns.length} path(s): ` +
            conns.map(c => c.type).join(", ");

        } else {
          infoStatus.innerHTML =
            `<span class="info-title">${state.selectedNode}</span> ` +
            "is unmapped (no connection defined)";
        }

      } else {
        infoStatus.textContent =
          "Click any row node or hover path to isolate schema relationship.";
      }

      /* Update mapping count */
      const mappedCount = initialData.connections.length;
      const unmappedCount =
        (initialData.source.length + initialData.target.length) -
        new Set([
          ...initialData.connections.map(c => c.from),
          ...initialData.connections.map(c => c.to)
        ]).size;

      document.getElementById("info-metrics").textContent =
        `${mappedCount} connections / ${unmappedCount} unmapped fields`;

    } catch (err) {
      console.error("Field Mapping Graph error:", err);
    }
  }

  /* Filter button handlers */
  document.getElementById("filter-chips").addEventListener("click", e => {
    const chip = e.target.closest(".lumi-chip");

    if (!chip) {
      return;
    }

    widget.querySelectorAll("#filter-chips .lumi-chip")
      .forEach(c => c.classList.remove("is-active"));

    chip.classList.add("is-active");
    state.typeFilter = chip.dataset.filter;

    updateViz();
  });

  /* Unmapped-field filter */
  const toggleBtn = document.getElementById("toggle-unmapped");

  toggleBtn.addEventListener("click", () => {
    state.hideUnmapped = !state.hideUnmapped;

    toggleBtn.classList.toggle(
      "is-active",
      state.hideUnmapped
    );

    updateViz();
  });

  /* Recalculate paths when the viewport changes size */
  window.addEventListener("resize", updateViz);

  /* Optional theme message support */
  const applyTheme = theme => {
    if (theme !== "dark" && theme !== "light") {
      return;
    }

    widget.setAttribute("data-theme", theme);
    updateViz();
  };

  if (
    window.matchMedia &&
    window.matchMedia("(prefers-color-scheme: dark)").matches
  ) {
    applyTheme("dark");
  }

  window.addEventListener("message", e => {
    if (
      e.data &&
      (
        e.data.type === "set-theme" ||
        e.data.type === "APPLY_THEME"
      )
    ) {
      applyTheme(e.data.theme);
    }
  });

  /* Initialize the graph */
  buildDOM();
  updateViz();

});
</script>

Comments