<!--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
Post a Comment