Files
data-meta-model/docs/sodh_viewer_legacy_v0.6.html

260 lines
44 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SODH Data - Instance Graph Viewer (A-Box)</title>
<style>
:root{--navy:#0A1F44;--gold:#B8935A;--blue:#1E4B9B;--green:#1E7A5F;--purple:#7A4E8B;--rust:#8B3A3A;--grey:#6B6B6B;--bg:#F7F6F2;--white:#FFF;--muted:#6B6B6B;}
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent;}
html,body{height:100%;margin:0;padding:0;}
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;background:var(--bg);color:#1A1A1A;display:flex;flex-direction:column;overflow:hidden;}
header{background:var(--navy);color:#fff;padding:12px 20px;display:flex;align-items:center;justify-content:space-between;box-shadow:0 2px 6px rgba(0,0,0,.15);flex-shrink:0;}
header h1{margin:0;font-size:17px;font-weight:600;}
header .subtitle{font-size:11px;color:rgba(255,255,255,.7);margin-top:2px;}
header .badge{background:var(--gold);color:var(--navy);padding:4px 10px;border-radius:10px;font-size:10px;font-weight:700;white-space:nowrap;}
main{flex:1;display:flex;overflow:hidden;min-height:0;position:relative;}
aside{width:280px;background:var(--white);border-right:1px solid #E4E2DA;overflow-y:auto;padding:16px;flex-shrink:0;-webkit-overflow-scrolling:touch;}
aside h2{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--muted);margin:0 0 8px 0;font-weight:700;}
aside h2:not(:first-of-type){margin-top:20px;}
.legend-row{display:flex;align-items:center;gap:8px;padding:4px 0;font-size:12px;cursor:pointer;}
.legend-dot{width:13px;height:13px;border-radius:50%;flex-shrink:0;border:2px solid rgba(0,0,0,.15);}
.legend-row.off{opacity:.35;}
.filter-row{display:flex;align-items:center;justify-content:space-between;padding:5px 0;font-size:12px;}
.filter-row input{width:17px;height:17px;cursor:pointer;accent-color:var(--blue);}
.controls{display:flex;flex-direction:column;gap:6px;}
.btn{background:var(--blue);color:#fff;border:none;padding:9px 12px;border-radius:4px;font-size:12px;font-weight:500;cursor:pointer;text-align:center;font-family:inherit;}
.btn.secondary{background:transparent;color:var(--blue);border:1px solid var(--blue);}
.btn.active{background:var(--navy);}
#wrap{flex:1;min-width:0;background:linear-gradient(180deg,#FCFBF7,#F0EEE4);position:relative;overflow:hidden;touch-action:none;}
svg{display:block;width:100%;height:100%;cursor:grab;}
svg.drag{cursor:grabbing;}
.node{cursor:pointer;}
.node:hover circle{filter:brightness(1.15);}
.nlabel{fill:#111;font-size:9px;pointer-events:none;text-anchor:middle;font-family:inherit;}
.elabel{font-size:8px;fill:#444;text-anchor:middle;pointer-events:none;}
.elabel-bg{fill:#F7F6F2;}
#zoom{position:absolute;bottom:16px;left:16px;display:flex;flex-direction:column;gap:4px;background:#fff;border-radius:6px;box-shadow:0 2px 8px rgba(0,0,0,.12);padding:4px;}
#zoom button{width:34px;height:34px;background:#fff;border:none;border-radius:4px;font-size:17px;cursor:pointer;color:var(--navy);font-weight:600;}
#zoom button:hover{background:#f0f0f0;}
#info{position:absolute;bottom:16px;right:16px;width:330px;max-width:calc(100vw - 32px);max-height:68vh;background:#fff;border-radius:6px;box-shadow:0 4px 20px rgba(10,31,68,.18);padding:16px;overflow-y:auto;display:none;-webkit-overflow-scrolling:touch;}
#info h3{margin:0 0 4px 0;font-size:15px;color:var(--navy);}
#info .tag{display:inline-block;padding:3px 8px;font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;border-radius:3px;color:#fff;margin:0 4px 8px 0;}
#info h4{font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--muted);margin:10px 0 4px 0;font-weight:700;}
#info .kv{font-size:12px;line-height:1.5;margin-bottom:3px;}
#info .kv b{color:#333;}
#info ul{margin:0;padding-left:16px;font-size:11px;}
#info .close{position:absolute;top:8px;right:10px;cursor:pointer;font-size:22px;color:var(--muted);border:none;background:none;padding:4px 8px;}
#colormode{margin-top:8px;font-size:12px;}
footer{background:var(--navy);color:rgba(255,255,255,.6);padding:6px 20px;font-size:10px;display:flex;justify-content:space-between;flex-shrink:0;}
#mtoggle{display:none;background:transparent;color:#fff;border:1px solid rgba(255,255,255,.4);padding:4px 10px;border-radius:4px;font-size:11px;font-weight:600;cursor:pointer;margin-right:10px;}
@media(max-width:700px){aside{position:absolute;z-index:20;height:100%;transform:translateX(-100%);transition:transform .25s;width:255px;box-shadow:4px 0 16px rgba(0,0,0,.15);}aside.open{transform:translateX(0);}#mtoggle{display:inline-block;}header h1{font-size:14px;}#info{width:calc(100vw - 32px);}}
</style>
</head>
<body>
<header>
<div style="display:flex;align-items:center;">
<button id="mtoggle" onclick="document.querySelector('aside').classList.toggle('open')">&#9776; Menu</button>
<div><h1>SODH Data &middot; Instance Graph (A-Box)</h1>
<div class="subtitle">Sell Out Data Hub back-documentation &mdash; NODES &middot; woven relationships</div></div>
</div>
<span class="badge">v0.6 &middot; 23 JUL 2026</span>
</header>
<main>
<aside>
<h2>Object type</h2>
<div id="type-legend"></div>
<h2>Colour nodes by</h2>
<div id="colormode">
<label><input type="radio" name="cmode" value="type" checked> Object type</label><br>
<label><input type="radio" name="cmode" value="domain"> Owning Data Domain</label>
</div>
<h2>Show relations</h2>
<div id="rel-filters"></div>
<h2>Layout</h2>
<div class="controls">
<button class="btn active" id="b-force" onclick="setLayout('force')">Force (network)</button>
<button class="btn" id="b-layer" onclick="setLayout('layer')">Layered (by class)</button>
<button class="btn secondary" onclick="resetView()">Reset view</button>
</div>
<h2>About</h2>
<div style="font-size:11px;color:var(--muted);line-height:1.5">Tap a node for its attributes and connections. This is the instance graph (A-Box) &mdash; the actual SODH objects and the relationships woven between them.<br><br>Owner: <b>B. Gourdon</b></div>
</aside>
<div id="wrap">
<svg id="svg" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="ar" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#888"/></marker>
</defs>
<g id="vp"><g id="eL"></g><g id="nL"></g></g>
</svg>
<div id="zoom"><button onclick="zoom(1.25)">+</button><button onclick="zoom(.8)">&minus;</button><button onclick="resetView()" style="font-size:13px">&#8635;</button></div>
<div id="info"><button class="close" onclick="document.getElementById('info').style.display='none'">&times;</button><div id="ic"></div></div>
</div>
</main>
<footer><span>Self-contained SVG viewer &middot; zero dependency</span><span>86 nodes &middot; 131 edges</span></footer>
<script>
var GRAPH = {"nodes": [{"id": "DD-06", "label": "DD-06\nSales Performance", "type": "DataDomain", "domain": "DD-06", "extra": {"DGL": "Bastien Gourdon", "Activation": "FullActivation"}}, {"id": "DD-10", "label": "DD-10\nProduct & Material", "type": "DataDomain", "domain": "DD-10", "extra": {"DGL": "Anas El Kesri", "Activation": "FullActivation"}}, {"id": "DD-04", "label": "DD-04\nCustomer & Channels", "type": "DataDomain", "domain": "DD-04", "extra": {"DGL": "TBD (vacant)", "Activation": "LightActivation"}}, {"id": "DD-16", "label": "DD-16\nFinancial Performance", "type": "DataDomain", "domain": "DD-16", "extra": {"DGL": "Gaelle Seret", "Activation": "FullActivation"}}, {"id": "DD-21", "label": "DD-21\nData Governance", "type": "DataDomain", "domain": "DD-21", "extra": {"DGL": "Bastien Gourdon", "Activation": "FullActivation"}}, {"id": "DD-05", "label": "DD-05\nSales Activation", "type": "DataDomain", "domain": "DD-05", "extra": {"DGL": "Alberic Piot", "Activation": "FullActivation"}}, {"id": "SD-06.01", "label": "SD-06.01\nSell Out", "type": "SubDomain", "domain": "DD-06", "extra": {"parent": "DD-06"}}, {"id": "SD-10.01", "label": "SD-10.01\nProduct Structure", "type": "SubDomain", "domain": "DD-10", "extra": {"parent": "DD-10"}}, {"id": "SD-04.01", "label": "SD-04.01\nCustomer & Outlet", "type": "SubDomain", "domain": "DD-04", "extra": {"parent": "DD-04"}}, {"id": "SD-16.01", "label": "SD-16.01\nCurrency & Exchange Rates", "type": "SubDomain", "domain": "DD-16", "extra": {"parent": "DD-16"}}, {"id": "SD-16.02", "label": "SD-16.02\nMarketing Entity & Fiscal Calendar", "type": "SubDomain", "domain": "DD-16", "extra": {"parent": "DD-16"}}, {"id": "SD-21.01", "label": "SD-21.01\nCalendar", "type": "SubDomain", "domain": "DD-21", "extra": {"parent": "DD-21"}}, {"id": "SD-05.01", "label": "SD-05.01\nPromotion", "type": "SubDomain", "domain": "DD-05", "extra": {"parent": "DD-05"}}, {"id": "BC-06.01-001", "label": "Sell Out", "type": "BusinessConcept", "domain": "DD-06", "extra": {"definition": "The sale of Pernod Ricard products by retail customers to end consumers, observed via panel or direct feed, as opposed to shipments from Pernod Ricard to its customers (Sell In).", "subdomain": "SD-06.01"}}, {"id": "BC-06.01-002", "label": "Retailer Stock", "type": "BusinessConcept", "domain": "DD-06", "extra": {"definition": "Inventory of Pernod Ricard products held at the retailer level, downstream of Pernod Ricard shipments.", "subdomain": "SD-06.01"}}, {"id": "BC-06.01-003", "label": "Panel Coverage", "type": "BusinessConcept", "domain": "DD-06", "extra": {"definition": "The scope of retail outlets observed by a panel provider, defining the universe over which distribution is computed.", "subdomain": "SD-06.01"}}, {"id": "BC-06.01-004", "label": "Distribution", "type": "BusinessConcept", "domain": "DD-06", "extra": {"definition": "The presence of a product across retail outlets: the number of points of sale carrying at least one Pernod Ricard product, related to the total number of points of sale in the Pernod Ricard universe.", "subdomain": "SD-06.01"}}, {"id": "BC-05.01-001", "label": "Promotion", "type": "BusinessConcept", "domain": "DD-05", "extra": {"definition": "A commercial mechanism applied at retail to stimulate sales (for example Temporary Price Reduction, Multibuy, Feature Display).", "subdomain": "SD-05.01"}}, {"id": "BO-06.01-001", "label": "Sell Out Volume", "type": "BusinessObject", "domain": "DD-06", "extra": {"definition": "Quantities of Pernod Ricard products sold by retail customers to end consumers, observed at retail.", "steward": "Sell Out Steward", "source": "SODH Gold / F_SELL_OUT"}}, {"id": "BO-06.01-002", "label": "Sell Out Value", "type": "BusinessObject", "domain": "DD-06", "extra": {"definition": "Monetary value of Pernod Ricard products sold to end consumers, expressed in multiple currencies (Euro, US Dollar, or local currency).", "steward": "Sell Out Steward", "source": "SODH Gold / F_SELL_OUT"}}, {"id": "BO-06.01-003", "label": "Sell Out Distribution", "type": "BusinessObject", "domain": "DD-06", "extra": {"definition": "Presence of Pernod Ricard products in retail: the number of points of sale carrying at least one Pernod Ricard product, related to the total number of points of sale in the Pernod Ricard universe.", "steward": "Sell Out Steward", "source": "SODH Gold / F_SELL_OUT_DISTRIBUTION"}}, {"id": "BO-06.01-004", "label": "Retailer Stock", "type": "BusinessObject", "domain": "DD-06", "extra": {"definition": "Inventory of Pernod Ricard products held at retailer level: forward stock volume, days of coverage and stock share.", "steward": "Sell Out Steward", "source": "SODH Gold / F_SELL_OUT"}}, {"id": "BO-06.01-005", "label": "Sell Out Baseline", "type": "BusinessObject", "domain": "DD-06", "extra": {"definition": "Modelled non-promoted volume and the incremental effect attributed to promotions, computed from observed Sell Out.", "steward": "Sell Out Steward", "source": "SODH Gold / F_SELL_OUT_BASELINE"}}, {"id": "BO-10.01-001", "label": "Product Structure", "type": "BusinessObject", "domain": "DD-10", "extra": {"definition": "The corporate product hierarchy from Brand down to Stock Keeping Unit, with Category, Sub-Category, Flavor and unit-of-measure standards. Owned by Product & Material; consumed by SODH.", "steward": "Anas El Kesri", "source": "MDM Product -> SODH [concepts TBC by DD-10]"}}, {"id": "BO-04.01-001", "label": "Customer & Outlet", "type": "BusinessObject", "domain": "DD-04", "extra": {"definition": "Retail customer hierarchy: Distributor, Tier-1 and Tier-2 Customer, Outlet and Channel. Owned by Customer & Channels; consumed by SODH.", "steward": "TBD (vacant)", "source": "MDM Customer -> SODH [concepts TBC by DD-04]"}}, {"id": "BO-16.01-001", "label": "Currency & Exchange Rates", "type": "BusinessObject", "domain": "DD-16", "extra": {"definition": "Currency codes and exchange rates used to convert local Sell Out Value into Euro or US Dollar for group reporting. Owned by Financial Performance; consumed by SODH.", "steward": "Gaelle Seret", "source": "Group Finance -> SODH [concepts TBC by DD-16]"}}, {"id": "BO-21.01-001", "label": "Calendar", "type": "BusinessObject", "domain": "DD-21", "extra": {"definition": "The Gregorian calendar dimension shared across all domains: Date, Day, Week, Month, Quarter and Public Holiday. Owned by Data Governance; consumed by SODH.", "steward": "Bastien Gourdon", "source": "Standard Calendar -> SODH [concepts TBC by DD-21]"}}, {"id": "BO-05.01-001", "label": "Promotion Type", "type": "BusinessObject", "domain": "DD-05", "extra": {"definition": "The controlled taxonomy of promotion mechanisms used to segment Sell Out measures. Owned by Sales Activation; consumed by SODH.", "steward": "Alberic Piot", "source": "Sales Activation -> SODH [concepts TBC by DD-05]"}}, {"id": "M-06.01-001", "label": "Sell Out Volume (9L)", "type": "Metric", "domain": "DD-06", "extra": {"formula": "SUM(GL_VOLUME_9L_NM)", "unit": "9L", "granularity": "SKU x Customer x Day", "physical": "GL_VOLUME_9L_NM", "note": "Core measure"}}, {"id": "M-06.01-002", "label": "Sell Out Volume (L)", "type": "Metric", "domain": "DD-06", "extra": {"formula": "SUM(GL_VOLUME_L_NM)", "unit": "L", "granularity": "SKU x Customer x Day", "physical": "GL_VOLUME_L_NM", "note": ""}}, {"id": "M-06.01-003", "label": "Sell Out Volume Non-Promoted (L)", "type": "Metric", "domain": "DD-06", "extra": {"formula": "SUM(GL_VOLUME_L_NO_PROMO_NM)", "unit": "L", "granularity": "SKU x Customer x Day", "physical": "GL_VOLUME_L_NO_PROMO_NM", "note": "Observed"}}, {"id": "M-06.01-004", "label": "Sell Out Volume Promoted (L)", "type": "Metric", "domain": "DD-06", "extra": {"formula": "SUM(GL_VOLUME_L_ANY_PROMO_NM)", "unit": "L", "granularity": "SKU x Customer x Day", "physical": "GL_VOLUME_L_ANY_PROMO_NM", "note": "Segmented via DD-05 taxonomy"}}, {"id": "M-06.01-010", "label": "Sell Out Value (EUR)", "type": "Metric", "domain": "DD-06", "extra": {"formula": "SUM(GL_VALUE_EUR_NM)", "unit": "EUR", "granularity": "SKU x Customer x Day", "physical": "GL_VALUE_EUR_NM", "note": "FX from DD-16"}}, {"id": "M-06.01-011", "label": "Sell Out Value (LC)", "type": "Metric", "domain": "DD-06", "extra": {"formula": "SUM(GL_VALUE_LC_NM)", "unit": "LC", "granularity": "SKU x Customer x Day", "physical": "GL_VALUE_LC_NM", "note": ""}}, {"id": "M-06.01-012", "label": "Sell Out Value (USD)", "type": "Metric", "domain": "DD-06", "extra": {"formula": "SUM(GL_VALUE_USD_NM)", "unit": "USD", "granularity": "SKU x Customer x Day", "physical": "GL_VALUE_USD_NM", "note": ""}}, {"id": "M-06.01-020", "label": "Sell Out Numeric Distribution (%)", "type": "Metric", "domain": "DD-06", "extra": {"formula": "COUNT(outlets carrying SKU) / COUNT(universe outlets) * 100", "unit": "%", "granularity": "SKU x Country x Period", "physical": "GL_NUMERIC_DISTRIBUTION_NM", "note": "Industry-standard"}}, {"id": "M-06.01-021", "label": "Sell Out Weighted Distribution (%)", "type": "Metric", "domain": "DD-06", "extra": {"formula": "SUM(cat. turnover of outlets carrying SKU) / SUM(total cat. turnover) * 100", "unit": "%", "granularity": "SKU x Country x Period", "physical": "GL_WEIGHTED_DISTRIBUTION_NM", "note": "Industry-standard"}}, {"id": "M-06.01-022", "label": "Sell Out Total Distribution Points (Index)", "type": "Metric", "domain": "DD-06", "extra": {"formula": "SUM(Weighted Distribution across SKUs of the brand)", "unit": "Index", "granularity": "Brand x Country x Period", "physical": "GL_TDP_NM", "note": "Acronym expanded"}}, {"id": "M-06.01-030", "label": "Retailer Stock Forward Volume (L)", "type": "Metric", "domain": "DD-06", "extra": {"formula": "SUM(GL_STOCK_FORWARD_VOLUME_L_NM)", "unit": "L", "granularity": "SKU x Customer x Day", "physical": "GL_STOCK_FORWARD_VOLUME_L_NM", "note": "'Forward'=downstream, NOT forecast"}}, {"id": "M-06.01-031", "label": "Retailer Stock Days of Coverage (Days)", "type": "Metric", "domain": "DD-06", "extra": {"formula": "Retailer Forward Stock Volume / avg daily Sell Out Volume", "unit": "Days", "granularity": "SKU x Customer x Day", "physical": "GL_DAYS_IN_COVERAGE", "note": ""}}, {"id": "M-06.01-032", "label": "Retailer Stock Share by-SKU (%)", "type": "Metric", "domain": "DD-06", "extra": {"formula": "SKU retailer stock / category retailer stock * 100", "unit": "%", "granularity": "SKU x Category x Period", "physical": "GL_SHARES_OF_FORWARD_STOCK_VOLUME_9L_NM", "note": ""}}, {"id": "M-06.01-040", "label": "Sell Out Baseline Volume Modelled (9L)", "type": "Metric", "domain": "DD-06", "extra": {"formula": "MODEL(no-promo scenario volume)", "unit": "9L", "granularity": "SKU x Customer x Week", "physical": "GL_BASELINE_VOLUME_9L_NM", "note": "Modelled (NR-010)"}}, {"id": "M-06.01-041", "label": "Sell Out Incremental Volume vs-Baseline (9L)", "type": "Metric", "domain": "DD-06", "extra": {"formula": "Observed Volume - Baseline Volume", "unit": "9L", "granularity": "SKU x Customer x Week", "physical": "GL_INCREMENTAL_VOLUME_9L_NM", "note": ""}}, {"id": "DE-10.01-0001", "label": "Brand Code", "type": "DataElement", "domain": "DD-10", "extra": {"format": "STRING(10)", "unit": "", "system": "MDM Product", "physical": "BRAND_CD", "note": ""}}, {"id": "DE-10.01-0002", "label": "Brand Name", "type": "DataElement", "domain": "DD-10", "extra": {"format": "STRING(100)", "unit": "", "system": "MDM Product", "physical": "BRAND_NM", "note": ""}}, {"id": "DE-10.01-0003", "label": "Product Category", "type": "DataElement", "domain": "DD-10", "extra": {"format": "STRING(50)", "unit": "", "system": "MDM Product", "physical": "CATEGORY_NM", "note": ""}}, {"id": "DE-10.01-0004", "label": "Product Sub-Category", "type": "DataElement", "domain": "DD-10", "extra": {"format": "STRING(50)", "unit": "", "system": "MDM Product", "physical": "SUB_CATEGORY_NM", "note": ""}}, {"id": "DE-10.01-0005", "label": "SKU Code", "type": "DataElement", "domain": "DD-10", "extra": {"format": "STRING(20)", "unit": "", "system": "MDM Product", "physical": "SKU_CD", "note": "SKU=Stock Keeping Unit"}}, {"id": "DE-10.01-0006", "label": "SKU Description", "type": "DataElement", "domain": "DD-10", "extra": {"format": "STRING(255)", "unit": "", "system": "MDM Product", "physical": "SKU_DESC", "note": ""}}, {"id": "DE-10.01-0007", "label": "Bottle Size (mL)", "type": "DataElement", "domain": "DD-10", "extra": {"format": "INTEGER", "unit": "mL", "system": "MDM Product", "physical": "BOTTLE_SIZE_ML", "note": "Drives 9L-eq"}}, {"id": "DE-10.01-0008", "label": "Pack Size (Count)", "type": "DataElement", "domain": "DD-10", "extra": {"format": "INTEGER", "unit": "Count", "system": "MDM Product", "physical": "PACK_SIZE_NR", "note": ""}}, {"id": "DE-10.01-0009", "label": "Flavor", "type": "DataElement", "domain": "DD-10", "extra": {"format": "STRING(50)", "unit": "", "system": "MDM Product", "physical": "FLAVOR_NM", "note": ""}}, {"id": "DE-10.01-0010", "label": "Innovation Flag", "type": "DataElement", "domain": "DD-10", "extra": {"format": "BOOLEAN", "unit": "", "system": "MDM Product", "physical": "INNOVATION_FLG", "note": "Ownership TBC"}}, {"id": "DE-10.01-0011", "label": "Strategic Priority Flag", "type": "DataElement", "domain": "DD-10", "extra": {"format": "BOOLEAN", "unit": "", "system": "MDM Product", "physical": "STRATEGIC_PRIORITY_FLG", "note": "Ownership TBC"}}, {"id": "DE-10.01-0012", "label": "Unit of Measure Standard", "type": "DataElement", "domain": "DD-10", "extra": {"format": "STRING(10)", "unit": "", "system": "MDM Product", "physical": "UOM_STANDARD_CD", "note": ""}}, {"id": "DE-04.01-0001", "label": "Distributor Code", "type": "DataElement", "domain": "DD-04", "extra": {"format": "STRING(20)", "unit": "", "system": "MDM Customer", "physical": "DISTRIBUTOR_CD", "note": ""}}, {"id": "DE-04.01-0002", "label": "Distributor Name", "type": "DataElement", "domain": "DD-04", "extra": {"format": "STRING(150)", "unit": "", "system": "MDM Customer", "physical": "DISTRIBUTOR_NM", "note": ""}}, {"id": "DE-04.01-0003", "label": "Distributor Branch Code", "type": "DataElement", "domain": "DD-04", "extra": {"format": "STRING(20)", "unit": "", "system": "MDM Customer", "physical": "DISTRIBUTOR_BRANCH_CD", "note": ""}}, {"id": "DE-04.01-0004", "label": "Customer Tier-1 Code", "type": "DataElement", "domain": "DD-04", "extra": {"format": "STRING(20)", "unit": "", "system": "MDM Customer", "physical": "CUSTOMER_T1_CD", "note": "Retailer HQ"}}, {"id": "DE-04.01-0005", "label": "Customer Tier-2 Code", "type": "DataElement", "domain": "DD-04", "extra": {"format": "STRING(20)", "unit": "", "system": "MDM Customer", "physical": "CUSTOMER_T2_CD", "note": "Store/PoS"}}, {"id": "DE-04.01-0006", "label": "Outlet Type", "type": "DataElement", "domain": "DD-04", "extra": {"format": "STRING(30)", "unit": "", "system": "MDM Customer", "physical": "OUTLET_TYPE_NM", "note": ""}}, {"id": "DE-04.01-0007", "label": "Channel", "type": "DataElement", "domain": "DD-04", "extra": {"format": "STRING(30)", "unit": "", "system": "MDM Customer", "physical": "CHANNEL_NM", "note": ""}}, {"id": "DE-04.01-0008", "label": "Country Code", "type": "DataElement", "domain": "DD-04", "extra": {"format": "STRING(2)", "unit": "", "system": "MDM Customer", "physical": "COUNTRY_CD", "note": "ISO 3166-1"}}, {"id": "DE-04.01-0009", "label": "Country Name", "type": "DataElement", "domain": "DD-04", "extra": {"format": "STRING(100)", "unit": "", "system": "MDM Customer", "physical": "COUNTRY_NM", "note": ""}}, {"id": "DE-04.01-0010", "label": "Point of Sale Count (Count)", "type": "DataElement", "domain": "DD-04", "extra": {"format": "INTEGER", "unit": "Count", "system": "MDM Customer", "physical": "POS_COUNT_NR", "note": ""}}, {"id": "DE-16.01-0001", "label": "Currency Code", "type": "DataElement", "domain": "DD-16", "extra": {"format": "STRING(3)", "unit": "", "system": "Group Finance", "physical": "CURRENCY_CD", "note": "ISO 4217"}}, {"id": "DE-16.01-0002", "label": "FX Rate to-Euro (Rate)", "type": "DataElement", "domain": "DD-16", "extra": {"format": "DECIMAL(18,8)", "unit": "Rate", "system": "Group Finance", "physical": "FX_RATE_TO_EUR", "note": ""}}, {"id": "DE-16.01-0003", "label": "FX Rate to-USD (Rate)", "type": "DataElement", "domain": "DD-16", "extra": {"format": "DECIMAL(18,8)", "unit": "Rate", "system": "Group Finance", "physical": "FX_RATE_TO_USD", "note": ""}}, {"id": "DE-16.02-0001", "label": "Marketing Entity Code", "type": "DataElement", "domain": "DD-16", "extra": {"format": "STRING(10)", "unit": "", "system": "Group Finance", "physical": "MARKETING_ENTITY_CD", "note": ""}}, {"id": "DE-16.02-0002", "label": "Fiscal Period Code", "type": "DataElement", "domain": "DD-16", "extra": {"format": "STRING(10)", "unit": "", "system": "Group Finance", "physical": "PR_FISCAL_PERIOD_CD", "note": ""}}, {"id": "DE-16.02-0003", "label": "Fiscal Quarter Code", "type": "DataElement", "domain": "DD-16", "extra": {"format": "STRING(10)", "unit": "", "system": "Group Finance", "physical": "PR_FISCAL_QUARTER_CD", "note": ""}}, {"id": "DE-21.01-0001", "label": "Calendar Date", "type": "DataElement", "domain": "DD-21", "extra": {"format": "DATE", "unit": "Date", "system": "Standard Calendar", "physical": "DATE_DT", "note": "Natural key"}}, {"id": "DE-21.01-0002", "label": "Day of Week Name", "type": "DataElement", "domain": "DD-21", "extra": {"format": "STRING(10)", "unit": "", "system": "Standard Calendar", "physical": "DAY_OF_WEEK_NM", "note": ""}}, {"id": "DE-21.01-0003", "label": "ISO Week Number (Count)", "type": "DataElement", "domain": "DD-21", "extra": {"format": "INTEGER", "unit": "Count", "system": "Standard Calendar", "physical": "ISO_WEEK_NR", "note": "ISO 8601"}}, {"id": "DE-21.01-0004", "label": "Month Number (Count)", "type": "DataElement", "domain": "DD-21", "extra": {"format": "INTEGER", "unit": "Count", "system": "Standard Calendar", "physical": "MONTH_NR", "note": ""}}, {"id": "DE-21.01-0005", "label": "Public Holiday Flag", "type": "DataElement", "domain": "DD-21", "extra": {"format": "BOOLEAN", "unit": "", "system": "Standard Calendar", "physical": "PUBLIC_HOLIDAY_FLG", "note": "By Country"}}, {"id": "DE-05.01-0001", "label": "Promotion Type Temporary Price Reduction Flag", "type": "DataElement", "domain": "DD-05", "extra": {"format": "BOOLEAN", "unit": "", "system": "SODH Gold (flag)", "physical": "GL_PROMO_TPR_FLG", "note": "TPR; owned by DD-05"}}, {"id": "DE-05.01-0002", "label": "Promotion Type Multibuy Flag", "type": "DataElement", "domain": "DD-05", "extra": {"format": "BOOLEAN", "unit": "", "system": "SODH Gold (flag)", "physical": "GL_PROMO_MULTIBUY_FLG", "note": ""}}, {"id": "DE-05.01-0003", "label": "Promotion Type Feature Display Flag", "type": "DataElement", "domain": "DD-05", "extra": {"format": "BOOLEAN", "unit": "", "system": "SODH Gold (flag)", "physical": "GL_PROMO_FEATURE_DISPLAY_FLG", "note": ""}}, {"id": "DE-05.01-0004", "label": "Promotion Type Brochure Flag", "type": "DataElement", "domain": "DD-05", "extra": {"format": "BOOLEAN", "unit": "", "system": "SODH Gold (flag)", "physical": "GL_PROMO_BROCHURE_FLG", "note": ""}}, {"id": "DO-06.01-001", "label": "Sell Out Volume Dataset", "type": "DataObject", "domain": "DD-06", "extra": {"steward": "Sell Out Steward", "physical": "F_SELL_OUT"}}, {"id": "DO-06.01-003", "label": "Sell Out Distribution Dataset", "type": "DataObject", "domain": "DD-06", "extra": {"steward": "Sell Out Steward", "physical": "F_SELL_OUT_DISTRIBUTION"}}, {"id": "DO-10.01-001", "label": "Product Dimension", "type": "DataObject", "domain": "DD-10", "extra": {"steward": "Anas El Kesri", "physical": "D_FINISHED_PRODUCT"}}, {"id": "DO-04.01-001", "label": "Customer Dimension", "type": "DataObject", "domain": "DD-04", "extra": {"steward": "TBD (vacant)", "physical": "D_CUSTOMER"}}, {"id": "DO-16.01-001", "label": "Currency Dimension", "type": "DataObject", "domain": "DD-16", "extra": {"steward": "Gaelle Seret", "physical": "D_CURRENCY"}}, {"id": "DO-21.01-001", "label": "Calendar Dimension", "type": "DataObject", "domain": "DD-21", "extra": {"steward": "Bastien Gourdon", "physical": "D_DATE"}}], "edges": [{"s": "SD-06.01", "t": "DD-06", "p": "belongsToDomain"}, {"s": "SD-10.01", "t": "DD-10", "p": "belongsToDomain"}, {"s": "SD-04.01", "t": "DD-04", "p": "belongsToDomain"}, {"s": "SD-16.01", "t": "DD-16", "p": "belongsToDomain"}, {"s": "SD-16.02", "t": "DD-16", "p": "belongsToDomain"}, {"s": "SD-21.01", "t": "DD-21", "p": "belongsToDomain"}, {"s": "SD-05.01", "t": "DD-05", "p": "belongsToDomain"}, {"s": "BO-06.01-001", "t": "SD-06.01", "p": "belongsToSubDomain"}, {"s": "BO-06.01-001", "t": "BC-06.01-001", "p": "composedOf"}, {"s": "BO-06.01-001", "t": "BC-06.01-003", "p": "composedOf"}, {"s": "BO-06.01-001", "t": "BC-05.01-001", "p": "composedOf"}, {"s": "BO-06.01-001", "t": "M-06.01-001", "p": "hasMetric"}, {"s": "BO-06.01-001", "t": "M-06.01-002", "p": "hasMetric"}, {"s": "BO-06.01-001", "t": "M-06.01-003", "p": "hasMetric"}, {"s": "BO-06.01-001", "t": "M-06.01-004", "p": "hasMetric"}, {"s": "BO-06.01-002", "t": "SD-06.01", "p": "belongsToSubDomain"}, {"s": "BO-06.01-002", "t": "BC-06.01-001", "p": "composedOf"}, {"s": "BO-06.01-002", "t": "BC-06.01-003", "p": "composedOf"}, {"s": "BO-06.01-002", "t": "BC-05.01-001", "p": "composedOf"}, {"s": "BO-06.01-002", "t": "M-06.01-010", "p": "hasMetric"}, {"s": "BO-06.01-002", "t": "M-06.01-011", "p": "hasMetric"}, {"s": "BO-06.01-002", "t": "M-06.01-012", "p": "hasMetric"}, {"s": "BO-06.01-003", "t": "SD-06.01", "p": "belongsToSubDomain"}, {"s": "BO-06.01-003", "t": "BC-06.01-004", "p": "composedOf"}, {"s": "BO-06.01-003", "t": "BC-06.01-003", "p": "composedOf"}, {"s": "BO-06.01-003", "t": "BC-06.01-001", "p": "composedOf"}, {"s": "BO-06.01-003", "t": "M-06.01-020", "p": "hasMetric"}, {"s": "BO-06.01-003", "t": "M-06.01-021", "p": "hasMetric"}, {"s": "BO-06.01-003", "t": "M-06.01-022", "p": "hasMetric"}, {"s": "BO-06.01-004", "t": "SD-06.01", "p": "belongsToSubDomain"}, {"s": "BO-06.01-004", "t": "BC-06.01-002", "p": "composedOf"}, {"s": "BO-06.01-004", "t": "BC-06.01-001", "p": "composedOf"}, {"s": "BO-06.01-004", "t": "BC-06.01-003", "p": "composedOf"}, {"s": "BO-06.01-004", "t": "M-06.01-030", "p": "hasMetric"}, {"s": "BO-06.01-004", "t": "M-06.01-031", "p": "hasMetric"}, {"s": "BO-06.01-004", "t": "M-06.01-032", "p": "hasMetric"}, {"s": "BO-06.01-005", "t": "SD-06.01", "p": "belongsToSubDomain"}, {"s": "BO-06.01-005", "t": "BC-06.01-001", "p": "composedOf"}, {"s": "BO-06.01-005", "t": "BC-05.01-001", "p": "composedOf"}, {"s": "BO-06.01-005", "t": "BC-06.01-003", "p": "composedOf"}, {"s": "BO-06.01-005", "t": "M-06.01-040", "p": "hasMetric"}, {"s": "BO-06.01-005", "t": "M-06.01-041", "p": "hasMetric"}, {"s": "BO-10.01-001", "t": "SD-10.01", "p": "belongsToSubDomain"}, {"s": "BO-10.01-001", "t": "BC-06.01-001", "p": "composedOf"}, {"s": "BO-10.01-001", "t": "BC-06.01-003", "p": "composedOf"}, {"s": "BO-10.01-001", "t": "BC-06.01-004", "p": "composedOf"}, {"s": "BO-04.01-001", "t": "SD-04.01", "p": "belongsToSubDomain"}, {"s": "BO-04.01-001", "t": "BC-06.01-001", "p": "composedOf"}, {"s": "BO-04.01-001", "t": "BC-06.01-003", "p": "composedOf"}, {"s": "BO-04.01-001", "t": "BC-06.01-004", "p": "composedOf"}, {"s": "BO-16.01-001", "t": "SD-16.01", "p": "belongsToSubDomain"}, {"s": "BO-16.01-001", "t": "BC-06.01-001", "p": "composedOf"}, {"s": "BO-16.01-001", "t": "BC-06.01-003", "p": "composedOf"}, {"s": "BO-16.01-001", "t": "BC-06.01-004", "p": "composedOf"}, {"s": "BO-21.01-001", "t": "SD-21.01", "p": "belongsToSubDomain"}, {"s": "BO-21.01-001", "t": "BC-06.01-001", "p": "composedOf"}, {"s": "BO-21.01-001", "t": "BC-06.01-003", "p": "composedOf"}, {"s": "BO-21.01-001", "t": "BC-06.01-004", "p": "composedOf"}, {"s": "BO-05.01-001", "t": "SD-05.01", "p": "belongsToSubDomain"}, {"s": "BO-05.01-001", "t": "BC-05.01-001", "p": "composedOf"}, {"s": "BO-05.01-001", "t": "BC-06.01-001", "p": "composedOf"}, {"s": "BO-05.01-001", "t": "BC-06.01-004", "p": "composedOf"}, {"s": "M-06.01-001", "t": "BC-06.01-001", "p": "measures"}, {"s": "M-06.01-002", "t": "BC-06.01-001", "p": "measures"}, {"s": "M-06.01-003", "t": "BC-06.01-001", "p": "measures"}, {"s": "M-06.01-004", "t": "BC-06.01-001", "p": "measures"}, {"s": "M-06.01-010", "t": "BC-06.01-001", "p": "measures"}, {"s": "M-06.01-011", "t": "BC-06.01-001", "p": "measures"}, {"s": "M-06.01-012", "t": "BC-06.01-001", "p": "measures"}, {"s": "M-06.01-020", "t": "BC-06.01-004", "p": "measures"}, {"s": "M-06.01-021", "t": "BC-06.01-004", "p": "measures"}, {"s": "M-06.01-022", "t": "BC-06.01-004", "p": "measures"}, {"s": "M-06.01-030", "t": "BC-06.01-002", "p": "measures"}, {"s": "M-06.01-031", "t": "BC-06.01-002", "p": "measures"}, {"s": "M-06.01-032", "t": "BC-06.01-002", "p": "measures"}, {"s": "M-06.01-040", "t": "BC-06.01-001", "p": "measures"}, {"s": "M-06.01-041", "t": "BC-06.01-001", "p": "measures"}, {"s": "DO-06.01-001", "t": "BO-06.01-001", "p": "implements"}, {"s": "DO-06.01-001", "t": "DE-05.01-0001", "p": "hasElement"}, {"s": "DO-06.01-001", "t": "DE-05.01-0002", "p": "hasElement"}, {"s": "DO-06.01-001", "t": "DE-05.01-0003", "p": "hasElement"}, {"s": "DO-06.01-001", "t": "DE-05.01-0004", "p": "hasElement"}, {"s": "DO-06.01-001", "t": "DO-04.01-001", "p": "references"}, {"s": "DO-06.01-001", "t": "DO-10.01-001", "p": "references"}, {"s": "DO-06.01-001", "t": "DO-21.01-001", "p": "references"}, {"s": "DO-06.01-001", "t": "DO-16.01-001", "p": "references"}, {"s": "DO-06.01-003", "t": "BO-06.01-003", "p": "implements"}, {"s": "DO-06.01-003", "t": "DE-05.01-0001", "p": "hasElement"}, {"s": "DO-06.01-003", "t": "DE-05.01-0002", "p": "hasElement"}, {"s": "DO-06.01-003", "t": "DE-05.01-0003", "p": "hasElement"}, {"s": "DO-06.01-003", "t": "DE-05.01-0004", "p": "hasElement"}, {"s": "DO-06.01-003", "t": "DO-04.01-001", "p": "references"}, {"s": "DO-06.01-003", "t": "DO-10.01-001", "p": "references"}, {"s": "DO-06.01-003", "t": "DO-21.01-001", "p": "references"}, {"s": "DO-10.01-001", "t": "BO-10.01-001", "p": "implements"}, {"s": "DO-10.01-001", "t": "DE-10.01-0001", "p": "hasElement"}, {"s": "DO-10.01-001", "t": "DE-10.01-0002", "p": "hasElement"}, {"s": "DO-10.01-001", "t": "DE-10.01-0003", "p": "hasElement"}, {"s": "DO-10.01-001", "t": "DE-10.01-0004", "p": "hasElement"}, {"s": "DO-10.01-001", "t": "DE-10.01-0005", "p": "hasElement"}, {"s": "DO-10.01-001", "t": "DE-10.01-0006", "p": "hasElement"}, {"s": "DO-10.01-001", "t": "DE-10.01-0007", "p": "hasElement"}, {"s": "DO-10.01-001", "t": "DE-10.01-0008", "p": "hasElement"}, {"s": "DO-10.01-001", "t": "DE-10.01-0009", "p": "hasElement"}, {"s": "DO-10.01-001", "t": "DE-10.01-0010", "p": "hasElement"}, {"s": "DO-10.01-001", "t": "DE-10.01-0011", "p": "hasElement"}, {"s": "DO-10.01-001", "t": "DE-10.01-0012", "p": "hasElement"}, {"s": "DO-04.01-001", "t": "BO-04.01-001", "p": "implements"}, {"s": "DO-04.01-001", "t": "DE-04.01-0001", "p": "hasElement"}, {"s": "DO-04.01-001", "t": "DE-04.01-0002", "p": "hasElement"}, {"s": "DO-04.01-001", "t": "DE-04.01-0003", "p": "hasElement"}, {"s": "DO-04.01-001", "t": "DE-04.01-0004", "p": "hasElement"}, {"s": "DO-04.01-001", "t": "DE-04.01-0005", "p": "hasElement"}, {"s": "DO-04.01-001", "t": "DE-04.01-0006", "p": "hasElement"}, {"s": "DO-04.01-001", "t": "DE-04.01-0007", "p": "hasElement"}, {"s": "DO-04.01-001", "t": "DE-04.01-0008", "p": "hasElement"}, {"s": "DO-04.01-001", "t": "DE-04.01-0009", "p": "hasElement"}, {"s": "DO-04.01-001", "t": "DE-04.01-0010", "p": "hasElement"}, {"s": "DO-16.01-001", "t": "BO-16.01-001", "p": "implements"}, {"s": "DO-16.01-001", "t": "DE-16.01-0001", "p": "hasElement"}, {"s": "DO-16.01-001", "t": "DE-16.01-0002", "p": "hasElement"}, {"s": "DO-16.01-001", "t": "DE-16.01-0003", "p": "hasElement"}, {"s": "DO-16.01-001", "t": "DE-16.02-0001", "p": "hasElement"}, {"s": "DO-16.01-001", "t": "DE-16.02-0002", "p": "hasElement"}, {"s": "DO-16.01-001", "t": "DE-16.02-0003", "p": "hasElement"}, {"s": "DO-21.01-001", "t": "BO-21.01-001", "p": "implements"}, {"s": "DO-21.01-001", "t": "DE-21.01-0001", "p": "hasElement"}, {"s": "DO-21.01-001", "t": "DE-21.01-0002", "p": "hasElement"}, {"s": "DO-21.01-001", "t": "DE-21.01-0003", "p": "hasElement"}, {"s": "DO-21.01-001", "t": "DE-21.01-0004", "p": "hasElement"}, {"s": "DO-21.01-001", "t": "DE-21.01-0005", "p": "hasElement"}]};
var TYPE_COLOR = {
DataDomain:'#0A1F44', SubDomain:'#33518A', BusinessObject:'#1E4B9B',
BusinessConcept:'#B8935A', Metric:'#C0392B', DataElement:'#1E7A5F',
DataObject:'#7A4E8B'
};
var DOMAIN_COLOR = {
'DD-06':'#0A1F44','DD-10':'#B8935A','DD-04':'#1E4B9B',
'DD-16':'#1E7A5F','DD-21':'#6B6B6B','DD-05':'#7A4E8B'
};
var REL_COLOR = {
belongsToDomain:'#888', belongsToSubDomain:'#888',
composedOf:'#B8935A', hasMetric:'#C0392B', measures:'#C0392B',
implements:'#7A4E8B', hasElement:'#1E7A5F', references:'#1E4B9B'
};
var TYPES = ['DataDomain','SubDomain','BusinessObject','BusinessConcept','Metric','DataElement','DataObject'];
var RELS = ['belongsToDomain','belongsToSubDomain','composedOf','hasMetric','measures','implements','hasElement','references'];
var SVGNS='http://www.w3.org/2000/svg';
var svg=document.getElementById('svg'), vp=document.getElementById('vp'), eL=document.getElementById('eL'), nL=document.getElementById('nL');
var pos={}, view={x:0,y:0,scale:1}, colorMode='type', layout='force';
var typeOn={}, relOn={};
TYPES.forEach(function(t){typeOn[t]=true;});
RELS.forEach(function(r){relOn[r]=true;});
function el(t,a){var e=document.createElementNS(SVGNS,t);if(a)for(var k in a)e.setAttribute(k,a[k]);return e;}
function nodeColor(n){return colorMode==='type'?(TYPE_COLOR[n.type]||'#555'):(DOMAIN_COLOR[n.domain]||'#555');}
function esc(s){return String(s).replace(/[&<>"']/g,function(c){return{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c];});}
// ---- Force layout (simple Fruchterman-Reingold style) ----
function forceLayout(){
var N=GRAPH.nodes, E=GRAPH.edges, W=1400, H=1000;
N.forEach(function(n,i){ if(!pos[n.id]) pos[n.id]={x:W/2+Math.cos(i)*300+(Math.random()-.5)*80, y:H/2+Math.sin(i)*300+(Math.random()-.5)*80};});
var k=110;
for(var it=0; it<220; it++){
var disp={}; N.forEach(function(n){disp[n.id]={x:0,y:0};});
for(var a=0;a<N.length;a++)for(var b=a+1;b<N.length;b++){
var na=N[a].id, nb=N[b].id, dx=pos[na].x-pos[nb].x, dy=pos[na].y-pos[nb].y;
var d=Math.sqrt(dx*dx+dy*dy)||.01, rep=k*k/d;
var ux=dx/d, uy=dy/d;
disp[na].x+=ux*rep; disp[na].y+=uy*rep; disp[nb].x-=ux*rep; disp[nb].y-=uy*rep;
}
E.forEach(function(e){
if(!pos[e.s]||!pos[e.t])return;
var dx=pos[e.s].x-pos[e.t].x, dy=pos[e.s].y-pos[e.t].y, d=Math.sqrt(dx*dx+dy*dy)||.01;
var att=d*d/k, ux=dx/d, uy=dy/d;
disp[e.s].x-=ux*att; disp[e.s].y-=uy*att; disp[e.t].x+=ux*att; disp[e.t].y+=uy*att;
});
var t=Math.max(2,30*(1-it/220));
N.forEach(function(n){
var dd=disp[n.id], len=Math.sqrt(dd.x*dd.x+dd.y*dd.y)||.01;
pos[n.id].x+=dd.x/len*Math.min(len,t); pos[n.id].y+=dd.y/len*Math.min(len,t);
});
}
}
// ---- Layered layout (by class row) ----
function layerLayout(){
var rows={DataDomain:0,SubDomain:1,BusinessObject:2,BusinessConcept:3,Metric:4,DataObject:5,DataElement:6};
var buckets={}; GRAPH.nodes.forEach(function(n){var r=rows[n.type];(buckets[r]=buckets[r]||[]).push(n);});
var maxW=0; Object.keys(buckets).forEach(function(r){maxW=Math.max(maxW,buckets[r].length);});
var Wpx=Math.max(1400,maxW*95);
Object.keys(buckets).forEach(function(r){
var arr=buckets[r], sp=Wpx/(arr.length+1);
arr.forEach(function(n,i){ pos[n.id]={x:sp*(i+1), y:90+parseInt(r)*135};});
});
}
function nodeRadius(n){return n.type==='DataDomain'?26:(n.type==='SubDomain'?22:(n.type==='BusinessObject'||n.type==='DataObject'?18:14));}
function render(){
eL.innerHTML=''; nL.innerHTML='';
// edges
GRAPH.edges.forEach(function(e){
if(!relOn[e.p])return;
var s=GRAPH.nodes.find(function(n){return n.id===e.s;}), t=GRAPH.nodes.find(function(n){return n.id===e.t;});
if(!s||!t||!typeOn[s.type]||!typeOn[t.type])return;
if(!pos[e.s]||!pos[e.t])return;
var x1=pos[e.s].x,y1=pos[e.s].y,x2=pos[e.t].x,y2=pos[e.t].y;
var dx=x2-x1,dy=y2-y1,d=Math.sqrt(dx*dx+dy*dy)||1, r=nodeRadius(t)+5;
var ex=x2-dx/d*r, ey=y2-dy/d*r;
eL.appendChild(el('line',{x1:x1,y1:y1,x2:ex,y2:ey,stroke:REL_COLOR[e.p]||'#999','stroke-width':1.2,'marker-end':'url(#ar)',opacity:.7}));
});
// nodes
GRAPH.nodes.forEach(function(n){
if(!typeOn[n.type]||!pos[n.id])return;
var p=pos[n.id], r=nodeRadius(n);
var g=el('g',{class:'node',transform:'translate('+p.x+','+p.y+')'});
g.appendChild(el('circle',{r:r,fill:nodeColor(n),stroke:'#1a1a1a','stroke-width':1}));
var short=n.label.split('\n')[0];
if(short.length>16) short=short.slice(0,15)+'…';
var tx=el('text',{class:'nlabel',y:r+10}); tx.textContent=short; g.appendChild(tx);
g.addEventListener('click',function(ev){ev.stopPropagation();showInfo(n);});
nL.appendChild(g);
});
}
function showInfo(n){
var out=GRAPH.edges.filter(function(e){return e.s===n.id;}).map(function(e){return '<code>'+e.p+'</code> &rarr; '+e.t;});
var inc=GRAPH.edges.filter(function(e){return e.t===n.id;}).map(function(e){return e.s+' <code>'+e.p+'</code>';});
var col=nodeColor(n);
var h='<span class="tag" style="background:'+col+'">'+n.type+'</span><span class="tag" style="background:'+(DOMAIN_COLOR[n.domain]||'#555')+'">'+n.domain+'</span>';
h+='<h3>'+esc(n.label.replace('\n',' &middot; '))+'</h3>';
var ex=n.extra||{};
Object.keys(ex).forEach(function(k){ if(ex[k]) h+='<div class="kv"><b>'+k+':</b> '+esc(ex[k])+'</div>';});
if(out.length) h+='<h4>Outgoing ('+out.length+')</h4><ul><li>'+out.join('</li><li>')+'</li></ul>';
if(inc.length) h+='<h4>Incoming ('+inc.length+')</h4><ul><li>'+inc.join('</li><li>')+'</li></ul>';
document.getElementById('ic').innerHTML=h;
document.getElementById('info').style.display='block';
}
// ---- view transforms ----
function applyView(){vp.setAttribute('transform','translate('+view.x+','+view.y+') scale('+view.scale+')');}
function fit(){
var xs=[],ys=[]; for(var id in pos){xs.push(pos[id].x);ys.push(pos[id].y);}
if(!xs.length)return;
var mnx=Math.min.apply(null,xs)-60,mxx=Math.max.apply(null,xs)+60,mny=Math.min.apply(null,ys)-40,mxy=Math.max.apply(null,ys)+40;
var rect=svg.getBoundingClientRect(),cw=mxx-mnx,ch=mxy-mny;
view.scale=Math.min(rect.width/cw,rect.height/ch)*.9; view.scale=Math.min(view.scale,1.4);
view.x=rect.width/2-(mnx+cw/2)*view.scale; view.y=rect.height/2-(mny+ch/2)*view.scale; applyView();
}
function resetView(){fit();}
function zoom(f){var rect=svg.getBoundingClientRect(),cx=rect.width/2,cy=rect.height/2,ns=Math.max(.15,Math.min(view.scale*f,3));view.x=cx-(cx-view.x)*(ns/view.scale);view.y=cy-(cy-view.y)*(ns/view.scale);view.scale=ns;applyView();}
function setLayout(m){layout=m;pos={};if(m==='force')forceLayout();else layerLayout();render();fit();
document.getElementById('b-force').classList.toggle('active',m==='force');
document.getElementById('b-layer').classList.toggle('active',m==='layer');}
// ---- build side panels ----
function buildLegend(){
var tl=document.getElementById('type-legend');
TYPES.forEach(function(t){
var row=document.createElement('div');row.className='legend-row';
row.innerHTML='<span class="legend-dot" style="background:'+TYPE_COLOR[t]+'"></span><span>'+t+'</span>';
row.onclick=function(){typeOn[t]=!typeOn[t];row.classList.toggle('off',!typeOn[t]);render();};
tl.appendChild(row);
});
var rf=document.getElementById('rel-filters');
RELS.forEach(function(r){
var row=document.createElement('div');row.className='filter-row';
row.innerHTML='<label style="display:flex;align-items:center;gap:6px;"><span style="width:12px;height:3px;background:'+(REL_COLOR[r]||'#999')+';display:inline-block;"></span>'+r+'</label>';
var cb=document.createElement('input');cb.type='checkbox';cb.checked=true;
cb.onchange=function(){relOn[r]=cb.checked;render();};
row.appendChild(cb); rf.appendChild(row);
});
}
document.querySelectorAll('input[name=cmode]').forEach(function(rb){rb.onchange=function(){colorMode=rb.value;render();};});
// ---- pan / zoom interactions ----
var drag=false,ds=null,vs=null,pinch=null;
svg.addEventListener('mousedown',function(e){drag=true;svg.classList.add('drag');ds={x:e.clientX,y:e.clientY};vs={x:view.x,y:view.y};});
window.addEventListener('mousemove',function(e){if(!drag)return;view.x=vs.x+(e.clientX-ds.x);view.y=vs.y+(e.clientY-ds.y);applyView();});
window.addEventListener('mouseup',function(){drag=false;svg.classList.remove('drag');});
svg.addEventListener('wheel',function(e){e.preventDefault();var f=e.deltaY<0?1.1:.9;var rect=svg.getBoundingClientRect(),mx=e.clientX-rect.left,my=e.clientY-rect.top,ns=Math.max(.15,Math.min(view.scale*f,3));view.x=mx-(mx-view.x)*(ns/view.scale);view.y=my-(my-view.y)*(ns/view.scale);view.scale=ns;applyView();},{passive:false});
svg.addEventListener('touchstart',function(e){if(e.touches.length===1){drag=true;ds={x:e.touches[0].clientX,y:e.touches[0].clientY};vs={x:view.x,y:view.y};}else if(e.touches.length===2){drag=false;var dx=e.touches[0].clientX-e.touches[1].clientX,dy=e.touches[0].clientY-e.touches[1].clientY;pinch={d:Math.sqrt(dx*dx+dy*dy),s:view.scale};}},{passive:true});
svg.addEventListener('touchmove',function(e){if(e.touches.length===1&&drag){view.x=vs.x+(e.touches[0].clientX-ds.x);view.y=vs.y+(e.touches[0].clientY-ds.y);applyView();e.preventDefault();}else if(e.touches.length===2&&pinch){var dx=e.touches[0].clientX-e.touches[1].clientX,dy=e.touches[0].clientY-e.touches[1].clientY,nd=Math.sqrt(dx*dx+dy*dy),ns=Math.max(.15,Math.min(pinch.s*(nd/pinch.d),3)),rect=svg.getBoundingClientRect(),cx=rect.width/2,cy=rect.height/2;view.x=cx-(cx-view.x)*(ns/view.scale);view.y=cy-(cy-view.y)*(ns/view.scale);view.scale=ns;applyView();e.preventDefault();}},{passive:false});
svg.addEventListener('touchend',function(){drag=false;pinch=null;});
buildLegend();
window.addEventListener('load',function(){setLayout('force');});
if(document.readyState==='complete')setLayout('force');
window.addEventListener('resize',fit);
</script>
</body>
</html>