tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
This commit is contained in:
+147
-69
@@ -13,7 +13,7 @@ Same visual language as the SODH viewer, with one substitution. Where SODH
|
||||
lays its columns out by owning domain, the T-Box has no domains -- its
|
||||
orthogonal axis is PROVENANCE: what governance asserts against what a
|
||||
harvester observes. The two-colour code carries that instead, blue for
|
||||
curated and dark gold for observed, and the seam is what crosses between them.
|
||||
defined and dark gold for captured, and the seam is what crosses between them.
|
||||
|
||||
No dependency: parses the Turtle subset this file uses, so it runs on the DSM
|
||||
system python without a venv.
|
||||
@@ -33,8 +33,8 @@ WANTED = {
|
||||
"rdfs:domain": "domain", "rdfs:range": "range",
|
||||
"owl:inverseOf": "inverseOf", "owl:deprecated": "deprecated",
|
||||
"dcterms:isReplacedBy": "isReplacedBy",
|
||||
"pr:harvestSource": "harvestSource", "pr:curationMode": "curationMode",
|
||||
"pr:hasAcronym": "acronym",
|
||||
"pr:harvestSource": "harvestSource", "pr:authoringMode": "authoringMode",
|
||||
"pr:hasAcronym": "acronym", "pr:hasShortLabel": "shortLabel",
|
||||
}
|
||||
|
||||
LAYER_ROOT = OrderedDict([
|
||||
@@ -43,8 +43,8 @@ LAYER_ROOT = OrderedDict([
|
||||
("pr:BusinessLayerObject", ("business", "Business", "what it means")),
|
||||
("pr:LogicalLayerObject", ("logical", "Logical",
|
||||
"how it is structured")),
|
||||
("pr:DeliveryLayerObject", ("delivery", "Delivery",
|
||||
"what we promise, to whom")),
|
||||
("pr:DeliveryLayerObject", ("delivery", "Delivery",
|
||||
"what we promise, to whom, until when")),
|
||||
("pr:PhysicalLayerObject", ("physical", "Physical",
|
||||
"what actually runs")),
|
||||
("pr:ConsumptionLayerObject", ("consumption", "Consumption",
|
||||
@@ -213,19 +213,24 @@ def build(subjects):
|
||||
layer = "vocab"
|
||||
else:
|
||||
layer = "roots"
|
||||
prov = ("observed" if "pr:ObservedObject" in anc
|
||||
else "curated" if "pr:CuratedObject" in anc else "structural")
|
||||
# provenance is INFERRED from the type-of chain, never asserted:
|
||||
# a Sub-domain is Defined because it is a kind of Ownership Layer
|
||||
# Object, which is a kind of Defined Object. No separate edge exists
|
||||
# because none is needed -- the subclass chain already says it.
|
||||
prov = ("captured" if "pr:CapturedObject" in anc
|
||||
else "defined" if "pr:DefinedObject" in anc else "structural")
|
||||
depth, p, guard = 0, parents.get(term), 0
|
||||
while p and p in classes and guard < 8:
|
||||
depth += 1; guard += 1; p = parents.get(p)
|
||||
nodes[term] = {
|
||||
"id": term, "kind": "Class",
|
||||
"label": first(rec, "label", term.split(":")[1]),
|
||||
"shortLabel": first(rec, "shortLabel"),
|
||||
"layer": layer, "prov": prov, "depth": depth,
|
||||
"parent": parents.get(term, ""),
|
||||
"comment": first(rec, "comment"),
|
||||
"harvest": first(rec, "harvestSource"),
|
||||
"curation": first(rec, "curationMode"),
|
||||
"authoring": first(rec, "authoringMode"),
|
||||
"acronym": first(rec, "acronym"),
|
||||
"deprecated": first(rec, "deprecated") in ("true", "True"),
|
||||
"replacedBy": first(rec, "isReplacedBy"),
|
||||
@@ -240,11 +245,12 @@ def build(subjects):
|
||||
return {
|
||||
"id": term, "kind": kind,
|
||||
"label": first(rec, "label", term.split(":")[1]),
|
||||
"shortLabel": first(rec, "shortLabel"),
|
||||
"comment": first(rec, "comment"),
|
||||
"domain": first(rec, "domain"), "range": first(rec, "range"),
|
||||
"inverseOf": first(rec, "inverseOf"),
|
||||
"subPropertyOf": first(rec, "subPropertyOf"),
|
||||
"curation": first(rec, "curationMode"),
|
||||
"authoring": first(rec, "authoringMode"),
|
||||
"harvest": first(rec, "harvestSource"),
|
||||
"functional": "owl:FunctionalProperty" in rec.get("type", []),
|
||||
"transitive": "owl:TransitiveProperty" in rec.get("type", []),
|
||||
@@ -298,7 +304,7 @@ def main():
|
||||
c = len([n for n in live_c if n["layer"] == l["id"]])
|
||||
if c:
|
||||
print(" %-28s %d" % (l["title"], c))
|
||||
for p in ("curated", "observed", "structural"):
|
||||
for p in ("defined", "captured", "structural"):
|
||||
c = len([n for n in live_c if n["prov"] == p])
|
||||
print(" %-28s %d" % (p, c))
|
||||
|
||||
@@ -528,10 +534,33 @@ var G = {mode:"strata", provs:[], sel:null, hot:null, labels:true,
|
||||
var NW=126, NH=32, LANEPAD=26, ROWGAP=15, BANDGAP=34, GUT=142;
|
||||
|
||||
function provColour(p){
|
||||
return p === "observed" ? "#8A6A28" : p === "curated" ? "#1E4B9B" : "#6B6B6B";
|
||||
return p === "captured" ? "#8A6A28" : p === "defined" ? "#1E4B9B" : "#6B6B6B";
|
||||
}
|
||||
var LANES = [["curated","Curated \u00b7 asserted by governance","#1E4B9B"],
|
||||
["observed","Observed \u00b7 harvested nightly","#8A6A28"],
|
||||
/* Scoped relations: these carry no rdfs:domain or rdfs:range on purpose,
|
||||
because each serves several classes with different targets and the scope
|
||||
belongs in the SHACL shapes. Both the graph and the cards read this list,
|
||||
so an edge drawn in one is never missing from the other. */
|
||||
var SCOPED_PAIRS = [
|
||||
["pr:belongsTo", "pr:SubDomain", "pr:DataDomain", "belongs to"],
|
||||
["pr:belongsTo", "pr:BusinessObject", "pr:SubDomain", "belongs to"],
|
||||
["pr:represents", "pr:DataObject", "pr:BusinessObject", "represents"],
|
||||
["pr:represents", "pr:DataElement", "pr:BusinessConcept", "represents"],
|
||||
["pr:monitoredBy", "pr:BusinessObject", "pr:DataSteward", "monitored by"],
|
||||
["pr:owningDomain", "pr:DefinedObject", "pr:DataDomain", "owning domain"],
|
||||
["pr:ownedBy", "pr:DefinedObject", "pr:Actor", "owned by"]
|
||||
];
|
||||
var ORDER = ["pr:MetaModelObject", "pr:DefinedObject", "pr:CapturedObject",
|
||||
"pr:OwnershipLayerObject", "pr:DataDomain", "pr:SubDomain", "pr:BusinessObject",
|
||||
"pr:BusinessLayerObject", "pr:BusinessConcept", "pr:Metric", "pr:KPI",
|
||||
"pr:LogicalLayerObject", "pr:DataObject", "pr:DataElement",
|
||||
"pr:DeliveryLayerObject", "pr:DataProduct", "pr:DataContract", "pr:DataInterface",
|
||||
"pr:PhysicalLayerObject", "pr:System", "pr:Database", "pr:Schema",
|
||||
"pr:DataStructure", "pr:BaseTable", "pr:View", "pr:ExternalTable", "pr:Field",
|
||||
"pr:KeyConstraint", "pr:PrimaryKey", "pr:ForeignKeyConstraint", "pr:Transformation",
|
||||
"pr:ConsumptionLayerObject", "pr:BIWorkspace", "pr:BIDataSource", "pr:BIField",
|
||||
"pr:BIReport"];
|
||||
var LANES = [["defined","Defined \u00b7 someone decided it","#1E4B9B"],
|
||||
["captured","Captured \u00b7 read from a running system","#8A6A28"],
|
||||
["structural","Structural","#6B6B6B"]];
|
||||
function live(a){ return a.filter(function(x){ return !x.deprecated; }); }
|
||||
function layerTitle(id){
|
||||
@@ -544,18 +573,43 @@ function buildGraph(){
|
||||
live(M.nodes).forEach(function(n){
|
||||
if(!G.layers[n.layer]) return;
|
||||
if(G.provs.length && G.provs.indexOf(n.prov) === -1) return;
|
||||
/* row = rank inside the layer, so the hierarchy reads DOWNWARD:
|
||||
layer root, then Domain, then Sub-domain, then Business object.
|
||||
Depth alone put siblings side by side, which read left to right. */
|
||||
var rank = ORDER.indexOf(n.id);
|
||||
keep[n.id] = {n:n, x:0, y:0,
|
||||
layer: M.layers.map(function(l){return l.id;}).indexOf(n.layer),
|
||||
row: Math.min(n.depth, 2)};
|
||||
row: rank > -1 ? rank : 90 + Math.min(n.depth, 5)};
|
||||
});
|
||||
var E = [];
|
||||
/* Properties deliberately left without rdfs:domain/range serve several
|
||||
classes with different targets, and their scope lives in the SHACL
|
||||
shapes instead. The graph reads domain and range, so those edges were
|
||||
simply absent -- Business Object to Sub-domain among them. The pairs are
|
||||
declared here, mirroring the shapes, rather than added to the T-Box where
|
||||
they would over-constrain the property. */
|
||||
SCOPED_PAIRS.forEach(function(sc){
|
||||
if(byId[sc[0]] && !byId[sc[0]].deprecated && keep[sc[1]] && keep[sc[2]]
|
||||
&& sc[1] !== sc[2])
|
||||
E.push({s:sc[1], t:sc[2], l:sc[3], cross:false});
|
||||
});
|
||||
var _unused = [
|
||||
["pr:belongsTo", "pr:SubDomain", "pr:DataDomain", "belongs to"],
|
||||
["pr:belongsTo", "pr:BusinessObject", "pr:SubDomain", "belongs to"],
|
||||
["pr:represents", "pr:DataObject", "pr:BusinessObject", "represents"],
|
||||
["pr:represents", "pr:DataElement", "pr:BusinessConcept", "represents"],
|
||||
["pr:monitoredBy", "pr:BusinessObject", "pr:DataSteward", "monitored by"],
|
||||
["pr:owningDomain", "pr:DefinedObject", "pr:DataDomain", "owning domain"],
|
||||
["pr:ownedBy", "pr:DefinedObject", "pr:Actor", "owned by"]
|
||||
];
|
||||
live(M.relations).forEach(function(r){
|
||||
if(keep[r.domain] && keep[r.range] && r.domain !== r.range)
|
||||
E.push({s:r.domain, t:r.range, l:r.label, cross:r.seam});
|
||||
});
|
||||
|
||||
live(M.nodes).forEach(function(n){
|
||||
if(n.parent && keep[n.id] && keep[n.parent])
|
||||
E.push({s:n.id, t:n.parent, l:"specialises", cross:false});
|
||||
E.push({s:n.id, t:n.parent, l:"type of", cross:false});
|
||||
});
|
||||
G.nodes = Object.keys(keep).map(function(k){ return keep[k]; });
|
||||
G.edges = E;
|
||||
@@ -569,19 +623,15 @@ function layoutGraph(idx){
|
||||
if(G.mode === "network"){ layoutForce(idx); return; }
|
||||
var lanes = LANES.map(function(L){ return L[0]; })
|
||||
.filter(function(c){ return G.nodes.some(function(n){ return n.n.prov === c; }); });
|
||||
/* One class per row. A lane is one column wide, so a layer reads DOWNWARDS:
|
||||
the layer class on top, then Domain, then Sub-domain, then Business
|
||||
Object. That is how a hierarchy is read; laying them left to right made
|
||||
the reader scan across for something that descends. */
|
||||
var laneW = {}, x = GUT;
|
||||
G._lanes = [];
|
||||
lanes.forEach(function(code){
|
||||
var members = G.nodes.filter(function(n){ return n.n.prov === code; });
|
||||
var widest = 1;
|
||||
M.layers.forEach(function(_, li){
|
||||
[0,1,2].forEach(function(ri){
|
||||
var c = members.filter(function(n){ return n.layer===li && n.row===ri; }).length;
|
||||
if(c) widest = Math.max(widest, Math.min(c, Math.ceil(Math.sqrt(c*1.6))));
|
||||
});
|
||||
});
|
||||
var w = widest*(NW+12) + LANEPAD*2;
|
||||
laneW[code] = {x:x, w:w, per:widest};
|
||||
var w = NW + LANEPAD*2;
|
||||
laneW[code] = {x:x, w:w};
|
||||
var L = LANES.filter(function(z){ return z[0] === code; })[0];
|
||||
G._lanes.push({code:code, x:x, w:w, label:L[1], colour:L[2]});
|
||||
x += w;
|
||||
@@ -591,30 +641,23 @@ function layoutGraph(idx){
|
||||
var y = 0;
|
||||
M.layers.forEach(function(b, li){
|
||||
b._y = y;
|
||||
var maxRows = 0;
|
||||
var rowTops = {};
|
||||
var cursor = y + 34;
|
||||
[0,1,2].forEach(function(ri){
|
||||
var any = G.nodes.filter(function(n){ return n.layer===li && n.row===ri; });
|
||||
if(!any.length){ rowTops[ri] = cursor; return; }
|
||||
rowTops[ri] = cursor;
|
||||
var deepest = 0;
|
||||
lanes.forEach(function(code){
|
||||
var g = any.filter(function(n){ return n.n.prov === code; });
|
||||
if(!g.length) return;
|
||||
var per = laneW[code].per;
|
||||
deepest = Math.max(deepest, Math.ceil(g.length/per));
|
||||
g.sort(function(p,q){ return p.n.label.localeCompare(q.n.label); });
|
||||
g.forEach(function(n, i){
|
||||
n.x = laneW[code].x + LANEPAD + (i%per)*(NW+12) + NW/2;
|
||||
n.y = cursor + Math.floor(i/per)*(NH+ROWGAP) + NH/2;
|
||||
});
|
||||
var any = G.nodes.filter(function(n){ return n.layer === li; });
|
||||
if(!any.length){ b._h = 0; return; }
|
||||
var cursor = y + 36, rows = 0;
|
||||
lanes.forEach(function(code){
|
||||
var g = any.filter(function(n){ return n.n.prov === code; });
|
||||
if(!g.length) return;
|
||||
g.sort(function(p, q){
|
||||
return ((ORDER.indexOf(p.n.id)+1)||99) - ((ORDER.indexOf(q.n.id)+1)||99)
|
||||
|| p.n.label.localeCompare(q.n.label); });
|
||||
g.forEach(function(n, i){
|
||||
n.x = laneW[code].x + laneW[code].w/2;
|
||||
n.y = cursor + i*(NH+ROWGAP) + NH/2;
|
||||
});
|
||||
cursor += deepest*(NH+ROWGAP) + ROWGAP;
|
||||
maxRows += deepest;
|
||||
rows = Math.max(rows, g.length);
|
||||
});
|
||||
b._h = maxRows ? (cursor - y + 12) : 0;
|
||||
y += b._h + (b._h ? BANDGAP : 0);
|
||||
b._h = rows*(NH+ROWGAP) + 48;
|
||||
y += b._h + BANDGAP;
|
||||
});
|
||||
G._height = y;
|
||||
}
|
||||
@@ -676,7 +719,7 @@ function renderGraph(){
|
||||
if(G.mode === "strata" && G.nodes.length){
|
||||
/* domain lanes: alternating tint, so a column reads as one domain */
|
||||
(G._lanes||[]).forEach(function(l, i){
|
||||
var isSubject = l.code === "curated";
|
||||
var isSubject = l.code === "defined";
|
||||
bL.appendChild(el("rect", {x:l.x, y:-6, width:l.w, height:G._height+10,
|
||||
fill:l.colour, opacity:isSubject ? .05 : .028}));
|
||||
if(i) bL.appendChild(el("line", {x1:l.x, y1:-6, x2:l.x, y2:G._height+4,
|
||||
@@ -733,7 +776,7 @@ function renderGraph(){
|
||||
specialisations a shape, never instantiated */
|
||||
g.appendChild(el("rect",{width:NW, height:NH, rx:3, fill:"#FFFFFF",
|
||||
stroke:c, "stroke-dasharray": n.n.abstract ? "5 3" : "none"}));
|
||||
var lines = wrapText(n.n.label, 21);
|
||||
var lines = wrapText(n.n.shortLabel || n.n.label, 21);
|
||||
lines.forEach(function(t,i){
|
||||
var e = el("text",{x:NW/2, y:NH/2+3.5+(i-(lines.length-1)/2)*10, fill:"#1A1A1A"});
|
||||
e.textContent = t; g.appendChild(e);
|
||||
@@ -807,33 +850,33 @@ function zoomG(f){
|
||||
function kpi(v,l,w){ return '<div class="kpi'+(w?' warn':'')+'"><b>'+v+'</b><span>'+l+'</span></div>'; }
|
||||
function renderScope(){
|
||||
var cls=live(M.nodes), rel=live(M.relations), att=live(M.attributes);
|
||||
var cur=cls.filter(function(n){return n.prov==="curated";});
|
||||
var obs=cls.filter(function(n){return n.prov==="observed";});
|
||||
var cur=cls.filter(function(n){return n.prov==="defined";});
|
||||
var obs=cls.filter(function(n){return n.prov==="captured";});
|
||||
var str=cls.filter(function(n){return n.prov==="structural";});
|
||||
var seam=rel.filter(function(r){return r.seam;});
|
||||
var dep=M.nodes.filter(function(n){return n.deprecated;})
|
||||
.concat(M.relations.concat(M.attributes).filter(function(r){return r.deprecated;}));
|
||||
var h=[];
|
||||
h.push('<p class="lede">The layer says what an object <b>is</b>. The provenance says <b>how it got here</b>.</p>');
|
||||
h.push('<p class="sublede">Curated objects are asserted by governance and reviewed through a pull request. '
|
||||
+ 'Observed objects are harvested from Snowflake, Tableau and Looker, rebuilt on every run and never edited '
|
||||
h.push('<p class="sublede">Defined objects are written by governance and reviewed through a pull request. '
|
||||
+ 'Captured objects are read from Snowflake, Tableau and Looker, rebuilt on every run and never edited '
|
||||
+ 'by hand. Without that split the ownership rules would demand an owner, a status and a version on every '
|
||||
+ 'one of some fifty thousand warehouse columns.</p>');
|
||||
h.push('<div class="kpis">'+kpi(cls.length,"Classes")+kpi(rel.length,"Relations")
|
||||
+kpi(att.length,"Attributes")+kpi(M.bridges.length,"Bridge properties")
|
||||
+kpi(seam.length,"Seam edges",true)+kpi(dep.length,"Deprecated",true)+'</div>');
|
||||
h.push('<h2 class="sec">Curated against observed</h2><div class="bar">'
|
||||
h.push('<h2 class="sec">Defined against captured</h2><div class="bar">'
|
||||
+'<div style="width:'+(cur.length/cls.length*100)+'%;background:#1E4B9B"></div>'
|
||||
+'<div style="width:'+(obs.length/cls.length*100)+'%;background:#8A6A28"></div>'
|
||||
+'<div style="width:'+(str.length/cls.length*100)+'%;background:#B9B4A7"></div></div>');
|
||||
h.push('<div class="legend">'
|
||||
+'<span><i style="background:#1E4B9B"></i>Curated \u2014 '+cur.length+'</span>'
|
||||
+'<span><i style="background:#8A6A28"></i>Observed \u2014 '+obs.length+'</span>'
|
||||
+'<span><i style="background:#1E4B9B"></i>Defined \u2014 '+cur.length+'</span>'
|
||||
+'<span><i style="background:#8A6A28"></i>Captured \u2014 '+obs.length+'</span>'
|
||||
+'<span><i style="background:#B9B4A7"></i>Structural \u2014 '+str.length+'</span></div>');
|
||||
h.push('<div class="note" style="max-width:880px;margin-bottom:26px"><b>Only '+seam.length
|
||||
+' relations cross between the two.</b> '+seam.map(function(r){return esc(r.label);}).join(" and ")
|
||||
+' \u2014 both proposed by the reconciliation job and confirmed by a steward. Everywhere else the '
|
||||
+'graph is either pure governance or pure machine, which is what keeps the curation cost bounded.</div>');
|
||||
+'graph is either pure governance or pure machine, which is what keeps the mapping effort bounded.</div>');
|
||||
h.push('<h2 class="sec">The layers</h2><div class="grid">');
|
||||
M.layers.forEach(function(l){
|
||||
var g=cls.filter(function(n){return n.layer===l.id;});
|
||||
@@ -861,9 +904,12 @@ function renderModel(){
|
||||
M.layers.forEach(function(l){
|
||||
var g=live(M.nodes).filter(function(n){return n.layer===l.id;});
|
||||
if(!g.length) return;
|
||||
g.sort(function(a,b){ return a.depth-b.depth || a.label.localeCompare(b.label); });
|
||||
g.sort(function(a,b){
|
||||
return a.depth - b.depth
|
||||
|| ((ORDER.indexOf(a.id)+1)||99) - ((ORDER.indexOf(b.id)+1)||99)
|
||||
|| a.label.localeCompare(b.label); });
|
||||
h.push('<h2 class="sec" style="color:'+provColour(g[0].prov)+'">'+esc(l.title)+' \u00b7 '+esc(l.tag)+'</h2>');
|
||||
h.push('<table><thead><tr><th>Class</th><th>Provenance</th><th class="hide-s">Specialises</th>'
|
||||
h.push('<table><thead><tr><th>Class</th><th>Provenance</th><th class="hide-s">Type of</th>'
|
||||
+'<th class="hide-s">What it is for</th></tr></thead><tbody>');
|
||||
g.forEach(function(n){
|
||||
h.push('<tr data-id="'+n.id+'"><td><span class="dot" style="background:'+provColour(n.prov)
|
||||
@@ -903,7 +949,7 @@ function propRows(){
|
||||
if(p.functional) t.push("exactly one");
|
||||
if(p.transitive) t.push("transitive");
|
||||
if(p.seam) t.push("seam");
|
||||
if(p.curation) t.push(p.curation.toLowerCase().replace(/_/g," "));
|
||||
if(p.authoring) t.push(p.authoring.toLowerCase().replace(/_/g," "));
|
||||
h.push('<tr data-id="'+p.id+'"><td><b>'+esc(p.label)+'</b><div class="mono">'+esc(p.id)+'</div></td>'
|
||||
+'<td class="hide-s">'+(byId[p.domain]?esc(clean(byId[p.domain].label)):'<span class="mono">any</span>')+'</td>'
|
||||
+'<td class="hide-s">'+(byId[p.range]?esc(clean(byId[p.range].label))
|
||||
@@ -939,8 +985,8 @@ function renderDep(){
|
||||
/* ------------------------------------------------------------ SHELL */
|
||||
function renderGraphShell(){
|
||||
var h=['<p class="lede">The meta model as a graph.</p>'
|
||||
+'<p class="sublede">Columns are provenance: <b style="color:#1E4B9B">curated</b> on the left, '
|
||||
+'<b style="color:#8A6A28">observed</b> on the right. Rows are layers. A <b>navy edge</b> crosses '
|
||||
+'<p class="sublede">Columns are provenance: <b style="color:#1E4B9B">defined</b> on the left, '
|
||||
+'<b style="color:#8A6A28">captured</b> on the right. Rows are layers. A <b>navy edge</b> crosses '
|
||||
+'between the two \u2014 that is the seam, the only place a steward confirms anything. Dashed outlines '
|
||||
+'are abstract classes, never instantiated.</p>'];
|
||||
h.push('<div class="gwrap"><svg id="g-svg" xmlns="http://www.w3.org/2000/svg"><defs>'
|
||||
@@ -1076,6 +1122,29 @@ M.relations.forEach(function(r){
|
||||
if(r.deprecated) return;
|
||||
if(r.domain) (FROM[r.domain]=FROM[r.domain]||[]).push(r.id);
|
||||
if(r.range) (TO[r.range]=TO[r.range]||[]).push(r.id); });
|
||||
/* The same scoped pairs the graph uses. Without them a Business Object shows
|
||||
no incoming relation at all, because belongsTo and represents carry no
|
||||
rdfs:domain -- their scope lives in the shapes. The card was silent about
|
||||
edges the graph was drawing. */
|
||||
SCOPED_PAIRS.forEach(function(sc){
|
||||
if(!byId[sc[0]] || byId[sc[0]].deprecated) return;
|
||||
(FROM[sc[1]] = FROM[sc[1]]||[]).push(sc[0]);
|
||||
(TO[sc[2]] = TO[sc[2]]||[]).push(sc[0]);
|
||||
});
|
||||
/* the scoped properties again: without this a Sub-domain card showed no
|
||||
incoming Business Object, and a Business Object card had no incoming list
|
||||
at all, because neither relation declares a domain or a range */
|
||||
[["pr:belongsTo","pr:SubDomain","pr:DataDomain"],
|
||||
["pr:belongsTo","pr:BusinessObject","pr:SubDomain"],
|
||||
["pr:represents","pr:DataObject","pr:BusinessObject"],
|
||||
["pr:represents","pr:DataElement","pr:BusinessConcept"],
|
||||
["pr:monitoredBy","pr:BusinessObject","pr:DataSteward"],
|
||||
["pr:owningDomain","pr:DefinedObject","pr:DataDomain"],
|
||||
["pr:ownedBy","pr:DefinedObject","pr:Actor"]].forEach(function(sc){
|
||||
if(!byId[sc[0]] || byId[sc[0]].deprecated) return;
|
||||
if((FROM[sc[1]]||[]).indexOf(sc[0])===-1) (FROM[sc[1]]=FROM[sc[1]]||[]).push(sc[0]);
|
||||
if((TO[sc[2]]||[]).indexOf(sc[0])===-1) (TO[sc[2]]=TO[sc[2]]||[]).push(sc[0]);
|
||||
});
|
||||
M.attributes.forEach(function(a){
|
||||
if(a.deprecated||!a.domain) return;
|
||||
(ATTRS[a.domain]=ATTRS[a.domain]||[]).push(a.id); });
|
||||
@@ -1104,7 +1173,10 @@ function paint(id){
|
||||
: n.kind==="Attribute" ? "Attribute" : "Bridge property";
|
||||
h.push('<div class="kind" style="color:'+provColour(n.prov||"structural")+'">'
|
||||
+ kindLabel + (n.layer ? ' \u00b7 ' + esc(layerTitle(n.layer)) : '') + '</div>');
|
||||
h.push('<h3>'+esc(clean(n.label))+'</h3>');
|
||||
h.push('<h3>'+esc(n.shortLabel || clean(n.label))+'</h3>');
|
||||
if(n.shortLabel && n.shortLabel !== clean(n.label))
|
||||
h.push('<div class="row" style="border:0;padding:0 0 6px"><span class="mono">'
|
||||
+ esc(clean(n.label)) + '</span><span></span></div>');
|
||||
h.push('<div class="iri">'+esc(n.id)+'</div>');
|
||||
if(n.deprecated) h.push('<div class="note"><b>Deprecated.</b> Kept so assertions written '
|
||||
+ 'against it keep classifying (LC-002)'
|
||||
@@ -1117,13 +1189,13 @@ function paint(id){
|
||||
if(n.kind==="Class"){
|
||||
at("Provenance", n.prov);
|
||||
at("Instantiated", n.abstract ? "never \u2014 abstract" : "yes");
|
||||
at("Curation mode", (n.curation||"").toLowerCase().replace(/_/g," "));
|
||||
at("Authoring mode", (n.authoring||"").toLowerCase().replace(/_/g," "));
|
||||
at("Acronym", n.acronym);
|
||||
} else {
|
||||
at("Cardinality", n.functional ? "exactly one" : "many");
|
||||
at("Transitive", n.transitive ? "yes" : "");
|
||||
at("Crosses the seam", n.seam ? "yes" : "");
|
||||
at("Curation mode", (n.curation||"").toLowerCase().replace(/_/g," "));
|
||||
at("Authoring mode", (n.authoring||"").toLowerCase().replace(/_/g," "));
|
||||
if(!byId[n.range]) at("Range", n.range);
|
||||
if(!n.domain) at("Domain", "any \u2014 scoped by SHACL per class");
|
||||
}
|
||||
@@ -1132,12 +1204,18 @@ function paint(id){
|
||||
}).join(""));
|
||||
|
||||
/* 2. what it is for */
|
||||
if(n.comment) h.push('<h4>What it is for</h4><p>'+esc(n.comment).replace(/\n/g,"<br>")+'</p>');
|
||||
h.push('<h4>What it is for</h4><p>' + (n.comment
|
||||
? esc(n.comment).replace(/\n/g,"<br>")
|
||||
: '<em style="color:var(--muted)">No definition yet \u2014 this term needs one '
|
||||
+ 'before it can be published.</em>') + '</p>');
|
||||
if(n.harvest) h.push('<h4>Harvested from</h4><div class="harv">'+esc(n.harvest)+'</div>');
|
||||
|
||||
/* 3. related terms, from the coarsest down */
|
||||
function link(label, ids){
|
||||
ids=(ids||[]).filter(function(i){return byId[i];});
|
||||
/* deprecated terms live on their own tab; showing them in a link list
|
||||
invites someone to use one */
|
||||
ids=(ids||[]).filter(function(i){
|
||||
return byId[i] && (!byId[i].deprecated || label === "Replaced by"); });
|
||||
if(!ids.length) return;
|
||||
h.push('<h4>'+label+'</h4>');
|
||||
ids.slice(0,16).forEach(function(i){
|
||||
@@ -1148,8 +1226,8 @@ function paint(id){
|
||||
if(ids.length>16) h.push('<div class="row"><span class="mono">\u2026 and '
|
||||
+(ids.length-16)+' more</span><span></span></div>');
|
||||
}
|
||||
link("Specialises", [n.parent, n.subPropertyOf]);
|
||||
link("Specialised by", CHILDREN[n.id]);
|
||||
link("Type of", [n.parent, n.subPropertyOf]);
|
||||
link("Types", CHILDREN[n.id]);
|
||||
link("Replaced by", [n.replacedBy]);
|
||||
link("From", [n.domain]);
|
||||
link("To", [n.range]);
|
||||
|
||||
Reference in New Issue
Block a user