Files
data-meta-model/generated/pr_metamodel_viewer.html
T

637 lines
64 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PR Data Meta Model &middot; T-Box v1.0</title>
<style>
:root{
--navy:#0A1F44; --gold:#B8935A; --ink:#1A1A1A;
--bg:#F7F6F2; --panel:#FFFFFF; --rule:#E4E2DA; --muted:#6B6B6B;
--c-roots:#0A1F44; --c-ownership:#B8935A; --c-business:#8B3A3A;
--c-logical:#7A4E8B; --c-physical:#1E4B9B; --c-consumption:#1E7A5F;
--c-context:#6B6B6B;
--mono:ui-monospace,"SF Mono",SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace;
--sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;
}
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html,body{height:100%;margin:0}
body{font-family:var(--sans);background:var(--bg);color:var(--ink);
display:flex;flex-direction:column;overflow:hidden}
:focus-visible{outline:2px solid var(--gold);outline-offset:2px}
/* ---------- chrome ---------- */
header{background:var(--navy);color:#fff;padding:10px 18px;display:flex;
align-items:center;gap:16px;flex-shrink:0;box-shadow:0 2px 6px rgba(0,0,0,.15)}
header h1{margin:0;font-family:var(--mono);font-size:13px;font-weight:600;
letter-spacing:.16em;text-transform:uppercase}
header .sub{font-size:11px;color:rgba(255,255,255,.62);margin-top:3px}
.stats{margin-left:auto;display:flex;gap:20px}
.stat{text-align:right;line-height:1.15}
.stat b{display:block;font-family:var(--mono);font-size:16px;color:var(--gold)}
.stat span{font-size:9px;text-transform:uppercase;letter-spacing:.09em;
color:rgba(255,255,255,.55)}
#mtoggle{display:none;background:transparent;color:#fff;
border:1px solid rgba(255,255,255,.4);padding:5px 10px;border-radius:3px;
font-size:11px;font-weight:600;cursor:pointer;font-family:inherit}
main{flex:1;display:flex;min-height:0;position:relative}
/* ---------- sidebar ---------- */
aside{width:255px;background:var(--panel);border-right:1px solid var(--rule);
overflow-y:auto;padding:16px;flex-shrink:0}
aside h2{font-family:var(--mono);font-size:9.5px;text-transform:uppercase;
letter-spacing:.14em;color:var(--muted);margin:22px 0 9px;font-weight:600}
aside h2:first-child{margin-top:0}
#q{width:100%;padding:8px 10px;border:1px solid var(--rule);border-radius:3px;
font-family:var(--mono);font-size:12px;background:var(--bg)}
#q:focus{border-color:var(--navy);outline:none;background:#fff}
.chip{display:flex;align-items:center;gap:9px;padding:5px 0;font-size:12.5px;
cursor:pointer;user-select:none;border:none;background:none;width:100%;
text-align:left;font-family:inherit;color:inherit}
.chip .swatch{width:11px;height:11px;flex-shrink:0;border-radius:2px}
.chip .n{margin-left:auto;font-family:var(--mono);font-size:10px;color:var(--muted)}
.chip.off{opacity:.34}
.seg{display:flex;border:1px solid var(--rule);border-radius:3px;overflow:hidden}
.seg button{flex:1;padding:7px 4px;background:#fff;border:none;cursor:pointer;
font-family:var(--mono);font-size:9.5px;letter-spacing:.07em;
text-transform:uppercase;color:var(--muted)}
.seg button+button{border-left:1px solid var(--rule)}
.seg button.on{background:var(--navy);color:#fff}
.opt{display:flex;align-items:center;gap:8px;padding:4px 0;font-size:12px;cursor:pointer}
.opt input{accent-color:var(--navy);width:15px;height:15px;cursor:pointer}
.note{font-size:11px;line-height:1.55;color:var(--muted)}
.note code{font-family:var(--mono);font-size:10.5px;background:var(--bg);padding:1px 4px}
/* ---------- canvas ---------- */
#wrap{flex:1;min-width:0;position:relative;overflow:hidden;touch-action:none;
background:
linear-gradient(90deg,rgba(10,31,68,.035) 1px,transparent 1px) 0 0/28px 28px,
linear-gradient(180deg,#FCFBF7,#EFEDE3)}
svg{display:block;width:100%;height:100%;cursor:grab}
svg.drag{cursor:grabbing}
.bandrect{opacity:.5}
.bandline{stroke:var(--rule);stroke-width:1}
.bandlabel{font-family:var(--mono);font-size:10px;letter-spacing:.15em;
text-transform:uppercase;font-weight:600}
.bandtag{font-family:var(--sans);font-size:9.5px;fill:var(--muted);font-style:italic}
.node{cursor:pointer}
.node rect{stroke-width:1.5}
.node text{font-size:10px;pointer-events:none;text-anchor:middle;font-weight:500}
.node.dim{opacity:.13}
.node.sel rect{stroke-width:3}
.edge{fill:none;stroke:#9AA0AB;stroke-width:1.2;marker-end:url(#ar)}
.edge.bridge{stroke:var(--navy);stroke-width:2;marker-end:url(#arb)}
.edge.uni{stroke:#C3BFB4;stroke-dasharray:3 3;marker-end:url(#aru)}
.edge.dim{opacity:.07}
.edge.hot{stroke:var(--gold);stroke-width:2.6;marker-end:url(#arg)}
.elabel{font-family:var(--mono);font-size:8px;fill:#5A6270;text-anchor:middle;
pointer-events:none}
.elabel.dim{opacity:.06}
.ebg{fill:#F4F2EC}
#zoom{position:absolute;left:14px;bottom:14px;display:flex;flex-direction:column;
gap:3px;background:#fff;border:1px solid var(--rule);border-radius:4px;padding:3px}
#zoom button{width:32px;height:32px;background:#fff;border:none;border-radius:3px;
font-size:16px;cursor:pointer;color:var(--navy);font-weight:600;font-family:inherit}
#zoom button:hover{background:var(--bg)}
#hint{position:absolute;right:14px;bottom:14px;font-family:var(--mono);
font-size:9.5px;color:var(--muted);background:rgba(255,255,255,.82);
padding:5px 9px;border-radius:3px;letter-spacing:.04em}
/* ---------- inspector ---------- */
#insp{position:absolute;top:0;right:0;width:352px;max-width:100%;height:100%;
background:var(--panel);border-left:1px solid var(--rule);
box-shadow:-6px 0 24px rgba(10,31,68,.10);overflow-y:auto;padding:20px 20px 40px;
transform:translateX(100%);transition:transform .22s ease}
#insp.open{transform:translateX(0)}
#insp .close{position:absolute;top:12px;right:14px;border:none;background:none;
font-size:22px;cursor:pointer;color:var(--muted);line-height:1;padding:2px 6px}
#insp .kicker{font-family:var(--mono);font-size:9px;letter-spacing:.16em;
text-transform:uppercase;font-weight:600}
#insp h3{margin:6px 0 3px;font-size:19px;letter-spacing:-.01em}
#insp .iri{font-family:var(--mono);font-size:10.5px;color:var(--muted);
word-break:break-all;margin-bottom:14px}
#insp h4{font-family:var(--mono);font-size:9px;text-transform:uppercase;
letter-spacing:.14em;color:var(--muted);margin:20px 0 7px;font-weight:600;
border-top:1px solid var(--rule);padding-top:10px}
#insp p{font-size:12.5px;line-height:1.62;margin:0 0 8px}
.pill{display:inline-block;padding:3px 8px;border-radius:2px;font-family:var(--mono);
font-size:9px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;
margin:0 5px 5px 0;border:1px solid}
.rel{display:grid;grid-template-columns:1fr auto;gap:4px 10px;align-items:baseline;
padding:5px 0;border-bottom:1px dotted var(--rule);font-size:12px}
.rel .p{font-family:var(--mono);font-size:10.5px;color:var(--navy)}
.rel .o{color:var(--muted);font-size:11.5px;text-align:right}
.rel .flag{font-family:var(--mono);font-size:8.5px;color:var(--gold);
letter-spacing:.06em;text-transform:uppercase}
.jump{background:none;border:none;padding:0;font:inherit;color:var(--navy);
cursor:pointer;text-decoration:underline;text-underline-offset:2px}
.harvest{background:var(--bg);border-left:2px solid var(--gold);padding:9px 11px;
font-family:var(--mono);font-size:10.5px;line-height:1.55;color:#4A5160}
footer{background:var(--navy);color:rgba(255,255,255,.55);padding:6px 18px;
font-family:var(--mono);font-size:9.5px;letter-spacing:.06em;display:flex;
justify-content:space-between;flex-shrink:0}
@media(max-width:860px){
aside{position:absolute;z-index:30;height:100%;transform:translateX(-100%);
transition:transform .22s;box-shadow:4px 0 18px rgba(0,0,0,.16)}
aside.open{transform:translateX(0)}
#mtoggle{display:inline-block}
.stats{display:none}
#insp{width:100%}
#hint{display:none}
}
@media(prefers-reduced-motion:reduce){*{transition:none!important}}
</style>
</head>
<body>
<header>
<button id="mtoggle" aria-label="Toggle filters">&#9776;</button>
<div>
<h1>PR Data Meta Model &middot; T-Box</h1>
<div class="sub">Five layers, crossed with a provenance axis: what governance asserts, what harvesters observe</div>
</div>
<div class="stats">
<div class="stat"><b id="s-cls">0</b><span>Classes</span></div>
<div class="stat"><b id="s-obj">0</b><span>Relations</span></div>
<div class="stat"><b id="s-dat">0</b><span>Attributes</span></div>
<div class="stat"><b id="s-brg">0</b><span>Seam edges</span></div>
</div>
</header>
<main>
<aside id="side">
<h2>Find a term</h2>
<input id="q" type="search" placeholder="metric, field, lineage&hellip;" autocomplete="off">
<h2>Layout</h2>
<div class="seg" role="group">
<button id="m-strata" class="on">Strata</button>
<button id="m-tax">Taxonomy</button>
<button id="m-free">Network</button>
</div>
<h2>Layers</h2>
<div id="bands"></div>
<h2>Show</h2>
<label class="opt"><input type="checkbox" id="o-uni"> Universal relations</label>
<label class="opt"><input type="checkbox" id="o-dep"> Deprecated terms</label>
<label class="opt"><input type="checkbox" id="o-lab" checked> Relation names</label>
<h2>Reading it</h2>
<p class="note">
Solid fill is <b>curated</b>: governance writes it, a pull request reviews it.
Hatched outline is <b>observed</b>: a harvester writes it nightly and no one
edits it by hand.<br><br>
The two navy edges are the seam &mdash; <code>materializedAs</code> and
<code>physicalizedIn</code>. They are the only place the two worlds meet,
and the only mapping a steward has to confirm.
</p>
</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="#9AA0AB"/></marker>
<marker id="arb" 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="#0A1F44"/></marker>
<marker id="aru" 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="#C3BFB4"/></marker>
<marker id="arg" 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="#B8935A"/></marker>
<pattern id="hatch" width="5" height="5" patternTransform="rotate(45)" patternUnits="userSpaceOnUse">
<line x1="0" y1="0" x2="0" y2="5" stroke="#0A1F44" stroke-width="1.1" opacity=".16"/>
</pattern>
</defs>
<g id="vp"><g id="bL"></g><g id="eL"></g><g id="tL"></g><g id="nL"></g></g>
</svg>
<div id="zoom">
<button id="z-in" aria-label="Zoom in">+</button>
<button id="z-out" aria-label="Zoom out">&minus;</button>
<button id="z-fit" aria-label="Fit to view" style="font-size:13px">&#8635;</button>
</div>
<div id="hint">drag to pan &middot; scroll to zoom &middot; click a class</div>
</div>
<div id="insp" role="dialog" aria-label="Term details">
<button class="close" id="insp-x" aria-label="Close">&times;</button>
<div id="insp-body"></div>
</div>
</main>
<footer>
<span>Build artefact of pr_metamodel.ttl &mdash; regenerate, never edit</span>
<span id="f-ver">T-Box v1.0</span>
</footer>
<script>
var M = {"bands":[{"id":"roots","title":"Model roots","tag":"How a term got here","color":"#0A1F44"},{"id":"ownership","title":"Ownership & Categorization","tag":"Who is accountable","color":"#B8935A"},{"id":"business","title":"Business","tag":"What the business means","color":"#8B3A3A"},{"id":"logical","title":"Logical","tag":"What we commit to deliver","color":"#7A4E8B"},{"id":"physical","title":"Physical","tag":"What actually runs","color":"#1E4B9B"},{"id":"consumption","title":"Consumption","tag":"What people open in the morning","color":"#1E7A5F"},{"id":"context","title":"Actors & vocabularies","tag":"Outside the object stack","color":"#6B6B6B"}],"nodes":[{"id":"pr:MetaModelObject","label":"Meta Model Object","band":"roots","prov":"neutral","parent":"","comment":"Anything the meta model describes. Actors are deliberately NOT Meta Model Objects.","harvest":"","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:CuratedObject","label":"Curated Object","band":"roots","prov":"curated","parent":"pr:MetaModelObject","comment":"Asserted by governance, edited through pull request on the domain TTL file, subject to the full ownership and lifecycle shape (identifier, name, owner, owning Data Domain, status, version).","harvest":"","curation":"ASSERTED","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:ObservedObject","label":"Observed Object","band":"roots","prov":"observed","parent":"pr:MetaModelObject","comment":"Harvested from a running system and rebuilt on every run. Never hand-edited. Carries sourceIdentifier, fullyQualifiedName and harvestedOn instead of curated ownership: attribution is INFERRED through the Data Element it is mapped to, not asserted on the node.","harvest":"","curation":"HARVESTED","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:OwnershipLayerObject","label":"Ownership & Categorization Layer Object","band":"ownership","prov":"curated","parent":"pr:CuratedObject","comment":"","harvest":"","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:BusinessLayerObject","label":"Business Layer Object","band":"business","prov":"curated","parent":"pr:CuratedObject","comment":"","harvest":"","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:LogicalLayerObject","label":"Logical Layer Object","band":"logical","prov":"curated","parent":"pr:CuratedObject","comment":"","harvest":"","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:PhysicalLayerObject","label":"Physical Layer Object","band":"physical","prov":"observed","parent":"pr:ObservedObject","comment":"","harvest":"","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:ConsumptionLayerObject","label":"Consumption Layer Object","band":"consumption","prov":"observed","parent":"pr:ObservedObject","comment":"The BI estate. Separate from Physical because BI assets have their own lifecycle and their own owner: a dashboard belongs to its author, not to the Data Domain that owns the underlying data.","harvest":"","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:DataDomain","label":"Data Domain","band":"ownership","prov":"curated","parent":"pr:OwnershipLayerObject","comment":"","harvest":"","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:SubDomain","label":"Sub-Domain","band":"ownership","prov":"curated","parent":"pr:OwnershipLayerObject","comment":"","harvest":"","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:BusinessObject","label":"Business Object","band":"ownership","prov":"curated","parent":"pr:OwnershipLayerObject","comment":"The unit of ownership attribution. Sits in the Ownership layer rather than the Business layer on purpose: a Business Object is what a Data Domain is accountable for, and what a Data Object implements. Its business meaning is carried by the Business Concepts it groups.","harvest":"","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:BusinessConcept","label":"Business Concept","band":"business","prov":"curated","parent":"pr:BusinessLayerObject","comment":"A shared business notion WITHOUT a formula. The presence of a formula is the sole discriminator against Metric (rule BR-003).","harvest":"","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:Metric","label":"Metric","band":"business","prov":"curated","parent":"pr:BusinessLayerObject","comment":"A quantifiable measurement WITH a formula, a unit and a grain. Under BR-013 a Metric carries the harmonised CALCULATION RULE; variants that differ only by analysis context are Data Elements linked back via computedBy.","harvest":"","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:KPI","label":"Key Performance Indicator","band":"business","prov":"curated","parent":"pr:Metric","comment":"An elevated Metric. Use this class rather than the deprecated isKPI flag.","harvest":"","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:DataObject","label":"Data Object","band":"logical","prov":"curated","parent":"pr:LogicalLayerObject","comment":"The pivot: implements exactly one Business Object, is composed of Data Elements, and is materialized as one or more Physical Relations. The single junction between governance and technology.","harvest":"","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:DataElement","label":"Data Element","band":"logical","prov":"curated","parent":"pr:LogicalLayerObject","comment":"The finest curated unit. Its mapping to a physical Field is the ONLY place where the curated graph and the observed graph meet: proposed automatically by name and type matching, confirmed by a Data Steward.","harvest":"","curation":"PROPOSED_CONFIRMED","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:DataAsset","label":"Data Asset","band":"logical","prov":"curated","parent":"pr:LogicalLayerObject","comment":"Directly consumable by a business user. A reusable building block is a Data Object, not a Data Asset (rule BR-009).","harvest":"","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:DataProduct","label":"Data Product","band":"logical","prov":"curated","parent":"pr:LogicalLayerObject","comment":"The unit of delivery: packages Data Assets and is governed by a Data Contract. Candidate alignment: dcat:Dataset / DPROD.","harvest":"","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:DataContract","label":"Data Contract","band":"logical","prov":"curated","parent":"pr:LogicalLayerObject","comment":"The service commitment attached to a Data Product: schema, freshness, quality thresholds, support. Candidate alignment: ODCS (Bitol).","harvest":"","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:System","label":"System","band":"physical","prov":"observed","parent":"pr:PhysicalLayerObject","comment":"A real system: Snowflake, MDM Product, MDM Customer, a panel provider feed, Tableau Server, Looker. Making this a node rather than a string is what makes 'what depends on MDM Product?' answerable.","harvest":"Manually seeded once, then referenced. Replaces the free-text hasSourceSystem.","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:Database","label":"Database","band":"physical","prov":"observed","parent":"pr:PhysicalLayerObject","comment":"","harvest":"SNOWFLAKE.INFORMATION_SCHEMA.DATABASES","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:Schema","label":"Schema","band":"physical","prov":"observed","parent":"pr:PhysicalLayerObject","comment":"","harvest":"SNOWFLAKE.INFORMATION_SCHEMA.SCHEMATA","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:PhysicalRelation","label":"Physical Relation","band":"physical","prov":"observed","parent":"pr:PhysicalLayerObject","comment":"Abstract parent of everything with columns. Specialised because impact traversal differs by kind: a View propagates impact to whatever it reads.","harvest":"SNOWFLAKE.INFORMATION_SCHEMA.TABLES","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:BaseTable","label":"Base Table","band":"physical","prov":"observed","parent":"pr:PhysicalRelation","comment":"","harvest":"","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:View","label":"View","band":"physical","prov":"observed","parent":"pr:PhysicalRelation","comment":"Depends on other relations through dependsOn, so impact is transitive.","harvest":"","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:MaterializedView","label":"Materialized View","band":"physical","prov":"observed","parent":"pr:PhysicalRelation","comment":"","harvest":"","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:ExternalTable","label":"External Table","band":"physical","prov":"observed","parent":"pr:PhysicalRelation","comment":"Backed by files outside the warehouse. Marks the boundary where lineage leaves Snowflake.","harvest":"","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:Table","label":"Table (deprecated)","band":"physical","prov":"observed","parent":"pr:PhysicalRelation","comment":"v0.9 term. Kept so that existing assertions still classify. Use BaseTable.","harvest":"","curation":"","deprecated":true,"replacedBy":"pr:BaseTable","members":[]},{"id":"pr:Field","label":"Field","band":"physical","prov":"observed","parent":"pr:PhysicalLayerObject","comment":"A physical column. The anchor of impact analysis: everything upstream reaches a Field, everything downstream starts from one.","harvest":"SNOWFLAKE.INFORMATION_SCHEMA.COLUMNS","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:KeyConstraint","label":"Key Constraint","band":"physical","prov":"observed","parent":"pr:PhysicalLayerObject","comment":"Gives foreign keys a home. Rule OW-006 says an FK is not a Data Element - true, but it still needs somewhere to live, and it lets the asserted DataObject-references-DataObject edge be checked against reality.","harvest":"SNOWFLAKE.INFORMATION_SCHEMA.TABLE_CONSTRAINTS + KEY_COLUMN_USAGE","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:PrimaryKey","label":"Primary Key","band":"physical","prov":"observed","parent":"pr:KeyConstraint","comment":"","harvest":"","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:ForeignKeyConstraint","label":"Foreign Key Constraint","band":"physical","prov":"observed","parent":"pr:KeyConstraint","comment":"","harvest":"","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:Transformation","label":"Transformation","band":"physical","prov":"observed","parent":"pr:PhysicalLayerObject","comment":"The code that turns inputs into outputs: a dbt model, a Snowflake task, an ADF pipeline. Subclass of prov:Activity so that lineage is expressed in a standard vocabulary rather than a private one.","harvest":"dbt manifest.json, Snowflake TASKS, Azure Data Factory pipelines","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:BIWorkspace","label":"BI Workspace","band":"consumption","prov":"observed","parent":"pr:ConsumptionLayerObject","comment":"","harvest":"Tableau Metadata API (Project/Site) ; Looker API (Model)","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:BIDataSource","label":"BI Data Source","band":"consumption","prov":"observed","parent":"pr:ConsumptionLayerObject","comment":"The semantic surface a report is built on. This is where BI meets the warehouse.","harvest":"Tableau Published Datasource ; Looker Explore","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:BIField","label":"BI Field","band":"consumption","prov":"observed","parent":"pr:ConsumptionLayerObject","comment":"A field as a business user sees it. realizesMetric is what turns a certified Metric from a definition on paper into something measurable against the real BI estate.","harvest":"Tableau calculated fields ; LookML measures and dimensions","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:BIReport","label":"BI Report","band":"consumption","prov":"observed","parent":"pr:ConsumptionLayerObject","comment":"What a Data Governance Lead actually cares about in an impact analysis: not that a column changed, but which reports break and who to warn.","harvest":"Tableau Workbook/Sheet ; Looker Look/Dashboard","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:Actor","label":"Actor","band":"context","prov":"neutral","parent":"","comment":"A person or a team. Deliberately NOT a MetaModelObject: people are not subject to ownership and lifecycle rules.","harvest":"","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:DataDomainOwner","label":"Data Domain Owner","band":"context","prov":"neutral","parent":"pr:Actor","comment":"","harvest":"","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:DataGovernanceLead","label":"Data Governance Lead (DGL)","band":"context","prov":"neutral","parent":"pr:Actor","comment":"","harvest":"","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:DataSteward","label":"Data Steward","band":"context","prov":"neutral","parent":"pr:Actor","comment":"","harvest":"","curation":"","deprecated":false,"replacedBy":"","members":[]},{"id":"pr:ActivationStatus","label":"Activation Status","band":"context","prov":"neutral","parent":"","comment":"","harvest":"","curation":"","deprecated":false,"replacedBy":"","members":[{"id":"pr:NotActivated","label":"Not Activated"},{"id":"pr:LightActivation","label":"Light Activation"},{"id":"pr:FullActivation","label":"Full Activation"}]},{"id":"pr:Environment","label":"Environment","band":"context","prov":"neutral","parent":"","comment":"Same logical object, several physical incarnations. Kept as a vocabulary rather than folded into the name so that DEV drift never pollutes PROD lineage.","harvest":"","curation":"","deprecated":false,"replacedBy":"","members":[{"id":"pr:Development","label":"Development"},{"id":"pr:UserAcceptance","label":"User Acceptance"},{"id":"pr:Production","label":"Production"}]},{"id":"pr:SystemType","label":"System Type","band":"context","prov":"neutral","parent":"","comment":"","harvest":"","curation":"","deprecated":false,"replacedBy":"","members":[{"id":"pr:SourceApplication","label":"Source Application"},{"id":"pr:MasterDataSystem","label":"Master Data System"},{"id":"pr:DataPlatform","label":"Data Platform"},{"id":"pr:BITool","label":"BI Tool"},{"id":"pr:ExternalProvider","label":"External Provider"}]}],"edges":[{"id":"pr:belongsToDomain","label":"belongs to domain","s":"pr:SubDomain","t":"pr:DataDomain","comment":"","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:belongsToSubDomain","label":"belongs to sub-domain","s":"pr:BusinessObject","t":"pr:SubDomain","comment":"","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:composedOf","label":"composed of","s":"pr:BusinessObject","t":"pr:BusinessConcept","comment":"Part-whole. In v0.9 this was declared owl:inverseOf pr:describes, which was semantically wrong: composition and aboutness are different relations, and a reasoner would infer that every Concept describing an Object is also a component of it.","harvest":"","curation":"","inverseOf":"pr:isComponentOf","subPropertyOf":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:isComponentOf","label":"is component of","s":"pr:BusinessConcept","t":"pr:BusinessObject","comment":"","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:hasMetric","label":"has metric","s":"pr:BusinessObject","t":"pr:Metric","comment":"","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:measures","label":"measures","s":"pr:Metric","t":"pr:BusinessConcept","comment":"","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:hasGrainDimension","label":"has grain dimension","s":"pr:Metric","t":"pr:DataObject","comment":"The dimensional Data Objects that define the Metric's grain. Replaces reading grain out of the free-text hasGranularity string: only a real reference lets you detect that two Metrics cannot be aggregated together.","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:computedBy","label":"computed by","s":"pr:Metric","t":"pr:DataElement","comment":"BR-013. Links a mother Metric to the granular Data Elements that implement it in a given analysis context (period, promo status, scope, unit, currency).","harvest":"","curation":"","inverseOf":"","subPropertyOf":"prov:wasDerivedFrom","functional":false,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:implements","label":"implements","s":"pr:DataObject","t":"pr:BusinessObject","comment":"The pivot. Exactly one, enforced by SHACL (BR-006).","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:hasElement","label":"has element","s":"pr:DataObject","t":"pr:DataElement","comment":"","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:references","label":"references","s":"pr:DataObject","t":"pr:DataObject","comment":"Foreign-key reference between Data Objects. NOT ownership. Now checkable against the harvested ForeignKeyConstraint objects.","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:usesDataObject","label":"uses Data Object","s":"pr:DataAsset","t":"pr:DataObject","comment":"","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:packages","label":"packages","s":"pr:DataProduct","t":"pr:DataAsset","comment":"","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:governedBy","label":"governed by","s":"pr:DataProduct","t":"pr:DataContract","comment":"","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:materializedAs","label":"materialized as","s":"pr:DataObject","t":"pr:PhysicalRelation","comment":"Data Object to physical relation. Not functional: the same Data Object exists in DEV, UAT and PROD, and sometimes across several tables.","harvest":"","curation":"PROPOSED_CONFIRMED","inverseOf":"","subPropertyOf":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:physicalizedIn","label":"physicalized in","s":"pr:DataElement","t":"pr:Field","comment":"Data Element to physical column, 1..n. Proposed by the reconciliation job on fullyQualifiedName and type, confirmed by a Data Steward. This single edge is what makes end-to-end impact analysis possible.","harvest":"","curation":"PROPOSED_CONFIRMED","inverseOf":"","subPropertyOf":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:hasSystemType","label":"has system type","s":"pr:System","t":"pr:SystemType","comment":"","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:hostedOn","label":"hosted on","s":"pr:Database","t":"pr:System","comment":"","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:inDatabase","label":"in database","s":"pr:Schema","t":"pr:Database","comment":"","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:inSchema","label":"in schema","s":"pr:PhysicalRelation","t":"pr:Schema","comment":"","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:hasField","label":"has field","s":"pr:PhysicalRelation","t":"pr:Field","comment":"Domain widened from Table to PhysicalRelation so that views and external tables carry columns too. Inverse now declared - in v0.9 hasField and livesIn were inverses in fact but not in the model, so they were free to diverge.","harvest":"","curation":"","inverseOf":"pr:livesIn","subPropertyOf":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:livesIn","label":"lives in","s":"pr:Field","t":"pr:PhysicalRelation","comment":"","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:deployedIn","label":"deployed in","s":"pr:ObservedObject","t":"pr:Environment","comment":"","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:constrainsRelation","label":"constrains relation","s":"pr:KeyConstraint","t":"pr:PhysicalRelation","comment":"","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:constrainsField","label":"constrains field","s":"pr:KeyConstraint","t":"pr:Field","comment":"Several fields for a composite key.","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:fromField","label":"from field","s":"pr:ForeignKeyConstraint","t":"pr:Field","comment":"","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:toField","label":"to field","s":"pr:ForeignKeyConstraint","t":"pr:Field","comment":"","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:consumes","label":"consumes","s":"pr:Transformation","t":"pr:PhysicalRelation","comment":"","harvest":"","curation":"","inverseOf":"","subPropertyOf":"prov:used","functional":false,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:produces","label":"produces","s":"pr:Transformation","t":"pr:PhysicalRelation","comment":"","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:dependsOn","label":"depends on","s":"pr:PhysicalRelation","t":"pr:PhysicalRelation","comment":"Relation-level dependency, view to table. Transitive by nature; in practice compute the closure at query time in Snowflake rather than materialising it with a reasoner.","harvest":"SNOWFLAKE.ACCOUNT_USAGE.OBJECT_DEPENDENCIES","curation":"","inverseOf":"","subPropertyOf":"","functional":false,"transitive":true,"deprecated":false,"replacedBy":""},{"id":"pr:derivedFrom","label":"derived from","s":"pr:Field","t":"pr:Field","comment":"Column-to-column lineage. The single highest-value edge in the model, and largely free: Snowflake already computes it.","harvest":"SNOWFLAKE.ACCOUNT_USAGE.ACCESS_HISTORY (column lineage) ; dbt manifest","curation":"","inverseOf":"","subPropertyOf":"prov:wasDerivedFrom","functional":false,"transitive":true,"deprecated":false,"replacedBy":""},{"id":"pr:inWorkspace","label":"in workspace","s":"pr:ConsumptionLayerObject","t":"pr:BIWorkspace","comment":"","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:biConsumes","label":"BI consumes","s":"pr:BIDataSource","t":"pr:PhysicalRelation","comment":"","harvest":"Tableau Metadata API (GraphQL) ; Looker Explore SQL","curation":"","inverseOf":"","subPropertyOf":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:hasBIField","label":"has BI field","s":"pr:BIDataSource","t":"pr:BIField","comment":"","harvest":"","curation":"","inverseOf":"pr:inBIDataSource","subPropertyOf":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:inBIDataSource","label":"in BI data source","s":"pr:BIField","t":"pr:BIDataSource","comment":"","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:biDerivedFrom","label":"BI derived from","s":"pr:BIField","t":"pr:Field","comment":"Closes the last gap in the impact chain: from a dashboard field back to the warehouse column.","harvest":"","curation":"","inverseOf":"","subPropertyOf":"prov:wasDerivedFrom","functional":false,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:realizesMetric","label":"realizes metric","s":"pr:BIField","t":"pr:Metric","comment":"The harmonisation proof. Counting distinct BI Fields realizing one certified Metric, and comparing their expressions, turns 'we harmonised the definitions' from a claim into a measurement over the real BI estate.","harvest":"","curation":"PROPOSED_CONFIRMED","inverseOf":"","subPropertyOf":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:usesBIDataSource","label":"uses BI data source","s":"pr:BIReport","t":"pr:BIDataSource","comment":"","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:hasBIOwner","label":"has BI owner","s":"pr:ConsumptionLayerObject","t":"pr:Actor","comment":"The author or publisher. This is who gets notified by an impact analysis, and it is NOT the Data Domain owner.","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:hasDGL","label":"has DGL","s":"pr:DataDomain","t":"pr:DataGovernanceLead","comment":"","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:hasActivationStatus","label":"activation status","s":"pr:DataDomain","t":"pr:ActivationStatus","comment":"","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":""}],"universal":[{"id":"pr:describes","label":"describes (deprecated)","s":"pr:MetaModelObject","t":"","comment":"Removed as a genuine relation. Its only use was as a false inverse of composedOf.","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":false,"transitive":false,"deprecated":true,"replacedBy":"pr:isComponentOf"},{"id":"pr:sourcedFrom","label":"sourced from","s":"pr:MetaModelObject","t":"pr:System","comment":"Replaces the free-text hasSourceSystem. Domain deliberately omitted: applies to Data Elements, Physical Relations and BI Data Sources alike.","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:ownedBy","label":"owned by","s":"pr:MetaModelObject","t":"pr:Actor","comment":"","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:owningDomain","label":"owning Data Domain","s":"pr:MetaModelObject","t":"pr:DataDomain","comment":"The single Data Domain with authority to define the object's rule. Required on Curated Objects, absent on Observed Objects: a Snowflake column is attributed through the Data Element mapped to it, never asserted directly.","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":""},{"id":"pr:hasSteward","label":"has Data Steward","s":"pr:MetaModelObject","t":"pr:DataSteward","comment":"","harvest":"","curation":"","inverseOf":"","subPropertyOf":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":""}],"datatypes":[{"id":"pr:hasIdentifier","label":"identifier","range":"xsd:string","comment":"","harvest":"","functional":true,"deprecated":false,"replacedBy":""},{"id":"pr:hasName","label":"canonical name","range":"xsd:string","comment":"","harvest":"","functional":true,"deprecated":false,"replacedBy":""},{"id":"pr:hasStatus","label":"status","range":"xsd:string","comment":"","harvest":"","functional":false,"deprecated":false,"replacedBy":""},{"id":"pr:hasVersion","label":"version","range":"xsd:string","comment":"","harvest":"","functional":false,"deprecated":false,"replacedBy":""},{"id":"pr:createdOn","label":"created on","range":"xsd:date","comment":"","harvest":"","functional":false,"deprecated":false,"replacedBy":""},{"id":"pr:lastReviewedOn","label":"last reviewed on","range":"xsd:date","comment":"","harvest":"","functional":false,"deprecated":false,"replacedBy":""},{"id":"pr:hasSource","label":"back-doc source","range":"xsd:string","comment":"","harvest":"","functional":false,"deprecated":false,"replacedBy":""},{"id":"pr:arbitrationStatus","label":"arbitration status","range":"xsd:string","comment":"TO_ARBITRATE blocks publication (OW-007). Use this on the object rather than tracking arbitration in a side spreadsheet.","harvest":"","functional":false,"deprecated":false,"replacedBy":""},{"id":"pr:hasBusinessDefinition","label":"business definition","range":"xsd:string","comment":"","harvest":"","functional":false,"deprecated":false,"replacedBy":""},{"id":"pr:hasTechnicalDefinition","label":"technical definition","range":"xsd:string","comment":"","harvest":"","functional":false,"deprecated":false,"replacedBy":""},{"id":"pr:hasAcronym","label":"acronym","range":"xsd:string","comment":"","harvest":"","functional":false,"deprecated":false,"replacedBy":""},{"id":"pr:hasSynonym","label":"synonym","range":"xsd:string","comment":"","harvest":"","functional":false,"deprecated":false,"replacedBy":""},{"id":"pr:hasBusinessRule","label":"business rule","range":"xsd:string","comment":"","harvest":"","functional":false,"deprecated":false,"replacedBy":""},{"id":"pr:hasFormula","label":"formula","range":"xsd:string","comment":"","harvest":"","functional":false,"deprecated":false,"replacedBy":""},{"id":"pr:hasUnit","label":"unit","range":"xsd:string","comment":"From the Units LoV. Note the LoV currently mixes physical units, currencies (ISO 4217) and value types (Index, Rate, Count) - three different concepts that should eventually be split.","harvest":"","functional":false,"deprecated":false,"replacedBy":""},{"id":"pr:hasGranularity","label":"granularity (label)","range":"xsd:string","comment":"Human-readable grain label, e.g. 'SKU x Customer x Day'. Keep in sync with hasGrainDimension, which is the machine-readable form.","harvest":"","functional":false,"deprecated":false,"replacedBy":""},{"id":"pr:hasTimeAggregation","label":"time aggregation","range":"xsd:string","comment":"","harvest":"","functional":false,"deprecated":false,"replacedBy":""},{"id":"pr:isKPI","label":"is KPI (deprecated)","range":"xsd:boolean","comment":"Duplicated the KPI class. Two ways to say the same thing are two ways to diverge.","harvest":"","functional":false,"deprecated":true,"replacedBy":"pr:KPI"},{"id":"pr:hasFormat","label":"logical format","range":"xsd:string","comment":"","harvest":"","functional":false,"deprecated":false,"replacedBy":""},{"id":"pr:hasExampleValue","label":"example value","range":"xsd:string","comment":"","harvest":"","functional":false,"deprecated":false,"replacedBy":""},{"id":"pr:hasSourceSystem","label":"source system (deprecated)","range":"xsd:string","comment":"Free text. 'Which objects depend on MDM Product?' was unanswerable because MDM Product was a string, not a node.","harvest":"","functional":false,"deprecated":true,"replacedBy":"pr:sourcedFrom"},{"id":"pr:hasSchemaName","label":"schema name (deprecated)","range":"xsd:string","comment":"","harvest":"","functional":false,"deprecated":true,"replacedBy":"pr:Schema"},{"id":"pr:physicalName","label":"physical name","range":"xsd:string","comment":"Recorded verbatim, never altered (NR-009). Must NOT be carried by a Metric - that is a layer leak; under BR-013 the physical name belongs on the Data Element.","harvest":"","functional":true,"deprecated":false,"replacedBy":""},{"id":"pr:fullyQualifiedName","label":"fully qualified name","range":"xsd:string","comment":"DATABASE.SCHEMA.RELATION[.COLUMN]. The actual join key against INFORMATION_SCHEMA: a bare physical name is ambiguous as soon as the same column name exists in two tables.","harvest":"","functional":true,"deprecated":false,"replacedBy":""},{"id":"pr:sourceIdentifier","label":"source identifier","range":"xsd:string","comment":"Native immutable id in the source system (Snowflake object id, Tableau LUID, Looker id). Makes reconciliation idempotent and survives renames, which names do not.","harvest":"","functional":true,"deprecated":false,"replacedBy":""},{"id":"pr:harvestedOn","label":"harvested on","range":"xsd:dateTime","comment":"Staleness marker. An Observed Object not refreshed by the last run is a deletion candidate, not a fact.","harvest":"","functional":false,"deprecated":false,"replacedBy":""},{"id":"pr:hasPhysicalDataType","label":"physical data type","range":"xsd:string","comment":"","harvest":"","functional":false,"deprecated":false,"replacedBy":""},{"id":"pr:isNullable","label":"is nullable","range":"xsd:boolean","comment":"","harvest":"","functional":false,"deprecated":false,"replacedBy":""},{"id":"pr:ordinalPosition","label":"ordinal position","range":"xsd:integer","comment":"","harvest":"","functional":false,"deprecated":false,"replacedBy":""},{"id":"pr:hasViewDefinition","label":"view definition","range":"xsd:string","comment":"","harvest":"","functional":false,"deprecated":false,"replacedBy":""},{"id":"pr:hasVendor","label":"vendor","range":"xsd:string","comment":"","harvest":"","functional":false,"deprecated":false,"replacedBy":""},{"id":"pr:definedBy","label":"defined by (code URI)","range":"xsd:anyURI","comment":"Git URI of the code behind a Transformation. Connects the graph to the repository.","harvest":"","functional":false,"deprecated":false,"replacedBy":""},{"id":"pr:approximateRowCount","label":"approximate row count","range":"xsd:integer","comment":"","harvest":"","functional":false,"deprecated":false,"replacedBy":""},{"id":"pr:lastQueriedOn","label":"last queried on","range":"xsd:dateTime","comment":"Lets the model say 'documented but unused for eight months'. A rationalisation argument that costs nothing and is denominated in warehouse spend.","harvest":"SNOWFLAKE.ACCOUNT_USAGE.ACCESS_HISTORY","functional":false,"deprecated":false,"replacedBy":""},{"id":"pr:queryCount30d","label":"query count (30 days)","range":"xsd:integer","comment":"","harvest":"","functional":false,"deprecated":false,"replacedBy":""},{"id":"pr:hasExpression","label":"BI expression","range":"xsd:string","comment":"The calculated-field formula, Tableau or LookML. Compared against the Metric formula to detect definition drift.","harvest":"","functional":false,"deprecated":false,"replacedBy":""}],"bridges":[{"id":"pr:denotes","label":"denotes","comment":"Links a documentation record (Business Object, Business Concept, Data Object) to the CLASS it describes in a domain ontology. Example: ex:BO_16_03_001 pr:denotes fin:Invoice ."},{"id":"pr:denotesProperty","label":"denotes property","comment":"Links a Data Element or Business Concept to the PROPERTY it describes in a domain ontology. Example: ex:DE_16_03_0001 pr:denotesProperty fin:netAmount ."},{"id":"pr:denotesActivity","label":"denotes activity","comment":"Links a Metric or Business Object to the PROCESS or EVENT it describes, typically a prov:Activity subclass. Example: ex:M_16_03_001 pr:denotesActivity fin:InvoiceIssuance ."}]};
/* ---------------- state ---------------- */
var NW=168, NH=46, BANDGAP=52, GUT=206;
var byId={}, off={}, mode='strata', showUni=false, showDep=false, showLab=true;
var sel=null, hot=null, view={x:0,y:0,s:1};
var svg=document.getElementById('svg'), vp=document.getElementById('vp');
var bL=document.getElementById('bL'), eL=document.getElementById('eL');
var tL=document.getElementById('tL'), nL=document.getElementById('nL');
var SEAM={'pr:materializedAs':1,'pr:physicalizedIn':1};
M.nodes.forEach(function(n){byId[n.id]=n;});
document.getElementById('s-cls').textContent=M.nodes.length;
document.getElementById('s-obj').textContent=M.edges.length+M.universal.length;
document.getElementById('s-dat').textContent=M.datatypes.length;
document.getElementById('s-brg').textContent=M.edges.filter(function(e){return SEAM[e.id];}).length;
function el(t,a){var e=document.createElementNS('http://www.w3.org/2000/svg',t);
if(a)for(var k in a)e.setAttribute(k,a[k]);return e;}
function bandOf(id){return M.bands.filter(function(b){return b.id===id;})[0];}
function visible(n){ if(off[n.band])return false; if(n.deprecated&&!showDep)return false; return true; }
/* ---------------- layout ---------------- */
function layout(){
var live=M.nodes.filter(visible);
if(mode==='tax'){ layoutTax(live); return live; }
var y=0;
M.bands.forEach(function(b){
var group=live.filter(function(n){return n.band===b.id;});
if(!group.length){b._h=0;b._y=y;return;}
var perRow=Math.max(3,Math.ceil(Math.sqrt(group.length*2.1)));
var rows=Math.ceil(group.length/perRow);
b._y=y; b._h=rows*(NH+20)+48; b._rows=rows; b._per=perRow;
group.forEach(function(n,i){
n.row=Math.floor(i/perRow);
n.x=GUT+(i%perRow)*(NW+26)+NW/2;
n.y=y+34+n.row*(NH+20)+NH/2;
});
y+=b._h+BANDGAP;
});
if(mode==='strata') relax(live);
else force(live);
return live;
}
/* barycentre relaxation on x only: connected classes drift together,
strata stay intact, which is the whole point of the drawing */
function relax(live){
var idx={}; live.forEach(function(n){idx[n.id]=n;});
var adj={}; live.forEach(function(n){adj[n.id]=[];});
M.edges.forEach(function(e){
if(idx[e.s]&&idx[e.t]&&(showDep||!e.deprecated)){
adj[e.s].push(e.t); adj[e.t].push(e.s);}
});
for(var it=0;it<70;it++){
live.forEach(function(n){
var ns=adj[n.id]; if(!ns.length)return;
var sum=0,c=0;
ns.forEach(function(m){if(idx[m]){sum+=idx[m].x;c++;}});
if(c) n.x += (sum/c - n.x)*0.22;
});
/* de-overlap inside each row */
var rows={};
live.forEach(function(n){var k=n.band+'|'+n.row;(rows[k]=rows[k]||[]).push(n);});
for(var k in rows){
var r=rows[k].sort(function(a,b){return a.x-b.x;});
for(var i=1;i<r.length;i++){
var need=(NW+22)-(r[i].x-r[i-1].x);
if(need>0){ r[i].x+=need*0.55; r[i-1].x-=need*0.45; }
}
r.forEach(function(n){ if(n.x<GUT+NW/2) n.x=GUT+NW/2; });
}
}
/* final strict pass: guarantee no overlap, preserve the order relaxation found */
var rows2={};
live.forEach(function(n){var k=n.band+'|'+n.row;(rows2[k]=rows2[k]||[]).push(n);});
for(var k2 in rows2){
var r2=rows2[k2].sort(function(a,b){return a.x-b.x;});
var cursor=Math.max(GUT+NW/2, r2[0].x);
r2.forEach(function(n){ n.x=Math.max(n.x,cursor); cursor=n.x+NW+22; });
}
}
/* plain force-directed network: no strata, for people who want the usual view */
function force(live){
var idx={}; live.forEach(function(n){idx[n.id]=n;});
var links=M.edges.filter(function(e){
return idx[e.s]&&idx[e.t]&&e.s!==e.t&&(showDep||!e.deprecated);});
var cx=live.reduce(function(a,n){return a+n.x;},0)/live.length;
var cy=live.reduce(function(a,n){return a+n.y;},0)/live.length;
for(var it=0;it<420;it++){
var k=1-it/420;
for(var i=0;i<live.length;i++){
var a=live[i];
for(var j=i+1;j<live.length;j++){
var b=live[j], dx=b.x-a.x, dy=(b.y-a.y)*1.9;
var d2=dx*dx+dy*dy||1, f=42000/d2;
if(f>9)f=9;
var d=Math.sqrt(d2);
a.x-=dx/d*f; a.y-=dy/d*f*0.5; b.x+=dx/d*f; b.y+=dy/d*f*0.5;
}
a.x+=(cx-a.x)*0.004*k; a.y+=(cy-a.y)*0.006*k;
}
links.forEach(function(e){
var a=idx[e.s], b=idx[e.t], dx=b.x-a.x, dy=b.y-a.y;
var d=Math.sqrt(dx*dx+dy*dy)||1, f=(d-235)*0.018*k;
a.x+=dx/d*f; a.y+=dy/d*f; b.x-=dx/d*f; b.y-=dy/d*f;
});
}
}
function layoutTax(live){
var kids={}, roots=[];
live.forEach(function(n){ if(n.parent&&byId[n.parent]&&visible(byId[n.parent]))
(kids[n.parent]=kids[n.parent]||[]).push(n); else roots.push(n); });
var cursor=0;
function place(n,depth){
var ch=kids[n.id]||[];
n.x=depth*(NW+58)+NW/2+30;
if(!ch.length){ n.y=cursor*(NH+13)+NH/2+20; cursor++; }
else{ ch.forEach(function(c){place(c,depth+1);});
n.y=(ch[0].y+ch[ch.length-1].y)/2; }
}
roots.forEach(function(r){place(r,0); cursor+=0.7;});
M.bands.forEach(function(b){b._h=0;});
}
/* ---------------- render ---------------- */
function render(){
var live=layout();
var idx={}; live.forEach(function(n){idx[n.id]=n;});
bL.innerHTML=''; eL.innerHTML=''; tL.innerHTML=''; nL.innerHTML='';
if(mode==='strata'){
var maxx=Math.max.apply(null,live.map(function(n){return n.x;}).concat([600]))+NW;
M.bands.forEach(function(b){
if(!b._h)return;
bL.appendChild(el('rect',{x:0,y:b._y,width:maxx+60,height:b._h,
fill:b.color,opacity:.045,class:'bandrect'}));
bL.appendChild(el('line',{x1:0,y1:b._y,x2:maxx+60,y2:b._y,class:'bandline'}));
bL.appendChild(el('rect',{x:GUT-5,y:b._y+10,width:3,height:b._h-22,
fill:b.color,opacity:.85}));
var t=el('text',{x:GUT-20,y:b._y+26,class:'bandlabel',fill:b.color,
'text-anchor':'end'});
t.textContent=b.title; bL.appendChild(t);
var g=el('text',{x:GUT-20,y:b._y+41,class:'bandtag','text-anchor':'end'});
g.textContent=b.tag; bL.appendChild(g);
});
}
var list=M.edges.slice();
if(showUni) list=list.concat(M.universal);
list.forEach(function(e){
if(!idx[e.s]||!idx[e.t])return;
if(e.deprecated&&!showDep)return;
var a=idx[e.s], b=idx[e.t], uni=M.universal.indexOf(e)>-1;
var cls='edge'+(uni?' uni':SEAM[e.id]?' bridge':'');
var p, mx, my;
if(a===b){
p='M'+(a.x-24)+','+(a.y-NH/2)+' C'+(a.x-70)+','+(a.y-70)+' '+(a.x+70)+','+(a.y-70)+' '+(a.x+24)+','+(a.y-NH/2);
mx=a.x; my=a.y-52;
}else{
var dx=b.x-a.x, dy=b.y-a.y, k=Math.abs(dy)>4?0.38:0;
var cx=a.x+dx/2+(-dy)*k*0.14, cy=a.y+dy/2+dx*k*0.05;
p='M'+a.x+','+a.y+' Q'+cx+','+cy+' '+b.x+','+b.y;
mx=(a.x+2*cx+b.x)/4; my=(a.y+2*cy+b.y)/4;
}
var path=el('path',{d:p,class:cls}); path.dataset.e=e.id;
path.dataset.a=e.s; path.dataset.b=e.t; eL.appendChild(path);
if(showLab&&!uni){
var w=e.label.length*4.6+8;
var bg=el('rect',{x:mx-w/2,y:my-6,width:w,height:11,rx:2,class:'ebg'});
bg.dataset.el=e.id; tL.appendChild(bg);
var tx=el('text',{x:mx,y:my+2.5,class:'elabel'});
tx.dataset.el=e.id; tx.textContent=e.label; tL.appendChild(tx);
}
});
live.forEach(function(n){
var col=bandOf(n.band).color;
var g=el('g',{class:'node','tabindex':'0','role':'button',
'aria-label':n.label,transform:'translate('+(n.x-NW/2)+','+(n.y-NH/2)+')'});
g.dataset.id=n.id;
g.appendChild(el('rect',{width:NW,height:NH,rx:3,
fill:n.prov==='observed'?'#fff':col,
'fill-opacity':n.prov==='observed'?1:.13,
stroke:col,'stroke-dasharray':n.deprecated?'4 3':'none'}));
if(n.prov==='observed')
g.appendChild(el('rect',{width:NW,height:NH,rx:3,fill:'url(#hatch)',stroke:'none'}));
var lines=wrap(n.label,22);
lines.forEach(function(s,i){
var t=el('text',{x:NW/2,y:NH/2+4+(i-(lines.length-1)/2)*11.8,
fill:n.deprecated?'#9A9A9A':'#1A1A1A',
'text-decoration':n.deprecated?'line-through':'none'});
t.textContent=s; g.appendChild(t);
});
nL.appendChild(g);
});
applyHot();
}
function wrap(s,max){
var w=s.split(' '), out=[], cur='';
w.forEach(function(x){
if((cur+' '+x).trim().length>max){ if(cur)out.push(cur); cur=x; }
else cur=(cur+' '+x).trim();
});
if(cur)out.push(cur);
return out.slice(0,3);
}
/* ---------------- focus / highlight ---------------- */
function neighbours(id){
var s={}; s[id]=1;
M.edges.concat(M.universal).forEach(function(e){
if(e.s===id)s[e.t]=1; if(e.t===id)s[e.s]=1;});
return s;
}
function applyHot(){
var id=hot||sel;
var nodes=nL.querySelectorAll('.node');
var paths=eL.querySelectorAll('.edge');
var labs=tL.querySelectorAll('[data-el]');
if(!id){
nodes.forEach(function(n){n.classList.remove('dim','sel');});
paths.forEach(function(p){p.classList.remove('dim','hot');});
labs.forEach(function(l){l.classList.remove('dim');});
return;
}
var keep=neighbours(id), live={};
paths.forEach(function(p){
var on=(p.dataset.a===id||p.dataset.b===id);
p.classList.toggle('hot',on); p.classList.toggle('dim',!on);
if(on)live[p.dataset.e]=1;
});
labs.forEach(function(l){l.classList.toggle('dim',!live[l.dataset.el]);});
nodes.forEach(function(n){
n.classList.toggle('dim',!keep[n.dataset.id]);
n.classList.toggle('sel',n.dataset.id===sel);
});
}
/* ---------------- inspector ---------------- */
var insp=document.getElementById('insp'), ibody=document.getElementById('insp-body');
function esc(s){return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;');}
function open(id){
var n=byId[id]; if(!n)return;
sel=id; var b=bandOf(n.band), h=[];
h.push('<div class="kicker" style="color:'+b.color+'">'+esc(b.title)+'</div>');
h.push('<h3>'+esc(n.label)+'</h3>');
h.push('<div class="iri">'+esc(n.id)+'</div>');
h.push('<span class="pill" style="color:'+b.color+';border-color:'+b.color+'">'+
(n.prov==='observed'?'Observed &middot; harvested':n.prov==='curated'?'Curated &middot; asserted':'Structural')+'</span>');
if(n.curation) h.push('<span class="pill" style="color:#B8935A;border-color:#B8935A">'+esc(n.curation)+'</span>');
if(n.deprecated) h.push('<span class="pill" style="color:#8B3A3A;border-color:#8B3A3A">Deprecated</span>');
if(n.comment) h.push('<h4>What it is for</h4><p>'+esc(n.comment)+'</p>');
if(n.replacedBy) h.push('<p><b>Replaced by</b> '+link(n.replacedBy)+'</p>');
if(n.harvest) h.push('<h4>Harvested from</h4><div class="harvest">'+esc(n.harvest)+'</div>');
if(n.parent) h.push('<h4>Specialises</h4><div class="rel"><span class="p">rdfs:subClassOf</span><span class="o">'+link(n.parent)+'</span></div>');
var kids=M.nodes.filter(function(m){return m.parent===id;});
if(kids.length) h.push('<h4>Specialised by</h4>'+kids.map(function(k){
return '<div class="rel"><span class="p">'+esc(k.label)+'</span><span class="o">'+
(k.deprecated?'deprecated':bandOf(k.band).title)+'</span></div>';}).join(''));
if(n.members&&n.members.length) h.push('<h4>Allowed values</h4>'+n.members.map(function(m){
return '<div class="rel"><span class="p">'+esc(m.label)+'</span><span class="o">'+esc(m.id)+'</span></div>';}).join(''));
var out=M.edges.concat(M.universal).filter(function(e){return e.s===id&&(showDep||!e.deprecated);});
var inc=M.edges.concat(M.universal).filter(function(e){return e.t===id&&(showDep||!e.deprecated);});
if(out.length) h.push('<h4>Points to</h4>'+out.map(rel).join(''));
if(inc.length) h.push('<h4>Pointed to by</h4>'+inc.map(function(e){
return '<div class="rel"><span class="p">'+esc(e.label)+'</span><span class="o">'+link(e.s)+'</span></div>';}).join(''));
ibody.innerHTML=h.join('');
insp.classList.add('open');
bindJumps(); applyHot();
}
function rel(e){
var flags=[]; if(e.functional)flags.push('exactly one');
if(e.transitive)flags.push('transitive');
if(SEAM[e.id])flags.push('seam');
return '<div class="rel"><span class="p">'+esc(e.label)+
(flags.length?' <span class="flag">'+flags.join(' &middot; ')+'</span>':'')+
'</span><span class="o">'+link(e.t)+'</span>'+
(e.comment?'<p style="grid-column:1/-1;margin:4px 0 0;font-size:11.5px;color:#6B6B6B">'+esc(e.comment)+'</p>':'')+
'</div>';
}
function link(id){
var n=byId[id];
return n?'<button class="jump" data-j="'+id+'">'+esc(n.label)+'</button>':esc(id);
}
function bindJumps(){
ibody.querySelectorAll('.jump').forEach(function(b){
b.onclick=function(){open(b.dataset.j); focusOn(b.dataset.j);};});
}
document.getElementById('insp-x').onclick=function(){
insp.classList.remove('open'); sel=null; applyHot();};
document.addEventListener('keydown',function(e){
if(e.key==='Escape'){insp.classList.remove('open'); sel=null; applyHot();}});
/* ---------------- view ---------------- */
function apply(){vp.setAttribute('transform','translate('+view.x+','+view.y+') scale('+view.s+')');}
function fit(){
var live=M.nodes.filter(visible); if(!live.length)return;
var xs=live.map(function(n){return n.x;}), ys=live.map(function(n){return n.y;});
var pad=(mode==='strata')?GUT+30:NW;
var mnx=Math.min.apply(null,xs)-pad, mxx=Math.max.apply(null,xs)+NW;
var mny=Math.min.apply(null,ys)-90, mxy=Math.max.apply(null,ys)+70;
var r=svg.getBoundingClientRect();
view.s=Math.min(r.width/(mxx-mnx),r.height/(mxy-mny),1.25);
view.x=(r.width-(mxx-mnx)*view.s)/2-mnx*view.s;
view.y=(r.height-(mxy-mny)*view.s)/2-mny*view.s;
apply();
}
function focusOn(id){
var n=byId[id]; if(!n)return;
var r=svg.getBoundingClientRect();
view.x=r.width/2-n.x*view.s-120; view.y=r.height/2-n.y*view.s; apply();
}
document.getElementById('z-in').onclick=function(){zoom(1.22);};
document.getElementById('z-out').onclick=function(){zoom(.82);};
document.getElementById('z-fit').onclick=fit;
function zoom(f){
var r=svg.getBoundingClientRect(), cx=r.width/2, cy=r.height/2;
var ns=Math.max(.18,Math.min(view.s*f,2.6));
view.x=cx-(cx-view.x)*(ns/view.s); view.y=cy-(cy-view.y)*(ns/view.s);
view.s=ns; apply();
}
var drag=false,ds,vs,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);apply();});
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, r=svg.getBoundingClientRect();
var mx=e.clientX-r.left,my=e.clientY-r.top;
var ns=Math.max(.18,Math.min(view.s*f,2.6));
view.x=mx-(mx-view.x)*(ns/view.s);view.y=my-(my-view.y)*(ns/view.s);
view.s=ns;apply();},{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.s};}},{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);apply();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;
var nd=Math.sqrt(dx*dx+dy*dy),ns=Math.max(.18,Math.min(pinch.s*(nd/pinch.d),2.6));
var r=svg.getBoundingClientRect(),cx=r.width/2,cy=r.height/2;
view.x=cx-(cx-view.x)*(ns/view.s);view.y=cy-(cy-view.y)*(ns/view.s);
view.s=ns;apply();e.preventDefault();}},{passive:false});
svg.addEventListener('touchend',function(){drag=false;pinch=null;});
nL.addEventListener('click',function(e){
var g=e.target.closest('.node'); if(g)open(g.dataset.id);});
nL.addEventListener('keydown',function(e){
var g=e.target.closest('.node');
if(g&&(e.key==='Enter'||e.key===' ')){e.preventDefault();open(g.dataset.id);}});
nL.addEventListener('mouseover',function(e){
var g=e.target.closest('.node'); if(g&&!sel){hot=g.dataset.id;applyHot();}});
nL.addEventListener('mouseout',function(e){
if(!e.relatedTarget||!e.relatedTarget.closest||!e.relatedTarget.closest('.node')){
if(!sel){hot=null;applyHot();}}});
/* ---------------- controls ---------------- */
var bandsBox=document.getElementById('bands');
M.bands.forEach(function(b){
var n=M.nodes.filter(function(x){return x.band===b.id;}).length;
if(!n)return;
var btn=document.createElement('button');
btn.className='chip'; btn.setAttribute('aria-pressed','true');
btn.innerHTML='<span class="swatch" style="background:'+b.color+'"></span>'+
b.title+'<span class="n">'+n+'</span>';
btn.onclick=function(){
off[b.id]=!off[b.id];
btn.classList.toggle('off',!!off[b.id]);
btn.setAttribute('aria-pressed',String(!off[b.id]));
render(); fit();};
bandsBox.appendChild(btn);
});
function setMode(m){
mode=m;
['strata','tax','free'].forEach(function(k){
document.getElementById('m-'+k).classList.toggle('on',k===m);});
render(); fit();
}
document.getElementById('m-strata').onclick=function(){setMode('strata');};
document.getElementById('m-tax').onclick=function(){setMode('tax');};
document.getElementById('m-free').onclick=function(){setMode('free');};
document.getElementById('o-uni').onchange=function(){showUni=this.checked;render();};
document.getElementById('o-dep').onchange=function(){showDep=this.checked;render();fit();};
document.getElementById('o-lab').onchange=function(){showLab=this.checked;render();};
document.getElementById('mtoggle').onclick=function(){
document.getElementById('side').classList.toggle('open');};
document.getElementById('q').addEventListener('input',function(){
var v=this.value.trim().toLowerCase();
if(!v){hot=null;applyHot();return;}
var hit=M.nodes.filter(visible).filter(function(n){
return (n.label+' '+n.id+' '+(n.comment||'')).toLowerCase().indexOf(v)>-1;});
nL.querySelectorAll('.node').forEach(function(g){
var on=hit.some(function(n){return n.id===g.dataset.id;});
g.classList.toggle('dim',!on);});
if(hit.length===1){open(hit[0].id);focusOn(hit[0].id);}
});
render(); fit();
window.addEventListener('resize',fit);
</script>
</body>
</html>