@origintrail-official/dkg-graph-viz 10.0.1 → 10.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["/private/tmp/dkg-release-10.0.1/packages/graph-viz/dist/index.cjs","../src/core/view-config.ts","../src/core/palette.ts","../src/core/provenance-resolver.ts","../src/core/temporal-filter.ts","../src/overlays/timeline-overlay.ts","../src/core/graph-model.ts","../src/core/prefix-manager.ts","../src/core/label-resolver.ts","../src/core/style-engine.ts","../src/core/hexagon-painter.ts","../src/core/focus-filter.ts","../src/core/reification-collapser.ts","../src/core/metadata-extractor.ts","../src/core/events.ts","../src/core/renderer.ts","../src/core/rdf-graph-viz.ts","../src/data-sources/oxigraph-source.ts","../src/data-sources/remote-sparql-source.ts"],"names":["range","link"],"mappings":"AAAA;ACOA,IAAM,iBAAA,EAAmB;AAAA,EACvB,+BAAA;AAAA,EACA,gCAAA;AAAA,EACA,+BAAA;AAAA,EACA,qBAAA;AAAA,EACA;AACF,CAAA;AAGA,SAAS,QAAA,CAAS,IAAA,EAAiB,SAAA,EAAkC;AACnE,EAAA,IAAA,CAAA,MAAW,GAAA,GAAM,gBAAA,EAAkB;AACjC,IAAA,MAAM,KAAA,kBAAO,IAAA,mBAAK,UAAA,6BAAY,GAAA,mBAAI,GAAA,EAAK,SAAS,GAAA;AAChD,IAAA,GAAA,CAAI,KAAA,GAAQ,IAAA,CAAK,OAAA,EAAS,CAAA,EAAG,OAAO,IAAA,CAAK,CAAC,CAAA,CAAE,KAAA;AAAA,EAC9C;AACA,EAAA,OAAO,IAAA;AACT;AAiOO,SAAS,eAAA,CAAgB,MAAA,EAAoB,KAAA,EAAyB;AAE3E,EAAA,GAAA,CAAI,MAAA,CAAO,KAAA,EAAO;AAChB,IAAA,MAAM,UAAA,EAAY,wBAAA,CAAyB,KAAA,EAAO,MAAA,CAAO,KAAA,CAAM,GAAG,CAAA;AAClE,IAAA,GAAA,CAAI,SAAA,EAAW;AACb,MAAA,SAAA,CAAU,eAAA,mBAAiB,MAAA,CAAO,KAAA,CAAM,cAAA,UAAkB,KAAA;AAC1D,MAAA,GAAA,CAAI,MAAA,CAAO,KAAA,CAAM,KAAA,EAAO;AACtB,QAAA,SAAA,CAAU,SAAA,EAAW,MAAA,CAAO,KAAA,CAAM,KAAA;AAAA,MACpC;AAAA,IACF;AAAA,EACF;AAGA,EAAA,GAAA,CAAI,MAAA,CAAO,aAAA,EAAe;AACxB,IAAA,IAAA,CAAA,MAAW,KAAA,GAAQ,KAAA,CAAM,KAAA,CAAM,MAAA,CAAO,CAAA,EAAG;AACvC,MAAA,GAAA,CAAI,IAAA,CAAK,QAAA,EAAU,QAAA;AACnB,MAAA,GAAA,CAAI,CAAC,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,CAAA,CAAA,EAAA,GAAK,CAAA,CAAE,QAAA,CAAS,oBAAoB,CAAC,CAAA,EAAG,QAAA;AAG7D,MAAA,MAAM,cAAA,EAAgB,KAAA,CAAM,YAAA,CAAa,IAAA,CAAK,EAAE,CAAA,CAAE,MAAA,CAAO,CAAA,CAAA,EAAA,GAAK,CAAA,CAAE,SAAA,CAAU,QAAA,CAAS,UAAU,CAAC,CAAA;AAC9F,MAAA,IAAA,CAAA,MAAW,KAAA,GAAQ,aAAA,EAAe;AAChC,QAAA,MAAM,WAAA,mBAAa,IAAA,CAAK,MAAA,CAAO,KAAA,CAAM,GAAG,CAAA,CAAE,GAAA,CAAI,CAAA,UAAK,IAAA;AACnD,QAAA,GAAA,CAAI,MAAA,CAAO,aAAA,CAAc,KAAA,CAAM,UAAU,CAAA,EAAG;AAC1C,UAAA,IAAA,CAAK,SAAA,EAAW,MAAA,CAAO,aAAA,CAAc,KAAA,CAAM,UAAU,CAAA;AACrD,UAAA,KAAA;AAAA,QACF;AAAA,MACF;AAGA,MAAA,GAAA,CAAI,CAAC,IAAA,CAAK,SAAA,GAAY,MAAA,CAAO,aAAA,CAAc,YAAA,EAAc;AACvD,QAAA,IAAA,CAAA,MAAW,CAAC,MAAA,EAAQ,WAAW,EAAA,GAAK,MAAA,CAAO,OAAA,CAAQ,MAAA,CAAO,aAAA,CAAc,YAAY,CAAA,EAAG;AACrF,UAAA,GAAA,CAAI,IAAA,CAAK,EAAA,CAAG,QAAA,CAAS,MAAM,EAAA,GAAK,MAAA,CAAO,aAAA,CAAc,KAAA,CAAM,WAAW,CAAA,EAAG;AACvE,YAAA,IAAA,CAAK,SAAA,EAAW,MAAA,CAAO,aAAA,CAAc,KAAA,CAAM,WAAW,CAAA;AACtD,YAAA,KAAA;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAGA,EAAA,GAAA,CAAI,MAAA,CAAO,SAAA,EAAW;AACpB,IAAA,MAAM,GAAA,EAAK,MAAA,CAAO,SAAA;AAClB,IAAA,MAAM,OAAA,EAA+C,CAAC,CAAA;AACtD,IAAA,MAAM,OAAA,EAAS,EAAA,CAAG,OAAA,IAAW,IAAA;AAE7B,IAAA,IAAA,CAAA,MAAW,CAAC,EAAA,EAAI,IAAI,EAAA,GAAK,KAAA,CAAM,KAAA,EAAO;AACpC,MAAA,IAAI,MAAA,EAAuB,IAAA;AAE3B,MAAA,GAAA,CAAI,EAAA,CAAG,OAAA,IAAW,SAAA,GAAY,EAAA,CAAG,SAAA,EAAW;AAC1C,QAAA,MAAM,MAAA,EAAQ,KAAA,CAAM,YAAA,CAAa,EAAE,CAAA,CAAE,MAAA,CAAO,CAAA,CAAA,EAAA,GAAK,CAAA,CAAE,SAAA,CAAU,QAAA,CAAS,EAAA,CAAG,SAAU,CAAC,CAAA;AACpF,QAAA,GAAA,CAAI,KAAA,CAAM,OAAA,EAAS,CAAA,EAAG;AACpB,UAAA,MAAM,OAAA,EAAS,KAAA,CAAM,KAAA,CAAM,GAAA,CAAI,KAAA,CAAM,CAAC,CAAA,CAAE,MAAM,CAAA;AAC9C,UAAA,GAAA,CAAI,MAAA,EAAQ;AACV,YAAA,MAAM,IAAA,EAAM,QAAA,CAAS,MAAA,EAAQ,EAAA,CAAG,QAAQ,CAAA;AACxC,YAAA,GAAA,CAAI,IAAA,IAAQ,IAAA,EAAM,MAAA,EAAQ,UAAA,CAAW,GAAG,CAAA;AAAA,UAC1C;AAAA,QACF;AAAA,MACF,EAAA,KAAO;AACL,QAAA,MAAM,IAAA,EAAM,QAAA,CAAS,IAAA,EAAM,EAAA,CAAG,QAAQ,CAAA;AACtC,QAAA,GAAA,CAAI,IAAA,IAAQ,IAAA,EAAM,MAAA,EAAQ,UAAA,CAAW,GAAG,CAAA;AAAA,MAC1C;AAEA,MAAA,GAAA,CAAI,MAAA,IAAU,KAAA,GAAQ,CAAC,KAAA,CAAM,KAAK,CAAA,EAAG;AACnC,QAAA,MAAM,UAAA,EAAY,EAAA,CAAG,SAAA;AACrB,QAAA,IAAI,KAAA;AACJ,QAAA,IAAI,QAAA,EAAU,KAAA;AACd,QAAA,GAAA,CAAI,MAAA,EAAQ,CAAA,EAAG;AAEb,UAAA,MAAM,cAAA,EAAgB,IAAA,CAAK,GAAA,CAAI,KAAK,CAAA;AACpC,UAAA,IAAA,CAAK,UAAA,EAAY,aAAA;AACjB,UAAA,MAAA,EAAQ,aAAA;AACR,UAAA,QAAA,EAAU,OAAA,EAAU,cAAA,GAAiB,UAAA,GAAa,cAAA,EAAgB,EAAA,EAAM,cAAA,GAAiB,SAAA;AAAA,QAC3F,EAAA,KAAO;AAEL,UAAA,IAAA,CAAK,UAAA,EAAY,KAAA;AACjB,UAAA,GAAA,CAAI,MAAA,EAAQ;AACV,YAAA,QAAA,EAAU,MAAA,GAAS,SAAA;AACnB,YAAA,MAAA,EAAQ,QAAA,EAAU,UAAA,EAAY,MAAA,EAAQ,CAAA;AAAA,UACxC,EAAA,KAAO;AACL,YAAA,QAAA,EAAU,MAAA,GAAS,SAAA;AACnB,YAAA,MAAA,EAAQ,KAAA;AAAA,UACV;AAAA,QACF;AACA,QAAA,GAAA,CAAI,OAAA,EAAS;AACX,UAAA,MAAA,CAAO,IAAA,CAAK,EAAE,EAAA,EAAI,MAAM,CAAC,CAAA;AAAA,QAC3B;AAAA,MACF;AAAA,IACF;AAGA,IAAA,MAAM,KAAA,mBAAO,EAAA,CAAG,IAAA,UAAQ,KAAA;AAGxB,IAAA,MAAA,CAAO,IAAA,CAAK,CAAC,CAAA,EAAG,CAAA,EAAA,GAAM,CAAA,CAAE,MAAA,EAAQ,CAAA,CAAE,KAAK,CAAA;AACvC,IAAA,MAAM,UAAA,EAAY,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,IAAI,CAAA;AAEtC,IAAA,GAAA,CAAI,SAAA,CAAU,OAAA,EAAS,CAAA,EAAG;AACxB,MAAA,MAAM,QAAA,mBAAU,EAAA,CAAG,OAAA,UAAW,GAAA;AAC9B,MAAA,MAAM,QAAA,mBAAU,EAAA,CAAG,OAAA,UAAA,kBAAY,EAAA,CAAG,cAAA,UAAkB,GAAA,GAAA;AACpD,MAAA,MAAM,SAAA,EAAW,SAAA,CAAU,CAAC,CAAA,CAAE,KAAA;AAC9B,MAAA,MAAM,QAAA,EAAU,SAAA,CAAU,SAAA,CAAU,OAAA,EAAS,CAAC,CAAA,CAAE,KAAA;AAEhD,MAAA,IAAA,CAAA,MAAW,EAAE,EAAA,EAAI,MAAM,EAAA,GAAK,SAAA,EAAW;AACrC,QAAA,MAAM,KAAA,EAAO,KAAA,CAAM,KAAA,CAAM,GAAA,CAAI,EAAE,CAAA;AAC/B,QAAA,GAAA,CAAI,CAAC,IAAA,EAAM,QAAA;AAEX,QAAA,IAAA,CAAK,WAAA,EAAa,IAAA;AAGlB,QAAA,GAAA,CAAI,IAAA,CAAK,cAAA,EAAgB,QAAA;AAGzB,QAAA,IAAI,UAAA;AACJ,QAAA,MAAM,WAAA,EAAa,IAAA,CAAK,GAAA,CAAI,SAAA,EAAW,OAAO,CAAA;AAC9C,QAAA,GAAA,CAAI,WAAA,EAAa,CAAA,EAAG;AAClB,UAAA,MAAM,OAAA,EAAS,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,GAAA,CAAI,QAAA,EAAU,OAAO,CAAC,CAAA;AACrD,UAAA,MAAM,OAAA,EAAS,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,GAAA,CAAI,QAAA,EAAU,OAAO,CAAC,CAAA;AACrD,UAAA,MAAM,SAAA,EAAW,OAAA,EAAS,MAAA;AAC1B,UAAA,MAAM,SAAA,EAAW,IAAA,CAAK,KAAA,CAAM,KAAK,CAAA;AACjC,UAAA,MAAM,QAAA,EAAU,SAAA,EAAW,EAAA,EAAA,CAAK,SAAA,EAAW,MAAA,EAAA,EAAU,SAAA,EAAW,GAAA;AAChE,UAAA,WAAA,EAAa,OAAA;AAAA,QACf,EAAA,KAAO;AACL,UAAA,WAAA,EAAa,CAAA;AAAA,QACf;AAEA,QAAA,IAAA,CAAK,eAAA,EAAiB,QAAA,EAAU,WAAA,EAAA,CAAc,QAAA,EAAU,OAAA,CAAA;AAAA,MAC1D;AAAA,IACF;AAAA,EACF;AAGA,EAAA,GAAA,CAAI,MAAA,CAAO,MAAA,EAAQ;AACjB,IAAA,MAAM,GAAA,EAAK,MAAA,CAAO,MAAA;AAClB,IAAA,MAAM,OAAA,EAA+C,CAAC,CAAA;AAEtD,IAAA,IAAA,CAAA,MAAW,CAAC,EAAA,EAAI,IAAI,EAAA,GAAK,KAAA,CAAM,KAAA,EAAO;AAEpC,MAAA,GAAA,CAAI,IAAA,CAAK,cAAA,EAAgB,QAAA;AAEzB,MAAA,MAAM,IAAA,EAAM,QAAA,CAAS,IAAA,EAAM,EAAA,CAAG,QAAQ,CAAA;AACtC,MAAA,GAAA,CAAI,IAAA,IAAQ,IAAA,EAAM;AAChB,QAAA,MAAM,EAAA,EAAI,UAAA,CAAW,GAAG,CAAA;AACxB,QAAA,GAAA,CAAI,CAAC,KAAA,CAAM,CAAC,EAAA,GAAK,EAAA,EAAI,CAAA,EAAG;AACtB,UAAA,MAAA,CAAO,IAAA,CAAK,EAAE,EAAA,EAAI,KAAA,EAAO,EAAE,CAAC,CAAA;AAAA,QAC9B;AAAA,MACF;AAAA,IACF;AAEA,IAAA,GAAA,CAAI,MAAA,CAAO,OAAA,EAAS,CAAA,EAAG;AACrB,MAAA,MAAM,OAAA,EAAS,IAAA,CAAK,GAAA,CAAI,GAAG,MAAA,CAAO,GAAA,CAAI,CAAA,CAAA,EAAA,GAAK,CAAA,CAAE,KAAK,CAAC,CAAA;AACnD,MAAA,MAAM,OAAA,EAAS,IAAA,CAAK,GAAA,CAAI,GAAG,MAAA,CAAO,GAAA,CAAI,CAAA,CAAA,EAAA,GAAK,CAAA,CAAE,KAAK,CAAC,CAAA;AAEnD,MAAA,IAAA,CAAA,MAAW,EAAE,EAAA,EAAI,MAAM,EAAA,GAAK,MAAA,EAAQ;AAClC,QAAA,MAAM,KAAA,EAAO,KAAA,CAAM,KAAA,CAAM,GAAA,CAAI,EAAE,CAAA;AAC/B,QAAA,GAAA,CAAI,CAAC,KAAA,GAAQ,IAAA,CAAK,cAAA,EAAgB,QAAA;AAElC,QAAA,IAAI,UAAA;AACJ,QAAA,GAAA,CAAI,EAAA,CAAG,MAAA,IAAU,KAAA,EAAO;AAEtB,UAAA,MAAM,OAAA,EAAS,IAAA,CAAK,KAAA,CAAM,MAAM,CAAA;AAChC,UAAA,MAAM,OAAA,EAAS,IAAA,CAAK,KAAA,CAAM,MAAM,CAAA;AAChC,UAAA,MAAM,SAAA,EAAW,OAAA,EAAS,MAAA;AAC1B,UAAA,WAAA,EAAa,SAAA,EAAW,EAAA,EAAA,CAAK,IAAA,CAAK,KAAA,CAAM,KAAK,EAAA,EAAI,MAAA,EAAA,EAAU,SAAA,EAAW,GAAA;AAAA,QACxE,EAAA,KAAO;AAEL,UAAA,MAAM,MAAA,EAAQ,OAAA,EAAS,MAAA;AACvB,UAAA,WAAA,EAAa,MAAA,EAAQ,EAAA,EAAA,CAAK,MAAA,EAAQ,MAAA,EAAA,EAAU,MAAA,EAAQ,GAAA;AAAA,QACtD;AAGA,QAAA,IAAA,CAAK,eAAA,EAAiB,IAAA,EAAM,WAAA,EAAa,GAAA;AAAA,MAC3C;AAAA,IACF;AAAA,EACF;AAGA,EAAA,GAAA,CAAI,MAAA,CAAO,SAAA,EAAW;AACpB,IAAA,IAAA,CAAA,MAAW,KAAA,GAAQ,KAAA,CAAM,KAAA,CAAM,MAAA,CAAO,CAAA,EAAG;AACvC,MAAA,GAAA,CAAI,IAAA,CAAK,QAAA,EAAU,QAAA;AACnB,MAAA,IAAA,CAAA,MAAW,KAAA,GAAQ,IAAA,CAAK,KAAA,EAAO;AAC7B,QAAA,MAAM,UAAA,mCAAY,IAAA,qBAAK,KAAA,mBAAM,GAAG,CAAA,qBAAE,GAAA,mBAAI,CAAA,6BAAG,KAAA,mBAAM,GAAG,CAAA,uBAAE,GAAA,qBAAI,GAAA,UAAK,IAAA;AAE7D,QAAA,MAAM,WAAA,EAAa,MAAA,CAAO,SAAA,CAAU,IAAI,EAAA,GACnC,MAAA,CAAO,SAAA,CAAU,CAAA,OAAA,EAAU,SAAS,CAAA,CAAA;AAGnB,QAAA;AACO,UAAA;AAC3B,UAAA;AACF,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AACF;AAGoF;AAEjD,EAAA;AACf,EAAA;AAGqC,EAAA;AAGzC,EAAA;AACZ,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AAE0B,EAAA;AACyB,IAAA;AAC3B,IAAA;AACxB,EAAA;AAEO,EAAA;AACT;AD5R0F;AACA;AE3IhD;AAClC,EAAA;AACM,EAAA;AACH,EAAA;AACM,EAAA;AACF,EAAA;AACE,EAAA;AACJ,EAAA;AACF,EAAA;AACK,EAAA;AACN,EAAA;AACC,EAAA;AACH,EAAA;AACA,EAAA;AACK,EAAA;AACA,EAAA;AACI,EAAA;AACJ,EAAA;AACC,EAAA;AACV,IAAA;AAAW,IAAA;AAAW,IAAA;AAAW,IAAA;AACjC,IAAA;AAAW,IAAA;AAAW,IAAA;AAAW,IAAA;AACjC,IAAA;AAAW,IAAA;AAAW,IAAA;AAAW,IAAA;AACnC,EAAA;AACF;AAE8C;AACtC,EAAA;AACM,EAAA;AACH,EAAA;AACM,EAAA;AACF,EAAA;AACE,EAAA;AACJ,EAAA;AACF,EAAA;AACK,EAAA;AACN,EAAA;AACC,EAAA;AACH,EAAA;AACA,EAAA;AACK,EAAA;AACA,EAAA;AACI,EAAA;AACJ,EAAA;AACC,EAAA;AACV,IAAA;AAAW,IAAA;AAAW,IAAA;AAAW,IAAA;AACjC,IAAA;AAAW,IAAA;AAAW,IAAA;AAAW,IAAA;AACjC,IAAA;AAAW,IAAA;AAAW,IAAA;AAAW,IAAA;AACnC,EAAA;AACF;AAE+C;AACvC,EAAA;AACM,EAAA;AACH,EAAA;AACM,EAAA;AACF,EAAA;AACE,EAAA;AACJ,EAAA;AACF,EAAA;AACK,EAAA;AACN,EAAA;AACC,EAAA;AACH,EAAA;AACA,EAAA;AACK,EAAA;AACA,EAAA;AACI,EAAA;AACJ,EAAA;AACC,EAAA;AACV,IAAA;AAAW,IAAA;AAAW,IAAA;AAAW,IAAA;AACjC,IAAA;AAAW,IAAA;AAAW,IAAA;AAAW,IAAA;AACjC,IAAA;AAAW,IAAA;AAAW,IAAA;AAAW,IAAA;AACnC,EAAA;AACF;AAE2C;AACnC,EAAA;AACM,EAAA;AACH,EAAA;AACM,EAAA;AACF,EAAA;AACE,EAAA;AACJ,EAAA;AACF,EAAA;AACK,EAAA;AACN,EAAA;AACC,EAAA;AACH,EAAA;AACA,EAAA;AACK,EAAA;AACA,EAAA;AACI,EAAA;AACJ,EAAA;AACC,EAAA;AACV,IAAA;AAAW,IAAA;AAAW,IAAA;AAAW,IAAA;AACjC,IAAA;AAAW,IAAA;AAAW,IAAA;AAAW,IAAA;AACjC,IAAA;AAAW,IAAA;AAAW,IAAA;AAAW,IAAA;AACnC,EAAA;AACF;AAGsD;AAC9C,EAAA;AACI,EAAA;AACC,EAAA;AACJ,EAAA;AACT;AASgB;AACV,EAAA;AAEU,EAAA;AACL,IAAA;AAC+B,EAAA;AACV,IAAA;AACvB,EAAA;AACE,IAAA;AACT,EAAA;AAEuB,EAAA;AAE2C,EAAA;AACpE;AAM0F;AAClD,EAAA;AACjB,IAAA;AACK,IAAA;AACO,IAAA;AACV,IAAA;AACU,IAAA;AACJ,IAAA;AACH,IAAA;AACM,IAAA;AACP,IAAA;AACC,IAAA;AACH,IAAA;AACA,IAAA;AACA,IAAA;AACI,IAAA;AACE,IAAA;AAC7B,EAAA;AAEmD,EAAA;AACV,IAAA;AACzC,EAAA;AACF;AF2J0F;AACA;AG7VlE;AACN,EAAA;AACd,IAAA;AACF,EAAA;AACgB,EAAA;AACd,IAAA;AACF,EAAA;AACiB,EAAA;AACf,IAAA;AACF,EAAA;AACa,EAAA;AACX,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACY,EAAA;AACV,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;AAEwB;AACtB,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAE0B;AACxB,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAagC;AAAA;AAAA;AAAA;AAAA;AAKG,EAAA;AACU,IAAA;AACkB,MAAA;AAC3D,IAAA;AACF,EAAA;AAEmF,EAAA;AACpD,IAAA;AACjB,MAAA;AACI,MAAA;AAChB,IAAA;AAGoE,IAAA;AAGY,IAAA;AAGG,IAAA;AAC/D,IAAA;AACC,MAAA;AAE6B,MAAA;AACjC,MAAA;AAC0C,QAAA;AAClD,MAAA;AAEqD,QAAA;AAC5D,MAAA;AACF,IAAA;AAGmC,IAAA;AACJ,IAAA;AAGS,IAAA;AACd,IAAA;AAC4C,MAAA;AACpB,QAAA;AAC9B,QAAA;AAE+D,UAAA;AAC3D,UAAA;AACkB,YAAA;AACN,cAAA;AAC+C,cAAA;AACpD,cAAA;AACvB,YAAA;AACF,UAAA;AACF,QAAA;AACF,MAAA;AACF,IAAA;AAGmD,IAAA;AACb,MAAA;AAC1B,MAAA;AACc,QAAA;AACN,UAAA;AACW,UAAA;AACa,UAAA;AACtB,UAAA;AAC8D,YAAA;AAC1D,YAAA;AACE,cAAA;AACoD,cAAA;AACjD,cAAA;AACvB,YAAA;AACF,UAAA;AACF,QAAA;AACF,MAAA;AACF,IAAA;AAG+C,IAAA;AACqB,MAAA;AACxD,MAAA;AACc,QAAA;AACN,UAAA;AACW,UAAA;AACP,UAAA;AACoB,UAAA;AACyC,UAAA;AAC1D,UAAA;AACvB,QAAA;AACF,MAAA;AACF,IAAA;AAE6B,IAAA;AACV,IAAA;AAEZ,IAAA;AACT,EAAA;AAAA;AAG8E,EAAA;AAC7C,IAAA;AACS,MAAA;AACM,MAAA;AAC9C,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAGsF,EAAA;AACrD,IAAA;AACO,MAAA;AACQ,MAAA;AAC9C,IAAA;AAEuC,IAAA;AACzC,EAAA;AAAA;AAGqD,EAAA;AACf,IAAA;AACI,MAAA;AACM,MAAA;AAC9C,IAAA;AACO,IAAA;AACT,EAAA;AACF;AHyT0F;AACA;AI5d5D;AAC5B,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAW4B;AACiB,iBAAA;AACD,kBAAA;AACX,kBAAA;AACvB,EAAA;AACA,EAAA;AAEsC,EAAA;AACmB,IAAA;AACpB,IAAA;AAC7C,EAAA;AAAA;AAG8B,EAAA;AACN,IAAA;AAEM,IAAA;AACF,IAAA;AAEY,IAAA;AACD,MAAA;AACzB,MAAA;AACoB,QAAA;AACiB,QAAA;AACN,QAAA;AACzC,MAAA;AACF,IAAA;AAEwB,IAAA;AACa,MAAA;AAEpB,MAAA;AACV,IAAA;AACa,MAAA;AACH,MAAA;AACjB,IAAA;AACF,EAAA;AAAA;AAGqC,EAAA;AACvB,IAAA;AACd,EAAA;AAAA;AAG0B,EAAA;AACZ,IAAA;AACd,EAAA;AAAA;AAG6B,EAAA;AACJ,IAAA;AACzB,EAAA;AAAA;AAG2B,EAAA;AACb,IAAA;AACd,EAAA;AAEgC,EAAA;AACV,IAAA;AACtB,EAAA;AAAA;AAG4B,EAAA;AACX,IAAA;AACjB,EAAA;AAAA;AAGyC,EAAA;AACD,IAAA;AACxC,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUoE,EAAA;AACpB,IAAA;AAEd,IAAA;AACI,IAAA;AAEP,IAAA;AACQ,MAAA;AACzB,MAAA;AACsB,QAAA;AACd,UAAA;AAChB,QAAA;AAC4B,MAAA;AACd,QAAA;AAChB,MAAA;AACF,IAAA;AAEO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAMiF,EAAA;AACjD,IAAA;AAEE,IAAA;AACkB,IAAA;AACqB,IAAA;AAE5C,IAAA;AAC4B,IAAA;AAEjB,IAAA;AACvB,MAAA;AACuC,QAAA;AAC3C,QAAA;AACR,MAAA;AACH,IAAA;AAE6C,IAAA;AAC1B,MAAA;AAC8C,QAAA;AAC/C,QAAA;AAChB,MAAA;AACa,MAAA;AACf,IAAA;AAEO,IAAA;AACT,EAAA;AAAA;AAGmD,EAAA;AACrB,IAAA;AAEkB,IAAA;AACJ,MAAA;AACU,QAAA;AACnB,QAAA;AACK,UAAA;AACP,UAAA;AACmB,YAAA;AAC5C,UAAA;AACF,QAAA;AACF,MAAA;AACF,IAAA;AAEO,IAAA;AACT,EAAA;AACF;AJ0b0F;AACA;AK9mB7D;AACnB,EAAA;AACA,EAAA;AACA,EAAA;AAAA;AAG4B,kBAAA;AACO,kBAAA;AACF,kBAAA;AACC,kBAAA;AACH,kBAAA;AACS,kBAAA;AACN,mBAAA;AACF,mBAAA;AAAA;AAGuB,mBAAA;AAC1C,mBAAA;AACsD,mBAAA;AAMzE,EAAA;AACkB,IAAA;AACH,IAAA;AACA,IAAA;AACjB,EAAA;AAAA;AAGiE,EAAA;AAC9C,IAAA;AACnB,EAAA;AAAA;AAGc,EAAA;AACI,IAAA;AAEW,IAAA;AACf,IAAA;AAE6B,IAAA;AAClB,IAAA;AACgB,IAAA;AACpB,IAAA;AACmB,IAAA;AAGiB,IAAA;AACC,IAAA;AACE,IAAA;AACJ,IAAA;AACG,IAAA;AACC,IAAA;AACJ,IAAA;AAGlC,IAAA;AAGc,IAAA;AACZ,IAAA;AAG4C,IAAA;AACD,oBAAA;AACnE,EAAA;AAAA;AAGgB,EAAA;AACH,IAAA;AACK,IAAA;AACI,MAAA;AACL,MAAA;AACf,IAAA;AACF,EAAA;AAAA;AAGuB,EAAA;AACC,IAAA;AACxB,EAAA;AAAA;AAGgB,EAAA;AACG,IAAA;AACU,IAAA;AACf,IAAA;AACG,MAAA;AACb,MAAA;AACF,IAAA;AAGkB,IAAA;AAC4B,MAAA;AACA,MAAA;AAC9C,IAAA;AAGoB,IAAA;AAG+D,IAAA;AACJ,IAAA;AAEzD,IAAA;AACxB,EAAA;AAAA;AAGa,EAAA;AACU,IAAA;AACH,IAAA;AAC6B,IAAA;AAEpB,IAAA;AACE,IAAA;AAE4B,IAAA;AAC1B,IAAA;AACS,IAAA;AAED,IAAA;AACW,MAAA;AACzB,MAAA;AACS,MAAA;AACiB,QAAA;AAC3B,QAAA;AACT,QAAA;AACX,QAAA;AACF,MAAA;AACiC,MAAA;AACb,MAAA;AACd,IAAA;AACV,EAAA;AAAA;AAGc,EAAA;AACM,IAAA;AAC6B,IAAA;AACvB,IAAA;AACU,MAAA;AACX,MAAA;AACvB,IAAA;AACF,EAAA;AAAA;AAG4B,EAAA;AACP,IAAA;AACuB,IAAA;AACtB,IAAA;AACtB,EAAA;AAAA;AAI6B,EAAA;AACA,IAAA;AACI,IAAA;AACA,IAAA;AAExB,IAAA;AAAA;AAAA;AAAA;AAAA;AAK4D,sCAAA;AAAA;AAEF,oCAAA;AAAA;AAAA;AAAA;AAIW,0DAAA;AAAiB;AAAA;AAAA;AAAA;AAAA,IAAA;AAM/F,EAAA;AAE+B,EAAA;AACV,IAAA;AACuB,IAAA;AAClB,IAAA;AACG,IAAA;AACL,IAAA;AACE,oBAAA;AAC1B,EAAA;AAEiC,EAAA;AACQ,IAAA;AACG,IAAA;AAClB,IAAA;AAC+B,IAAA;AACzD,EAAA;AAAA;AAGiC,EAAA;AACT,IAAA;AACgC,MAAA;AACtD,IAAA;AACF,EAAA;AAE4B,EAAA;AACL,IAAA;AACR,MAAA;AACN,IAAA;AAEuC,MAAA;AACK,QAAA;AACyB,QAAA;AACP,UAAA;AAC3C,UAAA;AACtB,QAAA;AACF,MAAA;AACU,MAAA;AACZ,IAAA;AACF,EAAA;AAE6B,EAAA;AACW,IAAA;AACX,IAAA;AACR,IAAA;AAGsC,IAAA;AAC3B,IAAA;AAEhB,IAAA;AACP,MAAA;AAAyC,QAAA;AACzC,MAAA;AAAyC,QAAA;AACzC,MAAA;AAA2C,QAAA;AAC3C,MAAA;AAA6C,QAAA;AAClD,MAAA;AAAgB,QAAA;AAClB,IAAA;AACF,EAAA;AAE+B,EAAA;AACT,IAAA;AACP,IAAA;AACqB,IAAA;AACxB,IAAA;AAEoD,IAAA;AACpC,IAAA;AAE4B,IAAA;AAClC,IAAA;AAEH,IAAA;AACA,IAAA;AACQ,IAAA;AAED,IAAA;AAEiB,IAAA;AACN,MAAA;AACP,MAAA;AACV,MAAA;AAC+B,MAAA;AACjD,IAAA;AACF,EAAA;AAE6C,EAAA;AACgC,IAAA;AAC7E,EAAA;AAE4C,EAAA;AACF,IAAA;AAC/B,MAAA;AAAc,MAAA;AAAiB,MAAA;AACvC,IAAA;AACH,EAAA;AAEmB,mBAAA;AAEW,EAAA;AACgB,IAAA;AAEA,IAAA;AAC5B,IAAA;AACI,IAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAAA;AAqIW,IAAA;AACjC,EAAA;AACF;AL6jB0F;AACA;AM39BzE;AAOiC;AACrB,EAAA;AACqB,EAAA;AACX,EAAA;AACwB,EAAA;AAC9B,EAAA;AACgB,EAAA;AACjD;AAGyC;AACZ,EAAA;AAC2B,EAAA;AAC1B,IAAA;AAC5B,EAAA;AACO,EAAA;AACT;AAG4E;AAChC,EAAA;AAC5C;AAewB;AACsB,mBAAA;AACA,mBAAA;AACrB,mBAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUU,mBAAA;AAAA;AAGa,mBAAA;AAAA;AAGG,mBAAA;AAAA;AAGD,mBAAA;AAAA;AAGC,mBAAA;AAAA;AAGD,mBAAA;AAEL,EAAA;AACjB,IAAA;AACc,MAAA;AACJ,QAAA;AAChC,MAAA;AACF,IAAA;AACF,EAAA;AAAA;AAGoC,EAAA;AACtB,IAAA;AACd,EAAA;AAAA;AAGyD,EAAA;AACzB,IAAA;AAChC,EAAA;AAAA;AAG0C,EAAA;AACZ,IAAA;AACjB,IAAA;AACF,MAAA;AACL,QAAA;AACQ,QAAA;AACD,QAAA;AACa,QAAA;AACV,QAAA;AACQ,QAAA;AACV,QAAA;AACI,QAAA;AACd,MAAA;AACuB,MAAA;AACzB,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAG0C,EAAA;AACqB,IAAA;AACF,IAAA;AAC7D,EAAA;AAAA;AAGmC,EAAA;AACG,IAAA;AACE,IAAA;AACiB,IAAA;AACS,IAAA;AAE7C,IAAA;AACd,MAAA;AACM,MAAA;AACE,MAAA;AACH,MAAA;AACT,IAAA;AAEuC,IAAA;AAGjB,IAAA;AACiB,MAAA;AACV,QAAA;AAC5B,MAAA;AACA,MAAA;AACF,IAAA;AAEsB,IAAA;AAEoB,MAAA;AAChB,QAAA;AACb,UAAA;AACU,UAAA;AACA,UAAA;AACnB,QAAA;AAC8C,QAAA;AAChC,QAAA;AACK,UAAA;AACZ,QAAA;AAC+B,UAAA;AACtC,QAAA;AACA,QAAA;AACF,MAAA;AAGsC,MAAA;AACJ,MAAA;AAER,MAAA;AACJ,QAAA;AACd,UAAA;AACI,UAAA;AACA,UAAA;AACG,UAAA;AACJ,UAAA;AACR,QAAA;AACW,QAAA;AACD,QAAA;AAGc,QAAA;AACD,QAAA;AACS,QAAA;AACF,QAAA;AACjC,MAAA;AAGyE,MAAA;AACpC,QAAA;AACrB,QAAA;AACM,UAAA;AACY,UAAA;AAChC,QAAA;AACgB,QAAA;AAClB,MAAA;AACK,IAAA;AAEiB,MAAA;AACN,QAAA;AACG,QAAA;AACA,QAAA;AACnB,MAAA;AAEwC,MAAA;AACQ,QAAA;AAChC,QAAA;AACK,UAAA;AACZ,QAAA;AAC+B,UAAA;AACtC,QAAA;AACK,MAAA;AAC2C,QAAA;AAClC,QAAA;AACK,UAAA;AACZ,QAAA;AACiC,UAAA;AACxC,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AAAA;AAG4C,EAAA;AACG,IAAA;AACA,IAAA;AAEpB,IAAA;AACP,MAAA;AAClB,IAAA;AAE8B,IAAA;AACA,IAAA;AACQ,IAAA;AAEF,IAAA;AACR,MAAA;AACN,QAAA;AACb,MAAA;AACe,QAAA;AACtB,MAAA;AACF,IAAA;AAEoC,IAAA;AACR,MAAA;AACN,QAAA;AACpB,MAAA;AACF,IAAA;AAEO,IAAA;AACL,MAAA;AACe,MAAA;AACf,MAAA;AACe,MAAA;AACiB,MAAA;AAClC,IAAA;AACF,EAAA;AAAA;AAG+C,EAAA;AACb,IAAA;AACM,IAAA;AAER,IAAA;AACQ,MAAA;AACE,MAAA;AACiB,MAAA;AACS,MAAA;AAGtC,MAAA;AAC0C,QAAA;AACpE,MAAA;AAC2C,MAAA;AAEpB,MAAA;AACW,QAAA;AACtB,QAAA;AACuC,UAAA;AACzB,UAAA;AACxB,QAAA;AACA,QAAA;AACF,MAAA;AAE6D,MAAA;AACzB,QAAA;AACT,QAAA;AACF,UAAA;AACA,UAAA;AAGe,0BAAA;AACF,0BAAA;AAEC,UAAA;AACD,UAAA;AACb,UAAA;AACA,UAAA;AAEC,UAAA;AACD,UAAA;AACvB,QAAA;AACK,MAAA;AAC2B,QAAA;AACtB,QAAA;AACgE,UAAA;AAC7C,UAAA;AACjB,UAAA;AAC2C,YAAA;AACxB,YAAA;AACR,cAAA;AACZ,YAAA;AACmB,cAAA;AAC1B,YAAA;AACsB,YAAA;AACxB,UAAA;AACF,QAAA;AACF,MAAA;AACF,IAAA;AAGgC,IAAA;AACK,IAAA;AAKjC,MAAA;AAEoB,QAAA;AACI,QAAA;AACD,QAAA;AACH,QAAA;AACtB,MAAA;AACF,IAAA;AAEO,IAAA;AACQ,MAAA;AACb,MAAA;AACa,MAAA;AACb,MAAA;AACgC,MAAA;AAClC,IAAA;AACF,EAAA;AAAA;AAG2C,EAAA;AACjB,IAAA;AAC1B,EAAA;AAAA;AAGsC,EAAA;AACC,IAAA;AACf,IAAA;AACO,IAAA;AACF,IAAA;AACM,MAAA;AACL,MAAA;AAC5B,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAGoC,EAAA;AACE,IAAA;AACd,IAAA;AACO,IAAA;AACF,IAAA;AACM,MAAA;AACL,MAAA;AAC5B,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAGwC,EAAA;AACJ,IAAA;AACE,IAAA;AACxB,IAAA;AACgB,MAAA;AACO,QAAA;AACI,QAAA;AACrC,MAAA;AACF,IAAA;AACkC,IAAA;AACvB,IAAA;AACgB,MAAA;AACQ,QAAA;AACI,QAAA;AACrC,MAAA;AACF,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAGyC,EAAA;AACV,IAAA;AACW,IAAA;AACE,MAAA;AAC/B,QAAA;AACT,MAAA;AACF,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAGc,EAAA;AACK,IAAA;AACA,IAAA;AACA,IAAA;AACI,IAAA;AACD,IAAA;AACtB,EAAA;AAAA;AAG0B,EAAA;AACH,IAAA;AACvB,EAAA;AACF;AN24B0F;AACA;AO5yCnD;AAChC,EAAA;AACC,EAAA;AACD,EAAA;AACA,EAAA;AACG,EAAA;AACF,EAAA;AACG,EAAA;AACL,EAAA;AACE,EAAA;AACA,EAAA;AACR;AAK2B;AACjB,EAAA;AAAA;AAEA,EAAA;AAE8B,EAAA;AACX,IAAA;AACD,IAAA;AAGwC,IAAA;AACjC,MAAA;AACD,MAAA;AAC9B,IAAA;AAEkB,IAAA;AACyC,MAAA;AAC1B,QAAA;AACD,QAAA;AAC9B,MAAA;AACF,IAAA;AACF,EAAA;AAAA;AAGmD,EAAA;AACb,IAAA;AACD,IAAA;AACrC,EAAA;AAAA;AAGuC,EAAA;AACgB,IAAA;AAC1B,MAAA;AAC3B,IAAA;AACF,EAAA;AAAA;AAGiD,EAAA;AACf,IAAA;AAClC,EAAA;AAAA;AAGiD,EAAA;AACb,IAAA;AACpC,EAAA;AAAA;AAAA;AAAA;AAAA;AAMoC,EAAA;AACQ,IAAA;AAChB,MAAA;AACe,QAAA;AACX,QAAA;AACK,UAAA;AAC/B,QAAA;AACF,MAAA;AACF,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAM6B,EAAA;AACM,IAAA;AACL,IAAA;AAGqD,IAAA;AACxE,MAAA;AACT,IAAA;AAEqC,IAAA;AACI,IAAA;AACL,IAAA;AAEP,IAAA;AAC/B,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAesC,EAAA;AACD,IAAA;AACa,IAAA;AAEZ,IAAA;AACc,IAAA;AAGF,IAAA;AAC1C,MAAA;AACoC,QAAA;AACM,QAAA;AACkB,QAAA;AACxD,MAAA;AAER,MAAA;AACF,IAAA;AAEoC,IAAA;AACc,IAAA;AACnB,MAAA;AAC/B,IAAA;AAEO,IAAA;AACT,EAAA;AAAA;AAG0B,EAAA;AACG,IAAA;AACgB,IAAA;AACxB,MAAA;AACnB,IAAA;AACO,IAAA;AACT,EAAA;AACF;APoxC0F;AACA;AQr6CzD;AAC/B,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAQ2B;AACjB,EAAA;AACA,EAAA;AACA,EAAA;AAEyE,EAAA;AAClE,IAAA;AACS,IAAA;AACwB,IAAA;AAChD,EAAA;AAEsB,EAAA;AACR,IAAA;AACd,EAAA;AAE2B,EAAA;AACZ,IAAA;AACf,EAAA;AAAA;AAG0C,EAAA;AAEE,IAAA;AACD,MAAA;AACN,MAAA;AACd,QAAA;AACnB,MAAA;AACF,IAAA;AAG8B,IAAA;AAChC,EAAA;AAAA;AAG0C,EAAA;AACH,IAAA;AACvC,EAAA;AAAA;AAGgC,EAAA;AACD,IAAA;AAC2C,MAAA;AACxE,IAAA;AAG4C,IAAA;AAC9C,EAAA;AAAA;AAG0C,EAAA;AACX,IAAA;AACmD,MAAA;AAChF,IAAA;AACgD,IAAA;AAClD,EAAA;AAAA;AAGoD,EAAA;AACf,IAAA;AACrC,EAAA;AAAA;AAAA;AAAA;AAAA;AAMiF,EAAA;AAC5C,IAAA;AACM,MAAA;AACzC,IAAA;AACmC,IAAA;AACM,MAAA;AACzC,IAAA;AACF,EAAA;AACF;AAO6C;AACpB,EAAA;AAGiB,EAAA;AAGU,EAAA;AACM,EAAA;AAGjC,EAAA;AACO,IAAA;AAC9B,EAAA;AAEmB,EAAA;AACrB;ARq4C0F;AACA;ASn/CzC;AACxB,EAAA;AAAA;AACO,EAAA;AAAA;AACA,EAAA;AAAA;AACD,EAAA;AAAA;AAC/B;AAeyB;AACf,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AAE2F,EAAA;AAC3E,IAAA;AACK,IAAA;AACsC,IAAA;AACA,IAAA;AAClD,IAAA;AACsB,MAAA;AACE,MAAA;AACQ,MAAA;AACA,MAAA;AACf,MAAA;AACkB,MAAA;AACY,MAAA;AACA,MAAA;AAC5B,MAAA;AACQ,MAAA;AACJ,MAAA;AACF,MAAA;AACJ,MAAA;AAChC,IAAA;AACF,EAAA;AAAA;AAG4B,EAAA;AACd,IAAA;AACd,EAAA;AAAA;AAGwC,EAAA;AACtB,IAAA;AAEiB,IAAA;AAC+C,IAAA;AACA,IAAA;AAClF,EAAA;AAE8C,EAAA;AAChC,IAAA;AACd,EAAA;AAAA;AAGoD,EAAA;AACoC,IAAA;AACxF,EAAA;AAAA;AAGsC,EAAA;AAEa,IAAA;AAC3B,IAAA;AAGS,IAAA;AACmB,MAAA;AACE,MAAA;AACT,QAAA;AACzC,MAAA;AACoC,MAAA;AACE,QAAA;AACtC,MAAA;AACF,IAAA;AAG+B,IAAA;AAC2C,MAAA;AACf,QAAA;AACnB,QAAA;AACtC,MAAA;AAE4D,MAAA;AAC1B,QAAA;AAClC,MAAA;AACF,IAAA;AAGoB,IAAA;AACtB,EAAA;AAAA;AAGsC,EAAA;AACsB,IAAA;AACJ,IAAA;AACT,MAAA;AAC7C,IAAA;AACkD,IAAA;AACE,MAAA;AACpD,IAAA;AACoB,IAAA;AACtB,EAAA;AAAA;AAG0C,EAAA;AACR,IAAA;AAClC,EAAA;AAAA;AAG6C,EAAA;AACgB,IAAA;AAC7D,EAAA;AACF;AAGyD;AAC/B,EAAA;AACP,EAAA;AAEkC,EAAA;AACA,EAAA;AACA,EAAA;AAEoB,EAAA;AACzE;AAE2E;AACP,EAAA;AAC/C,EAAA;AACZ,EAAA;AACmB,IAAA;AACA,IAAA;AACA,IAAA;AAC1B,EAAA;AACF;ATo9C0F;AACA;AU3mDzC;AACrC,EAAA;AACD,EAAA;AACA,EAAA;AACQ,EAAA;AACA,EAAA;AACf,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACmB,EAAA;AACL,EAAA;AAChB;AAG4D;AAShC;AAClB,EAAA;AACA,EAAA;AACa,mBAAA;AAAA;AAEb,EAAA;AAAA;AAEoC,mBAAA;AAAA;AAEkB,mBAAA;AAAA;AAExC,mBAAA;AAMpB,EAAA;AAC+C,IAAA;AAC3B,IAAA;AAGkB,IAAA;AACI,IAAA;AACf,MAAA;AACN,MAAA;AAC8B,QAAA;AACjD,MAAA;AACF,IAAA;AACF,EAAA;AAAA;AAGuC,EAAA;AACM,IAAA;AACa,IAAA;AAC1D,EAAA;AAAA;AAG2C,EAAA;AACrB,IAAA;AACtB,EAAA;AAAA;AAGsC,EAAA;AACG,IAAA;AACzC,EAAA;AAAA;AAGmE,EAAA;AAC9C,IAAA;AACrB,EAAA;AAAA;AAGmC,EAAA;AACd,IAAA;AACrB,EAAA;AAAA;AAGmC,EAAA;AAEJ,IAAA;AACsC,MAAA;AAChC,MAAA;AACiC,MAAA;AACtBA,MAAAA;AAC9C,IAAA;AAGmE,IAAA;AAGY,IAAA;AAEO,IAAA;AAErD,IAAA;AACiB,IAAA;AACA,IAAA;AACN,IAAA;AAC9C,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiBU,EAAA;AAEqB,IAAA;AACyB,MAAA;AACtD,IAAA;AAEsC,IAAA;AACV,IAAA;AACqB,IAAA;AACf,IAAA;AACI,IAAA;AAE7B,IAAA;AAGiC,IAAA;AACZ,MAAA;AACS,MAAA;AACvB,MAAA;AACc,MAAA;AACI,QAAA;AACY,QAAA;AACA,QAAA;AACZ,QAAA;AACR,QAAA;AACxB,MAAA;AACc,MAAA;AAC+C,MAAA;AACpD,MAAA;AACX,IAAA;AAGc,IAAA;AACc,IAAA;AACI,MAAA;AACQ,MAAA;AACA,MAAA;AACR,MAAA;AACR,MAAA;AACxB,IAAA;AACc,IAAA;AAGY,IAAA;AACuC,MAAA;AACI,MAAA;AACrC,MAAA;AACd,MAAA;AACX,IAAA;AACW,MAAA;AAClB,IAAA;AACS,IAAA;AAG+C,IAAA;AACH,IAAA;AACzB,MAAA;AACuB,MAAA;AACxC,MAAA;AACQ,MAAA;AACC,MAAA;AACpB,IAAA;AAG0C,IAAA;AACQ,MAAA;AACgB,MAAA;AACY,MAAA;AAChB,IAAA;AAElC,MAAA;AAC6C,MAAA;AAClE,IAAA;AACmD,MAAA;AACd,MAAA;AAC5C,IAAA;AAGgE,IAAA;AACrB,MAAA;AACO,MAAA;AAClD,IAAA;AAEW,IAAA;AACO,IAAA;AAGqC,IAAA;AACnC,MAAA;AACpB,IAAA;AAGmB,IAAA;AACgC,MAAA;AACnD,IAAA;AAEkB,IAAA;AAGK,IAAA;AACiC,MAAA;AACxD,IAAA;AAGkB,IAAA;AACD,IAAA;AACY,IAAA;AACmD,MAAA;AACtC,MAAA;AACU,MAAA;AAClC,MAAA;AACG,MAAA;AACD,MAAA;AACM,MAAA;AACpB,MAAA;AAC0B,QAAA;AAC5B,QAAA;AACiB,QAAA;AACnB,MAAA;AACkB,MAAA;AACpB,IAAA;AAEY,IAAA;AACL,IAAA;AACT,EAAA;AAAA;AAUQ,EAAA;AACiB,IAAA;AAEL,IAAA;AACP,IAAA;AAEuB,IAAA;AACW,IAAA;AAGhB,IAAA;AACH,MAAA;AACA,MAAA;AACG,MAAA;AAGb,MAAA;AAC2B,MAAA;AACF,MAAA;AACa,MAAA;AAChB,MAAA;AAC3B,MAAA;AAGU,MAAA;AACD,QAAA;AACE,QAAA;AACgB,QAAA;AAClB,QAAA;AACG,QAAA;AACe,QAAA;AACpC,MAAA;AACF,IAAA;AAG+C,IAAA;AACrB,MAAA;AACA,MAAA;AAEV,MAAA;AAC2B,MAAA;AACsB,MAAA;AAC7C,MAAA;AACT,MAAA;AAGS,MAAA;AACA,MAAA;AACU,MAAA;AACd,MAAA;AACC,MAAA;AACD,MAAA;AACQ,MAAA;AACK,MAAA;AACU,MAAA;AACA,MAAA;AAC1B,MAAA;AACb,IAAA;AAEkB,IAAA;AACpB,EAAA;AAAA;AAAA;AAAA;AAAA;AAYU,EAAA;AAC0B,IAAA;AACe,IAAA;AAEnC,IAAA;AACsB,IAAA;AACpB,IAAA;AACP,IAAA;AAEF,IAAA;AACT,EAAA;AAAA;AASQ,EAAA;AACsB,IAAA;AAEL,IAAA;AAEI,MAAA;AAEK,MAAA;AACP,QAAA;AACvB,MAAA;AACwB,MAAA;AACX,MAAA;AAES,MAAA;AACM,QAAA;AAEN,wBAAA;AACtB,MAAA;AACuB,MAAA;AAEA,QAAA;AACvB,MAAA;AACA,MAAA;AACF,IAAA;AAEU,IAAA;AAED,IAAA;AAGK,IAAA;AACc,IAAA;AACI,MAAA;AACQ,MAAA;AACA,MAAA;AACR,MAAA;AACR,MAAA;AACxB,IAAA;AACc,IAAA;AACL,IAAA;AAGgC,IAAA;AAEJ,IAAA;AACR,IAAA;AACV,IAAA;AACO,IAAA;AACP,IAAA;AACT,MAAA;AACO,MAAA;AACV,IAAA;AACG,MAAA;AACO,MAAA;AACjB,IAAA;AACkB,IAAA;AAC2C,IAAA;AAC3C,IAAA;AAEN,IAAA;AACd,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAeiB,EAAA;AAEkC,IAAA;AACH,MAAA;AACD,MAAA;AACd,QAAA;AACkC,QAAA;AACpD,UAAA;AACT,QAAA;AACF,MAAA;AACF,IAAA;AAGkB,IAAA;AACkB,MAAA;AACe,MAAA;AACrB,QAAA;AACK,UAAA;AAEQ,UAAA;AACrB,UAAA;AAGuD,UAAA;AACpC,UAAA;AACV,YAAA;AACsC,YAAA;AACpD,cAAA;AACT,YAAA;AACF,UAAA;AAGoD,UAAA;AACL,YAAA;AAChB,YAAA;AACP,cAAA;AACyC,cAAA;AACpD,gBAAA;AACT,cAAA;AACF,YAAA;AACF,UAAA;AACF,QAAA;AACF,MAAA;AACF,IAAA;AAEO,IAAA;AACT,EAAA;AACF;AAiB8D;AACzB,EAAA;AAGV,EAAA;AACM,IAAA;AACT,IAAA;AACe,IAAA;AACK,IAAA;AACoB,IAAA;AAER,IAAA;AACtD,EAAA;AAG6C,EAAA;AAC/B,EAAA;AACa,IAAA;AACF,IAAA;AACe,IAAA;AACe,IAAA;AACvD,EAAA;AAEoC,EAAA;AACtC;AAE2D;AACS,EAAA;AAC/C,EAAA;AACkE,EAAA;AACA,EAAA;AACA,EAAA;AACd,EAAA;AACzE;AVm9C0F;AACA;AW19D7C;AAChC,EAAA;AACL,EAAA;AACI,EAAA;AACK,EAAA;AACjB;AAMyB;AACf,EAAA;AACgC,mBAAA;AACJ,mBAAA;AACjB,mBAAA;AAE0B,EAAA;AACE,IAAA;AACD,IAAA;AAC9C,EAAA;AAE+B,EAAA;AACjB,IAAA;AACd,EAAA;AAEuB,EAAA;AACT,IAAA;AACd,EAAA;AAE0C,EAAA;AAC5B,IAAA;AACd,EAAA;AAAA;AAAA;AAAA;AAAA;AASE,EAAA;AAE+C,IAAA;AAC7B,MAAA;AAEyB,MAAA;AACrB,QAAA;AACpB,MAAA;AACgD,MAAA;AAClD,IAAA;AAEgB,IAAA;AAGM,IAAA;AACoB,MAAA;AACV,MAAA;AAChC,IAAA;AAEsB,IAAA;AACwB,MAAA;AAC9C,IAAA;AAGyB,IAAA;AAC6D,IAAA;AAChD,IAAA;AAEsC,IAAA;AAC9C,MAAA;AAEY,MAAA;AAES,MAAA;AACb,MAAA;AACoB,QAAA;AAEb,QAAA;AACN,UAAA;AACsB,UAAA;AACzD,QAAA;AACF,MAAA;AACF,IAAA;AAGiD,IAAA;AACA,IAAA;AAEZ,IAAA;AACN,MAAA;AACnB,MAAA;AAEoC,QAAA;AAC1B,QAAA;AACc,QAAA;AACI,UAAA;AACd,YAAA;AAClB,YAAA;AACF,UAAA;AACF,QAAA;AAC0B,QAAA;AAC5B,MAAA;AACF,IAAA;AAEyC,IAAA;AACyC,MAAA;AAC/C,QAAA;AACjC,MAAA;AACF,IAAA;AAEoD,IAAA;AACtD,EAAA;AAAA;AAGsC,EAAA;AAClB,IAAA;AACpB,EAAA;AAAA;AAGoD,EAAA;AAEL,IAAA;AAChB,IAAA;AACD,MAAA;AAC5B,IAAA;AACF,EAAA;AAAA;AAGgB,EAAA;AACE,IAAA;AACS,IAAA;AAC3B,EAAA;AAAA;AAGiC,EAAA;AACf,IAAA;AACiB,IAAA;AACnC,EAAA;AACF;AXw7D0F;AACA;AYnkErC;AACpC,EAAA;AACG,EAAA;AACE,EAAA;AACH,EAAA;AACnB;AAEoD;AACzC,EAAA;AAC0B,EAAA;AACrC;AAkBkC;AACxB,EAAA;AAE2C,EAAA;AAClC,IAAA;AAC8B,MAAA;AACE,MAAA;AAC/C,IAAA;AACF,EAAA;AAEuB,EAAA;AACD,IAAA;AACtB,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQyC,EAAA;AACG,IAAA;AAED,IAAA;AACG,IAAA;AAEf,IAAA;AACD,MAAA;AAE2B,MAAA;AACnC,MAAA;AAGwC,MAAA;AACe,QAAA;AACxD,UAAA;AACJ,UAAA;AACG,UAAA;AACZ,QAAA;AAGiE,QAAA;AACnD,QAAA;AAEe,UAAA;AACgC,YAAA;AACE,cAAA;AAC3D,cAAA;AACF,YAAA;AACF,UAAA;AAE+E,UAAA;AACP,YAAA;AACxE,UAAA;AACF,QAAA;AAGiE,QAAA;AACxB,QAAA;AAC0C,UAAA;AACnF,QAAA;AACF,MAAA;AAGyC,MAAA;AAC3C,IAAA;AAEO,IAAA;AACT,EAAA;AAAA;AAGiE,EAAA;AACzB,IAAA;AAEO,IAAA;AAEF,MAAA;AACU,QAAA;AAEjB,QAAA;AACR,UAAA;AACN,UAAA;AACE,UAAA;AACH,UAAA;AACD,UAAA;AAChB,QAAA;AAG4C,QAAA;AACd,QAAA;AACqB,UAAA;AACpB,YAAA;AAC6B,UAAA;AAC3B,YAAA;AACwB,UAAA;AAC3B,YAAA;AAC5B,UAAA;AACF,QAAA;AAG8C,QAAA;AACe,UAAA;AAC1B,YAAA;AACjC,UAAA;AACF,QAAA;AAI4C,QAAA;AACjB,UAAA;AACsC,UAAA;AAC7B,YAAA;AACwC,UAAA;AACtC,YAAA;AACgC,UAAA;AACnC,YAAA;AACjC,UAAA;AACF,QAAA;AAGgC,QAAA;AACtB,UAAA;AACA,UAAA;AACA,UAAA;AACT,QAAA;AAE6C,QAAA;AACZ,UAAA;AACN,YAAA;AAC+C,cAAA;AACvE,YAAA;AACF,UAAA;AACF,QAAA;AAC4C,QAAA;AAClB,UAAA;AACwD,YAAA;AAChF,UAAA;AACF,QAAA;AAEiB,QAAA;AACnB,MAAA;AACF,IAAA;AAEO,IAAA;AACT,EAAA;AACF;AZkhE0F;AACA;AajsEtD;AAAA;AAElC,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AAAA;AAEA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AAAA;AAEA,EAAA;AACF;AAQ+B;AACrB,EAAA;AAE6B,EAAA;AACR,IAAA;AACtB,MAAA;AACwB,MAAA;AAC5B,IAAA;AACH,EAAA;AAAA;AAG2B,EAAA;AACE,IAAA;AAC7B,EAAA;AAAA;AAG0C,EAAA;AACA,IAAA;AAC1C,EAAA;AAAA;AAG0C,EAAA;AACZ,IAAA;AACJ,MAAA;AACxB,IAAA;AACF,EAAA;AACF;AbwrE0F;AACA;Ac3uE3D;AACuD,mBAAA;AAAA;AAGF,EAAA;AAC9C,IAAA;AACxB,IAAA;AACM,MAAA;AACe,MAAA;AAC/B,IAAA;AACoD,IAAA;AAGvC,IAAA;AAC6C,MAAA;AAC1D,IAAA;AACF,EAAA;AAAA;AAG6E,EAAA;AACvC,IAAA;AAC3B,IAAA;AACgD,MAAA;AACzD,IAAA;AACF,EAAA;AAAA;AAGuE,EAAA;AACjC,IAAA;AAC3B,IAAA;AACoB,MAAA;AACrB,QAAA;AACoC,UAAA;AAC1B,QAAA;AACmD,UAAA;AACjE,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AAAA;AAGwC,EAAA;AAC3B,IAAA;AACkB,MAAA;AACtB,IAAA;AACgB,MAAA;AACvB,IAAA;AACF,EAAA;AACF;AduuE0F;AACA;Ae5xEnE;AAekC;AAC/C,EAAA;AACc,mBAAA;AACd,EAAA;AACA,EAAA;AACA,EAAA;AAC2C,mBAAA;AAQhD,mBAAA;AACmC,mBAAA;AACZ,mBAAA;AAAA;AAGE,mBAAA;AACC,mBAAA;AACW,mBAAA;AACf,mBAAA;AACL,mBAAA;AACM,mBAAA;AACY,mBAAA;AACZ,mBAAA;AACC,mBAAA;AACA,mBAAA;AACK,mBAAA;AAC8B,mBAAA;AAAA;AAGjB,EAAA;AACd,IAAA;AACM,IAAA;AACF,MAAA;AACjC,IAAA;AACF,EAAA;AAAA;AAGsD,mBAAA;AACjC,mBAAA;AACQ,mBAAA;AAEc,EAAA;AAChB,IAAA;AACC,IAAA;AACC,IAAA;AACL,IAAA;AACxB,EAAA;AAAA;AAGa,EAAA;AACM,IAAA;AAEqB,IAAA;AAEsC,IAAA;AACG,IAAA;AAGnC,IAAA;AACpB,MAAA;AACvB,IAAA;AAKE,IAAA;AAGY,MAAA;AAC4D,MAAA;AAC9D,QAAA;AACT,MAAA;AACO,MAAA;AAE4E,IAAA;AACxE,MAAA;AACS,MAAA;AACF,MAAA;AACA,MAAA;AAG6B,MAAA;AACJ,QAAA;AACL,QAAA;AACT,UAAA;AACS,UAAA;AAChB,UAAA;AACpB,QAAA;AACF,MAAA;AAGiB,MAAA;AACuC,MAAA;AACxB,QAAA;AACU,QAAA;AACQ,QAAA;AAClD,MAAA;AAEwE,MAAA;AACtD,MAAA;AAE+D,IAAA;AACtE,MAAA;AACS,MAAA;AACmC,MAAA;AAC5B,MAAA;AACT,MAAA;AACA,MAAA;AAEJ,MAAA;AACyB,MAAA;AACvB,MAAA;AACP,MAAA;AAEe,IAAA;AACbC,MAAAA;AACQ,MAAA;AACkC,QAAA;AACrD,MAAA;AACmB,MAAA;AAGmB,IAAA;AAIf,MAAA;AAEZA,MAAAA;AACS,MAAA;AAEQ,MAAA;AAChB,MAAA;AAEM,MAAA;AACD,MAAA;AAC6D,MAAA;AAExD,MAAA;AAEP,MAAA;AACA,MAAA;AACO,MAAA;AAES,MAAA;AACA,MAAA;AAEtB,MAAA;AACS,MAAA;AACgC,MAAA;AAClC,MAAA;AACG,MAAA;AAGsB,MAAA;AACzB,MAAA;AACA,MAAA;AACF,MAAA;AACV,MAAA;AACqB,QAAA;AACD,QAAA;AACJ,QAAA;AACD,QAAA;AACb,QAAA;AACN,MAAA;AACS,MAAA;AAEO,MAAA;AACc,MAAA;AAClB,MAAA;AAE6D,IAAA;AAC/C,MAAA;AAEA,IAAA;AACf,MAAA;AACQ,MAAA;AAC4B,QAAA;AAC/C,MAAA;AAE0B,IAAA;AACtB,MAAA;AAC6B,QAAA;AACzB,MAAA;AAER,MAAA;AAE6B,IAAA;AACzB,MAAA;AAC6B,QAAA;AACzB,MAAA;AAER,MAAA;AAE0B,IAAA;AACf,MAAA;AACS,MAAA;AACkB,QAAA;AACS,QAAA;AACd,QAAA;AAC1B,MAAA;AACiB,QAAA;AACwC,QAAA;AAC/B,QAAA;AACjC,MAAA;AAE0B,IAAA;AACfA,MAAAA;AACQ,MAAA;AAC4B,QAAA;AAC/C,MAAA;AAEkC,IAAA;AACkB,MAAA;AAMrD,IAAA;AACqE,MAAA;AAC3C,QAAA;AACM,QAAA;AAC/B,MAAA;AACD,IAAA;AAGwC,IAAA;AACrB,IAAA;AAAsB,MAAA;AAA4B,sBAAA;AAAG,IAAA;AACpC,IAAA;AACnB,IAAA;AAAgC,MAAA;AAAG,IAAA;AACZ,IAAA;AACrB,IAAA;AAAsB,MAAA;AAAG,IAAA;AACjD,EAAA;AAAA;AAAA;AAAA;AAAA;AAUQ,EAAA;AACsB,IAAA;AAIb,IAAA;AAC8B,IAAA;AACb,IAAA;AACkB,MAAA;AAClD,IAAA;AACkC,IAAA;AACF,IAAA;AACgB,MAAA;AAChD,IAAA;AACoB,IAAA;AACa,IAAA;AACiB,MAAA;AAClD,IAAA;AAC+B,IAAA;AACQ,IAAA;AACb,MAAA;AACD,MAAA;AACzB,IAAA;AAG0B,IAAA;AACS,MAAA;AACnC,IAAA;AAGgB,IAAA;AACmB,IAAA;AACa,MAAA;AAChD,IAAA;AACuC,IAAA;AAI0C,IAAA;AACvC,IAAA;AACZ,IAAA;AACH,MAAA;AACW,QAAA;AACL,QAAA;AACK,UAAA;AACP,UAAA;AAA+B,YAAA;AAAG,YAAA;AAAO,UAAA;AACpE,QAAA;AACF,MAAA;AACF,IAAA;AAC0C,IAAA;AACjB,IAAA;AACa,MAAA;AACN,MAAA;AACA,MAAA;AACD,MAAA;AACoB,MAAA;AAC/B,MAAA;AACe,MAAA;AACoB,QAAA;AACrD,MAAA;AACF,IAAA;AAE4C,IAAA;AACd,IAAA;AACG,MAAA;AAEW,MAAA;AACD,MAAA;AACV,MAAA;AACd,MAAA;AACf,QAAA;AACkE,QAAA;AACX,QAAA;AAC3B,QAAA;AACA,QAAA;AAGD,QAAA;AAGA,QAAA;AACf,QAAA;AACb,MAAA;AACH,IAAA;AAG4C,IAAA;AACb,IAAA;AACI,IAAA;AAC6C,MAAA;AACR,MAAA;AAExD,MAAA;AACC,QAAA;AACA,QAAA;AACD,QAAA;AACb,MAAA;AACH,IAAA;AAEqB,IAAA;AACA,IAAA;AAC4G,IAAA;AAC9F,IAAA;AACd,MAAA;AACT,QAAA;AACA,QAAA;AACC,QAAA;AACA,QAAA;AACmC,QAAA;AACA,QAAA;AAC7C,MAAA;AACH,IAAA;AACsB,IAAA;AAGoC,IAAA;AAC2B,MAAA;AAEA,MAAA;AAEnE,MAAA;AACjB,IAAA;AAEqB,IAAA;AACb,MAAA;AACA,MAAA;AACR,IAAA;AAGoD,IAAA;AACa,MAAA;AAC1B,MAAA;AACgC,QAAA;AAC3C,UAAA;AACM,UAAA;AAC/B,QAAA;AACqB,QAAA;AACjB,MAAA;AACR,IAAA;AAGkC,IAAA;AACkB,MAAA;AAClB,QAAA;AACM,UAAA;AACU,UAAA;AACe,YAAA;AAC3D,UAAA;AACF,QAAA;AACD,MAAA;AACI,IAAA;AAC8B,MAAA;AACrC,IAAA;AACF,EAAA;AAAA;AAGyE,EAAA;AAC/B,IAAA;AACjB,IAAA;AACsB,MAAA;AACpB,MAAA;AACiB,QAAA;AACxC,MAAA;AACF,IAAA;AACF,EAAA;AAAA;AAGgD,EAAA;AACJ,oBAAA;AAC5C,EAAA;AAAA;AAGoC,EAAA;AACV,IAAA;AAC1B,EAAA;AAAA;AAGsC,EAAA;AACX,IAAA;AAC4B,IAAA;AACpC,IAAA;AACnB,EAAA;AAAA;AAGgB,EAAA;AACI,IAAA;AACe,IAAA;AACO,IAAA;AACT,MAAA;AAC7B,MAAA;AACF,IAAA;AACsB,IAAA;AACxB,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOiC,EAAA;AACL,IAAA;AACH,IAAA;AACK,IAAA;AACH,MAAA;AACL,MAAA;AAEwC,MAAA;AACpD,QAAA;AAAiC,UAAA;AAAW,QAAA;AAAa,QAAA;AACxD,MAAA;AACsB,QAAA;AACvB,QAAA;AAAiC,UAAA;AAAW,QAAA;AAAa,QAAA;AACvC,QAAA;AACL,QAAA;AAC4D,UAAA;AAC9C,YAAA;AACL,YAAA;AACxB,UAAA;AACG,QAAA;AACP,MAAA;AACD,IAAA;AACH,EAAA;AAAA;AAG8B,EAAA;AACI,IAAA;AACQ,MAAA;AAClB,MAAA;AACtB,IAAA;AACF,EAAA;AAAA;AAGsF,EAAA;AACnE,IAAA;AACC,IAAA;AACQ,IAAA;AAC5B,EAAA;AAAA;AAG+D,EAAA;AACV,IAAA;AAEjB,IAAA;AACnB,IAAA;AAEmC,IAAA;AACZ,MAAA;AACJ,MAAA;AACU,QAAA;AACY,QAAA;AAC1B,UAAA;AAC1B,QAAA;AACF,MAAA;AACuB,MAAA;AAEO,MAAA;AACT,MAAA;AAEgD,MAAA;AACrE,MAAA;AAES,MAAA;AACK,MAAA;AAEuC,MAAA;AACA,MAAA;AACf,MAAA;AACZ,QAAA;AACA,QAAA;AACe,QAAA;AAC3B,QAAA;AAC0C,QAAA;AACA,QAAA;AACxB,QAAA;AACR,QAAA;AACxB,MAAA;AACc,MAAA;AACE,MAAA;AACO,MAAA;AACd,MAAA;AACS,MAAA;AAC0B,MAAA;AAC5B,MAAA;AACL,MAAA;AACC,MAAA;AACd,IAAA;AACF,EAAA;AAAA;AAGe,EAAA;AACI,IAAA;AAC8B,MAAA;AACE,MAAA;AACjD,IAAA;AACF,EAAA;AAAA;AAGgB,EAAA;AACK,IAAA;AACc,IAAA;AACA,MAAA;AACV,MAAA;AACvB,IAAA;AACiB,IAAA;AACS,MAAA;AACV,MAAA;AAChB,IAAA;AACyB,IAAA;AACC,IAAA;AACJ,IAAA;AACxB,EAAA;AAAA;AAG8C,EAAA;AAC1B,IAAA;AAG2B,IAAA;AAEN,IAAA;AAEQ,IAAA;AAED,IAAA;AACA,IAAA;AACA,IAAA;AAEC,IAAA;AACK,MAAA;AAErB,MAAA;AAGgB,QAAA;AACiC,UAAA;AAC7D,UAAA;AAC4D,UAAA;AACA,UAAA;AACT,UAAA;AACC,UAAA;AAEpB,QAAA;AACV,UAAA;AACtB,UAAA;AAC4D,UAAA;AACA,UAAA;AACM,UAAA;AAElC,QAAA;AACV,UAAA;AACtB,UAAA;AAC4D,UAAA;AACA,UAAA;AACG,UAAA;AAE/B,QAAA;AACV,UAAA;AACtB,UAAA;AAC4D,UAAA;AACA,UAAA;AACW,UAAA;AACnF,QAAA;AACE,MAAA;AAG2B,QAAA;AAGlC,MAAA;AACK,IAAA;AACiC,MAAA;AACxC,IAAA;AAGmD,IAAA;AACtB,IAAA;AAEF,IAAA;AACS,MAAA;AACa,MAAA;AAIM,MAAA;AACa,QAAA;AAC1B,QAAA;AACgC,UAAA;AAC3C,YAAA;AACM,YAAA;AAC/B,UAAA;AACqB,UAAA;AACjB,QAAA;AACR,MAAA;AACK,IAAA;AACwB,MAAA;AACP,MAAA;AACxB,IAAA;AAGmB,IAAA;AAK0B,IAAA;AACxB,MAAA;AAC4B,QAAA;AACvB,UAAA;AACpB,UAAA;AACF,QAAA;AACI,QAAA;AAAiC,UAAA;AAAW,QAAA;AAAa,QAAA;AACf,QAAA;AAChD,MAAA;AAC8C,MAAA;AAChD,IAAA;AACF,EAAA;AAAA;AAG0C,EAAA;AACtB,IAAA;AAC6D,IAAA;AACjF,EAAA;AACF;AAGuC;AAC7B,EAAA;AAC2B,EAAA;AACM,IAAA;AACzC,EAAA;AACO,EAAA;AACT;AAGoE;AACpC,EAAA;AACmC,EAAA;AAGJ,EAAA;AAE1B,EAAA;AACd,EAAA;AACkE,IAAA;AACzE,MAAA;AACA,IAAA;AACd,EAAA;AAEmC,EAAA;AACO,EAAA;AACzB,IAAA;AACsE,IAAA;AACzE,MAAA;AACA,IAAA;AACd,EAAA;AAEU,EAAA;AACA,EAAA;AACe,EAAA;AAC3B;Af0oE0F;AACA;AgBhzFjE;AACf,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACoC,mBAAA;AACpC,EAAA;AACA,EAAA;AACA,EAAA;AAC2C,mBAAA;AAC3B,mBAAA;AACG,mBAAA;AAEyC,EAAA;AACnD,IAAA;AACG,IAAA;AAG6C,IAAA;AACA,IAAA;AACR,IAAA;AAC7B,IAAA;AACJ,uBAAA;AACf,MAAA;AACE,MAAA;AACT,IAAA;AACqE,IAAA;AACS,IAAA;AAC9B,IAAA;AACwB,IAAA;AAC9B,IAAA;AACL,IAAA;AAEf,IAAA;AACpB,MAAA;AACiB,MAAA;AACC,MAAA;AACL,MAAA;AACf,IAAA;AAG8B,IAAA;AAExB,MAAA;AACqC,MAAA;AAAiB,QAAA;AAAI,MAAA;AAG7C,MAAA;AAEwC,MAAA;AACL,QAAA;AACrC,QAAA;AACG,MAAA;AAC+D,QAAA;AAC5B,QAAA;AACtC,QAAA;AACd,MAAA;AACI,IAAA;AAC8C,MAAA;AACb,MAAA;AACxC,IAAA;AAE4B,IAAA;AACF,MAAA;AACJ,MAAA;AACe,QAAA;AAC5B,MAAA;AAC0B,QAAA;AACwB,UAAA;AACtD,QAAA;AACH,MAAA;AACF,IAAA;AAG2C,IAAA;AACyB,MAAA;AACxC,MAAA;AAC5B,IAAA;AACF,EAAA;AAAA;AAAA;AAKiD,EAAA;AACgB,IAAA;AAC5B,IAAA;AACf,IAAA;AACtB,EAAA;AAAA;AAG+C,EAAA;AACgB,IAAA;AAC5B,IAAA;AACb,IAAA;AACtB,EAAA;AAAA;AAG+C,EAAA;AACc,IAAA;AACZ,IAAA;AACP,IAAA;AACpB,IAAA;AACtB,EAAA;AAAA;AAGyE,EAAA;AACZ,IAAA;AACpB,IAAA;AACnB,IAAA;AACtB,EAAA;AAAA;AAKQ,EAAA;AAC2C,IAAA;AACtB,MAAA;AACwC,QAAA;AACjE,MAAA;AAC+C,MAAA;AACjC,QAAA;AAC6C,QAAA;AAC3D,MAAA;AACO,MAAA;AACR,IAAA;AACmB,IAAA;AACtB,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA4B6E,EAAA;AAC/B,IAAA;AACjB,IAAA;AAC7B,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAO2D,EAAA;AAC9B,IAAA;AAC7B,EAAA;AAAA;AAAA;AAK4C,EAAA;AACI,IAAA;AAC7B,IAAA;AACuB,IAAA;AACjC,IAAA;AACT,EAAA;AAAA;AAG+C,EAAA;AACI,IAAA;AAChC,IAAA;AACuB,IAAA;AACjC,IAAA;AACT,EAAA;AAAA;AAAA;AAK2C,EAAA;AACR,IAAA;AACT,IAAA;AACc,MAAA;AAClB,QAAA;AACL,QAAA;AACX,QAAA;AACD,MAAA;AACH,IAAA;AACoB,IAAA;AACuB,oBAAA;AACT,IAAA;AACrB,MAAA;AACoC,MAAA;AAChD,IAAA;AACH,EAAA;AAAA;AAGuF,EAAA;AACf,oBAAA;AACxE,EAAA;AAAA;AAGiC,EAAA;AACiB,IAAA;AAC5B,IAAA;AACtB,EAAA;AAAA;AAGgB,EAAA;AACY,IAAA;AACN,IAAA;AACtB,EAAA;AAAA;AAAA;AAKqC,EAAA;AACvB,IAAA;AACd,EAAA;AAAA;AAG8C,EAAA;AAChC,IAAA;AACd,EAAA;AAAA;AAGgC,EAAA;AACK,IAAA;AACf,IAAA;AACiB,IAAA;AAC3B,MAAA;AAC6B,MAAA;AACJ,MAAA;AAClC,IAAA;AACH,EAAA;AAAA;AAGoC,EAAA;AACN,IAAA;AAC9B,EAAA;AAAA;AAGqB,EAAA;AACS,oBAAA;AAC9B,EAAA;AAAA;AAGsB,EAAA;AACS,oBAAA;AAC/B,EAAA;AAAA;AAGoC,EAAA;AACqB,IAAA;AACoB,IAAA;AACrB,IAAA;AACxD,EAAA;AAAA;AAGkC,EAAA;AACC,oBAAA;AACnC,EAAA;AAAA;AAGoC,EAAA;AACV,IAAA;AACJ,IAAA;AACe,MAAA;AAC5B,IAAA;AAC0B,MAAA;AACwB,QAAA;AACtD,MAAA;AACH,IAAA;AACF,EAAA;AAAA;AAAA;AAK2B,EAAA;AACE,IAAA;AAC7B,EAAA;AAAA;AAGoC,EAAA;AACP,IAAA;AAC0C,IAAA;AACjD,IAAA;AACtB,EAAA;AAAA;AAAA;AAKkF,EAAA;AAC3C,IAAA;AACvC,EAAA;AAAA;AAG6E,EAAA;AAC5C,IAAA;AACjC,EAAA;AAAA;AAAA;AAK2C,EAAA;AACZ,IAAA;AAC/B,EAAA;AAAA;AAGsC,EAAA;AACF,IAAA;AACpC,EAAA;AAAA;AAGoC,EAAA;AACF,IAAA;AAClC,EAAA;AAAA;AAGwB,EAAA;AACV,IAAA;AACd,EAAA;AAAA;AAG+B,EAAA;AACjB,IAAA;AACd,EAAA;AAAA;AAG8B,EAAA;AAChB,IAAA;AACd,EAAA;AAAA;AAAA;AASG,mBAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiBqC,EAAA;AAElB,IAAA;AAES,IAAA;AAEe,IAAA;AACvB,MAAA;AAEY,QAAA;AACX,UAAA;AACC,UAAA;AACI,UAAA;AACtB,QAAA;AAGiB,QAAA;AACqC,QAAA;AAC/B,UAAA;AACxB,QAAA;AACF,MAAA;AACF,IAAA;AAEoD,IAAA;AAC3B,IAAA;AAC3B,EAAA;AAAA;AAGuB,EAAA;AACgB,IAAA;AAEW,IAAA;AACT,MAAA;AAC3B,MAAA;AACe,QAAA;AACC,QAAA;AACI,QAAA;AAC9B,MAAA;AACF,IAAA;AAE2B,IAAA;AACyB,IAAA;AAC3B,IAAA;AAC3B,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQmC,EAAA;AACO,IAAA;AAC3B,IAAA;AACqE,MAAA;AAClF,IAAA;AAE8C,IAAA;AACpB,MAAA;AACA,QAAA;AAC8B,QAAA;AACxC,MAAA;AACf,IAAA;AACH,EAAA;AAAA;AAAA;AAAA;AAAA;AAMwB,EAAA;AACG,IAAA;AAEiB,IAAA;AACD,MAAA;AACE,MAAA;AAEwB,MAAA;AAEpB,QAAA;AACtB,QAAA;AACO,UAAA;AACA,QAAA;AACE,UAAA;AAC9B,QAAA;AACqC,QAAA;AAChC,MAAA;AACuD,QAAA;AAC9D,MAAA;AACF,IAAA;AAEsB,IAAA;AACxB,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiBoC,EAAA;AAEoC,IAAA;AAClC,IAAA;AAGS,IAAA;AAGlB,IAAA;AACoC,MAAA;AACE,MAAA;AAC1D,IAAA;AACgC,MAAA;AACvC,IAAA;AAEmC,IAAA;AAGe,IAAA;AACZ,IAAA;AAGR,IAAA;AAC6B,MAAA;AACpB,MAAA;AAGD,MAAA;AAEH,wBAAA;AAEH,QAAA;AACrB,UAAA;AACA,UAAA;AACE,UAAA;AACT,QAAA;AAC2C,QAAA;AACrB,UAAA;AACiB,UAAA;AACnC,YAAA;AACqC,YAAA;AACJ,YAAA;AAClC,UAAA;AACF,QAAA;AAC2B,QAAA;AAC9B,MAAA;AACK,IAAA;AAE0B,sBAAA;AACP,MAAA;AAC1B,IAAA;AAEoB,IAAA;AAGiB,IAAA;AAC0B,MAAA;AAClB,MAAA;AAC7C,IAAA;AAG+B,IAAA;AAC2B,MAAA;AACR,QAAA;AAChD,MAAA;AAEkC,MAAA;AACG,QAAA;AACrC,MAAA;AAEoE,MAAA;AACnD,QAAA;AACD,UAAA;AAC8B,UAAA;AACA,2BAAA;AAC5C,QAAA;AACF,MAAA;AACD,IAAA;AACH,EAAA;AAAA;AAGgB,EAAA;AACM,IAAA;AACtB,EAAA;AAAA;AAGgB,EAAA;AACiB,oBAAA;AACP,IAAA;AACA,oBAAA;AACD,IAAA;AACL,IAAA;AACS,IAAA;AAC7B,EAAA;AAAA;AAAA;AAK4C,EAAA;AACZ,IAAA;AACb,IAAA;AACnB,EAAA;AAAA;AAG4B,EAAA;AAEqB,IAAA;AACzB,MAAA;AACe,QAAA;AAC/B,UAAA;AAC8B,UAAA;AACK,UAAA;AACrC,QAAA;AACF,MAAA;AACF,IAAA;AAGqE,IAAA;AAGG,IAAA;AAEpD,IAAA;AACtB,EAAA;AAAA;AAG+B,EAAA;AAGR,IAAA;AACM,MAAA;AACF,QAAA;AACU,QAAA;AACR,UAAA;AACD,UAAA;AACrB,QAAA;AACH,MAAA;AACA,MAAA;AACF,IAAA;AAE4D,IAAA;AAGe,IAAA;AACtD,IAAA;AAC8B,MAAA;AACA,MAAA;AAEjB,MAAA;AACD,QAAA;AACD,UAAA;AAC5B,QAAA;AACF,MAAA;AACgC,MAAA;AACwC,QAAA;AAC1C,UAAA;AAC5B,QAAA;AACF,MAAA;AAEQ,MAAA;AACA,MAAA;AACV,IAAA;AAE0D,IAAA;AAGtB,IAAA;AACU,MAAA;AAC9C,IAAA;AACF,EAAA;AACF;AAIuC;AAEb,EAAA;AAC1B;AAE0C;AACP,EAAA;AACnB,EAAA;AAChB;AAE6C;AAKhC,EAAA;AAEb;AhB2qF0F;AACA;AiB71GnC;AACrC,mBAAA;AACM,mBAAA;AACG,mBAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAcmB,EAAA;AACzB,IAAA;AAG2B,IAAA;AAG/B,IAAA;AACmB,MAAA;AACmB,IAAA;AAC1B,MAAA;AACzB,IAAA;AAEiB,IAAA;AACgB,IAAA;AACnC,EAAA;AAE4B,EAAA;AACR,IAAA;AACoD,MAAA;AACtE,IAAA;AACF,EAAA;AAEgD,EAAA;AAC7B,IAAA;AACyC,IAAA;AAC5D,EAAA;AAE8C,EAAA;AAC3B,IAAA;AAC+B,IAAA;AAClD,EAAA;AAE0D,EAAA;AACvC,IAAA;AAEuB,IAAA;AAGL,IAAA;AACC,IAAA;AAET,IAAA;AACO,MAAA;AACI,MAAA;AACT,QAAA;AACF,QAAA;AACkB,QAAA;AAC3C,MAAA;AACqB,MAAA;AACvB,IAAA;AAEO,IAAA;AACqB,MAAA;AAC1B,MAAA;AACF,IAAA;AACF,EAAA;AAEgE,EAAA;AAC7C,IAAA;AAEqB,IAAA;AACR,IAAA;AAEJ,IAAA;AACX,MAAA;AAC4B,QAAA;AACI,QAAA;AACN,QAAA;AACL,QAAA;AACE,QAAA;AACnC,MAAA;AACH,IAAA;AAEiB,IAAA;AACnB,EAAA;AAEqC,EAAA;AAClB,IAAA;AACE,IAAA;AACrB,EAAA;AAEkC,EAAA;AACT,IAAA;AACzB,EAAA;AAAA;AAGiB,EAAA;AACH,IAAA;AACd,EAAA;AAAA;AAGwC,EAAA;AACpB,IAAA;AAEK,IAAA;AAChB,MAAA;AACS,QAAA;AACT,MAAA;AACmB,QAAA;AACR,MAAA;AAEF,QAAA;AACd,MAAA;AACA,MAAA;AACkC,QAAA;AACpC,IAAA;AACF,EAAA;AACF;AjBs0G0F;AACA;AkBx7G/B;AAChD,EAAA;AACD,EAAA;AAEgC,EAAA;AACvB,IAAA;AACI,MAAA;AACc,MAAA;AACH,MAAA;AACG,MAAA;AACjC,IAAA;AACuC,IAAA;AACzC,EAAA;AAEkC,EAAA;AACoB,IAAA;AACuB,IAAA;AAC7E,EAAA;AAEiD,EAAA;AAGlC,IAAA;AACf,EAAA;AAE0D,EAAA;AAC0B,IAAA;AACjD,IAAA;AAEK,IAAA;AAC6C,IAAA;AACjD,MAAA;AACL,MAAA;AACb,QAAA;AACU,UAAA;AACtB,QAAA;AACF,MAAA;AACO,MAAA;AACR,IAAA;AAE4B,IAAA;AAC/B,EAAA;AAEgE,EAAA;AACU,IAAA;AACrC,IAAA;AACM,IAAA;AACxB,IAAA;AACnB,EAAA;AAEqC,EAAA;AAC8C,IAAA;AAC7B,IAAA;AACtD,EAAA;AAEkC,EAAA;AAC5B,IAAA;AACqC,MAAA;AACE,MAAA;AACM,MAAA;AACrC,QAAA;AACW,QAAA;AACpB,MAAA;AACe,MAAA;AACV,IAAA;AACC,MAAA;AACT,IAAA;AACF,EAAA;AAE8E,EAAA;AACrC,IAAA;AACoC,IAAA;AAEvE,IAAA;AAa6C,MAAA;AACrC,QAAA;AACC,QAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOS,UAAA;AACA,UAAA;AACN,UAAA;AACZ,QAAA;AACM,QAAA;AACa,QAAA;AACpB,MAAA;AAEiB,MAAA;AACgE,QAAA;AAClF,MAAA;AAEO,MAAA;AACP,IAAA;AACoB,MAAA;AACtB,IAAA;AACF,EAAA;AAAA;AAGiD,EAAA;AACjB,IAAA;AACD,IAAA;AAEH,IAAA;AACE,MAAA;AACe,MAAA;AAGgC,MAAA;AAC3D,MAAA;AACC,QAAA;AACQ,UAAA;AACE,UAAA;AACH,UAAA;AACnB,QAAA;AACD,QAAA;AACF,MAAA;AAE+B,MAAA;AACjB,MAAA;AACC,QAAA;AACQ,UAAA;AACE,UAAA;AACH,UAAA;AACE,UAAA;AACA,UAAA;AACrB,QAAA;AACH,MAAA;AACF,IAAA;AAEO,IAAA;AACT,EAAA;AACF;AlBy5G0F;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/private/tmp/dkg-release-10.0.1/packages/graph-viz/dist/index.cjs","sourcesContent":[null,"import type { GraphNode, GraphEdge } from './types.js';\nimport type { GraphModel } from './graph-model.js';\nimport type { ColorPalette } from './palette.js';\nimport type { TemporalConfig } from './temporal-filter.js';\n\n// --- Namespace resolution helpers ---\n\nconst KNOWN_NAMESPACES = [\n 'https://guardiankg.org/vocab/',\n 'https://umanitek.ai/dkg/vocab/',\n 'http://umanitek.ai/dkg/vocab/',\n 'https://schema.org/',\n 'http://schema.org/',\n];\n\n/** Get a property value checking all known namespace variants */\nfunction getAnyNS(node: GraphNode, shortName: string): string | null {\n for (const ns of KNOWN_NAMESPACES) {\n const vals = node.properties?.get(ns + shortName);\n if (vals && vals.length > 0) return vals[0].value;\n }\n return null;\n}\n\n// --- ViewConfig Type ---\n\n/** Visual configuration for a single node type */\nexport interface NodeTypeConfig {\n color?: string;\n shape?: 'hexagon' | 'circle';\n /** Icon URL or data URI — assigned as imageUrl on matching nodes */\n icon?: string;\n /** Static size multiplier for all nodes of this type */\n sizeMultiplier?: number;\n}\n\n/** Focal entity configuration */\nexport interface FocalConfig {\n /** URI of the focal entity (checked across namespace variants) */\n uri: string;\n /** Image URL for the focal entity */\n image?: string;\n /** Size multiplier (default 1.5) */\n sizeMultiplier?: number;\n}\n\n/** Highlight / risk rule configuration */\nexport interface HighlightConfig {\n /** Property short name that drives visual emphasis (checked across namespaces) */\n property: string;\n /** Where to find the property: on the node itself, or on a linked entity */\n source?: 'self' | 'linked';\n /** Predicate short name to follow when source='linked' (e.g., 'hasSentiment') */\n linkedVia?: string;\n /** Absolute value above this threshold = highlighted */\n threshold: number;\n /** Border color for highlighted nodes */\n color: string;\n /** How many top-scoring nodes get the size boost (default 100) */\n topN?: number;\n /**\n * Size multiplier for top-N nodes.\n * If sizeMin/sizeMax are set, this is ignored (continuous scaling is used).\n * Otherwise this is a flat multiplier applied to all top-N nodes (default 2).\n */\n sizeMultiplier?: number;\n /** Minimum sizeMultiplier for continuous scaling (lowest-risk in top N). Default: 1.0 */\n sizeMin?: number;\n /** Maximum sizeMultiplier for continuous scaling (highest-risk in top N). Default: 3.0 */\n sizeMax?: number;\n /**\n * Invert the scoring: treat LOW values as high risk.\n * Useful when the property represents a \"safety\" signal (e.g., sentimentScore)\n * where 0.0 = dangerous and 1.0 = safe.\n * Default: false\n */\n invert?: boolean;\n}\n\n/** Size-by rule: scale node size based on a property */\nexport interface SizeByConfig {\n /** Property short name (e.g., 'totalInteractions') */\n property: string;\n /** Scale function (default 'log') */\n scale?: 'linear' | 'log';\n}\n\n/** Icon mapping: assign icons to posts by platform edge */\nexport interface PlatformIconConfig {\n /** Map of platform ID (last path segment of platform URI) to icon URL/data URI */\n icons: Record<string, string>;\n /** URL fallback patterns: map of URL substring to platform key */\n urlFallbacks?: Record<string, string>;\n}\n\n/** Configuration for a single tooltip field */\nexport interface TooltipFieldConfig {\n /** Display label in the tooltip */\n label: string;\n /** Property short name to display (checked across known namespaces) */\n property: string;\n /** Where to find the property: directly on the node ('self') or on a linked entity */\n source?: 'self' | 'linked';\n /** Predicate short name to follow when source='linked' */\n linkedVia?: string;\n /** Display format: 'text' (default), 'number', 'date' */\n format?: 'text' | 'number' | 'date';\n}\n\n/** Tooltip configuration */\nexport interface TooltipConfig {\n /**\n * Properties to try for the tooltip title, in priority order.\n * Each entry is a short name checked across known namespaces.\n * Falls back to the node's resolved label if none match.\n *\n * @example [\"text\", \"name\", \"title\"]\n */\n titleProperties?: string[];\n\n /**\n * Template for the tooltip subtitle line.\n * Supports {property} placeholders (short names).\n * Special tokens: {platform}, {author}, {date}, {type}\n *\n * @example \"{platform} · {author} · {date}\"\n */\n subtitleTemplate?: string;\n\n /** Max characters for the title before truncation. Default: 60 */\n titleMaxLength?: number;\n\n /** Additional fields to show in the tooltip */\n fields?: TooltipFieldConfig[];\n}\n\n/** Animation configuration for visual liveliness */\nexport interface AnimationConfig {\n /** Enable link particles flowing along edges. Default: false */\n linkParticles?: boolean;\n /** Number of particles per link. Default: 1 */\n linkParticleCount?: number;\n /** Particle speed (0-1 range, fraction of link length per frame). Default: 0.005 */\n linkParticleSpeed?: number;\n /** Particle color. Default: 'rgba(100,150,255,0.6)' */\n linkParticleColor?: string;\n /** Particle width in pixels. Default: 1.5 */\n linkParticleWidth?: number;\n /** Keep the force simulation slightly warm so nodes gently drift. Default: false */\n drift?: boolean;\n /** Alpha target for the warm simulation (0-0.1 range). Default: 0.008 */\n driftAlpha?: number;\n /** Enable breathing/pulse animation on high-risk nodes. Default: false */\n riskPulse?: boolean;\n /** Highlight edges of hovered node with faster, brighter particles. Default: false */\n hoverTrace?: boolean;\n /** Fade-in nodes/edges on initial load. Default: false */\n fadeIn?: boolean;\n}\n\n/** Trust visualization configuration */\nexport interface TrustConfig {\n /** Enable trust indicators. Default: false */\n enabled: boolean;\n /** Minimum distinct sources for \"verified\" visual treatment. Default: 2 */\n minSources?: number;\n /** Show source count badge on nodes. Default: true */\n showBadge?: boolean;\n /** Visual style for multi-source trust: 'border' | 'opacity' | 'glow'. Default: 'border' */\n style?: 'opacity' | 'border' | 'glow';\n}\n\n/** Knowledge Asset boundary visualization configuration */\nexport interface KnowledgeAssetConfig {\n /** Enable KA boundary rendering. Default: false */\n enabled: boolean;\n /** Predicate linking a node to its Knowledge Asset. Default: 'dkg:partOfAsset' */\n membershipPredicate?: string;\n /** Show convex hull boundaries around KA groups. Default: true */\n showBoundaries?: boolean;\n /** Boundary fill opacity (0-1). Default: 0.06 */\n boundaryOpacity?: number;\n}\n\n/**\n * Declarative view configuration for the graph visualizer.\n *\n * Controls what nodes look like, which are important, and how they're sized —\n * all without writing code. The same graph data can be visualized differently\n * by swapping view configs.\n */\nexport interface ViewConfig {\n /** Human-readable name for this view */\n name: string;\n\n /** Visual rules per RDF type (compact or full URI) */\n nodeTypes?: Record<string, NodeTypeConfig>;\n\n /** Optional focal entity (rendered large, always on top) */\n focal?: FocalConfig;\n\n /** Highlight rule: which property drives risk/importance coloring */\n highlight?: HighlightConfig;\n\n /** Size-by rule: scale nodes by a numeric property */\n sizeBy?: SizeByConfig;\n\n /** RDF type short names rendered as circles instead of hexagons */\n circleTypes?: string[];\n\n /** Platform icon mapping for SocialMediaPosting nodes */\n platformIcons?: PlatformIconConfig;\n\n /** Tooltip configuration for hover cards */\n tooltip?: TooltipConfig;\n\n /** Animation configuration for visual liveliness */\n animation?: AnimationConfig;\n\n /** Color palette: preset name ('dark', 'midnight', 'cyberpunk', 'light') or custom palette object */\n palette?: string | ColorPalette;\n /** Overrides for individual palette colors (merged on top of base) */\n paletteOverrides?: Partial<ColorPalette>;\n\n /** Trust visualization based on multi-source provenance */\n trust?: TrustConfig;\n\n /** Knowledge Asset boundary visualization */\n knowledgeAssets?: KnowledgeAssetConfig;\n\n /** Temporal timeline configuration */\n temporal?: TemporalConfig;\n\n /** Default SPARQL CONSTRUCT query for this view */\n defaultSparql?: string;\n}\n\n// --- ViewConfig Applicator ---\n\n/**\n * Apply a ViewConfig to a loaded graph model.\n *\n * Sets riskScore, isHighRisk, sizeMultiplier, and imageUrl on nodes\n * based on the declarative rules in the config.\n *\n * Call this AFTER loading triples into the model, BEFORE refresh().\n */\nexport function applyViewConfig(config: ViewConfig, model: GraphModel): void {\n // 1. Focal entity\n if (config.focal) {\n const focalNode = findNodeAcrossNamespaces(model, config.focal.uri);\n if (focalNode) {\n focalNode.sizeMultiplier = config.focal.sizeMultiplier ?? 1.5;\n if (config.focal.image) {\n focalNode.imageUrl = config.focal.image;\n }\n }\n }\n\n // 2. Platform icons\n if (config.platformIcons) {\n for (const node of model.nodes.values()) {\n if (node.imageUrl) continue;\n if (!node.types.some(t => t.includes('SocialMediaPosting'))) continue;\n\n // Check platform edges\n const platformEdges = model.getEdgesFrom(node.id).filter(e => e.predicate.includes('platform'));\n for (const edge of platformEdges) {\n const platformId = edge.target.split('/').pop() ?? '';\n if (config.platformIcons.icons[platformId]) {\n node.imageUrl = config.platformIcons.icons[platformId];\n break;\n }\n }\n\n // URL fallback\n if (!node.imageUrl && config.platformIcons.urlFallbacks) {\n for (const [substr, platformKey] of Object.entries(config.platformIcons.urlFallbacks)) {\n if (node.id.includes(substr) && config.platformIcons.icons[platformKey]) {\n node.imageUrl = config.platformIcons.icons[platformKey];\n break;\n }\n }\n }\n }\n }\n\n // 3. Highlight / risk scoring — continuous size scaling\n if (config.highlight) {\n const hl = config.highlight;\n const scores: Array<{ id: string; score: number }> = [];\n const invert = hl.invert === true;\n\n for (const [id, node] of model.nodes) {\n let value: number | null = null;\n\n if (hl.source === 'linked' && hl.linkedVia) {\n const edges = model.getEdgesFrom(id).filter(e => e.predicate.includes(hl.linkedVia!));\n if (edges.length > 0) {\n const linked = model.nodes.get(edges[0].target);\n if (linked) {\n const raw = getAnyNS(linked, hl.property);\n if (raw !== null) value = parseFloat(raw);\n }\n }\n } else {\n const raw = getAnyNS(node, hl.property);\n if (raw !== null) value = parseFloat(raw);\n }\n\n if (value !== null && !isNaN(value)) {\n const threshold = hl.threshold;\n let score: number;\n let include = false;\n if (value < 0) {\n // Negative values: magnitude = risk (e.g. negative sentiment)\n const riskMagnitude = Math.abs(value);\n node.riskScore = riskMagnitude;\n score = riskMagnitude;\n include = invert ? (riskMagnitude <= threshold && riskMagnitude > 0) : (riskMagnitude >= threshold);\n } else {\n // Positive values: normal = high is risky, invert = low is risky\n node.riskScore = value;\n if (invert) {\n include = value <= threshold;\n score = include ? threshold - value : 0; // lower value → higher score (riskier)\n } else {\n include = value >= threshold;\n score = value;\n }\n }\n if (include) {\n scores.push({ id, score });\n }\n }\n }\n\n // Mark top N as high-risk with CONTINUOUS size scaling\n const topN = hl.topN ?? 100;\n // Both modes: sort descending so highest risk score is first and gets largest size.\n // (Invert uses score = threshold - value, so low value → high score.)\n scores.sort((a, b) => b.score - a.score);\n const topScores = scores.slice(0, topN);\n\n if (topScores.length > 0) {\n const minSize = hl.sizeMin ?? 1.0;\n const maxSize = hl.sizeMax ?? (hl.sizeMultiplier ?? 3.0);\n const rawFirst = topScores[0].score;\n const rawLast = topScores[topScores.length - 1].score;\n\n for (const { id, score } of topScores) {\n const node = model.nodes.get(id);\n if (!node) continue;\n\n node.isHighRisk = true;\n\n // Don't override focal entity's sizeMultiplier\n if (node.sizeMultiplier) continue;\n\n // Log-scale normalization: highest risk (first in topScores) gets normalized 1 → maxSize\n let normalized: number;\n const scoreRange = Math.abs(rawFirst - rawLast);\n if (scoreRange > 0) {\n const logMin = Math.log1p(Math.min(rawFirst, rawLast));\n const logMax = Math.log1p(Math.max(rawFirst, rawLast));\n const logRange = logMax - logMin;\n const logScore = Math.log1p(score);\n const logNorm = logRange > 0 ? (logScore - logMin) / logRange : 0.5;\n normalized = logNorm;\n } else {\n normalized = 1.0;\n }\n\n node.sizeMultiplier = minSize + normalized * (maxSize - minSize);\n }\n }\n }\n\n // 4. Size-by rule: scale nodes by a numeric property\n if (config.sizeBy) {\n const sb = config.sizeBy;\n const values: Array<{ id: string; value: number }> = [];\n\n for (const [id, node] of model.nodes) {\n // Skip nodes that already have explicit sizeMultiplier (focal, highlight)\n if (node.sizeMultiplier) continue;\n\n const raw = getAnyNS(node, sb.property);\n if (raw !== null) {\n const v = parseFloat(raw);\n if (!isNaN(v) && v > 0) {\n values.push({ id, value: v });\n }\n }\n }\n\n if (values.length > 0) {\n const maxVal = Math.max(...values.map(v => v.value));\n const minVal = Math.min(...values.map(v => v.value));\n\n for (const { id, value } of values) {\n const node = model.nodes.get(id);\n if (!node || node.sizeMultiplier) continue;\n\n let normalized: number;\n if (sb.scale === 'log') {\n // Log scale: handles power-law distributions (social media metrics)\n const logMin = Math.log1p(minVal);\n const logMax = Math.log1p(maxVal);\n const logRange = logMax - logMin;\n normalized = logRange > 0 ? (Math.log1p(value) - logMin) / logRange : 0.5;\n } else {\n // Linear scale\n const range = maxVal - minVal;\n normalized = range > 0 ? (value - minVal) / range : 0.5;\n }\n\n // Map to sizeMultiplier range: 0.5x (min) to 3x (max)\n node.sizeMultiplier = 0.5 + normalized * 2.5;\n }\n }\n }\n\n // 5. Node type icons (for types that specify an icon but aren't platform-based)\n if (config.nodeTypes) {\n for (const node of model.nodes.values()) {\n if (node.imageUrl) continue;\n for (const type of node.types) {\n const shortType = type.split('/').pop()?.split('#').pop() ?? '';\n // Check both full URI and short forms\n const typeConfig = config.nodeTypes[type]\n || config.nodeTypes[`schema:${shortType}`]\n || config.nodeTypes[`vocab:${shortType}`]\n || config.nodeTypes[shortType];\n if (typeConfig?.icon) {\n node.imageUrl = typeConfig.icon;\n break;\n }\n }\n }\n }\n}\n\n/** Find a node trying multiple namespace variants of a URI */\nfunction findNodeAcrossNamespaces(model: GraphModel, uri: string): GraphNode | null {\n // Try exact match first\n const exact = model.nodes.get(uri);\n if (exact) return exact;\n\n // Extract the path part after the last known namespace prefix\n const pathPart = uri.replace(/^https?:\\/\\/[^\\/]+\\//, '');\n\n // Try common namespace bases\n const bases = [\n 'https://guardiankg.org/',\n 'https://umanitek.ai/dkg/',\n 'http://umanitek.ai/dkg/',\n ];\n\n for (const base of bases) {\n const candidate = model.nodes.get(base + pathPart);\n if (candidate) return candidate;\n }\n\n return null;\n}\n","/**\n * Color palette system for the graph visualizer.\n *\n * A palette provides a complete, cohesive set of colors for the entire\n * visualization — surfaces, text, accents, semantic colors, and graph-specific\n * colors. Developers can use a built-in preset or supply a custom palette.\n */\n\nexport interface ColorPalette {\n name: string;\n\n // Surface\n background: string;\n surface: string;\n surfaceBorder: string;\n\n // Text\n textPrimary: string;\n textSecondary: string;\n textMuted: string;\n\n // Accent / brand\n primary: string;\n primaryMuted: string;\n\n // Semantic\n danger: string;\n warning: string;\n safe: string;\n info: string;\n\n // Graph-specific\n edgeColor: string;\n edgeLabel: string;\n particleColor: string;\n focalGlow: string;\n\n // Auto-assigned node type colors (cycled across uncolored types)\n nodeColors: string[];\n}\n\n// ── Built-in presets ──\n\nexport const PALETTE_DARK: ColorPalette = {\n name: 'dark',\n background: '#0a0a0f',\n surface: 'rgba(20, 20, 30, 0.95)',\n surfaceBorder: 'rgba(100, 100, 140, 0.3)',\n textPrimary: '#e2e8f0',\n textSecondary: '#94a3b8',\n textMuted: '#64748b',\n primary: '#6366f1',\n primaryMuted: 'rgba(120, 120, 220, 0.5)',\n danger: '#e04040',\n warning: '#f59e0b',\n safe: '#10b981',\n info: '#3b82f6',\n edgeColor: '#1a1a3a',\n edgeLabel: '#94a3b8',\n particleColor: 'rgba(100, 160, 255, 0.5)',\n focalGlow: 'rgba(99, 102, 241, 0.3)',\n nodeColors: [\n '#6366f1', '#8b5cf6', '#a855f7', '#3b82f6',\n '#06b6d4', '#10b981', '#f59e0b', '#ef4444',\n '#ec4899', '#14b8a6', '#f97316', '#8b5cf6',\n ],\n};\n\nexport const PALETTE_MIDNIGHT: ColorPalette = {\n name: 'midnight',\n background: '#050510',\n surface: 'rgba(10, 10, 25, 0.95)',\n surfaceBorder: 'rgba(60, 80, 160, 0.3)',\n textPrimary: '#c8d6e5',\n textSecondary: '#7f8fa6',\n textMuted: '#4a5568',\n primary: '#3b82f6',\n primaryMuted: 'rgba(59, 130, 246, 0.4)',\n danger: '#dc2626',\n warning: '#d97706',\n safe: '#059669',\n info: '#2563eb',\n edgeColor: '#0f1a3a',\n edgeLabel: '#7f8fa6',\n particleColor: 'rgba(59, 130, 246, 0.5)',\n focalGlow: 'rgba(59, 130, 246, 0.3)',\n nodeColors: [\n '#3b82f6', '#2563eb', '#1d4ed8', '#06b6d4',\n '#0891b2', '#0e7490', '#8b5cf6', '#7c3aed',\n '#6366f1', '#4f46e5', '#14b8a6', '#0d9488',\n ],\n};\n\nexport const PALETTE_CYBERPUNK: ColorPalette = {\n name: 'cyberpunk',\n background: '#0a0a0a',\n surface: 'rgba(15, 15, 15, 0.95)',\n surfaceBorder: 'rgba(0, 255, 136, 0.2)',\n textPrimary: '#e0ffe0',\n textSecondary: '#80c080',\n textMuted: '#407040',\n primary: '#00ff88',\n primaryMuted: 'rgba(0, 255, 136, 0.3)',\n danger: '#ff2d6a',\n warning: '#ffaa00',\n safe: '#00ff88',\n info: '#00d4ff',\n edgeColor: '#0a1a0a',\n edgeLabel: '#80c080',\n particleColor: 'rgba(0, 255, 136, 0.5)',\n focalGlow: 'rgba(0, 255, 136, 0.3)',\n nodeColors: [\n '#00ff88', '#ff2d6a', '#00d4ff', '#ffaa00',\n '#ff6b35', '#a855f7', '#06ffd0', '#ff1493',\n '#39ff14', '#ff6ec7', '#00bfff', '#ffd700',\n ],\n};\n\nexport const PALETTE_LIGHT: ColorPalette = {\n name: 'light',\n background: '#f8fafc',\n surface: 'rgba(255, 255, 255, 0.95)',\n surfaceBorder: 'rgba(148, 163, 184, 0.3)',\n textPrimary: '#1e293b',\n textSecondary: '#475569',\n textMuted: '#94a3b8',\n primary: '#4f46e5',\n primaryMuted: 'rgba(79, 70, 229, 0.3)',\n danger: '#dc2626',\n warning: '#d97706',\n safe: '#059669',\n info: '#2563eb',\n edgeColor: '#cbd5e1',\n edgeLabel: '#475569',\n particleColor: 'rgba(79, 70, 229, 0.4)',\n focalGlow: 'rgba(79, 70, 229, 0.2)',\n nodeColors: [\n '#4f46e5', '#7c3aed', '#9333ea', '#2563eb',\n '#0891b2', '#059669', '#d97706', '#dc2626',\n '#db2777', '#0d9488', '#ea580c', '#7c3aed',\n ],\n};\n\n/** All built-in palettes indexed by name */\nexport const PALETTES: Record<string, ColorPalette> = {\n dark: PALETTE_DARK,\n midnight: PALETTE_MIDNIGHT,\n cyberpunk: PALETTE_CYBERPUNK,\n light: PALETTE_LIGHT,\n};\n\n/**\n * Resolve a palette from a name string or custom object, with optional overrides.\n * Falls back to 'dark' if the name is not found.\n */\nexport function resolvePalette(\n palette?: string | ColorPalette,\n overrides?: Partial<ColorPalette>,\n): ColorPalette {\n let base: ColorPalette;\n\n if (!palette) {\n base = PALETTE_DARK;\n } else if (typeof palette === 'string') {\n base = PALETTES[palette] ?? PALETTE_DARK;\n } else {\n base = palette;\n }\n\n if (!overrides) return base;\n\n return { ...base, ...overrides, name: overrides.name ?? base.name };\n}\n\n/**\n * Inject palette colors as CSS custom properties on a container element.\n * Demo UIs can reference these as var(--gv-primary), etc.\n */\nexport function injectPaletteCssVars(container: HTMLElement, palette: ColorPalette): void {\n const props: Record<string, string> = {\n '--gv-bg': palette.background,\n '--gv-surface': palette.surface,\n '--gv-surface-border': palette.surfaceBorder,\n '--gv-text': palette.textPrimary,\n '--gv-text-secondary': palette.textSecondary,\n '--gv-text-muted': palette.textMuted,\n '--gv-primary': palette.primary,\n '--gv-primary-muted': palette.primaryMuted,\n '--gv-danger': palette.danger,\n '--gv-warning': palette.warning,\n '--gv-safe': palette.safe,\n '--gv-info': palette.info,\n '--gv-edge': palette.edgeColor,\n '--gv-particle': palette.particleColor,\n '--gv-focal-glow': palette.focalGlow,\n };\n\n for (const [prop, value] of Object.entries(props)) {\n container.style.setProperty(prop, value);\n }\n}\n","import type { GraphNode, ProvenanceInfo } from './types.js';\nimport type { GraphModel } from './graph-model.js';\n\n/**\n * Known provenance-related predicate URIs.\n * These are checked across multiple namespace variants.\n */\nconst PROV_PREDICATES = {\n wasDerivedFrom: [\n 'http://www.w3.org/ns/prov#wasDerivedFrom',\n ],\n wasGeneratedBy: [\n 'http://www.w3.org/ns/prov#wasGeneratedBy',\n ],\n generatedAtTime: [\n 'http://www.w3.org/ns/prov#generatedAtTime',\n ],\n contentHash: [\n 'https://umanitek.ai/dkg/vocab/contentHash',\n 'https://guardiankg.org/vocab/contentHash',\n 'http://umanitek.ai/dkg/vocab/contentHash',\n ],\n dataSource: [\n 'https://umanitek.ai/dkg/vocab/dataSource',\n 'https://guardiankg.org/vocab/dataSource',\n 'http://umanitek.ai/dkg/vocab/dataSource',\n ],\n};\n\nconst NAME_PREDICATES = [\n 'https://schema.org/name',\n 'http://schema.org/name',\n 'http://xmlns.com/foaf/0.1/name',\n 'http://www.w3.org/2000/01/rdf-schema#label',\n];\n\nconst DATA_SOURCE_TYPES = [\n 'https://umanitek.ai/dkg/vocab/DataSource',\n 'http://umanitek.ai/dkg/vocab/DataSource',\n 'https://guardiankg.org/vocab/DataSource',\n 'http://www.w3.org/ns/prov#Agent',\n];\n\n/**\n * Resolves provenance information for graph nodes by traversing\n * metadata properties and following provenance edges.\n *\n * Populates `node.provenance` with:\n * - Data sources (URIs + human-readable names)\n * - Generation timestamp\n * - Generating agent/model\n * - Content hash\n * - UAL and blockchain anchor (when available)\n */\nexport class ProvenanceResolver {\n /**\n * Resolve provenance for all nodes in the model.\n * Should be called after data is loaded and before rendering.\n */\n resolve(model: GraphModel): void {\n for (const node of model.nodes.values()) {\n node.provenance = this._resolveNodeProvenance(node, model);\n }\n }\n\n private _resolveNodeProvenance(node: GraphNode, model: GraphModel): ProvenanceInfo {\n const info: ProvenanceInfo = {\n sources: [],\n sourceNames: [],\n };\n\n // 1. Content hash (from properties or metadata)\n info.contentHash = this._findValue(node, PROV_PREDICATES.contentHash);\n\n // 2. Generation timestamp\n info.generatedAt = this._findMetadataValue(node, PROV_PREDICATES.generatedAtTime);\n\n // 3. Generated by (agent/model)\n const generatedByUri = this._findMetadataValue(node, PROV_PREDICATES.wasGeneratedBy);\n if (generatedByUri) {\n info.generatedBy = generatedByUri;\n // Try to resolve the agent's name\n const agentNode = model.nodes.get(generatedByUri);\n if (agentNode) {\n info.generatedByName = this._resolveName(agentNode) ?? undefined;\n } else {\n // Use the last path segment as a fallback name\n info.generatedByName = generatedByUri.split('/').pop() ?? generatedByUri;\n }\n }\n\n // 4. Data sources — follow wasDerivedFrom edges\n const sourceUris = new Set<string>();\n const sourceNames: string[] = [];\n\n // Direct edges from this node\n const edges = model.getEdgesFrom(node.id);\n for (const edge of edges) {\n if (PROV_PREDICATES.wasDerivedFrom.some(p => edge.predicate === p)) {\n const targetNode = model.nodes.get(edge.target);\n if (targetNode) {\n // Check if this is a DataSource entity\n const isDataSource = targetNode.types.some(t => DATA_SOURCE_TYPES.includes(t));\n if (isDataSource) {\n if (!sourceUris.has(edge.target)) {\n sourceUris.add(edge.target);\n const name = this._resolveName(targetNode) ?? edge.target.split('/').pop() ?? edge.target;\n sourceNames.push(name);\n }\n }\n }\n }\n }\n\n // Also check metadata for wasDerivedFrom (may be stored as metadata)\n for (const pred of PROV_PREDICATES.wasDerivedFrom) {\n const vals = node.metadata?.get(pred);\n if (vals) {\n for (const v of vals) {\n const uri = v.value;\n if (sourceUris.has(uri)) continue;\n const targetNode = model.nodes.get(uri);\n if (targetNode) {\n const isDataSource = targetNode.types.some(t => DATA_SOURCE_TYPES.includes(t));\n if (isDataSource) {\n sourceUris.add(uri);\n const name = this._resolveName(targetNode) ?? uri.split('/').pop() ?? uri;\n sourceNames.push(name);\n }\n }\n }\n }\n }\n\n // Also check for direct dataSource predicates\n for (const pred of PROV_PREDICATES.dataSource) {\n const vals = node.properties?.get(pred) ?? node.metadata?.get(pred);\n if (vals) {\n for (const v of vals) {\n const uri = v.value;\n if (sourceUris.has(uri)) continue;\n sourceUris.add(uri);\n const targetNode = model.nodes.get(uri);\n const name = targetNode ? (this._resolveName(targetNode) ?? uri.split('/').pop() ?? uri) : (uri.split('/').pop() ?? uri);\n sourceNames.push(name);\n }\n }\n }\n\n info.sources = [...sourceUris];\n info.sourceNames = sourceNames;\n\n return info;\n }\n\n /** Find a value from a node's properties, checking multiple predicate URIs */\n private _findValue(node: GraphNode, predicates: string[]): string | undefined {\n for (const pred of predicates) {\n const vals = node.properties?.get(pred);\n if (vals && vals.length > 0) return vals[0].value;\n }\n return undefined;\n }\n\n /** Find a value from a node's metadata, checking multiple predicate URIs */\n private _findMetadataValue(node: GraphNode, predicates: string[]): string | undefined {\n for (const pred of predicates) {\n const vals = node.metadata?.get(pred);\n if (vals && vals.length > 0) return vals[0].value;\n }\n // Also check properties as fallback\n return this._findValue(node, predicates);\n }\n\n /** Resolve a human-readable name for a node */\n private _resolveName(node: GraphNode): string | null {\n for (const pred of NAME_PREDICATES) {\n const vals = node.properties?.get(pred);\n if (vals && vals.length > 0) return vals[0].value;\n }\n return null;\n }\n}\n","import type { GraphNode } from './types.js';\nimport type { GraphModel } from './graph-model.js';\n\n/**\n * Configuration for temporal filtering.\n */\nexport interface TemporalConfig {\n /** Enable the temporal timeline. Default: false */\n enabled: boolean;\n /** Property short names to scan for dates (checked across known namespaces).\n * Default: ['dateCreated'] */\n dateProperties?: string[];\n /** Whether to show nodes that have no date property. Default: true */\n showUndated?: boolean;\n /** Auto-play speed in ms per step. Default: 200 */\n playSpeed?: number;\n /** Step granularity for playback. Default: 'day' */\n stepSize?: 'hour' | 'day' | 'week' | 'month';\n}\n\nconst KNOWN_DATE_NAMESPACES = [\n 'https://schema.org/',\n 'http://schema.org/',\n 'https://guardiankg.org/vocab/',\n 'https://umanitek.ai/dkg/vocab/',\n 'http://umanitek.ai/dkg/vocab/',\n 'http://www.w3.org/ns/prov#',\n];\n\n/**\n * Temporal filter: scans nodes for date properties, manages a progressive\n * time cursor, and returns the set of node IDs visible up to that cursor.\n *\n * Usage:\n * filter.scan(model);\n * filter.setCursor(someDate);\n * const visible = filter.getVisibleNodeIds(); // nodes with date <= cursor\n */\nexport class TemporalFilter {\n private _nodeDates = new Map<string, Date>();\n private _dateRange: [Date, Date] | null = null;\n private _cursor: Date | null = null;\n private _dateProperties: string[];\n private _showUndated: boolean;\n\n constructor(config?: Partial<TemporalConfig>) {\n this._dateProperties = config?.dateProperties ?? ['dateCreated'];\n this._showUndated = config?.showUndated ?? true;\n }\n\n /** Scan all nodes in the model for date properties. Call after data ingestion. */\n scan(model: GraphModel): void {\n this._nodeDates.clear();\n\n let earliest: Date | null = null;\n let latest: Date | null = null;\n\n for (const [id, node] of model.nodes) {\n const date = this._extractDate(node);\n if (date) {\n this._nodeDates.set(id, date);\n if (!earliest || date < earliest) earliest = date;\n if (!latest || date > latest) latest = date;\n }\n }\n\n if (earliest && latest) {\n this._dateRange = [earliest, latest];\n // Default cursor: show everything\n this._cursor = latest;\n } else {\n this._dateRange = null;\n this._cursor = null;\n }\n }\n\n /** Get the global date range [earliest, latest], or null if no dates found. */\n get dateRange(): [Date, Date] | null {\n return this._dateRange;\n }\n\n /** Get the current cursor position. */\n get cursor(): Date | null {\n return this._cursor;\n }\n\n /** Number of nodes that have a date assigned. */\n get datedNodeCount(): number {\n return this._nodeDates.size;\n }\n\n /** Whether to show nodes without date properties. */\n get showUndated(): boolean {\n return this._showUndated;\n }\n\n set showUndated(value: boolean) {\n this._showUndated = value;\n }\n\n /** Set the cursor (cutoff date). Nodes with date <= cursor are visible. */\n setCursor(date: Date): void {\n this._cursor = date;\n }\n\n /** Get the date for a specific node, or null if undated. */\n getNodeDate(nodeId: string): Date | null {\n return this._nodeDates.get(nodeId) ?? null;\n }\n\n /**\n * Get the set of node IDs visible at the current cursor position.\n * - Dated nodes: visible if date <= cursor\n * - Undated nodes: visible if showUndated is true\n *\n * If no date range was found (no dated nodes), returns null\n * (meaning: don't filter, show everything).\n */\n getVisibleNodeIds(allNodeIds: Iterable<string>): Set<string> | null {\n if (!this._dateRange || !this._cursor) return null;\n\n const visible = new Set<string>();\n const cursor = this._cursor.getTime();\n\n for (const id of allNodeIds) {\n const date = this._nodeDates.get(id);\n if (date) {\n if (date.getTime() <= cursor) {\n visible.add(id);\n }\n } else if (this._showUndated) {\n visible.add(id);\n }\n }\n\n return visible;\n }\n\n /**\n * Compute histogram buckets for the date distribution.\n * Returns an array of { date: Date, count: number } entries.\n */\n computeHistogram(bucketCount: number = 50): Array<{ date: Date; count: number }> {\n if (!this._dateRange) return [];\n\n const [earliest, latest] = this._dateRange;\n const range = latest.getTime() - earliest.getTime();\n if (range <= 0) return [{ date: earliest, count: this._nodeDates.size }];\n\n const bucketSize = range / bucketCount;\n const buckets: Array<{ date: Date; count: number }> = [];\n\n for (let i = 0; i < bucketCount; i++) {\n buckets.push({\n date: new Date(earliest.getTime() + i * bucketSize),\n count: 0,\n });\n }\n\n for (const date of this._nodeDates.values()) {\n const idx = Math.min(\n Math.floor((date.getTime() - earliest.getTime()) / bucketSize),\n bucketCount - 1\n );\n buckets[idx].count++;\n }\n\n return buckets;\n }\n\n /** Extract the earliest date from a node's properties across known namespaces. */\n private _extractDate(node: GraphNode): Date | null {\n let earliest: Date | null = null;\n\n for (const shortName of this._dateProperties) {\n for (const ns of KNOWN_DATE_NAMESPACES) {\n const vals = node.properties?.get(ns + shortName);\n if (vals && vals.length > 0) {\n const d = new Date(vals[0].value);\n if (!isNaN(d.getTime())) {\n if (!earliest || d < earliest) earliest = d;\n }\n }\n }\n }\n\n return earliest;\n }\n}\n","import type { TemporalFilter, TemporalConfig } from '../core/temporal-filter.js';\n\n/**\n * DOM-based timeline overlay rendered inside the visualization container.\n *\n * Shows a single progressive slider: drag right to reveal more of the graph\n * chronologically. Includes play/pause, date labels, visible node count,\n * and a mini histogram showing node density over time.\n */\nexport class TimelineOverlay {\n private _container: HTMLElement;\n private _filter: TemporalFilter;\n private _config: TemporalConfig;\n\n // DOM elements\n private _root: HTMLElement | null = null;\n private _slider: HTMLInputElement | null = null;\n private _dateLabel: HTMLElement | null = null;\n private _countLabel: HTMLElement | null = null;\n private _playBtn: HTMLElement | null = null;\n private _histCanvas: HTMLCanvasElement | null = null;\n private _startLabel: HTMLElement | null = null;\n private _endLabel: HTMLElement | null = null;\n\n // State\n private _playInterval: ReturnType<typeof setInterval> | null = null;\n private _isPlaying = false;\n private _onChange: ((cursor: Date, visibleCount: number) => void) | null = null;\n\n constructor(\n container: HTMLElement,\n filter: TemporalFilter,\n config: TemporalConfig,\n ) {\n this._container = container;\n this._filter = filter;\n this._config = config;\n }\n\n /** Set callback for cursor changes */\n onChange(fn: (cursor: Date, visibleCount: number) => void): void {\n this._onChange = fn;\n }\n\n /** Build and attach the overlay DOM. Call after filter.scan(). */\n mount(): void {\n if (this._root) return; // already mounted\n\n const range = this._filter.dateRange;\n if (!range) return; // no dates found\n\n this._root = document.createElement('div');\n this._root.className = 'gv-timeline-overlay';\n this._root.innerHTML = this._buildHTML();\n this._injectStyles();\n this._container.appendChild(this._root);\n\n // Cache element refs\n this._slider = this._root.querySelector('.gv-tl-slider') as HTMLInputElement;\n this._dateLabel = this._root.querySelector('.gv-tl-date') as HTMLElement;\n this._countLabel = this._root.querySelector('.gv-tl-count') as HTMLElement;\n this._playBtn = this._root.querySelector('.gv-tl-play') as HTMLElement;\n this._histCanvas = this._root.querySelector('.gv-tl-hist') as HTMLCanvasElement;\n this._startLabel = this._root.querySelector('.gv-tl-start') as HTMLElement;\n this._endLabel = this._root.querySelector('.gv-tl-end') as HTMLElement;\n\n // Draw histogram\n this._drawHistogram();\n\n // Set initial slider position to max (show everything)\n this._slider.value = this._slider.max;\n this._updateDateLabel();\n\n // Event listeners\n this._slider.addEventListener('input', () => this._onSliderInput());\n this._playBtn?.addEventListener('click', () => this._togglePlay());\n }\n\n /** Remove the overlay from the DOM */\n unmount(): void {\n this.pause();\n if (this._root) {\n this._root.remove();\n this._root = null;\n }\n }\n\n /** Whether the overlay is currently mounted */\n get mounted(): boolean {\n return this._root !== null;\n }\n\n /** Update the overlay after data changes (re-scan, redraw histogram) */\n refresh(): void {\n if (!this._root) return;\n const range = this._filter.dateRange;\n if (!range) {\n this.unmount();\n return;\n }\n\n // Update slider range\n if (this._slider) {\n this._slider.min = String(range[0].getTime());\n this._slider.max = String(range[1].getTime());\n }\n\n // Redraw histogram\n this._drawHistogram();\n\n // Update labels\n if (this._startLabel) this._startLabel.textContent = this._formatDateShort(range[0]);\n if (this._endLabel) this._endLabel.textContent = this._formatDateShort(range[1]);\n\n this._updateDateLabel();\n }\n\n /** Start auto-play from current position */\n play(): void {\n if (this._isPlaying) return;\n this._isPlaying = true;\n if (this._playBtn) this._playBtn.textContent = '⏸';\n\n const range = this._filter.dateRange;\n if (!range || !this._slider) return;\n\n const totalRange = range[1].getTime() - range[0].getTime();\n const stepMs = this._getStepMs();\n const speed = this._config.playSpeed ?? 200;\n\n this._playInterval = setInterval(() => {\n const current = parseInt(this._slider!.value, 10);\n const next = current + stepMs;\n if (next >= range[1].getTime()) {\n this._slider!.value = String(range[1].getTime());\n this._onSliderInput();\n this.pause();\n return;\n }\n this._slider!.value = String(next);\n this._onSliderInput();\n }, speed);\n }\n\n /** Pause auto-play */\n pause(): void {\n this._isPlaying = false;\n if (this._playBtn) this._playBtn.textContent = '▶';\n if (this._playInterval) {\n clearInterval(this._playInterval);\n this._playInterval = null;\n }\n }\n\n /** Set the slider to a specific date programmatically */\n setCursor(date: Date): void {\n if (!this._slider) return;\n this._slider.value = String(date.getTime());\n this._onSliderInput();\n }\n\n // --- Private ---\n\n private _buildHTML(): string {\n const range = this._filter.dateRange!;\n const minMs = range[0].getTime();\n const maxMs = range[1].getTime();\n\n return `\n <div class=\"gv-tl-bar\">\n <button class=\"gv-tl-play\" title=\"Play/Pause timeline\">▶</button>\n <div class=\"gv-tl-track\">\n <div class=\"gv-tl-labels\">\n <span class=\"gv-tl-start\">${this._formatDateShort(range[0])}</span>\n <span class=\"gv-tl-date\"></span>\n <span class=\"gv-tl-end\">${this._formatDateShort(range[1])}</span>\n </div>\n <div class=\"gv-tl-slider-wrap\">\n <canvas class=\"gv-tl-hist\" width=\"600\" height=\"32\"></canvas>\n <input type=\"range\" class=\"gv-tl-slider\" min=\"${minMs}\" max=\"${maxMs}\" value=\"${maxMs}\" step=\"1\" />\n </div>\n </div>\n <div class=\"gv-tl-count\" title=\"Visible nodes\">—</div>\n </div>\n `;\n }\n\n private _onSliderInput(): void {\n if (!this._slider) return;\n const ms = parseInt(this._slider.value, 10);\n const date = new Date(ms);\n this._filter.setCursor(date);\n this._updateDateLabel();\n this._onChange?.(date, 0); // count will be set by the caller after re-render\n }\n\n private _updateDateLabel(): void {\n if (!this._dateLabel || !this._slider) return;\n const ms = parseInt(this._slider.value, 10);\n const date = new Date(ms);\n this._dateLabel.textContent = this._formatDateFull(date);\n }\n\n /** Update the visible count badge (called externally after render) */\n updateCount(count: number): void {\n if (this._countLabel) {\n this._countLabel.textContent = count.toLocaleString();\n }\n }\n\n private _togglePlay(): void {\n if (this._isPlaying) {\n this.pause();\n } else {\n // If at the end, reset to beginning\n if (this._slider && this._filter.dateRange) {\n const current = parseInt(this._slider.value, 10);\n if (current >= this._filter.dateRange[1].getTime() - this._getStepMs()) {\n this._slider.value = String(this._filter.dateRange[0].getTime());\n this._onSliderInput();\n }\n }\n this.play();\n }\n }\n\n private _getStepMs(): number {\n const step = this._config.stepSize ?? 'day';\n const range = this._filter.dateRange;\n if (!range) return 86400000;\n\n // Scale steps so that 100 steps covers the full range\n const totalRange = range[1].getTime() - range[0].getTime();\n const autoStep = totalRange / 200;\n\n switch (step) {\n case 'hour': return Math.max(3600000, autoStep);\n case 'day': return Math.max(86400000, autoStep);\n case 'week': return Math.max(604800000, autoStep);\n case 'month': return Math.max(2592000000, autoStep);\n default: return autoStep;\n }\n }\n\n private _drawHistogram(): void {\n const canvas = this._histCanvas;\n if (!canvas) return;\n const ctx = canvas.getContext('2d');\n if (!ctx) return;\n\n const buckets = this._filter.computeHistogram(canvas.width / 4);\n if (buckets.length === 0) return;\n\n const maxCount = Math.max(...buckets.map(b => b.count));\n if (maxCount === 0) return;\n\n const w = canvas.width;\n const h = canvas.height;\n const barW = w / buckets.length;\n\n ctx.clearRect(0, 0, w, h);\n\n for (let i = 0; i < buckets.length; i++) {\n const ratio = buckets[i].count / maxCount;\n const barH = ratio * (h - 2);\n ctx.fillStyle = 'rgba(99, 102, 241, 0.25)';\n ctx.fillRect(i * barW, h - barH, barW - 1, barH);\n }\n }\n\n private _formatDateShort(date: Date): string {\n return date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });\n }\n\n private _formatDateFull(date: Date): string {\n return date.toLocaleDateString('en-US', {\n month: 'short', day: 'numeric', year: 'numeric',\n });\n }\n\n private _styleId = 'gv-timeline-styles';\n\n private _injectStyles(): void {\n if (document.getElementById(this._styleId)) return;\n\n const style = document.createElement('style');\n style.id = this._styleId;\n style.textContent = `\n .gv-timeline-overlay {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n z-index: 90;\n pointer-events: none;\n }\n .gv-tl-bar {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 10px 16px 12px;\n background: linear-gradient(to top,\n var(--gv-bg, rgba(10, 10, 15, 0.95)) 0%,\n var(--gv-bg, rgba(10, 10, 15, 0.85)) 60%,\n transparent 100%);\n pointer-events: auto;\n }\n .gv-tl-play {\n width: 32px;\n height: 32px;\n border-radius: 50%;\n border: 1px solid var(--gv-surface-border, rgba(100, 100, 140, 0.3));\n background: var(--gv-surface, rgba(20, 20, 30, 0.9));\n color: var(--gv-text, #e0e0e0);\n cursor: pointer;\n font-size: 14px;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n transition: background 0.2s, border-color 0.2s;\n padding: 0;\n line-height: 1;\n }\n .gv-tl-play:hover {\n background: var(--gv-primary-muted, rgba(99, 102, 241, 0.3));\n border-color: var(--gv-primary, rgba(99, 102, 241, 0.5));\n }\n .gv-tl-track {\n flex: 1;\n min-width: 0;\n position: relative;\n }\n .gv-tl-labels {\n display: flex;\n justify-content: space-between;\n font-size: 10px;\n color: var(--gv-text-muted, #666);\n margin-bottom: 2px;\n padding: 0 2px;\n }\n .gv-tl-date {\n font-weight: 600;\n color: var(--gv-text-secondary, #aaa);\n font-size: 11px;\n }\n .gv-tl-slider-wrap {\n position: relative;\n height: 32px;\n }\n .gv-tl-hist {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n pointer-events: none;\n opacity: 0.7;\n }\n .gv-tl-slider {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n margin: 0;\n -webkit-appearance: none;\n appearance: none;\n background: transparent;\n cursor: pointer;\n z-index: 2;\n }\n .gv-tl-slider::-webkit-slider-runnable-track {\n height: 4px;\n border-radius: 2px;\n background: var(--gv-surface-border, rgba(100, 100, 140, 0.3));\n margin-top: 14px;\n }\n .gv-tl-slider::-webkit-slider-thumb {\n -webkit-appearance: none;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n background: var(--gv-primary, #6366f1);\n border: 2px solid var(--gv-bg, #0a0a0f);\n margin-top: -6px;\n box-shadow: 0 0 8px var(--gv-primary-muted, rgba(99, 102, 241, 0.4));\n transition: transform 0.15s ease;\n }\n .gv-tl-slider::-webkit-slider-thumb:hover {\n transform: scale(1.2);\n }\n .gv-tl-slider::-moz-range-track {\n height: 4px;\n border-radius: 2px;\n background: var(--gv-surface-border, rgba(100, 100, 140, 0.3));\n border: none;\n }\n .gv-tl-slider::-moz-range-thumb {\n width: 16px;\n height: 16px;\n border-radius: 50%;\n background: var(--gv-primary, #6366f1);\n border: 2px solid var(--gv-bg, #0a0a0f);\n box-shadow: 0 0 8px var(--gv-primary-muted, rgba(99, 102, 241, 0.4));\n }\n .gv-tl-count {\n min-width: 40px;\n padding: 4px 8px;\n border-radius: 12px;\n background: var(--gv-surface, rgba(20, 20, 30, 0.9));\n border: 1px solid var(--gv-surface-border, rgba(100, 100, 140, 0.3));\n color: var(--gv-primary, #6366f1);\n font-size: 12px;\n font-weight: 600;\n text-align: center;\n flex-shrink: 0;\n white-space: nowrap;\n }\n `;\n document.head.appendChild(style);\n }\n}\n","import type {\n RdfTriple,\n RdfValue,\n GraphNode,\n GraphEdge,\n ChangeSet,\n PrefixMap,\n} from './types.js';\n\nconst RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';\n\n/**\n * Determine if an object value looks like a URI (named node) or blank node.\n * URIs are absolute IRIs (`scheme:`) or are wrapped in <>.\n * Blank nodes start with _: prefix.\n */\nfunction isResourceObject(value: string): boolean {\n const trimmed = value.trim();\n if (!trimmed || trimmed.startsWith('\"')) return false;\n if (trimmed.startsWith('_:')) return true;\n if (trimmed.startsWith('<') && trimmed.endsWith('>')) return true;\n if (/\\s/.test(trimmed)) return false;\n return /^[A-Za-z][A-Za-z0-9+.-]*:/.test(trimmed);\n}\n\n/** Strip angle brackets from a URI if present */\nfunction cleanUri(value: string): string {\n const trimmed = value.trim();\n if (trimmed.startsWith('<') && trimmed.endsWith('>')) {\n return trimmed.slice(1, -1);\n }\n return trimmed;\n}\n\n/** Generate a deterministic edge ID */\nfunction edgeId(subject: string, predicate: string, object: string): string {\n return `${subject}\\0${predicate}\\0${object}`;\n}\n\n/**\n * Subject-centric RDF graph model for visualization.\n *\n * - Literal objects → stored as properties on the subject node\n * - URI/blank node objects → stored as edges between nodes\n * - rdf:type → stored in node.types, not as an edge\n *\n * Supports incremental add/remove with stable node and edge IDs.\n *\n * Maintains adjacency indexes (_outEdges, _inEdges) for O(degree) edge\n * lookups instead of O(E) full scans. These are used by FocusFilter BFS,\n * ReificationCollapser, and ViewConfig application on the render hot path.\n */\nexport class GraphModel {\n readonly nodes = new Map<string, GraphNode>();\n readonly edges = new Map<string, GraphEdge>();\n prefixes: PrefixMap = {};\n\n /**\n * Stores triples in canonical form — subject, predicate, and (when the\n * object is a resource) object are `cleanUri`-normalised at ingress so\n * downstream consumers and lookup paths (e.g. `removeTriples`,\n * reification mirrors, diff exporters) all operate on a single\n * representation. Literal objects are kept verbatim so quoting,\n * datatypes, and language tags survive the round-trip.\n */\n private _triples: RdfTriple[] = [];\n\n /** Set of predicate URIs treated as metadata (not rendered as edges) */\n private _metadataPredicates = new Set<string>();\n\n /** Adjacency index: nodeId → Set of edge IDs originating from this node */\n private _outEdges = new Map<string, Set<string>>();\n\n /** Adjacency index: nodeId → Set of edge IDs pointing to this node */\n private _inEdges = new Map<string, Set<string>>();\n\n /** Knowledge Asset groups: KA URI → Set of member node IDs */\n readonly kaGroups = new Map<string, Set<string>>();\n\n /** Predicate used for KA membership (set via setKaMembershipPredicate) */\n private _kaMembershipPredicate: string | null = null;\n\n constructor(metadataPredicates?: string[]) {\n if (metadataPredicates) {\n for (const p of metadataPredicates) {\n this._metadataPredicates.add(p);\n }\n }\n }\n\n /** Get all stored triples */\n get triples(): readonly RdfTriple[] {\n return this._triples;\n }\n\n /** Set the predicate used for Knowledge Asset membership grouping */\n setKaMembershipPredicate(predicate: string | null): void {\n this._kaMembershipPredicate = predicate;\n }\n\n /** Get or create a node */\n private ensureNode(id: string): GraphNode {\n let node = this.nodes.get(id);\n if (!node) {\n node = {\n id,\n types: [],\n label: id,\n properties: new Map(),\n imageUrl: null,\n metadata: new Map(),\n degree: 0,\n isBoundary: false,\n };\n this.nodes.set(id, node);\n }\n return node;\n }\n\n /** Ensure adjacency sets exist for a node */\n private ensureAdjacency(id: string): void {\n if (!this._outEdges.has(id)) this._outEdges.set(id, new Set());\n if (!this._inEdges.has(id)) this._inEdges.set(id, new Set());\n }\n\n /** Add a single triple to the model */\n addTriple(triple: RdfTriple): void {\n const subj = cleanUri(triple.subject);\n const pred = cleanUri(triple.predicate);\n const objectIsResource = isResourceObject(triple.object);\n const obj = objectIsResource ? cleanUri(triple.object) : triple.object;\n\n this._triples.push({\n ...triple,\n subject: subj,\n predicate: pred,\n object: obj,\n });\n\n const subjectNode = this.ensureNode(subj);\n\n // rdf:type → add to node types, don't create edge\n if (pred === RDF_TYPE) {\n if (!subjectNode.types.includes(obj)) {\n subjectNode.types.push(obj);\n }\n return;\n }\n\n if (objectIsResource) {\n // Metadata predicates → store as metadata properties, not edges\n if (this._metadataPredicates.has(pred)) {\n const val: RdfValue = {\n value: obj,\n datatype: triple.datatype,\n language: triple.language,\n };\n const existing = subjectNode.metadata.get(pred);\n if (existing) {\n existing.push(val);\n } else {\n subjectNode.metadata.set(pred, [val]);\n }\n return;\n }\n\n // Resource object → create edge + ensure target node\n const targetNode = this.ensureNode(obj);\n const eid = edgeId(subj, pred, obj);\n\n if (!this.edges.has(eid)) {\n this.edges.set(eid, {\n id: eid,\n source: subj,\n target: obj,\n predicate: pred,\n label: pred,\n });\n subjectNode.degree++;\n targetNode.degree++;\n\n // Maintain adjacency indexes\n this.ensureAdjacency(subj);\n this.ensureAdjacency(obj);\n this._outEdges.get(subj)!.add(eid);\n this._inEdges.get(obj)!.add(eid);\n }\n\n // Track KA membership\n if (this._kaMembershipPredicate && pred === this._kaMembershipPredicate) {\n let members = this.kaGroups.get(obj);\n if (!members) {\n members = new Set();\n this.kaGroups.set(obj, members);\n }\n members.add(subj);\n }\n } else {\n // Literal object → store as property or metadata\n const val: RdfValue = {\n value: triple.object,\n datatype: triple.datatype,\n language: triple.language,\n };\n\n if (this._metadataPredicates.has(pred)) {\n const existing = subjectNode.metadata.get(pred);\n if (existing) {\n existing.push(val);\n } else {\n subjectNode.metadata.set(pred, [val]);\n }\n } else {\n const existing = subjectNode.properties.get(pred);\n if (existing) {\n existing.push(val);\n } else {\n subjectNode.properties.set(pred, [val]);\n }\n }\n }\n }\n\n /** Add multiple triples and return the changeset */\n addTriples(triples: RdfTriple[]): ChangeSet {\n const nodesBefore = new Set(this.nodes.keys());\n const edgesBefore = new Set(this.edges.keys());\n\n for (const t of triples) {\n this.addTriple(t);\n }\n\n const addedNodes: string[] = [];\n const addedEdges: string[] = [];\n const modifiedNodes = new Set<string>();\n\n for (const id of this.nodes.keys()) {\n if (!nodesBefore.has(id)) {\n addedNodes.push(id);\n } else {\n modifiedNodes.add(id);\n }\n }\n\n for (const id of this.edges.keys()) {\n if (!edgesBefore.has(id)) {\n addedEdges.push(id);\n }\n }\n\n return {\n addedNodes,\n removedNodes: [],\n addedEdges,\n removedEdges: [],\n modifiedNodes: [...modifiedNodes],\n };\n }\n\n /** Remove triples matching the given criteria and return changeset */\n removeTriples(triples: RdfTriple[]): ChangeSet {\n const removedEdges: string[] = [];\n const modifiedNodes = new Set<string>();\n\n for (const triple of triples) {\n const subj = cleanUri(triple.subject);\n const pred = cleanUri(triple.predicate);\n const objectIsResource = isResourceObject(triple.object);\n const obj = objectIsResource ? cleanUri(triple.object) : triple.object;\n\n // Match against the canonical form `_triples` stores (see field doc).\n const idx = this._triples.findIndex(\n (t) => t.subject === subj && t.predicate === pred && t.object === obj\n );\n if (idx !== -1) this._triples.splice(idx, 1);\n\n if (pred === RDF_TYPE) {\n const node = this.nodes.get(subj);\n if (node) {\n node.types = node.types.filter((t) => t !== obj);\n modifiedNodes.add(subj);\n }\n continue;\n }\n\n if (objectIsResource && !this._metadataPredicates.has(pred)) {\n const eid = edgeId(subj, pred, obj);\n if (this.edges.has(eid)) {\n this.edges.delete(eid);\n removedEdges.push(eid);\n\n // Maintain adjacency indexes\n this._outEdges.get(subj)?.delete(eid);\n this._inEdges.get(obj)?.delete(eid);\n\n const srcNode = this.nodes.get(subj);\n const tgtNode = this.nodes.get(obj);\n if (srcNode) srcNode.degree--;\n if (tgtNode) tgtNode.degree--;\n\n modifiedNodes.add(subj);\n modifiedNodes.add(obj);\n }\n } else {\n const node = this.nodes.get(subj);\n if (node) {\n const store = this._metadataPredicates.has(pred) ? node.metadata : node.properties;\n const vals = store.get(pred);\n if (vals) {\n const filtered = vals.filter((v) => v.value !== obj);\n if (filtered.length === 0) {\n store.delete(pred);\n } else {\n store.set(pred, filtered);\n }\n modifiedNodes.add(subj);\n }\n }\n }\n }\n\n // Remove orphan nodes (no edges, no properties, no types)\n const removedNodes: string[] = [];\n for (const [id, node] of this.nodes) {\n if (\n node.degree === 0 &&\n node.types.length === 0 &&\n node.properties.size === 0 &&\n node.metadata.size === 0\n ) {\n this.nodes.delete(id);\n this._outEdges.delete(id);\n this._inEdges.delete(id);\n removedNodes.push(id);\n }\n }\n\n return {\n addedNodes: [],\n removedNodes,\n addedEdges: [],\n removedEdges,\n modifiedNodes: [...modifiedNodes],\n };\n }\n\n /** Get a node by ID */\n getNode(id: string): GraphNode | undefined {\n return this.nodes.get(id);\n }\n\n /** Get all edges originating from a node — O(degree) via adjacency index */\n getEdgesFrom(id: string): GraphEdge[] {\n const edgeIds = this._outEdges.get(id);\n if (!edgeIds) return [];\n const result: GraphEdge[] = [];\n for (const eid of edgeIds) {\n const edge = this.edges.get(eid);\n if (edge) result.push(edge);\n }\n return result;\n }\n\n /** Get all edges pointing to a node — O(degree) via adjacency index */\n getEdgesTo(id: string): GraphEdge[] {\n const edgeIds = this._inEdges.get(id);\n if (!edgeIds) return [];\n const result: GraphEdge[] = [];\n for (const eid of edgeIds) {\n const edge = this.edges.get(eid);\n if (edge) result.push(edge);\n }\n return result;\n }\n\n /** Get all neighbor node IDs (both directions) — O(degree) via adjacency index */\n getNeighborIds(id: string): Set<string> {\n const neighbors = new Set<string>();\n const outIds = this._outEdges.get(id);\n if (outIds) {\n for (const eid of outIds) {\n const edge = this.edges.get(eid);\n if (edge) neighbors.add(edge.target);\n }\n }\n const inIds = this._inEdges.get(id);\n if (inIds) {\n for (const eid of inIds) {\n const edge = this.edges.get(eid);\n if (edge) neighbors.add(edge.source);\n }\n }\n return neighbors;\n }\n\n /** Get the node with the highest degree */\n getHighestDegreeNode(): GraphNode | null {\n let best: GraphNode | null = null;\n for (const node of this.nodes.values()) {\n if (!best || node.degree > best.degree) {\n best = node;\n }\n }\n return best;\n }\n\n /** Clear all data */\n clear(): void {\n this.nodes.clear();\n this.edges.clear();\n this._triples = [];\n this._outEdges.clear();\n this._inEdges.clear();\n }\n\n /** Total number of triples */\n get tripleCount(): number {\n return this._triples.length;\n }\n}\n","import type { PrefixMap } from './types.js';\n\n/** Well-known namespace prefixes shipped as defaults */\nconst WELL_KNOWN_PREFIXES: PrefixMap = {\n rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',\n rdfs: 'http://www.w3.org/2000/01/rdf-schema#',\n owl: 'http://www.w3.org/2002/07/owl#',\n xsd: 'http://www.w3.org/2001/XMLSchema#',\n schema: 'https://schema.org/',\n foaf: 'http://xmlns.com/foaf/0.1/',\n dcterms: 'http://purl.org/dc/terms/',\n dc: 'http://purl.org/dc/elements/1.1/',\n prov: 'http://www.w3.org/ns/prov#',\n skos: 'http://www.w3.org/2004/02/skos/core#',\n};\n\n/**\n * Manages namespace prefix ↔ URI mappings for compacting and expanding URIs.\n */\nexport class PrefixManager {\n private _prefixes: Map<string, string>;\n /** Reverse map: namespace URI → prefix */\n private _reverse: Map<string, string>;\n\n constructor(userPrefixes?: PrefixMap) {\n this._prefixes = new Map();\n this._reverse = new Map();\n\n // Load well-known first, then user overrides\n for (const [prefix, ns] of Object.entries(WELL_KNOWN_PREFIXES)) {\n this._prefixes.set(prefix, ns);\n this._reverse.set(ns, prefix);\n }\n\n if (userPrefixes) {\n for (const [prefix, ns] of Object.entries(userPrefixes)) {\n this._prefixes.set(prefix, ns);\n this._reverse.set(ns, prefix);\n }\n }\n }\n\n /** Register a new prefix */\n addPrefix(prefix: string, namespace: string): void {\n this._prefixes.set(prefix, namespace);\n this._reverse.set(namespace, prefix);\n }\n\n /** Merge in multiple prefixes */\n addPrefixes(prefixes: PrefixMap): void {\n for (const [prefix, ns] of Object.entries(prefixes)) {\n this.addPrefix(prefix, ns);\n }\n }\n\n /** Get the namespace URI for a prefix */\n getNamespace(prefix: string): string | undefined {\n return this._prefixes.get(prefix);\n }\n\n /** Get the prefix for a namespace URI */\n getPrefix(namespace: string): string | undefined {\n return this._reverse.get(namespace);\n }\n\n /**\n * Compact a full URI to prefixed form (e.g., \"https://schema.org/name\" → \"schema:name\").\n * Returns null if no matching prefix is found.\n */\n compact(uri: string): string | null {\n for (const [ns, prefix] of this._reverse) {\n if (uri.startsWith(ns)) {\n const localName = uri.slice(ns.length);\n if (localName.length > 0) {\n return `${prefix}:${localName}`;\n }\n }\n }\n return null;\n }\n\n /**\n * Expand a prefixed term to a full URI (e.g., \"schema:name\" → \"https://schema.org/name\").\n * Returns the input unchanged if it's already a full URI or no prefix matches.\n */\n expand(term: string): string {\n const colonIdx = term.indexOf(':');\n if (colonIdx === -1) return term;\n\n // Don't expand things that look like full URIs\n if (term.startsWith('http://') || term.startsWith('https://') || term.startsWith('urn:')) {\n return term;\n }\n\n const prefix = term.slice(0, colonIdx);\n const localName = term.slice(colonIdx + 1);\n const ns = this._prefixes.get(prefix);\n\n return ns ? ns + localName : term;\n }\n\n /**\n * Extract a short, human-readable local name from a URI.\n *\n * Strategy, in priority order:\n * 1. Part after the last `#` (RDF fragment: http://ex.org/ns#Foo → Foo)\n * 2. Part after the last `/` (HTTP path: http://ex.org/foo/Bar → Bar)\n * 3. If the URI contains URL-encoded slashes (%2F), decode them and\n * take the basename. This handles URIs like\n * `urn:dkg:code:file:packages%2Fnode-ui%2Fsrc%2Fui%2FApp.tsx`,\n * which previously returned the entire URI.\n * 4. Part after the last `:` (URN tail: urn:dkg:task:my-slug → my-slug)\n * 5. The URI itself (last resort).\n */\n static localName(uri: string): string {\n const hashIdx = uri.lastIndexOf('#');\n if (hashIdx !== -1) return uri.slice(hashIdx + 1);\n\n const slashIdx = uri.lastIndexOf('/');\n if (slashIdx !== -1) return uri.slice(slashIdx + 1);\n\n // URL-encoded path: decode and take the last segment.\n if (uri.includes('%2F') || uri.includes('%2f')) {\n try {\n const decoded = decodeURIComponent(uri);\n const decodedSlash = decoded.lastIndexOf('/');\n if (decodedSlash !== -1) return decoded.slice(decodedSlash + 1);\n } catch {\n // Malformed encoding — fall through to colon split.\n }\n }\n\n const colonIdx = uri.lastIndexOf(':');\n if (colonIdx !== -1 && colonIdx < uri.length - 1) {\n return uri.slice(colonIdx + 1);\n }\n\n return uri;\n }\n\n /** Get all registered prefixes */\n get prefixes(): PrefixMap {\n const result: PrefixMap = {};\n for (const [prefix, ns] of this._prefixes) {\n result[prefix] = ns;\n }\n return result;\n }\n}\n","import type { GraphNode, LabelMode, LabelConfig, GraphEdge } from './types.js';\nimport { PrefixManager } from './prefix-manager.js';\n\n/** Default label predicates in priority order */\nconst DEFAULT_LABEL_PREDICATES = [\n 'http://www.w3.org/2000/01/rdf-schema#label',\n 'https://schema.org/name',\n 'http://xmlns.com/foaf/0.1/name',\n 'http://purl.org/dc/terms/title',\n 'http://purl.org/dc/elements/1.1/title',\n 'https://schema.org/alternateName',\n];\n\n/**\n * Resolves display labels for nodes and edges based on the current label mode.\n *\n * - \"strict\": Shows compact prefixed URI (e.g., \"schema:Person\", \"prov:wasGeneratedBy\")\n * - \"humanized\": Shows friendly label (e.g., \"Person\", \"was generated by\")\n */\nexport class LabelResolver {\n private _mode: LabelMode;\n private _prefixManager: PrefixManager;\n private _labelPredicates: string[];\n\n constructor(mode: LabelMode, prefixManager: PrefixManager, config?: LabelConfig) {\n this._mode = mode;\n this._prefixManager = prefixManager;\n this._labelPredicates = config?.predicates ?? DEFAULT_LABEL_PREDICATES;\n }\n\n get mode(): LabelMode {\n return this._mode;\n }\n\n set mode(value: LabelMode) {\n this._mode = value;\n }\n\n /** Resolve a display label for a node */\n resolveNodeLabel(node: GraphNode): string {\n // First: check if the node has an explicit label property\n for (const pred of this._labelPredicates) {\n const values = node.properties.get(pred);\n if (values && values.length > 0) {\n return values[0].value;\n }\n }\n\n // Fall back to the URI-derived label\n return this.resolveUri(node.id);\n }\n\n /** Resolve a display label for an edge */\n resolveEdgeLabel(edge: GraphEdge): string {\n return this.resolveUri(edge.predicate);\n }\n\n /** Resolve a display label for a URI */\n resolveUri(uri: string): string {\n if (this._mode === 'strict') {\n return this._prefixManager.compact(uri) ?? PrefixManager.localName(uri);\n }\n\n // Humanized mode\n return humanize(PrefixManager.localName(uri));\n }\n\n /** Resolve a type label (used for type badges) */\n resolveTypeLabel(typeUri: string): string {\n if (this._mode === 'strict') {\n return this._prefixManager.compact(typeUri) ?? PrefixManager.localName(typeUri);\n }\n return humanize(PrefixManager.localName(typeUri));\n }\n\n /** Resolve label for a predicate (used in property lists) */\n resolvePredicateLabel(predicateUri: string): string {\n return this.resolveUri(predicateUri);\n }\n\n /**\n * Update all labels on nodes and edges in the graph.\n * Call this after changing label mode or adding new data.\n */\n updateLabels(nodes: Map<string, GraphNode>, edges: Map<string, GraphEdge>): void {\n for (const node of nodes.values()) {\n node.label = this.resolveNodeLabel(node);\n }\n for (const edge of edges.values()) {\n edge.label = this.resolveEdgeLabel(edge);\n }\n }\n}\n\n/**\n * Convert a camelCase or PascalCase local name into a human-friendly label.\n * e.g., \"wasGeneratedBy\" → \"was generated by\"\n * e.g., \"SocialMediaPosting\" → \"Social Media Posting\"\n */\nfunction humanize(localName: string): string {\n if (!localName) return '';\n\n // Handle snake_case\n let result = localName.replace(/_/g, ' ');\n\n // Insert spaces before capitals in camelCase/PascalCase\n result = result.replace(/([a-z])([A-Z])/g, '$1 $2');\n result = result.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');\n\n // Lowercase everything for humanized display (except if it's a short acronym)\n if (result.length > 4) {\n result = result.toLowerCase();\n }\n\n return result.trim();\n}\n","import type { GraphNode, GraphEdge, StyleConfig } from './types.js';\nimport type { ColorPalette } from './palette.js';\nimport { PALETTE_DARK } from './palette.js';\nimport { PrefixManager } from './prefix-manager.js';\n\n/** Auto-assigned namespace colors for common vocabularies */\nconst NAMESPACE_COLORS: Record<string, string> = {\n 'https://schema.org/': '#8b5cf6', // Purple for schema.org\n 'http://www.w3.org/ns/prov#': '#3b82f6', // Blue for PROV-O\n 'http://xmlns.com/foaf/0.1/': '#10b981', // Emerald for FOAF\n 'http://purl.org/dc/terms/': '#f59e0b', // Amber for Dublin Core\n};\n\n/**\n * Resolves colors for nodes and edges based on configurable rules.\n *\n * When a ColorPalette is provided, it drives default colors:\n * - defaultNodeColor → palette.primary\n * - defaultEdgeColor → palette.edgeColor\n * - Label/text colors → palette.textPrimary/textSecondary\n *\n * Priority (highest first):\n * 1. Per-class color (matching rdf:type)\n * 2. Per-namespace color (matching namespace of first rdf:type)\n * 3. Default color (from palette or config)\n */\nexport class StyleEngine {\n private _config: Required<StyleConfig>;\n private _prefixManager: PrefixManager;\n private _palette: ColorPalette;\n private _hasExplicitDefaultNodeColor: boolean;\n private _hasExplicitDefaultEdgeColor: boolean;\n\n constructor(config: StyleConfig | undefined, prefixManager: PrefixManager, palette?: ColorPalette) {\n this._prefixManager = prefixManager;\n this._palette = palette ?? PALETTE_DARK;\n this._hasExplicitDefaultNodeColor = config?.defaultNodeColor !== undefined;\n this._hasExplicitDefaultEdgeColor = config?.defaultEdgeColor !== undefined;\n this._config = {\n nodeColors: config?.nodeColors ?? {},\n classColors: config?.classColors ?? {},\n namespaceColors: config?.namespaceColors ?? {},\n predicateColors: config?.predicateColors ?? {},\n gradient: config?.gradient ?? true,\n gradientIntensity: config?.gradientIntensity ?? 0.3,\n defaultNodeColor: config?.defaultNodeColor ?? this._palette.primary,\n defaultEdgeColor: config?.defaultEdgeColor ?? this._palette.edgeColor,\n edgeWidth: config?.edgeWidth ?? 1.5,\n edgeArrowSize: config?.edgeArrowSize ?? 4,\n borderWidth: config?.borderWidth ?? 1.5,\n fontFamily: config?.fontFamily ?? 'system-ui, -apple-system, sans-serif',\n fontSize: config?.fontSize ?? 11,\n };\n }\n\n /** The active color palette */\n get palette(): ColorPalette {\n return this._palette;\n }\n\n /** Update the active palette (recalculates defaults not explicitly set in config) */\n setPalette(palette: ColorPalette): void {\n this._palette = palette;\n // Only override defaults that weren't explicitly set by the user\n this._config = { ...this._config };\n if (!this._hasExplicitDefaultNodeColor) this._config.defaultNodeColor = palette.primary;\n if (!this._hasExplicitDefaultEdgeColor) this._config.defaultEdgeColor = palette.edgeColor;\n }\n\n get config(): Readonly<Required<StyleConfig>> {\n return this._config;\n }\n\n /** Update per-node color overrides (merges with existing) */\n setNodeColors(colors: Record<string, string>): void {\n this._config = { ...this._config, nodeColors: { ...this._config.nodeColors, ...colors } };\n }\n\n /** Get the fill color for a node */\n getNodeColor(node: GraphNode): string {\n // 0. Per-node color override (highest priority — used for trust-level coloring)\n const nodeColor = this._config.nodeColors[node.id];\n if (nodeColor) return nodeColor;\n\n // 1. Check per-class colors (user-supplied, using compact or full URIs)\n for (const type of node.types) {\n const compact = this._prefixManager.compact(type);\n if (compact && this._config.classColors[compact]) {\n return this._config.classColors[compact];\n }\n if (this._config.classColors[type]) {\n return this._config.classColors[type];\n }\n }\n\n // 2. Check per-namespace colors (user-supplied first, then built-in)\n for (const type of node.types) {\n for (const [ns, color] of Object.entries(this._config.namespaceColors)) {\n const fullNs = this._prefixManager.getNamespace(ns) ?? ns;\n if (type.startsWith(fullNs)) return color;\n }\n // Built-in namespace colors\n for (const [ns, color] of Object.entries(NAMESPACE_COLORS)) {\n if (type.startsWith(ns)) return color;\n }\n }\n\n // 3. Default\n return this._config.defaultNodeColor;\n }\n\n /** Get the color for an edge */\n getEdgeColor(edge: GraphEdge): string {\n const compact = this._prefixManager.compact(edge.predicate);\n if (compact && this._config.predicateColors[compact]) {\n return this._config.predicateColors[compact];\n }\n if (this._config.predicateColors[edge.predicate]) {\n return this._config.predicateColors[edge.predicate];\n }\n return this._config.defaultEdgeColor;\n }\n\n /** Get border color (slightly lighter than fill) */\n getBorderColor(fillColor: string): string {\n return lightenHex(fillColor, 0.3);\n }\n\n /** Get gradient center color (lighter) */\n getGradientCenter(fillColor: string): string {\n return lightenHex(fillColor, this._config.gradientIntensity);\n }\n}\n\n/** Lighten a hex color by a factor (0.0 = no change, 1.0 = white) */\nfunction lightenHex(hex: string, factor: number): string {\n const rgb = hexToRgb(hex);\n if (!rgb) return hex;\n\n const r = Math.round(rgb.r + (255 - rgb.r) * factor);\n const g = Math.round(rgb.g + (255 - rgb.g) * factor);\n const b = Math.round(rgb.b + (255 - rgb.b) * factor);\n\n return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;\n}\n\nfunction hexToRgb(hex: string): { r: number; g: number; b: number } | null {\n const match = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n if (!match) return null;\n return {\n r: parseInt(match[1], 16),\n g: parseInt(match[2], 16),\n b: parseInt(match[3], 16),\n };\n}\n","import type { GraphNode, GraphEdge, HexagonConfig } from './types.js';\nimport { StyleEngine } from './style-engine.js';\nimport { PrefixManager } from './prefix-manager.js';\n\nconst DEFAULT_HEXAGON: Required<HexagonConfig> = {\n baseSize: 36,\n minSize: 24,\n maxSize: 72,\n scaleWithDegree: true,\n imagePredicates: [\n 'https://schema.org/image',\n 'http://xmlns.com/foaf/0.1/img',\n 'http://xmlns.com/foaf/0.1/depiction',\n ],\n imageUrlPredicate: 'https://schema.org/url',\n circleTypes: [],\n};\n\n/** Cache for loaded images */\nconst imageCache = new Map<string, HTMLImageElement | null>();\n\n/**\n * Draws hexagonal nodes on a Canvas 2D context.\n * Called by force-graph's nodeCanvasObject callback.\n *\n * Nodes whose rdf:type matches a `circleTypes` entry are drawn as simple\n * filled circles (much cheaper — no gradient, no border, no image, no label).\n */\nexport class HexagonPainter {\n private _config: Required<HexagonConfig>;\n private _styleEngine: StyleEngine;\n private _maxDegree = 1;\n /** Expanded circleTypes — full URIs resolved from compact forms */\n private _circleTypeSet: Set<string>;\n /** Callback to trigger graph repaint when an image finishes loading */\n private _onImageLoad: (() => void) | null = null;\n /** Trust visualization style */\n private _trustStyle: 'opacity' | 'border' | 'glow' | 'none' = 'none';\n /** Whether to show provenance badges */\n private _showBadges = true;\n\n constructor(\n config: HexagonConfig | undefined,\n styleEngine: StyleEngine,\n prefixManager?: PrefixManager\n ) {\n this._config = { ...DEFAULT_HEXAGON, ...config };\n this._styleEngine = styleEngine;\n\n // Build a set of full URIs for circle types (expand compact forms)\n this._circleTypeSet = new Set<string>();\n for (const t of this._config.circleTypes) {\n this._circleTypeSet.add(t);\n if (prefixManager) {\n this._circleTypeSet.add(prefixManager.expand(t));\n }\n }\n }\n\n /** Check if a node should render as a simple circle */\n isCircleNode(node: GraphNode): boolean {\n if (this._circleTypeSet.size === 0) return false;\n return node.types.some((t) => this._circleTypeSet.has(t));\n }\n\n /** Set callback to trigger repaint when images load */\n setImageLoadCallback(cb: () => void): void {\n this._onImageLoad = cb;\n }\n\n /** Update the max degree for scaling calculations */\n setMaxDegree(maxDegree: number): void {\n this._maxDegree = Math.max(1, maxDegree);\n }\n\n /** Set the trust visualization style */\n setTrustStyle(style: 'opacity' | 'border' | 'glow' | 'none'): void {\n this._trustStyle = style;\n }\n\n /** Enable/disable provenance badges */\n setShowBadges(show: boolean): void {\n this._showBadges = show;\n }\n\n /** Get the radius for a node (optionally scaled by degree, boosted if has image) */\n getRadius(node: GraphNode): number {\n // Circle nodes get a slightly smaller base\n if (this.isCircleNode(node)) {\n if (!this._config.scaleWithDegree) return this._config.minSize * 0.7;\n const ratio = node.degree / this._maxDegree;\n const range = this._config.maxSize * 0.6 - this._config.minSize * 0.7;\n return this._config.minSize * 0.7 + ratio * range;\n }\n\n // Size multipliers: explicit sizeMultiplier > isHighRisk > default\n const multiplier = node.sizeMultiplier ?? (node.isHighRisk ? 2.0 : 1.0);\n\n // Nodes with images get a minimum size so the image is visible\n const imageMinSize = node.imageUrl ? Math.max(this._config.maxSize * 0.6, 8) : 0;\n\n if (!this._config.scaleWithDegree) return Math.max(this._config.baseSize, imageMinSize) * multiplier;\n\n const ratio = node.degree / this._maxDegree;\n const range = this._config.maxSize - this._config.minSize;\n const degreeSize = this._config.minSize + ratio * range;\n return Math.max(degreeSize, imageMinSize) * multiplier;\n }\n\n /**\n * Paint a node onto the canvas.\n * Circle-type nodes get a minimal filled circle (fast path).\n * All others get the full hexagon treatment.\n *\n * @param pulseScale - Optional multiplier for risk pulse animation (1.0 = no pulse)\n * @returns the bounding radius for hit detection.\n */\n paint(\n ctx: CanvasRenderingContext2D,\n node: GraphNode,\n x: number,\n y: number,\n globalScale: number,\n pulseScale = 1.0\n ): number {\n // Fast path: simple circle for low-priority node types\n if (this.isCircleNode(node)) {\n return this.paintCircle(ctx, node, x, y, globalScale);\n }\n\n const baseRadius = this.getRadius(node);\n const radius = baseRadius * pulseScale;\n const color = this._styleEngine.getNodeColor(node);\n const palette = this._styleEngine.palette;\n const styleConfig = this._styleEngine.config;\n\n ctx.save();\n\n // Risk pulse glow (larger translucent hexagon behind)\n if (pulseScale > 1.03 && node.isHighRisk) {\n const glowRadius = radius * 1.25;\n const glowAlpha = (pulseScale - 1) * 2.5; // 0–0.45\n ctx.beginPath();\n for (let i = 0; i < 6; i++) {\n const angle = (Math.PI / 3) * i;\n const hx = x + glowRadius * Math.cos(angle);\n const hy = y + glowRadius * Math.sin(angle);\n if (i === 0) ctx.moveTo(hx, hy);\n else ctx.lineTo(hx, hy);\n }\n ctx.closePath();\n ctx.fillStyle = `rgba(220, 50, 50, ${Math.min(0.4, glowAlpha)})`;\n ctx.fill();\n }\n\n // Draw hexagon path (flat-top orientation)\n ctx.beginPath();\n for (let i = 0; i < 6; i++) {\n const angle = (Math.PI / 3) * i;\n const hx = x + radius * Math.cos(angle);\n const hy = y + radius * Math.sin(angle);\n if (i === 0) ctx.moveTo(hx, hy);\n else ctx.lineTo(hx, hy);\n }\n ctx.closePath();\n\n // Fill with gradient or flat color\n if (styleConfig.gradient) {\n const gradient = ctx.createRadialGradient(x, y, 0, x, y, radius);\n gradient.addColorStop(0, this._styleEngine.getGradientCenter(color));\n gradient.addColorStop(1, color);\n ctx.fillStyle = gradient;\n } else {\n ctx.fillStyle = color;\n }\n ctx.fill();\n\n // Trust glow (multi-source nodes get a colored glow behind)\n const sourceCount = node.provenance?.sources?.length ?? 0;\n if (sourceCount >= 2 && this._trustStyle === 'glow') {\n ctx.shadowColor = palette.info;\n ctx.shadowBlur = Math.min(12, sourceCount * 3) / globalScale;\n ctx.fill(); // re-fill with shadow\n ctx.shadowBlur = 0;\n ctx.shadowColor = 'transparent';\n }\n\n // Border — risk, trust, or normal\n if (node.riskScore && node.riskScore > 0) {\n const riskIntensity = Math.min(1, node.riskScore);\n ctx.strokeStyle = `rgba(220, 50, 50, ${0.5 + riskIntensity * 0.5})`;\n ctx.lineWidth = (styleConfig.borderWidth + (node.isHighRisk ? 1.5 : 0.5)) / globalScale;\n } else if (sourceCount >= 2 && this._trustStyle === 'border') {\n // Multi-source trust: thicker, brighter border\n ctx.strokeStyle = palette.info;\n ctx.lineWidth = (styleConfig.borderWidth + Math.min(3, sourceCount)) / globalScale;\n } else {\n ctx.strokeStyle = this._styleEngine.getBorderColor(color);\n ctx.lineWidth = styleConfig.borderWidth / globalScale;\n }\n\n // Boundary node indicator (subtle lighter border, no dashes)\n if (node.isBoundary && !(node.riskScore && node.riskScore > 0)) {\n ctx.strokeStyle = lightenColor(color, 0.5);\n ctx.lineWidth = (styleConfig.borderWidth + 1) / globalScale;\n }\n\n ctx.stroke();\n ctx.setLineDash([]);\n\n // Trust opacity adjustment\n if (sourceCount < 2 && this._trustStyle === 'opacity') {\n ctx.globalAlpha = 0.7;\n }\n\n // Avatar image (clipped to hexagon)\n if (node.imageUrl) {\n this.paintAvatar(ctx, node.imageUrl, x, y, radius);\n }\n\n ctx.globalAlpha = 1;\n\n // Badges (source count + verified indicator)\n if (globalScale > 3.0) {\n this._paintBadges(ctx, node, x, y, radius, globalScale);\n }\n\n // Label below hexagon — fade in/out with zoom\n const fadeStart = 0.3;\n const fadeFull = 0.8;\n if (globalScale > fadeStart) {\n const opacity = Math.min(1, (globalScale - fadeStart) / (fadeFull - fadeStart));\n const fontSize = styleConfig.fontSize / globalScale;\n ctx.font = `${fontSize}px ${styleConfig.fontFamily}`;\n ctx.textAlign = 'center';\n ctx.textBaseline = 'top';\n ctx.globalAlpha = opacity;\n ctx.fillStyle = palette.textPrimary;\n ctx.fillText(\n truncateLabel(node.label, 24),\n x,\n y + radius + 2 / globalScale\n );\n ctx.globalAlpha = 1;\n }\n\n ctx.restore();\n return radius;\n }\n\n /** Paint provenance badges on a node */\n private _paintBadges(\n ctx: CanvasRenderingContext2D,\n node: GraphNode,\n x: number,\n y: number,\n radius: number,\n globalScale: number\n ): void {\n if (!this._showBadges) return;\n\n const prov = node.provenance;\n if (!prov) return;\n\n const palette = this._styleEngine.palette;\n const badgeSize = Math.max(4, 8 / globalScale);\n\n // Source count badge (bottom-right)\n if (prov.sources.length > 0) {\n const bx = x + radius * 0.7;\n const by = y + radius * 0.6;\n const count = prov.sources.length;\n\n // Badge circle\n ctx.beginPath();\n ctx.arc(bx, by, badgeSize, 0, 2 * Math.PI);\n const intensity = Math.min(1, count / 3);\n ctx.fillStyle = count >= 2 ? palette.info : palette.textMuted;\n ctx.globalAlpha = 0.4 + intensity * 0.6;\n ctx.fill();\n\n // Badge number\n if (badgeSize > 3) {\n ctx.fillStyle = '#fff';\n ctx.globalAlpha = 1;\n ctx.font = `bold ${badgeSize * 1.2}px system-ui`;\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n ctx.fillText(String(count), bx, by);\n }\n }\n\n // Verified badge (top-right) — checkmark for content hash\n if (prov.contentHash || prov.blockchainAnchor) {\n const bx = x + radius * 0.7;\n const by = y - radius * 0.6;\n\n ctx.beginPath();\n ctx.arc(bx, by, badgeSize, 0, 2 * Math.PI);\n ctx.fillStyle = prov.blockchainAnchor ? palette.safe : palette.info;\n ctx.globalAlpha = 0.85;\n ctx.fill();\n\n // Checkmark\n ctx.globalAlpha = 1;\n ctx.strokeStyle = '#fff';\n ctx.lineWidth = badgeSize * 0.3;\n ctx.lineCap = 'round';\n ctx.lineJoin = 'round';\n ctx.beginPath();\n const s = badgeSize * 0.5;\n ctx.moveTo(bx - s * 0.5, by);\n ctx.lineTo(bx - s * 0.1, by + s * 0.4);\n ctx.lineTo(bx + s * 0.5, by - s * 0.3);\n ctx.stroke();\n }\n\n ctx.globalAlpha = 1;\n }\n\n /**\n * Fast path: paint a minimal filled circle.\n * No gradient, no border, no image, no label — just a flat arc.\n */\n private paintCircle(\n ctx: CanvasRenderingContext2D,\n node: GraphNode,\n x: number,\n y: number,\n _globalScale: number\n ): number {\n const radius = this.getRadius(node);\n const color = this._styleEngine.getNodeColor(node);\n\n ctx.beginPath();\n ctx.arc(x, y, radius, 0, 2 * Math.PI);\n ctx.fillStyle = color;\n ctx.fill();\n\n return radius;\n }\n\n /** Paint an avatar image clipped to the hexagon shape */\n private paintAvatar(\n ctx: CanvasRenderingContext2D,\n url: string,\n x: number,\n y: number,\n radius: number\n ): void {\n let img = imageCache.get(url);\n\n if (img === undefined) {\n // Start loading\n const newImg = new Image();\n // Only set crossOrigin for remote URLs — data URIs break with CORS attributes\n if (!url.startsWith('data:')) {\n newImg.crossOrigin = 'anonymous';\n }\n imageCache.set(url, null); // Mark as loading\n newImg.src = url;\n\n newImg.onload = () => {\n imageCache.set(url, newImg);\n // Trigger graph repaint so the image appears\n this._onImageLoad?.();\n };\n newImg.onerror = () => {\n // Don't permanently cache failures — allow retry\n imageCache.delete(url);\n };\n return;\n }\n\n if (!img) return; // Still loading\n\n ctx.save();\n\n // Clip to hexagon\n ctx.beginPath();\n for (let i = 0; i < 6; i++) {\n const angle = (Math.PI / 3) * i;\n const hx = x + radius * Math.cos(angle);\n const hy = y + radius * Math.sin(angle);\n if (i === 0) ctx.moveTo(hx, hy);\n else ctx.lineTo(hx, hy);\n }\n ctx.closePath();\n ctx.clip();\n\n // Draw image centered in the hexagon, cropped by hexagon clip path\n const imgAspect = img.naturalWidth / img.naturalHeight;\n // Full photos (http/https URLs) fill the hexagon; small SVG icons stay compact\n const isIcon = url.startsWith('data:');\n const scale = isIcon ? 0.5 : 1.0;\n let drawW: number, drawH: number;\n const size = radius * 2 * scale;\n if (imgAspect > 1) {\n drawH = size;\n drawW = size * imgAspect;\n } else {\n drawW = size;\n drawH = size / imgAspect;\n }\n ctx.globalAlpha = 0.95;\n ctx.drawImage(img, x - drawW / 2, y - drawH / 2, drawW, drawH);\n ctx.globalAlpha = 1;\n\n ctx.restore();\n }\n\n /**\n * Resolve image URL for a node from its properties,\n * following indirect links if needed.\n *\n * Two resolution strategies:\n * 1. Direct: node has a literal property (schema:image → \"https://...jpg\")\n * 2. Indirect: node links to an image node which has the URL\n * (node → schema:image → ImageObject → schema:url → \"https://...jpg\")\n */\n resolveImageUrl(\n node: GraphNode,\n getNode: (id: string) => GraphNode | undefined,\n getEdgesFrom?: (id: string) => GraphEdge[]\n ): string | null {\n // Strategy 1: Direct literal image URL on the node\n for (const pred of this._config.imagePredicates) {\n const literalVals = node.properties.get(pred);\n if (literalVals && literalVals.length > 0) {\n const url = literalVals[0].value;\n if (url.startsWith('http://') || url.startsWith('https://')) {\n return url;\n }\n }\n }\n\n // Strategy 2: Indirect — follow edge to an intermediate image node\n if (getEdgesFrom) {\n const edges = getEdgesFrom(node.id);\n for (const pred of this._config.imagePredicates) {\n for (const edge of edges) {\n if (edge.predicate !== pred) continue;\n\n const imageNode = getNode(edge.target);\n if (!imageNode) continue;\n\n // Check the intermediate node for the actual URL\n const urlVals = imageNode.properties.get(this._config.imageUrlPredicate);\n if (urlVals && urlVals.length > 0) {\n const url = urlVals[0].value;\n if (url.startsWith('http://') || url.startsWith('https://')) {\n return url;\n }\n }\n\n // Also check if the intermediate node has a direct image literal\n for (const imgPred of this._config.imagePredicates) {\n const vals = imageNode.properties.get(imgPred);\n if (vals && vals.length > 0) {\n const url = vals[0].value;\n if (url.startsWith('http://') || url.startsWith('https://')) {\n return url;\n }\n }\n }\n }\n }\n }\n\n return null;\n }\n}\n\n/**\n * Shorten a node label to fit under a hexagon.\n *\n * For path-like labels (\"packages/node-ui/src/ui/views/ProjectView.tsx\")\n * the informative bit is usually the *basename*, not the prefix — so we\n * middle-elide to preserve a hint of the parent and the full tail:\n * packages/node-ui/src/ui/views/ProjectView.tsx\n * → packages/…/ProjectView.tsx\n *\n * For `#123 Some Pull Request Title`-style labels we also preserve the\n * leading identifier (`#123 …`) because the number is more identifying\n * than the first words of the title.\n *\n * Everything else falls back to a tail ellipsis.\n */\nfunction truncateLabel(label: string, maxLen: number): string {\n if (label.length <= maxLen) return label;\n\n // Path-like: keep first segment + basename, elide the middle.\n if (label.includes('/')) {\n const parts = label.split('/');\n const head = parts[0];\n const tail = parts[parts.length - 1];\n const candidate = `${head}/\\u2026/${tail}`;\n if (parts.length >= 3 && candidate.length <= maxLen) return candidate;\n // Still too long — just keep the basename.\n if (tail.length <= maxLen - 2) return `\\u2026/${tail}`;\n }\n\n // \"#123 Title Of The Thing\" — keep the number, elide the title tail.\n const numMatch = /^(#\\d+)\\s+(.+)$/.exec(label);\n if (numMatch) {\n const prefix = numMatch[1];\n const rest = numMatch[2];\n const room = maxLen - prefix.length - 2;\n if (room > 4) return `${prefix} ${rest.slice(0, room)}\\u2026`;\n }\n\n return label.slice(0, maxLen - 1) + '\\u2026';\n}\n\nfunction lightenColor(hex: string, factor: number): string {\n const match = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n if (!match) return hex;\n const r = Math.round(parseInt(match[1], 16) + (255 - parseInt(match[1], 16)) * factor);\n const g = Math.round(parseInt(match[2], 16) + (255 - parseInt(match[2], 16)) * factor);\n const b = Math.round(parseInt(match[3], 16) + (255 - parseInt(match[3], 16)) * factor);\n return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;\n}\n","import type { GraphNode, GraphEdge, FocusConfig } from './types.js';\nimport type { GraphModel } from './graph-model.js';\n\nconst DEFAULT_FOCUS: Required<FocusConfig> = {\n focalNode: null,\n hops: 2,\n maxNodes: 200,\n expandOnClick: true,\n};\n\n/**\n * Filters a large GraphModel down to a renderable subgraph\n * centered around a focal node with N-hop expansion.\n */\nexport class FocusFilter {\n private _config: Required<FocusConfig>;\n private _visibleNodes = new Set<string>();\n private _focalNode: string | null = null;\n private _enabled = false;\n\n constructor(config: FocusConfig | undefined) {\n this._config = { ...DEFAULT_FOCUS, ...config };\n this._focalNode = this._config.focalNode ?? null;\n }\n\n get focalNode(): string | null {\n return this._focalNode;\n }\n\n get enabled(): boolean {\n return this._enabled;\n }\n\n get visibleNodeIds(): ReadonlySet<string> {\n return this._visibleNodes;\n }\n\n /**\n * Compute the visible subgraph. Call this after data changes.\n * Automatically enables focus mode if the graph exceeds maxNodes.\n */\n compute(model: GraphModel): {\n nodes: Map<string, GraphNode>;\n edges: Map<string, GraphEdge>;\n } {\n // If graph is small enough, show everything\n if (model.nodes.size <= this._config.maxNodes) {\n this._enabled = false;\n // Mark all as non-boundary\n for (const node of model.nodes.values()) {\n node.isBoundary = false;\n }\n return { nodes: model.nodes, edges: model.edges };\n }\n\n this._enabled = true;\n\n // Pick focal node if not set\n if (!this._focalNode) {\n const best = model.getHighestDegreeNode();\n this._focalNode = best?.id ?? null;\n }\n\n if (!this._focalNode) {\n return { nodes: new Map(), edges: new Map() };\n }\n\n // BFS from focal node up to N hops\n this._visibleNodes.clear();\n const queue: Array<{ id: string; depth: number }> = [{ id: this._focalNode, depth: 0 }];\n this._visibleNodes.add(this._focalNode);\n\n while (queue.length > 0 && this._visibleNodes.size < this._config.maxNodes) {\n const current = queue.shift()!;\n\n if (current.depth >= this._config.hops) continue;\n\n const neighbors = model.getNeighborIds(current.id);\n for (const neighborId of neighbors) {\n if (this._visibleNodes.size >= this._config.maxNodes) break;\n\n if (!this._visibleNodes.has(neighborId)) {\n this._visibleNodes.add(neighborId);\n queue.push({ id: neighborId, depth: current.depth + 1 });\n }\n }\n }\n\n // Build filtered node/edge maps and mark boundary nodes\n const filteredNodes = new Map<string, GraphNode>();\n const filteredEdges = new Map<string, GraphEdge>();\n\n for (const id of this._visibleNodes) {\n const node = model.getNode(id);\n if (node) {\n // Check if this node has hidden neighbors (= boundary)\n const allNeighbors = model.getNeighborIds(id);\n node.isBoundary = false;\n for (const nid of allNeighbors) {\n if (!this._visibleNodes.has(nid)) {\n node.isBoundary = true;\n break;\n }\n }\n filteredNodes.set(id, node);\n }\n }\n\n for (const edge of model.edges.values()) {\n if (this._visibleNodes.has(edge.source) && this._visibleNodes.has(edge.target)) {\n filteredEdges.set(edge.id, edge);\n }\n }\n\n return { nodes: filteredNodes, edges: filteredEdges };\n }\n\n /** Change the focal node and recompute */\n setFocus(nodeId: string | null): void {\n this._focalNode = nodeId;\n }\n\n /** Expand a boundary node: add it as a secondary focus point */\n expandNode(nodeId: string, model: GraphModel): void {\n // Add the node's immediate neighbors to visible set\n const neighbors = model.getNeighborIds(nodeId);\n for (const nid of neighbors) {\n this._visibleNodes.add(nid);\n }\n }\n\n /** Disable focus filtering (show all) */\n disable(): void {\n this._enabled = false;\n this._visibleNodes.clear();\n }\n\n /** Re-enable with current or new config */\n enable(focalNode?: string): void {\n this._enabled = true;\n if (focalNode) this._focalNode = focalNode;\n }\n}\n","import type { ReificationConfig, ReificationPattern, PropertyAnnotation } from './types.js';\nimport { GraphModel } from './graph-model.js';\n\n/** Standard RDF reification pattern */\nconst STANDARD_RDF_REIFICATION: ReificationPattern = {\n statementType: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#Statement',\n subjectPredicate: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#subject',\n predicatePredicate: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#predicate',\n objectPredicate: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#object',\n};\n\nconst DEFAULT_CONFIG: Required<ReificationConfig> = {\n enabled: false,\n patterns: [STANDARD_RDF_REIFICATION],\n};\n\ninterface DetectedStatement {\n statementNodeId: string;\n reifiedSubject: string | null;\n reifiedPredicate: string | null;\n reifiedObject: string | null;\n /** All other properties on the statement node (provenance, timestamps, etc.) */\n annotations: Array<{ predicate: string; value: string; datatype?: string }>;\n}\n\n/**\n * Detects and collapses reified statement nodes.\n *\n * When a node matches a reification pattern (has the right rdf:type + subject/predicate/object links),\n * it is removed from the visual graph. Its non-structural properties (provenance, timestamps, etc.)\n * are attached as annotations to the corresponding property on the reified subject node.\n */\nexport class ReificationCollapser {\n private _config: Required<ReificationConfig>;\n\n constructor(config: ReificationConfig | undefined) {\n this._config = {\n enabled: config?.enabled ?? DEFAULT_CONFIG.enabled,\n patterns: config?.patterns ?? DEFAULT_CONFIG.patterns,\n };\n }\n\n get enabled(): boolean {\n return this._config.enabled;\n }\n\n /**\n * Process the graph model: detect reified statements, collapse them,\n * and attach their metadata as annotations on the original properties.\n *\n * Returns the set of node IDs that were collapsed (removed from visual rendering).\n */\n collapse(model: GraphModel): Set<string> {\n if (!this._config.enabled) return new Set();\n\n const collapsedNodeIds = new Set<string>();\n const detected = this.detectStatements(model);\n\n for (const stmt of detected) {\n if (!stmt.reifiedSubject) continue;\n\n const subjectNode = model.getNode(stmt.reifiedSubject);\n if (!subjectNode) continue;\n\n // Attach annotations to the correct property on the subject node\n if (stmt.reifiedPredicate && stmt.annotations.length > 0) {\n const annotations: PropertyAnnotation[] = stmt.annotations.map((a) => ({\n predicate: a.predicate,\n value: a.value,\n datatype: a.datatype,\n }));\n\n // Find the property values for the reified predicate\n const propValues = subjectNode.properties.get(stmt.reifiedPredicate);\n if (propValues) {\n // Try to match the specific value and attach annotations\n for (const pv of propValues) {\n if (stmt.reifiedObject && pv.value === stmt.reifiedObject) {\n pv.annotations = [...(pv.annotations ?? []), ...annotations];\n break;\n }\n }\n // If no specific match, attach to the first value\n if (propValues.length > 0 && !propValues.some((pv) => pv.annotations?.length)) {\n propValues[0].annotations = [...(propValues[0].annotations ?? []), ...annotations];\n }\n }\n\n // Also check metadata map\n const metaValues = subjectNode.metadata.get(stmt.reifiedPredicate);\n if (metaValues && metaValues.length > 0) {\n metaValues[0].annotations = [...(metaValues[0].annotations ?? []), ...annotations];\n }\n }\n\n // Mark the statement node for removal from visual graph\n collapsedNodeIds.add(stmt.statementNodeId);\n }\n\n return collapsedNodeIds;\n }\n\n /** Detect reified statements in the model */\n private detectStatements(model: GraphModel): DetectedStatement[] {\n const results: DetectedStatement[] = [];\n\n for (const pattern of this._config.patterns) {\n // Find all nodes with the statement type\n for (const node of model.nodes.values()) {\n if (!node.types.includes(pattern.statementType)) continue;\n\n const stmt: DetectedStatement = {\n statementNodeId: node.id,\n reifiedSubject: null,\n reifiedPredicate: null,\n reifiedObject: null,\n annotations: [],\n };\n\n // Extract subject/predicate/object links from edges\n const edgesFrom = model.getEdgesFrom(node.id);\n for (const edge of edgesFrom) {\n if (edge.predicate === pattern.subjectPredicate) {\n stmt.reifiedSubject = edge.target;\n } else if (edge.predicate === pattern.predicatePredicate) {\n stmt.reifiedPredicate = edge.target;\n } else if (edge.predicate === pattern.objectPredicate) {\n stmt.reifiedObject = edge.target;\n }\n }\n\n // Also check properties for the object (might be a literal)\n for (const [pred, values] of node.properties) {\n if (pred === pattern.objectPredicate && values.length > 0) {\n stmt.reifiedObject = values[0].value;\n }\n }\n\n // Also check metadata map — needed when the reification predicates\n // (aboutSubject, forPredicate, hasValue) are configured as metadata\n for (const [pred, values] of node.metadata) {\n if (values.length === 0) continue;\n if (!stmt.reifiedSubject && pred === pattern.subjectPredicate) {\n stmt.reifiedSubject = values[0].value;\n } else if (!stmt.reifiedPredicate && pred === pattern.predicatePredicate) {\n stmt.reifiedPredicate = values[0].value;\n } else if (!stmt.reifiedObject && pred === pattern.objectPredicate) {\n stmt.reifiedObject = values[0].value;\n }\n }\n\n // Everything else on the statement node becomes an annotation\n const structuralPreds = new Set([\n pattern.subjectPredicate,\n pattern.predicatePredicate,\n pattern.objectPredicate,\n ]);\n\n for (const [pred, values] of node.properties) {\n if (!structuralPreds.has(pred)) {\n for (const v of values) {\n stmt.annotations.push({ predicate: pred, value: v.value, datatype: v.datatype });\n }\n }\n }\n for (const [pred, values] of node.metadata) {\n for (const v of values) {\n stmt.annotations.push({ predicate: pred, value: v.value, datatype: v.datatype });\n }\n }\n\n results.push(stmt);\n }\n }\n\n return results;\n }\n}\n","import type { MetadataConfig } from './types.js';\n\n/** Default metadata predicates — properties shown in the metadata panel, not as edges */\nconst DEFAULT_METADATA_PREDICATES = [\n // PROV-O\n 'http://www.w3.org/ns/prov#wasGeneratedBy',\n 'http://www.w3.org/ns/prov#wasAttributedTo',\n 'http://www.w3.org/ns/prov#wasDerivedFrom',\n 'http://www.w3.org/ns/prov#startedAtTime',\n 'http://www.w3.org/ns/prov#endedAtTime',\n 'http://www.w3.org/ns/prov#generatedAtTime',\n 'http://www.w3.org/ns/prov#wasStartedBy',\n 'http://www.w3.org/ns/prov#wasEndedBy',\n // Dublin Core\n 'http://purl.org/dc/terms/created',\n 'http://purl.org/dc/terms/modified',\n 'http://purl.org/dc/terms/source',\n 'http://purl.org/dc/terms/creator',\n 'http://purl.org/dc/terms/publisher',\n // Common admin\n 'http://www.w3.org/2002/07/owl#versionInfo',\n];\n\n/**\n * Provides the list of predicates that should be treated as metadata\n * (shown in the detail panel, not rendered as graph edges).\n *\n * This is a simple config resolver — the actual separation happens in GraphModel.\n */\nexport class MetadataExtractor {\n private _predicates: Set<string>;\n\n constructor(config?: MetadataConfig) {\n this._predicates = new Set([\n ...DEFAULT_METADATA_PREDICATES,\n ...(config?.predicates ?? []),\n ]);\n }\n\n /** Get all metadata predicate URIs */\n get predicates(): string[] {\n return [...this._predicates];\n }\n\n /** Check if a predicate is a metadata predicate */\n isMetadata(predicateUri: string): boolean {\n return this._predicates.has(predicateUri);\n }\n\n /** Add additional metadata predicates at runtime */\n addPredicates(predicates: string[]): void {\n for (const p of predicates) {\n this._predicates.add(p);\n }\n }\n}\n","import type { GraphEventType, GraphEventMap, GraphEventHandler } from './types.js';\n\n/**\n * Typed event emitter for graph visualization events.\n */\nexport class GraphEventEmitter {\n private _handlers = new Map<GraphEventType, Set<GraphEventHandler<GraphEventType>>>();\n\n /** Subscribe to an event */\n on<T extends GraphEventType>(event: T, handler: GraphEventHandler<T>): () => void {\n let set = this._handlers.get(event);\n if (!set) {\n set = new Set();\n this._handlers.set(event, set);\n }\n set.add(handler as GraphEventHandler<GraphEventType>);\n\n // Return unsubscribe function\n return () => {\n set!.delete(handler as GraphEventHandler<GraphEventType>);\n };\n }\n\n /** Unsubscribe from an event */\n off<T extends GraphEventType>(event: T, handler: GraphEventHandler<T>): void {\n const set = this._handlers.get(event);\n if (set) {\n set.delete(handler as GraphEventHandler<GraphEventType>);\n }\n }\n\n /** Emit an event to all registered handlers */\n emit<T extends GraphEventType>(event: T, data: GraphEventMap[T]): void {\n const set = this._handlers.get(event);\n if (set) {\n for (const handler of set) {\n try {\n (handler as GraphEventHandler<T>)(data);\n } catch (err) {\n console.error(`[dkg-graph-viz] Error in ${event} handler:`, err);\n }\n }\n }\n }\n\n /** Remove all handlers for an event, or all handlers if no event specified */\n removeAll(event?: GraphEventType): void {\n if (event) {\n this._handlers.delete(event);\n } else {\n this._handlers.clear();\n }\n }\n}\n","/* eslint-disable @typescript-eslint/no-explicit-any */\nimport ForceGraph from 'force-graph';\nimport type { GraphNode, GraphEdge, ForceNode, ForceLink } from './types.js';\nimport type { RendererBackend, RendererBackendConfig } from './renderer-backend.js';\nimport type { AnimationConfig } from './view-config.js';\nimport type { ColorPalette } from './palette.js';\nimport { HexagonPainter } from './hexagon-painter.js';\nimport { StyleEngine } from './style-engine.js';\nimport { GraphEventEmitter } from './events.js';\n\n/**\n * 2D Canvas renderer backend using force-graph.\n *\n * Renders hexagonal nodes via HexagonPainter on an HTML5 Canvas 2D context.\n * Implements RendererBackend so it can be swapped with a 3D renderer.\n */\nexport class Canvas2DRenderer implements RendererBackend {\n private _container: HTMLElement;\n private _graph: any = null;\n private _hexPainter: HexagonPainter;\n private _styleEngine: StyleEngine;\n private _events: GraphEventEmitter;\n private _currentNodes = new Map<string, ForceNode>();\n private _positionCache = new Map<string, {\n x?: number;\n y?: number;\n vx?: number;\n vy?: number;\n fx?: number | null;\n fy?: number | null;\n }>();\n private _currentLinks: ForceLink[] = [];\n private _repaintPending = false;\n\n // Animation state\n private _riskPulseEnabled = false;\n private _hoverTraceEnabled = false;\n private _hoveredNodeId: string | null = null;\n private _fadeInEnabled = false;\n private _loadTime = 0;\n private _fadeInDuration = 1500;\n private _animFrameId: number | null = null;\n private _initialFitDone = false;\n private _autoFitDisabled = false;\n private _lastTopologyKey = '';\n private _continuousSimulation = false;\n private _fitTimeoutId: ReturnType<typeof setTimeout> | null = null;\n\n /** force-graph version compatibility guard */\n private _setAlphaTarget(value: number): void {\n const maybeFn = this._graph?.d3AlphaTarget;\n if (typeof maybeFn === 'function') {\n maybeFn.call(this._graph, value);\n }\n }\n\n // KA boundary state\n private _kaGroups: Map<string, Set<string>> = new Map();\n private _kaEnabled = false;\n private _kaBoundaryOpacity = 0.06;\n\n constructor(config: RendererBackendConfig) {\n this._container = config.container;\n this._hexPainter = config.hexPainter;\n this._styleEngine = config.styleEngine;\n this._events = config.events;\n }\n\n /** Initialize the force-graph instance */\n init(): void {\n if (this._graph) return;\n\n const styleConfig = this._styleEngine.config;\n\n const width = this._container.clientWidth || this._container.offsetWidth || 800;\n const height = this._container.clientHeight || this._container.offsetHeight || 600;\n\n // When an image finishes loading, nudge the graph to repaint\n this._hexPainter.setImageLoadCallback(() => {\n this._scheduleRepaint();\n });\n\n this._graph = (ForceGraph as any)()(this._container)\n .width(width)\n .height(height)\n .backgroundColor('transparent')\n .nodeCanvasObjectMode((node: any) => {\n // Featured nodes (sizeMultiplier) render in 'after' pass — always on top\n const fn = node as ForceNode;\n if (fn._graphNode?.sizeMultiplier && fn._graphNode.sizeMultiplier > 1) {\n return 'after';\n }\n return 'replace';\n })\n .nodeCanvasObject((node: any, ctx: CanvasRenderingContext2D, globalScale: number) => {\n const fn = node as ForceNode;\n if (!fn._graphNode) return;\n const x = fn.x ?? 0;\n const y = fn.y ?? 0;\n\n // Fade-in on initial load\n if (this._fadeInEnabled && this._loadTime > 0) {\n const elapsed = performance.now() - this._loadTime;\n if (elapsed < this._fadeInDuration) {\n const t = elapsed / this._fadeInDuration;\n const ease = 1 - Math.pow(1 - t, 3); // easeOutCubic\n ctx.globalAlpha = ease;\n }\n }\n\n // Risk pulse: sin-modulated scale for high-risk nodes (wall-clock time)\n let pulseScale = 1.0;\n if (this._riskPulseEnabled && fn._graphNode.isHighRisk) {\n const t = performance.now() / 1000; // seconds\n const hash = simpleHash(fn._graphNode.id);\n pulseScale = 1 + 0.15 * Math.sin(t * 1.25 + hash);\n }\n\n this._hexPainter.paint(ctx, fn._graphNode, x, y, globalScale, pulseScale);\n ctx.globalAlpha = 1;\n })\n .nodePointerAreaPaint((node: any, color: string, ctx: CanvasRenderingContext2D) => {\n const fn = node as ForceNode;\n if (!fn._graphNode) return;\n const radius = this._hexPainter.getRadius(fn._graphNode);\n const hitRadius = radius + 6;\n const x = fn.x ?? 0;\n const y = fn.y ?? 0;\n\n ctx.beginPath();\n ctx.arc(x, y, hitRadius, 0, Math.PI * 2);\n ctx.fillStyle = color;\n ctx.fill();\n })\n .linkColor((link: any) => {\n const fl = link as ForceLink;\n if (fl._graphEdge) {\n return this._styleEngine.getEdgeColor(fl._graphEdge);\n }\n return styleConfig.defaultEdgeColor;\n })\n .linkWidth(styleConfig.edgeWidth)\n .linkDirectionalArrowLength(styleConfig.edgeArrowSize)\n .linkDirectionalArrowRelPos(1)\n .linkCanvasObjectMode(() => 'after')\n .linkCanvasObject((link: any, ctx: CanvasRenderingContext2D, globalScale: number) => {\n if (globalScale < 0.3) return;\n\n const fl = link as ForceLink;\n if (!fl._graphEdge) return;\n\n const label = fl._graphEdge.label;\n if (!label) return;\n\n const fadeStart = 0.3;\n const fadeFull = 0.8;\n const opacity = Math.min(1, (globalScale - fadeStart) / (fadeFull - fadeStart));\n\n const fontSize = 12 / globalScale;\n\n const src = fl.source as any;\n const tgt = fl.target as any;\n if (!src.x || !tgt.x) return;\n\n const midX = (src.x + tgt.x) / 2;\n const midY = (src.y + tgt.y) / 2;\n\n ctx.save();\n ctx.globalAlpha = opacity;\n ctx.font = `${fontSize}px ${styleConfig.fontFamily}`;\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n\n // Background pill for readability\n const textWidth = ctx.measureText(label).width;\n const pad = 2 / globalScale;\n ctx.fillStyle = 'rgba(15, 23, 42, 0.75)';\n ctx.beginPath();\n ctx.roundRect(\n midX - textWidth / 2 - pad,\n midY - fontSize / 2 - pad,\n textWidth + pad * 2,\n fontSize + pad * 2,\n 2 / globalScale\n );\n ctx.fill();\n\n ctx.fillStyle = '#94a3b8';\n ctx.fillText(label, midX, midY);\n ctx.restore();\n })\n .onRenderFramePre((ctx: CanvasRenderingContext2D, _globalScale: number) => {\n this._drawKaBoundaries(ctx);\n })\n .onNodeClick((node: any) => {\n const fn = node as ForceNode;\n if (fn._graphNode) {\n this._events.emit('node:click', fn._graphNode);\n }\n })\n .onNodeDrag((_node: any) => {\n try {\n this._graph.d3ReheatSimulation();\n } catch {\n /* noop */\n }\n })\n .onNodeDragEnd((_node: any) => {\n try {\n this._graph.d3ReheatSimulation();\n } catch {\n /* noop */\n }\n })\n .onNodeHover((node: any) => {\n const fn = node as ForceNode | null;\n if (fn?._graphNode) {\n this._hoveredNodeId = fn._graphNode.id;\n this._events.emit('node:hover', fn._graphNode);\n this._container.style.cursor = 'pointer';\n } else {\n this._hoveredNodeId = null;\n this._events.emit('node:unhover', null as unknown as GraphNode);\n this._container.style.cursor = 'default';\n }\n })\n .onLinkClick((link: any) => {\n const fl = link as ForceLink;\n if (fl._graphEdge) {\n this._events.emit('edge:click', fl._graphEdge);\n }\n })\n .onBackgroundClick((_event: any) => {\n this._events.emit('background:click', { x: 0, y: 0 });\n })\n .cooldownTicks(200)\n .d3AlphaDecay(0.02)\n .d3VelocityDecay(0.3)\n .warmupTicks(60)\n .onEngineStop(() => {\n if (!this._autoFitDisabled && !this._initialFitDone && this._graph) {\n this._initialFitDone = true;\n this._graph.zoomToFit(400, 40);\n }\n });\n\n // Tune the default d3 forces for better spread with large graphs\n const charge = this._graph.d3Force('charge') as any;\n if (charge?.strength) { charge.strength(-150); charge.distanceMax?.(1000); }\n const link = this._graph.d3Force('link') as any;\n if (link?.distance) { link.distance(80).strength(0.2); }\n const center = this._graph.d3Force('center') as any;\n if (center?.strength) { center.strength(0.03); }\n }\n\n /**\n * Render the graph with the given nodes and edges.\n * Preserves positions of existing nodes for smooth transitions.\n */\n render(\n nodes: Map<string, GraphNode>,\n edges: Map<string, GraphEdge>,\n collapsedNodeIds?: Set<string>\n ): void {\n if (!this._graph) this.init();\n\n // Reset auto-fit when graph topology changes.\n // Sorted node IDs + sorted edge source->target keys for strong collision resistance.\n let topoHash = 0;\n const sortedNodeIds = [...nodes.keys()].sort();\n for (const id of sortedNodeIds) {\n topoHash = ((topoHash * 31) + simpleHash(id)) | 0;\n }\n const sortedEdgeKeys: string[] = [];\n for (const e of edges.values()) {\n sortedEdgeKeys.push(`${e.source}\\0${e.target}`);\n }\n sortedEdgeKeys.sort();\n for (const ek of sortedEdgeKeys) {\n topoHash = ((topoHash * 31) + simpleHash(ek)) | 0;\n }\n const topoKey = String(topoHash);\n if (topoKey !== this._lastTopologyKey) {\n this._lastTopologyKey = topoKey;\n this._initialFitDone = false;\n }\n\n // Record load time for fade-in animation\n if (this._loadTime === 0) {\n this._loadTime = performance.now();\n }\n\n // Update max degree for hexagon scaling\n let maxDegree = 1;\n for (const node of nodes.values()) {\n if (node.degree > maxDegree) maxDegree = node.degree;\n }\n this._hexPainter.setMaxDegree(maxDegree);\n\n // Build force-graph node array, preserving positions for existing nodes\n // and assigning chronological x-positions for dated nodes\n const dateNs = ['http://schema.org/dateCreated', 'https://schema.org/dateCreated'];\n const nodeDates = new Map<string, number>();\n for (const [id, gn] of nodes) {\n for (const ns of dateNs) {\n const vals = gn.properties?.get(ns);\n if (vals && vals.length > 0) {\n const d = new Date(vals[0].value);\n if (!isNaN(d.getTime())) { nodeDates.set(id, d.getTime()); break; }\n }\n }\n }\n let xByDate: Map<string, number> | null = null;\n if (nodeDates.size >= 2) {\n const times = [...nodeDates.values()];\n const minT = Math.min(...times);\n const maxT = Math.max(...times);\n const range = maxT - minT || 1;\n const spread = Math.max(400, nodeDates.size * 120);\n xByDate = new Map();\n for (const [id, t] of nodeDates) {\n xByDate.set(id, ((t - minT) / range - 0.5) * spread);\n }\n }\n\n const newNodes = new Map<string, ForceNode>();\n for (const [id, gn] of nodes) {\n if (collapsedNodeIds?.has(id)) continue;\n\n const existing = this._currentNodes.get(id);\n const cached = this._positionCache.get(id);\n const chronoX = xByDate?.get(id);\n newNodes.set(id, {\n id,\n x: existing?.x ?? cached?.x ?? chronoX ?? (Math.random() - 0.5) * 800,\n y: existing?.y ?? cached?.y ?? (Math.random() - 0.5) * 400,\n vx: existing?.vx ?? cached?.vx,\n vy: existing?.vy ?? cached?.vy,\n fx: typeof existing?.fx === 'number'\n ? existing.fx\n : (typeof cached?.fx === 'number' ? cached.fx : undefined),\n fy: typeof existing?.fy === 'number'\n ? existing.fy\n : (typeof cached?.fy === 'number' ? cached.fy : undefined),\n _graphNode: gn,\n });\n }\n\n // Build force-graph link array\n const validNodeIds = new Set(newNodes.keys());\n const newLinks: ForceLink[] = [];\n for (const edge of edges.values()) {\n if (collapsedNodeIds?.has(edge.source) || collapsedNodeIds?.has(edge.target)) continue;\n if (!validNodeIds.has(edge.source) || !validNodeIds.has(edge.target)) continue;\n\n newLinks.push({\n source: edge.source,\n target: edge.target,\n _graphEdge: edge,\n });\n }\n\n this._currentNodes = newNodes;\n this._currentLinks = newLinks;\n const freshCache = new Map<string, { x?: number; y?: number; vx?: number; vy?: number; fx?: number | null; fy?: number | null }>();\n for (const [id, node] of newNodes) {\n freshCache.set(id, {\n x: node.x,\n y: node.y,\n vx: node.vx,\n vy: node.vy,\n fx: typeof node.fx === 'number' ? node.fx : undefined,\n fy: typeof node.fy === 'number' ? node.fy : undefined,\n });\n }\n this._positionCache = freshCache;\n\n // Sort nodes so featured/high-risk nodes are last (painted on top)\n const sortedNodes = [...newNodes.values()].sort((a, b) => {\n const aOrder = (a._graphNode?.sizeMultiplier && a._graphNode.sizeMultiplier > 1) ? 100\n : a._graphNode?.isHighRisk ? 10 : 0;\n const bOrder = (b._graphNode?.sizeMultiplier && b._graphNode.sizeMultiplier > 1) ? 100\n : b._graphNode?.isHighRisk ? 10 : 0;\n return aOrder - bOrder;\n });\n\n this._graph.graphData({\n nodes: sortedNodes,\n links: newLinks,\n });\n\n // Schedule initial fit independently of engine stop (handles continuous mode too)\n if (!this._autoFitDisabled && !this._initialFitDone) {\n if (this._fitTimeoutId !== null) clearTimeout(this._fitTimeoutId);\n this._fitTimeoutId = setTimeout(() => {\n if (this._graph && !this._autoFitDisabled && !this._initialFitDone) {\n this._initialFitDone = true;\n this._graph.zoomToFit(400, 40);\n }\n this._fitTimeoutId = null;\n }, 300);\n }\n\n // Add a gentle x-force to maintain chronological left→right ordering\n if (xByDate && xByDate.size >= 2) {\n this._graph.d3Force('chronoX', (alpha: number) => {\n for (const fn of sortedNodes) {\n const targetX = xByDate!.get(fn.id);\n if (targetX !== undefined && fn.fx == null) {\n fn.vx = ((fn.vx ?? 0) + (targetX - (fn.x ?? 0)) * 0.05 * alpha);\n }\n }\n });\n } else {\n this._graph.d3Force('chronoX', null);\n }\n }\n\n /** Focus the camera on a specific node */\n centerOnNode(nodeId: string, durationMs = 350, zoomLevel?: number): void {\n const fn = this._currentNodes.get(nodeId);\n if (fn && this._graph) {\n this._graph.centerAt(fn.x, fn.y, durationMs);\n if (zoomLevel != null) {\n this._graph.zoom(zoomLevel, durationMs);\n }\n }\n }\n\n /** Fit all nodes in view */\n zoomToFit(padding = 40, durationMs = 500): void {\n this._graph?.zoomToFit(durationMs, padding);\n }\n\n /** Prevent the renderer from automatically calling zoomToFit when the simulation settles. */\n set autoFitDisabled(value: boolean) {\n this._autoFitDisabled = value;\n }\n\n /** Get the underlying canvas element for export */\n getCanvas(): HTMLCanvasElement | null {\n if (!this._graph) return null;\n const canvas = this._container.querySelector('canvas');\n return canvas ?? null;\n }\n\n /** Repaint node/edge visuals without rebuilding graph data. */\n refresh(): void {\n if (!this._graph) return;\n const maybeRefresh = this._graph.refresh;\n if (typeof maybeRefresh === 'function') {\n maybeRefresh.call(this._graph);\n return;\n }\n this._scheduleRepaint();\n }\n\n /**\n * Schedule a debounced repaint — used when images finish loading\n * after the simulation has cooled down. Limits the reheat to 3\n * ticks so the charge force can't displace settled nodes noticeably.\n */\n private _scheduleRepaint(): void {\n if (this._repaintPending) return;\n this._repaintPending = true;\n requestAnimationFrame(() => {\n this._repaintPending = false;\n if (!this._graph) return;\n\n if (this._continuousSimulation || this._riskPulseEnabled) {\n try { this._graph.d3ReheatSimulation(); } catch { /* noop */ }\n } else {\n this._graph.cooldownTicks(3);\n try { this._graph.d3ReheatSimulation(); } catch { /* noop */ }\n this._setAlphaTarget(0);\n setTimeout(() => {\n if (this._graph && !this._continuousSimulation && !this._riskPulseEnabled) {\n this._graph.cooldownTicks(0);\n this._setAlphaTarget(0);\n }\n }, 60);\n }\n });\n }\n\n /** Stop the animation loop */\n private _stopAnimLoop(): void {\n if (this._animFrameId !== null) {\n cancelAnimationFrame(this._animFrameId);\n this._animFrameId = null;\n }\n }\n\n /** Configure Knowledge Asset boundary rendering */\n setKaGroups(groups: Map<string, Set<string>>, enabled: boolean, opacity = 0.06): void {\n this._kaGroups = groups;\n this._kaEnabled = enabled;\n this._kaBoundaryOpacity = opacity;\n }\n\n /** Draw convex hulls around KA groups */\n private _drawKaBoundaries(ctx: CanvasRenderingContext2D): void {\n if (!this._kaEnabled || this._kaGroups.size === 0) return;\n\n const palette = this._styleEngine.palette;\n let colorIdx = 0;\n\n for (const [_kaUri, memberIds] of this._kaGroups) {\n const points: [number, number][] = [];\n for (const nodeId of memberIds) {\n const fn = this._currentNodes.get(nodeId);\n if (fn && fn.x !== undefined && fn.y !== undefined) {\n points.push([fn.x, fn.y]);\n }\n }\n if (points.length < 3) continue;\n\n const hull = convexHull(points);\n if (hull.length < 3) continue;\n\n const color = palette.nodeColors[colorIdx % palette.nodeColors.length];\n colorIdx++;\n\n ctx.save();\n ctx.beginPath();\n // Expand hull slightly with padding\n const cx = hull.reduce((s, p) => s + p[0], 0) / hull.length;\n const cy = hull.reduce((s, p) => s + p[1], 0) / hull.length;\n for (let i = 0; i < hull.length; i++) {\n const dx = hull[i][0] - cx;\n const dy = hull[i][1] - cy;\n const len = Math.sqrt(dx * dx + dy * dy);\n const pad = 30;\n const px = hull[i][0] + (len > 0 ? (dx / len) * pad : 0);\n const py = hull[i][1] + (len > 0 ? (dy / len) * pad : 0);\n if (i === 0) ctx.moveTo(px, py);\n else ctx.lineTo(px, py);\n }\n ctx.closePath();\n ctx.fillStyle = color;\n ctx.globalAlpha = this._kaBoundaryOpacity;\n ctx.fill();\n ctx.strokeStyle = color;\n ctx.globalAlpha = this._kaBoundaryOpacity * 2.5;\n ctx.lineWidth = 1;\n ctx.stroke();\n ctx.restore();\n }\n }\n\n /** Resize to fit container */\n resize(): void {\n if (this._graph) {\n this._graph.width(this._container.clientWidth);\n this._graph.height(this._container.clientHeight);\n }\n }\n\n /** Clean up */\n destroy(): void {\n this._stopAnimLoop();\n if (this._fitTimeoutId !== null) {\n clearTimeout(this._fitTimeoutId);\n this._fitTimeoutId = null;\n }\n if (this._graph) {\n this._graph._destructor();\n this._graph = null;\n }\n this._currentNodes.clear();\n this._positionCache.clear();\n this._currentLinks = [];\n }\n\n /** Apply animation configuration: link particles, drift, pulse, hover trace, fade-in */\n applyAnimation(config: AnimationConfig): void {\n if (!this._graph) return;\n\n // Risk pulse\n this._riskPulseEnabled = config.riskPulse ?? false;\n // Fade-in\n this._fadeInEnabled = config.fadeIn ?? false;\n // Hover trace\n this._hoverTraceEnabled = config.hoverTrace ?? false;\n\n const baseSpeed = config.linkParticleSpeed ?? 0.005;\n const baseColor = config.linkParticleColor ?? 'rgba(100,150,255,0.6)';\n const baseWidth = config.linkParticleWidth ?? 1.5;\n\n if (config.linkParticles || config.hoverTrace) {\n const particleCount = config.linkParticleCount ?? 1;\n\n if (this._hoverTraceEnabled) {\n // Per-link callbacks for hover trace\n this._graph\n .linkDirectionalParticles((link: any) => {\n if (!this._hoveredNodeId) return config.linkParticles ? particleCount : 0;\n const fl = link as ForceLink;\n const src = typeof fl.source === 'object' ? (fl.source as any).id : fl.source;\n const tgt = typeof fl.target === 'object' ? (fl.target as any).id : fl.target;\n const isHovered = src === this._hoveredNodeId || tgt === this._hoveredNodeId;\n return isHovered ? 4 : (config.linkParticles ? particleCount : 0);\n })\n .linkDirectionalParticleSpeed((link: any) => {\n if (!this._hoveredNodeId) return baseSpeed;\n const fl = link as ForceLink;\n const src = typeof fl.source === 'object' ? (fl.source as any).id : fl.source;\n const tgt = typeof fl.target === 'object' ? (fl.target as any).id : fl.target;\n return (src === this._hoveredNodeId || tgt === this._hoveredNodeId) ? 0.02 : baseSpeed;\n })\n .linkDirectionalParticleWidth((link: any) => {\n if (!this._hoveredNodeId) return baseWidth;\n const fl = link as ForceLink;\n const src = typeof fl.source === 'object' ? (fl.source as any).id : fl.source;\n const tgt = typeof fl.target === 'object' ? (fl.target as any).id : fl.target;\n return (src === this._hoveredNodeId || tgt === this._hoveredNodeId) ? 3 : baseWidth;\n })\n .linkDirectionalParticleColor((link: any) => {\n if (!this._hoveredNodeId) return baseColor;\n const fl = link as ForceLink;\n const src = typeof fl.source === 'object' ? (fl.source as any).id : fl.source;\n const tgt = typeof fl.target === 'object' ? (fl.target as any).id : fl.target;\n return (src === this._hoveredNodeId || tgt === this._hoveredNodeId) ? '#60a0ff' : baseColor;\n });\n } else {\n this._graph\n .linkDirectionalParticles(particleCount)\n .linkDirectionalParticleSpeed(baseSpeed)\n .linkDirectionalParticleWidth(baseWidth)\n .linkDirectionalParticleColor(() => baseColor);\n }\n } else {\n this._graph.linkDirectionalParticles(0);\n }\n\n // When drift or pulse is active, the simulation must run indefinitely\n const needsContinuousRender = config.drift || this._riskPulseEnabled;\n this._continuousSimulation = needsContinuousRender;\n\n if (needsContinuousRender) {\n this._graph.cooldownTicks(Infinity);\n this._setAlphaTarget(config.driftAlpha ?? 0.008);\n\n // render() runs before applyAnimation(), so the fallback in render() may\n // not fire when _continuousSimulation wasn't set yet. Trigger here too.\n if (!this._autoFitDisabled && !this._initialFitDone) {\n if (this._fitTimeoutId !== null) clearTimeout(this._fitTimeoutId);\n this._fitTimeoutId = setTimeout(() => {\n if (this._graph && !this._autoFitDisabled && !this._initialFitDone) {\n this._initialFitDone = true;\n this._graph.zoomToFit(400, 40);\n }\n this._fitTimeoutId = null;\n }, 300);\n }\n } else {\n this._graph.cooldownTicks(120);\n this._setAlphaTarget(0);\n }\n\n // Stop any previous animation loop before potentially starting a new one\n this._stopAnimLoop();\n\n // For risk pulse, we need force-graph to keep calling nodeCanvasObject.\n // Drift keeps the simulation warm, which triggers continuous redraws.\n // If pulse is on but drift is off, reheat periodically to force redraws.\n if (this._riskPulseEnabled && !config.drift) {\n const loop = () => {\n if (!this._riskPulseEnabled || !this._graph) {\n this._animFrameId = null;\n return;\n }\n try { this._graph.d3ReheatSimulation(); } catch { /* noop */ }\n this._animFrameId = requestAnimationFrame(loop);\n };\n this._animFrameId = requestAnimationFrame(loop);\n }\n }\n\n /** Apply palette: update background color */\n applyPalette(palette: ColorPalette): void {\n if (!this._graph) return;\n this._graph.backgroundColor(palette.background === '#0a0a0f' ? 'transparent' : palette.background);\n }\n}\n\n/** Simple numeric hash of a string (for phase-offsetting pulse animations) */\nfunction simpleHash(s: string): number {\n let h = 0;\n for (let i = 0; i < s.length; i++) {\n h = ((h << 5) - h + s.charCodeAt(i)) | 0;\n }\n return h;\n}\n\n/** Graham scan convex hull algorithm */\nfunction convexHull(points: [number, number][]): [number, number][] {\n if (points.length < 3) return points;\n const pts = [...points].sort((a, b) => a[0] - b[0] || a[1] - b[1]);\n\n const cross = (o: [number, number], a: [number, number], b: [number, number]) =>\n (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);\n\n const lower: [number, number][] = [];\n for (const p of pts) {\n while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0)\n lower.pop();\n lower.push(p);\n }\n\n const upper: [number, number][] = [];\n for (let i = pts.length - 1; i >= 0; i--) {\n const p = pts[i];\n while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0)\n upper.pop();\n upper.push(p);\n }\n\n lower.pop();\n upper.pop();\n return lower.concat(upper);\n}\n\n/**\n * @deprecated Use Canvas2DRenderer instead. This alias exists for backward compatibility.\n */\nexport const ForceGraphRenderer = Canvas2DRenderer;\n","import type {\n RdfGraphVizConfig,\n RdfTriple,\n GraphNode,\n GraphEdge,\n GraphEventType,\n GraphEventHandler,\n LabelMode,\n ChangeSet,\n} from './types.js';\nimport type { GraphDataSource } from '../data-sources/types.js';\nimport type { ViewConfig } from './view-config.js';\nimport type { RendererBackend } from './renderer-backend.js';\nimport { applyViewConfig } from './view-config.js';\nimport { resolvePalette, injectPaletteCssVars } from './palette.js';\nimport { ProvenanceResolver } from './provenance-resolver.js';\nimport { TemporalFilter } from './temporal-filter.js';\nimport { TimelineOverlay } from '../overlays/timeline-overlay.js';\nimport { GraphModel } from './graph-model.js';\nimport { PrefixManager } from './prefix-manager.js';\nimport { LabelResolver } from './label-resolver.js';\nimport { StyleEngine } from './style-engine.js';\nimport { HexagonPainter } from './hexagon-painter.js';\nimport { FocusFilter } from './focus-filter.js';\nimport { ReificationCollapser } from './reification-collapser.js';\nimport { MetadataExtractor } from './metadata-extractor.js';\nimport { GraphEventEmitter } from './events.js';\nimport { Canvas2DRenderer } from './renderer.js';\n\n/**\n * Main entry point for the RDF Graph Visualization library.\n *\n * @example\n * ```typescript\n * const viz = new RdfGraphViz(document.getElementById('graph')!, {\n * labelMode: 'humanized',\n * style: { classColors: { 'schema:Person': '#7C3AED' } },\n * focus: { hops: 2, maxNodes: 200 },\n * });\n *\n * await viz.loadNTriples(ntriplesString);\n * viz.on('node:click', (node) => console.log(node));\n * ```\n */\nexport class RdfGraphViz {\n private _model: GraphModel;\n private _prefixManager: PrefixManager;\n private _labelResolver: LabelResolver;\n private _styleEngine: StyleEngine;\n private _hexPainter: HexagonPainter;\n private _focusFilter: FocusFilter;\n private _reificationCollapser: ReificationCollapser;\n private _metadataExtractor: MetadataExtractor;\n private _events: GraphEventEmitter;\n private _renderer: RendererBackend;\n private _config: RdfGraphVizConfig;\n private _collapsedNodeIds = new Set<string>();\n private _rendererReady: Promise<void>;\n private _container: HTMLElement;\n private _temporalFilter: TemporalFilter;\n private _timelineOverlay: TimelineOverlay | null = null;\n private _renderQueued = false;\n private _autoFitDisabled = false;\n\n constructor(container: HTMLElement, config: RdfGraphVizConfig = {}) {\n this._config = config;\n this._container = container;\n\n // Initialize subsystems\n this._metadataExtractor = new MetadataExtractor(config.metadata);\n this._model = new GraphModel(this._metadataExtractor.predicates);\n this._prefixManager = new PrefixManager(config.prefixes);\n this._labelResolver = new LabelResolver(\n config.labelMode ?? 'humanized',\n this._prefixManager,\n config.labels\n );\n this._styleEngine = new StyleEngine(config.style, this._prefixManager);\n this._hexPainter = new HexagonPainter(config.hexagon, this._styleEngine, this._prefixManager);\n this._focusFilter = new FocusFilter(config.focus);\n this._reificationCollapser = new ReificationCollapser(config.reification);\n this._temporalFilter = new TemporalFilter();\n this._events = new GraphEventEmitter();\n\n const backendConfig = {\n container,\n hexPainter: this._hexPainter,\n styleEngine: this._styleEngine,\n events: this._events,\n };\n\n // Create renderer backend based on config (default: 2D canvas)\n if (config.renderer === '3d') {\n // Dynamic import for tree-shaking: 3d-force-graph only loaded when needed\n let resolveReady: () => void;\n this._rendererReady = new Promise((r) => { resolveReady = r; });\n\n // Temporary placeholder until async import resolves\n this._renderer = null as unknown as RendererBackend;\n\n import('./renderer-3d.js').then(({ WebGL3DRenderer }) => {\n this._renderer = new WebGL3DRenderer(backendConfig);\n resolveReady();\n }).catch((err) => {\n console.warn('[dkg-graph-viz] 3D renderer not available, falling back to 2D:', err.message);\n this._renderer = new Canvas2DRenderer(backendConfig);\n resolveReady();\n });\n } else {\n this._renderer = new Canvas2DRenderer(backendConfig);\n this._rendererReady = Promise.resolve();\n }\n\n if (config.autoFitDisabled) {\n this._autoFitDisabled = true;\n if (this._renderer) {\n this._renderer.autoFitDisabled = true;\n } else {\n this._rendererReady.then(() => {\n if (this._renderer) this._renderer.autoFitDisabled = true;\n });\n }\n }\n\n // Auto-resize on container resize\n if (typeof ResizeObserver !== 'undefined') {\n const observer = new ResizeObserver(() => this._renderer?.resize());\n observer.observe(container);\n }\n }\n\n // --- Data Loading ---\n\n /** Load N-Triples or N-Quads string */\n async loadNTriples(input: string): Promise<void> {\n const { parseNTriples } = await import('../parsers/ntriples.js');\n const triples = parseNTriples(input);\n this._ingest(triples);\n }\n\n /** Load N-Quads string */\n async loadNQuads(input: string): Promise<void> {\n const { parseNQuads } = await import('../parsers/ntriples.js');\n const triples = parseNQuads(input);\n this._ingest(triples);\n }\n\n /** Load Turtle string */\n async loadTurtle(input: string): Promise<void> {\n const { parseTurtle } = await import('../parsers/turtle.js');\n const { triples, prefixes } = parseTurtle(input);\n this._prefixManager.addPrefixes(prefixes);\n this._ingest(triples);\n }\n\n /** Load JSON-LD (object or string) */\n async loadJsonLd(input: Record<string, unknown> | string): Promise<void> {\n const { parseJsonLd } = await import('../parsers/jsonld.js');\n const triples = await parseJsonLd(input);\n this._ingest(triples);\n }\n\n /** Load raw triple array (synchronous — no parsing needed) */\n loadTriples(\n input: Array<RdfTriple | { s: string; p: string; o: string } | [string, string, string]>\n ): void {\n const triples: RdfTriple[] = input.map((item) => {\n if (Array.isArray(item)) {\n return { subject: item[0], predicate: item[1], object: item[2] };\n }\n if ('s' in item && 'p' in item && 'o' in item) {\n const spo = item as { s: string; p: string; o: string };\n return { subject: spo.s, predicate: spo.p, object: spo.o };\n }\n return item as RdfTriple;\n });\n this._ingest(triples);\n }\n\n // --- Data Source Loading ---\n\n /**\n * Load data from a GraphDataSource using a SPARQL CONSTRUCT query.\n *\n * This is the recommended way to load data from a knowledge graph.\n * The source can be an in-browser Oxigraph store or a remote SPARQL endpoint.\n *\n * @param source - A GraphDataSource (OxigraphSource, RemoteSparqlSource, etc.)\n * @param sparql - SPARQL CONSTRUCT query that returns the triples to visualize\n *\n * @example\n * ```typescript\n * const source = new OxigraphSource();\n * await source.init();\n * await source.loadNTriples(ntData);\n *\n * await viz.loadFromSource(source, `\n * CONSTRUCT { ?s ?p ?o }\n * WHERE {\n * ?s a <https://schema.org/Person> ;\n * ?p ?o .\n * } LIMIT 5000\n * `);\n * ```\n */\n async loadFromSource(source: GraphDataSource, sparql: string): Promise<void> {\n const result = await source.construct(sparql);\n this._ingest(result.triples);\n }\n\n /**\n * Execute a SPARQL SELECT query against a data source.\n * Returns raw bindings — useful for analytics, counts, and lookups\n * without loading triples into the graph model.\n */\n async querySource(source: GraphDataSource, sparql: string) {\n return source.select(sparql);\n }\n\n // --- Incremental Updates ---\n\n /** Add triples to the existing graph */\n addTriples(triples: RdfTriple[]): ChangeSet {\n const changes = this._model.addTriples(triples);\n this._postIngest();\n this._events.emit('data:change', changes);\n return changes;\n }\n\n /** Remove triples from the graph */\n removeTriples(triples: RdfTriple[]): ChangeSet {\n const changes = this._model.removeTriples(triples);\n this._postIngest();\n this._events.emit('data:change', changes);\n return changes;\n }\n\n // --- Navigation ---\n\n /** Focus on a specific node with N-hop expansion */\n focus(nodeId: string, hops?: number): void {\n this._focusFilter.setFocus(nodeId);\n if (hops !== undefined) {\n this._focusFilter = new FocusFilter({\n ...this._config.focus,\n focalNode: nodeId,\n hops,\n });\n }\n this._renderCurrent();\n this._renderer?.centerOnNode(nodeId, 500, 2);\n this._events.emit('focus:change', {\n focalNode: nodeId,\n visibleNodes: this._focusFilter.visibleNodeIds.size,\n });\n }\n\n /** Recenter camera on a node without changing focus filter state. */\n centerOnNode(nodeId: string, opts?: { durationMs?: number; zoomLevel?: number }): void {\n this._renderer?.centerOnNode(nodeId, opts?.durationMs, opts?.zoomLevel);\n }\n\n /** Expand a boundary node's neighborhood */\n expandNode(nodeId: string): void {\n this._focusFilter.expandNode(nodeId, this._model);\n this._renderCurrent();\n }\n\n /** Show all nodes (disable focus filter) */\n showAll(): void {\n this._focusFilter.disable();\n this._renderCurrent();\n }\n\n // --- Timeline ---\n\n /** Get the temporal filter (for advanced usage) */\n get temporalFilter(): TemporalFilter {\n return this._temporalFilter;\n }\n\n /** Get the timeline overlay (for advanced usage, null if not enabled) */\n get timelineOverlay(): TimelineOverlay | null {\n return this._timelineOverlay;\n }\n\n /** Set the temporal cursor: only show nodes with dates up to this point */\n setTimeCursor(date: Date): void {\n this._temporalFilter.setCursor(date);\n this._renderCurrent();\n this._events.emit('temporal:change', {\n cursor: date,\n visibleCount: this._lastVisibleCount(),\n totalDated: this._temporalFilter.datedNodeCount,\n });\n }\n\n /** Get the date range of nodes in the graph */\n getDateRange(): [Date, Date] | null {\n return this._temporalFilter.dateRange;\n }\n\n /** Start timeline playback */\n playTimeline(): void {\n this._timelineOverlay?.play();\n }\n\n /** Pause timeline playback */\n pauseTimeline(): void {\n this._timelineOverlay?.pause();\n }\n\n /** Count visible nodes after last render (for event payloads) */\n private _lastVisibleCount(): number {\n const { nodes } = this._focusFilter.compute(this._model);\n const temporalVisible = this._temporalFilter.getVisibleNodeIds(nodes.keys());\n return temporalVisible ? temporalVisible.size : nodes.size;\n }\n\n /** Fit all visible nodes in view */\n zoomToFit(padding?: number): void {\n this._renderer?.zoomToFit(padding);\n }\n\n /** Prevent the renderer from automatically calling zoomToFit when the simulation settles. */\n set autoFitDisabled(value: boolean) {\n this._autoFitDisabled = value;\n if (this._renderer) {\n this._renderer.autoFitDisabled = value;\n } else {\n this._rendererReady.then(() => {\n if (this._renderer) this._renderer.autoFitDisabled = value;\n });\n }\n }\n\n // --- Label Mode ---\n\n /** Get current label mode */\n get labelMode(): LabelMode {\n return this._labelResolver.mode;\n }\n\n /** Set label mode and re-render */\n setLabelMode(mode: LabelMode): void {\n this._labelResolver.mode = mode;\n this._labelResolver.updateLabels(this._model.nodes, this._model.edges);\n this._renderCurrent();\n }\n\n // --- Events ---\n\n /** Subscribe to a graph event */\n on<T extends GraphEventType>(event: T, handler: GraphEventHandler<T>): () => void {\n return this._events.on(event, handler);\n }\n\n /** Unsubscribe from a graph event */\n off<T extends GraphEventType>(event: T, handler: GraphEventHandler<T>): void {\n this._events.off(event, handler);\n }\n\n // --- Accessors ---\n\n /** Get a node by ID */\n getNode(id: string): GraphNode | undefined {\n return this._model.getNode(id);\n }\n\n /** Get all edges from a node */\n getEdgesFrom(id: string): GraphEdge[] {\n return this._model.getEdgesFrom(id);\n }\n\n /** Get all edges to a node */\n getEdgesTo(id: string): GraphEdge[] {\n return this._model.getEdgesTo(id);\n }\n\n /** Get the underlying graph model (for advanced usage) */\n get model(): GraphModel {\n return this._model;\n }\n\n /** Get the style engine (for dynamic per-node color overrides) */\n get styleEngine(): StyleEngine {\n return this._styleEngine;\n }\n\n /** Get the prefix manager (for compact/expand operations) */\n get prefixes(): PrefixManager {\n return this._prefixManager;\n }\n\n // --- Highlight ---\n\n /** Saved visual state for highlighted nodes, used by clearHighlight() to revert */\n private _highlightState = new Map<string, {\n riskScore?: number;\n isHighRisk?: boolean;\n sizeMultiplier?: number;\n }>();\n\n /**\n * Visually emphasize a set of nodes without modifying underlying data.\n * Typically used to highlight SPARQL query results in the current view.\n *\n * @example\n * ```typescript\n * const result = await viz.querySource(kg, `\n * SELECT ?post WHERE {\n * ?post <https://schema.org/interactionCount> ?risk .\n * FILTER(?risk > 50)\n * }\n * `);\n * viz.highlightNodes(result.bindings.map(b => b.post));\n * ```\n */\n highlightNodes(nodeIds: string[]): void {\n // Revert any previous highlight first\n this.clearHighlight();\n\n const idSet = new Set(nodeIds);\n\n for (const [id, node] of this._model.nodes) {\n if (idSet.has(id)) {\n // Save current state for revert\n this._highlightState.set(id, {\n riskScore: node.riskScore,\n isHighRisk: node.isHighRisk,\n sizeMultiplier: node.sizeMultiplier,\n });\n\n // Apply highlight emphasis\n node.isHighRisk = true;\n if (!node.sizeMultiplier || node.sizeMultiplier < 2.0) {\n node.sizeMultiplier = 2.0;\n }\n }\n }\n\n if (this._renderer?.refresh) this._renderer.refresh();\n else this._renderCurrent();\n }\n\n /** Revert all highlighted nodes to their original visual state */\n clearHighlight(): void {\n if (this._highlightState.size === 0) return;\n\n for (const [id, saved] of this._highlightState) {\n const node = this._model.nodes.get(id);\n if (node) {\n node.riskScore = saved.riskScore;\n node.isHighRisk = saved.isHighRisk;\n node.sizeMultiplier = saved.sizeMultiplier;\n }\n }\n\n this._highlightState.clear();\n if (this._renderer?.refresh) this._renderer.refresh();\n else this._renderCurrent();\n }\n\n // --- Export ---\n\n /**\n * Export the current visualization as a PNG image.\n * Captures the canvas content from the active renderer backend.\n */\n async exportImage(): Promise<Blob> {\n const canvas = this._renderer.getCanvas();\n if (!canvas) {\n throw new Error('Cannot export: renderer not initialized or no canvas available');\n }\n\n return new Promise<Blob>((resolve, reject) => {\n canvas.toBlob((blob) => {\n if (blob) resolve(blob);\n else reject(new Error('Canvas toBlob returned null'));\n }, 'image/png');\n });\n }\n\n /**\n * Export the current graph model as N-Triples string.\n * Serializes all triples currently loaded in the visualization.\n */\n exportTriples(): string {\n const lines: string[] = [];\n\n for (const triple of this._model.triples) {\n const subj = formatNTerm(triple.subject);\n const pred = formatNTerm(triple.predicate);\n\n if (triple.datatype || triple.language || !isUri(triple.object)) {\n // Literal\n let obj = `\"${escapeNTriples(triple.object)}\"`;\n if (triple.language) {\n obj += `@${triple.language}`;\n } else if (triple.datatype) {\n obj += `^^<${triple.datatype}>`;\n }\n lines.push(`${subj} ${pred} ${obj} .`);\n } else {\n lines.push(`${subj} ${pred} ${formatNTerm(triple.object)} .`);\n }\n }\n\n return lines.join('\\n');\n }\n\n // --- Lifecycle ---\n\n /**\n * Apply a ViewConfig to the current graph.\n *\n * Sets focal entity styling, platform icons, risk/highlight scores,\n * and node type icons based on the declarative config.\n * Automatically calls refresh() to re-render.\n *\n * @example\n * ```typescript\n * await viz.loadFromSource(kg, sparql);\n * viz.applyViewConfig(config);\n * ```\n */\n applyView(config: ViewConfig): void {\n // Resolve and apply palette\n const palette = resolvePalette(config.palette, config.paletteOverrides);\n this._styleEngine.setPalette(palette);\n\n // Inject CSS custom properties for demo/app UIs\n injectPaletteCssVars(this._container, palette);\n\n // Configure trust visualization\n if (config.trust?.enabled) {\n this._hexPainter.setTrustStyle(config.trust.style ?? 'border');\n this._hexPainter.setShowBadges(config.trust.showBadge !== false);\n } else {\n this._hexPainter.setTrustStyle('none');\n }\n\n applyViewConfig(config, this._model);\n\n // Resolve provenance for all nodes\n const provenanceResolver = new ProvenanceResolver();\n provenanceResolver.resolve(this._model);\n\n // Configure temporal filter\n if (config.temporal?.enabled) {\n this._temporalFilter = new TemporalFilter(config.temporal);\n this._temporalFilter.scan(this._model);\n\n // Mount timeline overlay if dates were found\n if (this._temporalFilter.dateRange) {\n // Unmount previous overlay if any\n this._timelineOverlay?.unmount();\n\n this._timelineOverlay = new TimelineOverlay(\n this._container,\n this._temporalFilter,\n config.temporal,\n );\n this._timelineOverlay.onChange((cursor) => {\n this._renderCurrent();\n this._events.emit('temporal:change', {\n cursor,\n visibleCount: this._lastVisibleCount(),\n totalDated: this._temporalFilter.datedNodeCount,\n });\n });\n this._timelineOverlay.mount();\n }\n } else {\n // Disable timeline if previously enabled\n this._timelineOverlay?.unmount();\n this._timelineOverlay = null;\n }\n\n this._renderCurrent();\n\n // Configure KA membership grouping\n if (config.knowledgeAssets?.enabled) {\n const kaPred = config.knowledgeAssets.membershipPredicate ?? 'dkg:partOfAsset';\n this._model.setKaMembershipPredicate(kaPred);\n }\n\n // Apply animation after renderer is fully ready (3D init is async)\n this._rendererReady.then(() => {\n if (config.animation && this._renderer?.applyAnimation) {\n this._renderer.applyAnimation(config.animation);\n }\n // Apply palette to renderer (background, etc.)\n if (this._renderer?.applyPalette) {\n this._renderer.applyPalette(palette);\n }\n // Apply KA boundaries\n if (config.knowledgeAssets?.enabled && this._renderer?.setKaGroups) {\n this._renderer.setKaGroups(\n this._model.kaGroups,\n config.knowledgeAssets.showBoundaries !== false,\n config.knowledgeAssets.boundaryOpacity ?? 0.06\n );\n }\n });\n }\n\n /** Force a visual re-render (call after modifying node properties externally) */\n refresh(): void {\n this._renderCurrent();\n }\n\n /** Destroy the visualization and clean up resources */\n destroy(): void {\n this._timelineOverlay?.unmount();\n this._timelineOverlay = null;\n this._renderer?.destroy();\n this._events.removeAll();\n this._model.clear();\n this._highlightState.clear();\n }\n\n // --- Internal ---\n\n /** Ingest triples, resolve labels/images, and render */\n private _ingest(triples: RdfTriple[]): void {\n this._model.addTriples(triples);\n this._postIngest();\n }\n\n /** Post-ingestion processing: resolve images, labels, collapse, render */\n private _postIngest(): void {\n // Resolve image URLs for all nodes (direct and indirect)\n for (const node of this._model.nodes.values()) {\n if (!node.imageUrl) {\n node.imageUrl = this._hexPainter.resolveImageUrl(\n node,\n (id) => this._model.getNode(id),\n (id) => this._model.getEdgesFrom(id)\n );\n }\n }\n\n // Resolve labels\n this._labelResolver.updateLabels(this._model.nodes, this._model.edges);\n\n // Collapse reified statements\n this._collapsedNodeIds = this._reificationCollapser.collapse(this._model);\n\n this._renderCurrent();\n }\n\n /** Render with current focus filter + temporal filter state */\n private _renderCurrent(): void {\n // In 3D mode, renderer module loads asynchronously.\n // Queue one deferred render instead of throwing on early data loads.\n if (!this._renderer) {\n if (!this._renderQueued) {\n this._renderQueued = true;\n this._rendererReady.then(() => {\n this._renderQueued = false;\n this._renderCurrent();\n });\n }\n return;\n }\n\n let { nodes, edges } = this._focusFilter.compute(this._model);\n\n // Apply temporal filter: hide nodes outside the current time window\n const temporalVisible = this._temporalFilter.getVisibleNodeIds(nodes.keys());\n if (temporalVisible) {\n const filteredNodes = new Map<string, GraphNode>();\n const filteredEdges = new Map<string, GraphEdge>();\n\n for (const [id, node] of nodes) {\n if (temporalVisible.has(id)) {\n filteredNodes.set(id, node);\n }\n }\n for (const [id, edge] of edges) {\n if (filteredNodes.has(edge.source) && filteredNodes.has(edge.target)) {\n filteredEdges.set(id, edge);\n }\n }\n\n nodes = filteredNodes;\n edges = filteredEdges;\n }\n\n this._renderer.render(nodes, edges, this._collapsedNodeIds);\n\n // Update timeline overlay count badge\n if (this._timelineOverlay?.mounted) {\n this._timelineOverlay.updateCount(nodes.size);\n }\n }\n}\n\n// --- N-Triples serialization helpers ---\n\nfunction isUri(value: string): boolean {\n return value.startsWith('http://') || value.startsWith('https://') ||\n value.startsWith('urn:') || value.startsWith('_:');\n}\n\nfunction formatNTerm(uri: string): string {\n if (uri.startsWith('_:')) return uri;\n return `<${uri}>`;\n}\n\nfunction escapeNTriples(str: string): string {\n return str\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, '\\\\n')\n .replace(/\\r/g, '\\\\r')\n .replace(/\\t/g, '\\\\t');\n}\n","import type { RdfTriple } from '../core/types.js';\nimport type {\n GraphDataSource,\n SparqlSelectResult,\n SparqlConstructResult,\n SparqlBinding,\n} from './types.js';\n\n/**\n * In-browser knowledge graph powered by Oxigraph WASM.\n *\n * Provides a full SPARQL 1.1 store that runs entirely in the browser —\n * perfect for development, demos, and offline usage.\n *\n * @example\n * ```typescript\n * const source = new OxigraphSource();\n * await source.init();\n * await source.loadNTriples(ntriplesString);\n *\n * const result = await source.construct(`\n * CONSTRUCT { ?s ?p ?o }\n * WHERE { ?s a <https://schema.org/SocialMediaPosting> ; ?p ?o }\n * LIMIT 1000\n * `);\n *\n * viz.loadTriples(result.triples);\n * ```\n */\nexport class OxigraphSource implements GraphDataSource {\n readonly name = 'Oxigraph (in-browser)';\n private _store: any = null;\n private _oxigraph: any = null;\n\n /**\n * Initialize the Oxigraph WASM module.\n * Must be called before any other method.\n *\n * @param wasmUrl - Optional URL to the `web_bg.wasm` file.\n * In Vite/browser environments, pass this explicitly to avoid\n * bundler issues with WASM file resolution:\n * ```\n * await source.init('/data/oxigraph.wasm');\n * ```\n * In Node.js, this can be omitted.\n */\n async init(wasmUrl?: string): Promise<void> {\n if (this._store) return;\n\n // Dynamic import — uses the web entry point for browser\n const oxigraph = await import('oxigraph/web');\n\n // Initialize WASM — pass explicit URL if provided (needed for Vite/bundlers)\n if (wasmUrl) {\n await oxigraph.default(wasmUrl);\n } else if (typeof oxigraph.default === 'function') {\n await oxigraph.default();\n }\n\n this._oxigraph = oxigraph;\n this._store = new oxigraph.Store();\n }\n\n private ensureReady(): void {\n if (!this._store) {\n throw new Error('OxigraphSource not initialized. Call init() first.');\n }\n }\n\n async loadNTriples(data: string): Promise<void> {\n this.ensureReady();\n this._store.load(data, { format: 'application/n-triples' });\n }\n\n async loadTurtle(data: string): Promise<void> {\n this.ensureReady();\n this._store.load(data, { format: 'text/turtle' });\n }\n\n async select(sparql: string): Promise<SparqlSelectResult> {\n this.ensureReady();\n\n const results = this._store.query(sparql);\n\n // Oxigraph returns an array of Map<Variable, Term> for SELECT\n const bindings: SparqlBinding[] = [];\n const variableSet = new Set<string>();\n\n for (const row of results) {\n const binding: SparqlBinding = {};\n for (const [variable, term] of row) {\n const varName = variable.value;\n variableSet.add(varName);\n binding[varName] = this.termToString(term);\n }\n bindings.push(binding);\n }\n\n return {\n variables: [...variableSet],\n bindings,\n };\n }\n\n async construct(sparql: string): Promise<SparqlConstructResult> {\n this.ensureReady();\n\n const quads = this._store.query(sparql);\n const triples: RdfTriple[] = [];\n\n for (const quad of quads) {\n triples.push({\n subject: this.termToString(quad.subject),\n predicate: this.termToString(quad.predicate),\n object: this.termToString(quad.object),\n datatype: quad.object.datatype?.value,\n language: quad.object.language || undefined,\n });\n }\n\n return { triples };\n }\n\n async tripleCount(): Promise<number> {\n this.ensureReady();\n return this._store.size;\n }\n\n async isReady(): Promise<boolean> {\n return this._store !== null;\n }\n\n /** Get the raw Oxigraph store for advanced usage */\n get store(): any {\n return this._store;\n }\n\n /** Convert an Oxigraph RDF term to a string */\n private termToString(term: any): string {\n if (!term) return '';\n\n switch (term.termType) {\n case 'NamedNode':\n return term.value;\n case 'BlankNode':\n return `_:${term.value}`;\n case 'Literal': {\n // Return just the value string — datatype/language handled separately\n return term.value;\n }\n default:\n return term.value ?? String(term);\n }\n }\n}\n","import type { RdfTriple } from '../core/types.js';\nimport type {\n GraphDataSource,\n SparqlSelectResult,\n SparqlConstructResult,\n SparqlBinding,\n} from './types.js';\n\nexport interface RemoteSparqlConfig {\n /** SPARQL endpoint URL (e.g., http://localhost:9999/blazegraph/sparql) */\n endpoint: string;\n /** Optional namespace/repository path appended to endpoint */\n namespace?: string;\n /** Additional HTTP headers (e.g., for auth) */\n headers?: Record<string, string>;\n /** Request timeout in milliseconds (default: 30000) */\n timeoutMs?: number;\n}\n\n/**\n * Remote SPARQL endpoint data source.\n *\n * Connects to any SPARQL 1.1 compatible endpoint (Blazegraph, GraphDB,\n * OriginTrail DKG node, Virtuoso, etc.) via HTTP.\n *\n * @example\n * ```typescript\n * const source = new RemoteSparqlSource({\n * endpoint: 'http://localhost:9999/blazegraph/sparql',\n * namespace: 'kb',\n * });\n *\n * const result = await source.construct(`\n * CONSTRUCT { ?s ?p ?o }\n * WHERE { ?s a <https://schema.org/SocialMediaPosting> ; ?p ?o }\n * LIMIT 1000\n * `);\n *\n * viz.loadTriples(result.triples);\n * ```\n */\nexport class RemoteSparqlSource implements GraphDataSource {\n readonly name: string;\n private _config: Required<RemoteSparqlConfig>;\n\n constructor(config: RemoteSparqlConfig) {\n this._config = {\n endpoint: config.endpoint,\n namespace: config.namespace ?? '',\n headers: config.headers ?? {},\n timeoutMs: config.timeoutMs ?? 30000,\n };\n this.name = `SPARQL: ${this.endpointUrl}`;\n }\n\n private get endpointUrl(): string {\n const base = this._config.endpoint.replace(/\\/$/, '');\n return this._config.namespace ? `${base}/namespace/${this._config.namespace}/sparql` : base;\n }\n\n async loadNTriples(_data: string): Promise<void> {\n // For remote sources, data is already in the store.\n // Could implement SPARQL UPDATE INSERT DATA if needed.\n console.warn('RemoteSparqlSource.loadNTriples: remote stores are read-only from the viz. Load data via your KG admin tools.');\n }\n\n async select(sparql: string): Promise<SparqlSelectResult> {\n const response = await this.executeQuery(sparql, 'application/sparql-results+json');\n const json = await response.json();\n\n const variables = json.head?.vars ?? [];\n const bindings: SparqlBinding[] = (json.results?.bindings ?? []).map((row: any) => {\n const binding: SparqlBinding = {};\n for (const v of variables) {\n if (row[v]) {\n binding[v] = row[v].value;\n }\n }\n return binding;\n });\n\n return { variables, bindings };\n }\n\n async construct(sparql: string): Promise<SparqlConstructResult> {\n const response = await this.executeQuery(sparql, 'application/n-triples');\n const ntText = await response.text();\n const triples = this.parseNTriples(ntText);\n return { triples };\n }\n\n async tripleCount(): Promise<number> {\n const result = await this.select('SELECT (COUNT(*) AS ?count) WHERE { ?s ?p ?o }');\n return parseInt(result.bindings[0]?.count ?? '0', 10);\n }\n\n async isReady(): Promise<boolean> {\n try {\n const controller = new AbortController();\n setTimeout(() => controller.abort(), 5000);\n const response = await fetch(this.endpointUrl, {\n method: 'GET',\n signal: controller.signal,\n });\n return response.ok;\n } catch {\n return false;\n }\n }\n\n private async executeQuery(sparql: string, accept: string): Promise<Response> {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), this._config.timeoutMs);\n\n try {\n // W3C SPARQL 1.1 Protocol \"query via POST directly\" (§2.1.3): send the\n // raw query as the request body with `Content-Type:\n // application/sparql-query`, NOT URL-encoded form data.\n //\n // The old `application/x-www-form-urlencoded` + `query=<encoded>` form\n // hits Jetty's default 200 KB form-content limit on Blazegraph\n // (`UTFDataFormatException` / HTTP 400 \"Unable to parse form content\")\n // for large queries — e.g. a CONSTRUCT/SELECT carrying a big `VALUES`\n // block — which is the same defect fixed in the Blazegraph/SPARQL-HTTP\n // storage adapters. Direct POST has no such form-size cap and is\n // supported by every SPARQL 1.1 endpoint (Blazegraph, GraphDB,\n // Virtuoso, OriginTrail DKG node, …).\n const response = await fetch(this.endpointUrl, {\n method: 'POST',\n headers: {\n // Caller-supplied headers (e.g. auth) go FIRST so the\n // protocol-required headers below cannot be overridden. The body is\n // always a raw SPARQL query, so a caller default like\n // `Content-Type: application/x-www-form-urlencoded` would corrupt the\n // request (and recreate the large-query form-size regression) or make\n // a compliant endpoint reject it.\n ...this._config.headers,\n 'Content-Type': 'application/sparql-query',\n 'Accept': accept,\n },\n body: sparql,\n signal: controller.signal,\n });\n\n if (!response.ok) {\n throw new Error(`SPARQL query failed: ${response.status} ${response.statusText}`);\n }\n\n return response;\n } finally {\n clearTimeout(timeout);\n }\n }\n\n /** Minimal N-Triples parser for CONSTRUCT results */\n private parseNTriples(text: string): RdfTriple[] {\n const triples: RdfTriple[] = [];\n const lines = text.split('\\n');\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith('#')) continue;\n\n // Match: <subject> <predicate> <object> . OR <s> <p> \"literal\"^^<dt> .\n const uriMatch = trimmed.match(/^<([^>]+)>\\s+<([^>]+)>\\s+<([^>]+)>\\s*\\.$/);\n if (uriMatch) {\n triples.push({\n subject: uriMatch[1],\n predicate: uriMatch[2],\n object: uriMatch[3],\n });\n continue;\n }\n\n const litMatch = trimmed.match(/^<([^>]+)>\\s+<([^>]+)>\\s+\"(.+)\"(?:\\^\\^<([^>]+)>|@(\\S+))?\\s*\\.$/);\n if (litMatch) {\n triples.push({\n subject: litMatch[1],\n predicate: litMatch[2],\n object: litMatch[3],\n datatype: litMatch[4],\n language: litMatch[5],\n });\n }\n }\n\n return triples;\n }\n}\n"]}
1
+ {"version":3,"sources":["/Users/aleatoric/dev/dkg-npm-10.0.2/packages/graph-viz/dist/index.cjs","../src/core/view-config.ts","../src/core/palette.ts","../src/core/provenance-resolver.ts","../src/core/temporal-filter.ts","../src/overlays/timeline-overlay.ts","../src/core/graph-model.ts","../src/core/prefix-manager.ts","../src/core/label-resolver.ts","../src/core/style-engine.ts","../src/core/hexagon-painter.ts","../src/core/focus-filter.ts","../src/core/reification-collapser.ts","../src/core/metadata-extractor.ts","../src/core/events.ts","../src/core/renderer.ts","../src/core/rdf-graph-viz.ts","../src/data-sources/oxigraph-source.ts","../src/data-sources/remote-sparql-source.ts"],"names":["range","link"],"mappings":"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","file":"/Users/aleatoric/dev/dkg-npm-10.0.2/packages/graph-viz/dist/index.cjs","sourcesContent":[null,"import type { GraphNode, GraphEdge } from './types.js';\nimport type { GraphModel } from './graph-model.js';\nimport type { ColorPalette } from './palette.js';\nimport type { TemporalConfig } from './temporal-filter.js';\n\n// --- Namespace resolution helpers ---\n\nconst KNOWN_NAMESPACES = [\n 'https://guardiankg.org/vocab/',\n 'https://umanitek.ai/dkg/vocab/',\n 'http://umanitek.ai/dkg/vocab/',\n 'https://schema.org/',\n 'http://schema.org/',\n];\n\n/** Get a property value checking all known namespace variants */\nfunction getAnyNS(node: GraphNode, shortName: string): string | null {\n for (const ns of KNOWN_NAMESPACES) {\n const vals = node.properties?.get(ns + shortName);\n if (vals && vals.length > 0) return vals[0].value;\n }\n return null;\n}\n\n// --- ViewConfig Type ---\n\n/** Visual configuration for a single node type */\nexport interface NodeTypeConfig {\n color?: string;\n shape?: 'hexagon' | 'circle';\n /** Icon URL or data URI — assigned as imageUrl on matching nodes */\n icon?: string;\n /** Static size multiplier for all nodes of this type */\n sizeMultiplier?: number;\n}\n\n/** Focal entity configuration */\nexport interface FocalConfig {\n /** URI of the focal entity (checked across namespace variants) */\n uri: string;\n /** Image URL for the focal entity */\n image?: string;\n /** Size multiplier (default 1.5) */\n sizeMultiplier?: number;\n}\n\n/** Highlight / risk rule configuration */\nexport interface HighlightConfig {\n /** Property short name that drives visual emphasis (checked across namespaces) */\n property: string;\n /** Where to find the property: on the node itself, or on a linked entity */\n source?: 'self' | 'linked';\n /** Predicate short name to follow when source='linked' (e.g., 'hasSentiment') */\n linkedVia?: string;\n /** Absolute value above this threshold = highlighted */\n threshold: number;\n /** Border color for highlighted nodes */\n color: string;\n /** How many top-scoring nodes get the size boost (default 100) */\n topN?: number;\n /**\n * Size multiplier for top-N nodes.\n * If sizeMin/sizeMax are set, this is ignored (continuous scaling is used).\n * Otherwise this is a flat multiplier applied to all top-N nodes (default 2).\n */\n sizeMultiplier?: number;\n /** Minimum sizeMultiplier for continuous scaling (lowest-risk in top N). Default: 1.0 */\n sizeMin?: number;\n /** Maximum sizeMultiplier for continuous scaling (highest-risk in top N). Default: 3.0 */\n sizeMax?: number;\n /**\n * Invert the scoring: treat LOW values as high risk.\n * Useful when the property represents a \"safety\" signal (e.g., sentimentScore)\n * where 0.0 = dangerous and 1.0 = safe.\n * Default: false\n */\n invert?: boolean;\n}\n\n/** Size-by rule: scale node size based on a property */\nexport interface SizeByConfig {\n /** Property short name (e.g., 'totalInteractions') */\n property: string;\n /** Scale function (default 'log') */\n scale?: 'linear' | 'log';\n}\n\n/** Icon mapping: assign icons to posts by platform edge */\nexport interface PlatformIconConfig {\n /** Map of platform ID (last path segment of platform URI) to icon URL/data URI */\n icons: Record<string, string>;\n /** URL fallback patterns: map of URL substring to platform key */\n urlFallbacks?: Record<string, string>;\n}\n\n/** Configuration for a single tooltip field */\nexport interface TooltipFieldConfig {\n /** Display label in the tooltip */\n label: string;\n /** Property short name to display (checked across known namespaces) */\n property: string;\n /** Where to find the property: directly on the node ('self') or on a linked entity */\n source?: 'self' | 'linked';\n /** Predicate short name to follow when source='linked' */\n linkedVia?: string;\n /** Display format: 'text' (default), 'number', 'date' */\n format?: 'text' | 'number' | 'date';\n}\n\n/** Tooltip configuration */\nexport interface TooltipConfig {\n /**\n * Properties to try for the tooltip title, in priority order.\n * Each entry is a short name checked across known namespaces.\n * Falls back to the node's resolved label if none match.\n *\n * @example [\"text\", \"name\", \"title\"]\n */\n titleProperties?: string[];\n\n /**\n * Template for the tooltip subtitle line.\n * Supports {property} placeholders (short names).\n * Special tokens: {platform}, {author}, {date}, {type}\n *\n * @example \"{platform} · {author} · {date}\"\n */\n subtitleTemplate?: string;\n\n /** Max characters for the title before truncation. Default: 60 */\n titleMaxLength?: number;\n\n /** Additional fields to show in the tooltip */\n fields?: TooltipFieldConfig[];\n}\n\n/** Animation configuration for visual liveliness */\nexport interface AnimationConfig {\n /** Enable link particles flowing along edges. Default: false */\n linkParticles?: boolean;\n /** Number of particles per link. Default: 1 */\n linkParticleCount?: number;\n /** Particle speed (0-1 range, fraction of link length per frame). Default: 0.005 */\n linkParticleSpeed?: number;\n /** Particle color. Default: 'rgba(100,150,255,0.6)' */\n linkParticleColor?: string;\n /** Particle width in pixels. Default: 1.5 */\n linkParticleWidth?: number;\n /** Keep the force simulation slightly warm so nodes gently drift. Default: false */\n drift?: boolean;\n /** Alpha target for the warm simulation (0-0.1 range). Default: 0.008 */\n driftAlpha?: number;\n /** Enable breathing/pulse animation on high-risk nodes. Default: false */\n riskPulse?: boolean;\n /** Highlight edges of hovered node with faster, brighter particles. Default: false */\n hoverTrace?: boolean;\n /** Fade-in nodes/edges on initial load. Default: false */\n fadeIn?: boolean;\n}\n\n/** Trust visualization configuration */\nexport interface TrustConfig {\n /** Enable trust indicators. Default: false */\n enabled: boolean;\n /** Minimum distinct sources for \"verified\" visual treatment. Default: 2 */\n minSources?: number;\n /** Show source count badge on nodes. Default: true */\n showBadge?: boolean;\n /** Visual style for multi-source trust: 'border' | 'opacity' | 'glow'. Default: 'border' */\n style?: 'opacity' | 'border' | 'glow';\n}\n\n/** Knowledge Asset boundary visualization configuration */\nexport interface KnowledgeAssetConfig {\n /** Enable KA boundary rendering. Default: false */\n enabled: boolean;\n /** Predicate linking a node to its Knowledge Asset. Default: 'dkg:partOfAsset' */\n membershipPredicate?: string;\n /** Show convex hull boundaries around KA groups. Default: true */\n showBoundaries?: boolean;\n /** Boundary fill opacity (0-1). Default: 0.06 */\n boundaryOpacity?: number;\n}\n\n/**\n * Declarative view configuration for the graph visualizer.\n *\n * Controls what nodes look like, which are important, and how they're sized —\n * all without writing code. The same graph data can be visualized differently\n * by swapping view configs.\n */\nexport interface ViewConfig {\n /** Human-readable name for this view */\n name: string;\n\n /** Visual rules per RDF type (compact or full URI) */\n nodeTypes?: Record<string, NodeTypeConfig>;\n\n /** Optional focal entity (rendered large, always on top) */\n focal?: FocalConfig;\n\n /** Highlight rule: which property drives risk/importance coloring */\n highlight?: HighlightConfig;\n\n /** Size-by rule: scale nodes by a numeric property */\n sizeBy?: SizeByConfig;\n\n /** RDF type short names rendered as circles instead of hexagons */\n circleTypes?: string[];\n\n /** Platform icon mapping for SocialMediaPosting nodes */\n platformIcons?: PlatformIconConfig;\n\n /** Tooltip configuration for hover cards */\n tooltip?: TooltipConfig;\n\n /** Animation configuration for visual liveliness */\n animation?: AnimationConfig;\n\n /** Color palette: preset name ('dark', 'midnight', 'cyberpunk', 'light') or custom palette object */\n palette?: string | ColorPalette;\n /** Overrides for individual palette colors (merged on top of base) */\n paletteOverrides?: Partial<ColorPalette>;\n\n /** Trust visualization based on multi-source provenance */\n trust?: TrustConfig;\n\n /** Knowledge Asset boundary visualization */\n knowledgeAssets?: KnowledgeAssetConfig;\n\n /** Temporal timeline configuration */\n temporal?: TemporalConfig;\n\n /** Default SPARQL CONSTRUCT query for this view */\n defaultSparql?: string;\n}\n\n// --- ViewConfig Applicator ---\n\n/**\n * Apply a ViewConfig to a loaded graph model.\n *\n * Sets riskScore, isHighRisk, sizeMultiplier, and imageUrl on nodes\n * based on the declarative rules in the config.\n *\n * Call this AFTER loading triples into the model, BEFORE refresh().\n */\nexport function applyViewConfig(config: ViewConfig, model: GraphModel): void {\n // 1. Focal entity\n if (config.focal) {\n const focalNode = findNodeAcrossNamespaces(model, config.focal.uri);\n if (focalNode) {\n focalNode.sizeMultiplier = config.focal.sizeMultiplier ?? 1.5;\n if (config.focal.image) {\n focalNode.imageUrl = config.focal.image;\n }\n }\n }\n\n // 2. Platform icons\n if (config.platformIcons) {\n for (const node of model.nodes.values()) {\n if (node.imageUrl) continue;\n if (!node.types.some(t => t.includes('SocialMediaPosting'))) continue;\n\n // Check platform edges\n const platformEdges = model.getEdgesFrom(node.id).filter(e => e.predicate.includes('platform'));\n for (const edge of platformEdges) {\n const platformId = edge.target.split('/').pop() ?? '';\n if (config.platformIcons.icons[platformId]) {\n node.imageUrl = config.platformIcons.icons[platformId];\n break;\n }\n }\n\n // URL fallback\n if (!node.imageUrl && config.platformIcons.urlFallbacks) {\n for (const [substr, platformKey] of Object.entries(config.platformIcons.urlFallbacks)) {\n if (node.id.includes(substr) && config.platformIcons.icons[platformKey]) {\n node.imageUrl = config.platformIcons.icons[platformKey];\n break;\n }\n }\n }\n }\n }\n\n // 3. Highlight / risk scoring — continuous size scaling\n if (config.highlight) {\n const hl = config.highlight;\n const scores: Array<{ id: string; score: number }> = [];\n const invert = hl.invert === true;\n\n for (const [id, node] of model.nodes) {\n let value: number | null = null;\n\n if (hl.source === 'linked' && hl.linkedVia) {\n const edges = model.getEdgesFrom(id).filter(e => e.predicate.includes(hl.linkedVia!));\n if (edges.length > 0) {\n const linked = model.nodes.get(edges[0].target);\n if (linked) {\n const raw = getAnyNS(linked, hl.property);\n if (raw !== null) value = parseFloat(raw);\n }\n }\n } else {\n const raw = getAnyNS(node, hl.property);\n if (raw !== null) value = parseFloat(raw);\n }\n\n if (value !== null && !isNaN(value)) {\n const threshold = hl.threshold;\n let score: number;\n let include = false;\n if (value < 0) {\n // Negative values: magnitude = risk (e.g. negative sentiment)\n const riskMagnitude = Math.abs(value);\n node.riskScore = riskMagnitude;\n score = riskMagnitude;\n include = invert ? (riskMagnitude <= threshold && riskMagnitude > 0) : (riskMagnitude >= threshold);\n } else {\n // Positive values: normal = high is risky, invert = low is risky\n node.riskScore = value;\n if (invert) {\n include = value <= threshold;\n score = include ? threshold - value : 0; // lower value → higher score (riskier)\n } else {\n include = value >= threshold;\n score = value;\n }\n }\n if (include) {\n scores.push({ id, score });\n }\n }\n }\n\n // Mark top N as high-risk with CONTINUOUS size scaling\n const topN = hl.topN ?? 100;\n // Both modes: sort descending so highest risk score is first and gets largest size.\n // (Invert uses score = threshold - value, so low value → high score.)\n scores.sort((a, b) => b.score - a.score);\n const topScores = scores.slice(0, topN);\n\n if (topScores.length > 0) {\n const minSize = hl.sizeMin ?? 1.0;\n const maxSize = hl.sizeMax ?? (hl.sizeMultiplier ?? 3.0);\n const rawFirst = topScores[0].score;\n const rawLast = topScores[topScores.length - 1].score;\n\n for (const { id, score } of topScores) {\n const node = model.nodes.get(id);\n if (!node) continue;\n\n node.isHighRisk = true;\n\n // Don't override focal entity's sizeMultiplier\n if (node.sizeMultiplier) continue;\n\n // Log-scale normalization: highest risk (first in topScores) gets normalized 1 → maxSize\n let normalized: number;\n const scoreRange = Math.abs(rawFirst - rawLast);\n if (scoreRange > 0) {\n const logMin = Math.log1p(Math.min(rawFirst, rawLast));\n const logMax = Math.log1p(Math.max(rawFirst, rawLast));\n const logRange = logMax - logMin;\n const logScore = Math.log1p(score);\n const logNorm = logRange > 0 ? (logScore - logMin) / logRange : 0.5;\n normalized = logNorm;\n } else {\n normalized = 1.0;\n }\n\n node.sizeMultiplier = minSize + normalized * (maxSize - minSize);\n }\n }\n }\n\n // 4. Size-by rule: scale nodes by a numeric property\n if (config.sizeBy) {\n const sb = config.sizeBy;\n const values: Array<{ id: string; value: number }> = [];\n\n for (const [id, node] of model.nodes) {\n // Skip nodes that already have explicit sizeMultiplier (focal, highlight)\n if (node.sizeMultiplier) continue;\n\n const raw = getAnyNS(node, sb.property);\n if (raw !== null) {\n const v = parseFloat(raw);\n if (!isNaN(v) && v > 0) {\n values.push({ id, value: v });\n }\n }\n }\n\n if (values.length > 0) {\n const maxVal = Math.max(...values.map(v => v.value));\n const minVal = Math.min(...values.map(v => v.value));\n\n for (const { id, value } of values) {\n const node = model.nodes.get(id);\n if (!node || node.sizeMultiplier) continue;\n\n let normalized: number;\n if (sb.scale === 'log') {\n // Log scale: handles power-law distributions (social media metrics)\n const logMin = Math.log1p(minVal);\n const logMax = Math.log1p(maxVal);\n const logRange = logMax - logMin;\n normalized = logRange > 0 ? (Math.log1p(value) - logMin) / logRange : 0.5;\n } else {\n // Linear scale\n const range = maxVal - minVal;\n normalized = range > 0 ? (value - minVal) / range : 0.5;\n }\n\n // Map to sizeMultiplier range: 0.5x (min) to 3x (max)\n node.sizeMultiplier = 0.5 + normalized * 2.5;\n }\n }\n }\n\n // 5. Node type icons (for types that specify an icon but aren't platform-based)\n if (config.nodeTypes) {\n for (const node of model.nodes.values()) {\n if (node.imageUrl) continue;\n for (const type of node.types) {\n const shortType = type.split('/').pop()?.split('#').pop() ?? '';\n // Check both full URI and short forms\n const typeConfig = config.nodeTypes[type]\n || config.nodeTypes[`schema:${shortType}`]\n || config.nodeTypes[`vocab:${shortType}`]\n || config.nodeTypes[shortType];\n if (typeConfig?.icon) {\n node.imageUrl = typeConfig.icon;\n break;\n }\n }\n }\n }\n}\n\n/** Find a node trying multiple namespace variants of a URI */\nfunction findNodeAcrossNamespaces(model: GraphModel, uri: string): GraphNode | null {\n // Try exact match first\n const exact = model.nodes.get(uri);\n if (exact) return exact;\n\n // Extract the path part after the last known namespace prefix\n const pathPart = uri.replace(/^https?:\\/\\/[^\\/]+\\//, '');\n\n // Try common namespace bases\n const bases = [\n 'https://guardiankg.org/',\n 'https://umanitek.ai/dkg/',\n 'http://umanitek.ai/dkg/',\n ];\n\n for (const base of bases) {\n const candidate = model.nodes.get(base + pathPart);\n if (candidate) return candidate;\n }\n\n return null;\n}\n","/**\n * Color palette system for the graph visualizer.\n *\n * A palette provides a complete, cohesive set of colors for the entire\n * visualization — surfaces, text, accents, semantic colors, and graph-specific\n * colors. Developers can use a built-in preset or supply a custom palette.\n */\n\nexport interface ColorPalette {\n name: string;\n\n // Surface\n background: string;\n surface: string;\n surfaceBorder: string;\n\n // Text\n textPrimary: string;\n textSecondary: string;\n textMuted: string;\n\n // Accent / brand\n primary: string;\n primaryMuted: string;\n\n // Semantic\n danger: string;\n warning: string;\n safe: string;\n info: string;\n\n // Graph-specific\n edgeColor: string;\n edgeLabel: string;\n particleColor: string;\n focalGlow: string;\n\n // Auto-assigned node type colors (cycled across uncolored types)\n nodeColors: string[];\n}\n\n// ── Built-in presets ──\n\nexport const PALETTE_DARK: ColorPalette = {\n name: 'dark',\n background: '#0a0a0f',\n surface: 'rgba(20, 20, 30, 0.95)',\n surfaceBorder: 'rgba(100, 100, 140, 0.3)',\n textPrimary: '#e2e8f0',\n textSecondary: '#94a3b8',\n textMuted: '#64748b',\n primary: '#6366f1',\n primaryMuted: 'rgba(120, 120, 220, 0.5)',\n danger: '#e04040',\n warning: '#f59e0b',\n safe: '#10b981',\n info: '#3b82f6',\n edgeColor: '#1a1a3a',\n edgeLabel: '#94a3b8',\n particleColor: 'rgba(100, 160, 255, 0.5)',\n focalGlow: 'rgba(99, 102, 241, 0.3)',\n nodeColors: [\n '#6366f1', '#8b5cf6', '#a855f7', '#3b82f6',\n '#06b6d4', '#10b981', '#f59e0b', '#ef4444',\n '#ec4899', '#14b8a6', '#f97316', '#8b5cf6',\n ],\n};\n\nexport const PALETTE_MIDNIGHT: ColorPalette = {\n name: 'midnight',\n background: '#050510',\n surface: 'rgba(10, 10, 25, 0.95)',\n surfaceBorder: 'rgba(60, 80, 160, 0.3)',\n textPrimary: '#c8d6e5',\n textSecondary: '#7f8fa6',\n textMuted: '#4a5568',\n primary: '#3b82f6',\n primaryMuted: 'rgba(59, 130, 246, 0.4)',\n danger: '#dc2626',\n warning: '#d97706',\n safe: '#059669',\n info: '#2563eb',\n edgeColor: '#0f1a3a',\n edgeLabel: '#7f8fa6',\n particleColor: 'rgba(59, 130, 246, 0.5)',\n focalGlow: 'rgba(59, 130, 246, 0.3)',\n nodeColors: [\n '#3b82f6', '#2563eb', '#1d4ed8', '#06b6d4',\n '#0891b2', '#0e7490', '#8b5cf6', '#7c3aed',\n '#6366f1', '#4f46e5', '#14b8a6', '#0d9488',\n ],\n};\n\nexport const PALETTE_CYBERPUNK: ColorPalette = {\n name: 'cyberpunk',\n background: '#0a0a0a',\n surface: 'rgba(15, 15, 15, 0.95)',\n surfaceBorder: 'rgba(0, 255, 136, 0.2)',\n textPrimary: '#e0ffe0',\n textSecondary: '#80c080',\n textMuted: '#407040',\n primary: '#00ff88',\n primaryMuted: 'rgba(0, 255, 136, 0.3)',\n danger: '#ff2d6a',\n warning: '#ffaa00',\n safe: '#00ff88',\n info: '#00d4ff',\n edgeColor: '#0a1a0a',\n edgeLabel: '#80c080',\n particleColor: 'rgba(0, 255, 136, 0.5)',\n focalGlow: 'rgba(0, 255, 136, 0.3)',\n nodeColors: [\n '#00ff88', '#ff2d6a', '#00d4ff', '#ffaa00',\n '#ff6b35', '#a855f7', '#06ffd0', '#ff1493',\n '#39ff14', '#ff6ec7', '#00bfff', '#ffd700',\n ],\n};\n\nexport const PALETTE_LIGHT: ColorPalette = {\n name: 'light',\n background: '#f8fafc',\n surface: 'rgba(255, 255, 255, 0.95)',\n surfaceBorder: 'rgba(148, 163, 184, 0.3)',\n textPrimary: '#1e293b',\n textSecondary: '#475569',\n textMuted: '#94a3b8',\n primary: '#4f46e5',\n primaryMuted: 'rgba(79, 70, 229, 0.3)',\n danger: '#dc2626',\n warning: '#d97706',\n safe: '#059669',\n info: '#2563eb',\n edgeColor: '#cbd5e1',\n edgeLabel: '#475569',\n particleColor: 'rgba(79, 70, 229, 0.4)',\n focalGlow: 'rgba(79, 70, 229, 0.2)',\n nodeColors: [\n '#4f46e5', '#7c3aed', '#9333ea', '#2563eb',\n '#0891b2', '#059669', '#d97706', '#dc2626',\n '#db2777', '#0d9488', '#ea580c', '#7c3aed',\n ],\n};\n\n/** All built-in palettes indexed by name */\nexport const PALETTES: Record<string, ColorPalette> = {\n dark: PALETTE_DARK,\n midnight: PALETTE_MIDNIGHT,\n cyberpunk: PALETTE_CYBERPUNK,\n light: PALETTE_LIGHT,\n};\n\n/**\n * Resolve a palette from a name string or custom object, with optional overrides.\n * Falls back to 'dark' if the name is not found.\n */\nexport function resolvePalette(\n palette?: string | ColorPalette,\n overrides?: Partial<ColorPalette>,\n): ColorPalette {\n let base: ColorPalette;\n\n if (!palette) {\n base = PALETTE_DARK;\n } else if (typeof palette === 'string') {\n base = PALETTES[palette] ?? PALETTE_DARK;\n } else {\n base = palette;\n }\n\n if (!overrides) return base;\n\n return { ...base, ...overrides, name: overrides.name ?? base.name };\n}\n\n/**\n * Inject palette colors as CSS custom properties on a container element.\n * Demo UIs can reference these as var(--gv-primary), etc.\n */\nexport function injectPaletteCssVars(container: HTMLElement, palette: ColorPalette): void {\n const props: Record<string, string> = {\n '--gv-bg': palette.background,\n '--gv-surface': palette.surface,\n '--gv-surface-border': palette.surfaceBorder,\n '--gv-text': palette.textPrimary,\n '--gv-text-secondary': palette.textSecondary,\n '--gv-text-muted': palette.textMuted,\n '--gv-primary': palette.primary,\n '--gv-primary-muted': palette.primaryMuted,\n '--gv-danger': palette.danger,\n '--gv-warning': palette.warning,\n '--gv-safe': palette.safe,\n '--gv-info': palette.info,\n '--gv-edge': palette.edgeColor,\n '--gv-particle': palette.particleColor,\n '--gv-focal-glow': palette.focalGlow,\n };\n\n for (const [prop, value] of Object.entries(props)) {\n container.style.setProperty(prop, value);\n }\n}\n","import type { GraphNode, ProvenanceInfo } from './types.js';\nimport type { GraphModel } from './graph-model.js';\n\n/**\n * Known provenance-related predicate URIs.\n * These are checked across multiple namespace variants.\n */\nconst PROV_PREDICATES = {\n wasDerivedFrom: [\n 'http://www.w3.org/ns/prov#wasDerivedFrom',\n ],\n wasGeneratedBy: [\n 'http://www.w3.org/ns/prov#wasGeneratedBy',\n ],\n generatedAtTime: [\n 'http://www.w3.org/ns/prov#generatedAtTime',\n ],\n contentHash: [\n 'https://umanitek.ai/dkg/vocab/contentHash',\n 'https://guardiankg.org/vocab/contentHash',\n 'http://umanitek.ai/dkg/vocab/contentHash',\n ],\n dataSource: [\n 'https://umanitek.ai/dkg/vocab/dataSource',\n 'https://guardiankg.org/vocab/dataSource',\n 'http://umanitek.ai/dkg/vocab/dataSource',\n ],\n};\n\nconst NAME_PREDICATES = [\n 'https://schema.org/name',\n 'http://schema.org/name',\n 'http://xmlns.com/foaf/0.1/name',\n 'http://www.w3.org/2000/01/rdf-schema#label',\n];\n\nconst DATA_SOURCE_TYPES = [\n 'https://umanitek.ai/dkg/vocab/DataSource',\n 'http://umanitek.ai/dkg/vocab/DataSource',\n 'https://guardiankg.org/vocab/DataSource',\n 'http://www.w3.org/ns/prov#Agent',\n];\n\n/**\n * Resolves provenance information for graph nodes by traversing\n * metadata properties and following provenance edges.\n *\n * Populates `node.provenance` with:\n * - Data sources (URIs + human-readable names)\n * - Generation timestamp\n * - Generating agent/model\n * - Content hash\n * - UAL and blockchain anchor (when available)\n */\nexport class ProvenanceResolver {\n /**\n * Resolve provenance for all nodes in the model.\n * Should be called after data is loaded and before rendering.\n */\n resolve(model: GraphModel): void {\n for (const node of model.nodes.values()) {\n node.provenance = this._resolveNodeProvenance(node, model);\n }\n }\n\n private _resolveNodeProvenance(node: GraphNode, model: GraphModel): ProvenanceInfo {\n const info: ProvenanceInfo = {\n sources: [],\n sourceNames: [],\n };\n\n // 1. Content hash (from properties or metadata)\n info.contentHash = this._findValue(node, PROV_PREDICATES.contentHash);\n\n // 2. Generation timestamp\n info.generatedAt = this._findMetadataValue(node, PROV_PREDICATES.generatedAtTime);\n\n // 3. Generated by (agent/model)\n const generatedByUri = this._findMetadataValue(node, PROV_PREDICATES.wasGeneratedBy);\n if (generatedByUri) {\n info.generatedBy = generatedByUri;\n // Try to resolve the agent's name\n const agentNode = model.nodes.get(generatedByUri);\n if (agentNode) {\n info.generatedByName = this._resolveName(agentNode) ?? undefined;\n } else {\n // Use the last path segment as a fallback name\n info.generatedByName = generatedByUri.split('/').pop() ?? generatedByUri;\n }\n }\n\n // 4. Data sources — follow wasDerivedFrom edges\n const sourceUris = new Set<string>();\n const sourceNames: string[] = [];\n\n // Direct edges from this node\n const edges = model.getEdgesFrom(node.id);\n for (const edge of edges) {\n if (PROV_PREDICATES.wasDerivedFrom.some(p => edge.predicate === p)) {\n const targetNode = model.nodes.get(edge.target);\n if (targetNode) {\n // Check if this is a DataSource entity\n const isDataSource = targetNode.types.some(t => DATA_SOURCE_TYPES.includes(t));\n if (isDataSource) {\n if (!sourceUris.has(edge.target)) {\n sourceUris.add(edge.target);\n const name = this._resolveName(targetNode) ?? edge.target.split('/').pop() ?? edge.target;\n sourceNames.push(name);\n }\n }\n }\n }\n }\n\n // Also check metadata for wasDerivedFrom (may be stored as metadata)\n for (const pred of PROV_PREDICATES.wasDerivedFrom) {\n const vals = node.metadata?.get(pred);\n if (vals) {\n for (const v of vals) {\n const uri = v.value;\n if (sourceUris.has(uri)) continue;\n const targetNode = model.nodes.get(uri);\n if (targetNode) {\n const isDataSource = targetNode.types.some(t => DATA_SOURCE_TYPES.includes(t));\n if (isDataSource) {\n sourceUris.add(uri);\n const name = this._resolveName(targetNode) ?? uri.split('/').pop() ?? uri;\n sourceNames.push(name);\n }\n }\n }\n }\n }\n\n // Also check for direct dataSource predicates\n for (const pred of PROV_PREDICATES.dataSource) {\n const vals = node.properties?.get(pred) ?? node.metadata?.get(pred);\n if (vals) {\n for (const v of vals) {\n const uri = v.value;\n if (sourceUris.has(uri)) continue;\n sourceUris.add(uri);\n const targetNode = model.nodes.get(uri);\n const name = targetNode ? (this._resolveName(targetNode) ?? uri.split('/').pop() ?? uri) : (uri.split('/').pop() ?? uri);\n sourceNames.push(name);\n }\n }\n }\n\n info.sources = [...sourceUris];\n info.sourceNames = sourceNames;\n\n return info;\n }\n\n /** Find a value from a node's properties, checking multiple predicate URIs */\n private _findValue(node: GraphNode, predicates: string[]): string | undefined {\n for (const pred of predicates) {\n const vals = node.properties?.get(pred);\n if (vals && vals.length > 0) return vals[0].value;\n }\n return undefined;\n }\n\n /** Find a value from a node's metadata, checking multiple predicate URIs */\n private _findMetadataValue(node: GraphNode, predicates: string[]): string | undefined {\n for (const pred of predicates) {\n const vals = node.metadata?.get(pred);\n if (vals && vals.length > 0) return vals[0].value;\n }\n // Also check properties as fallback\n return this._findValue(node, predicates);\n }\n\n /** Resolve a human-readable name for a node */\n private _resolveName(node: GraphNode): string | null {\n for (const pred of NAME_PREDICATES) {\n const vals = node.properties?.get(pred);\n if (vals && vals.length > 0) return vals[0].value;\n }\n return null;\n }\n}\n","import type { GraphNode } from './types.js';\nimport type { GraphModel } from './graph-model.js';\n\n/**\n * Configuration for temporal filtering.\n */\nexport interface TemporalConfig {\n /** Enable the temporal timeline. Default: false */\n enabled: boolean;\n /** Property short names to scan for dates (checked across known namespaces).\n * Default: ['dateCreated'] */\n dateProperties?: string[];\n /** Whether to show nodes that have no date property. Default: true */\n showUndated?: boolean;\n /** Auto-play speed in ms per step. Default: 200 */\n playSpeed?: number;\n /** Step granularity for playback. Default: 'day' */\n stepSize?: 'hour' | 'day' | 'week' | 'month';\n}\n\nconst KNOWN_DATE_NAMESPACES = [\n 'https://schema.org/',\n 'http://schema.org/',\n 'https://guardiankg.org/vocab/',\n 'https://umanitek.ai/dkg/vocab/',\n 'http://umanitek.ai/dkg/vocab/',\n 'http://www.w3.org/ns/prov#',\n];\n\n/**\n * Temporal filter: scans nodes for date properties, manages a progressive\n * time cursor, and returns the set of node IDs visible up to that cursor.\n *\n * Usage:\n * filter.scan(model);\n * filter.setCursor(someDate);\n * const visible = filter.getVisibleNodeIds(); // nodes with date <= cursor\n */\nexport class TemporalFilter {\n private _nodeDates = new Map<string, Date>();\n private _dateRange: [Date, Date] | null = null;\n private _cursor: Date | null = null;\n private _dateProperties: string[];\n private _showUndated: boolean;\n\n constructor(config?: Partial<TemporalConfig>) {\n this._dateProperties = config?.dateProperties ?? ['dateCreated'];\n this._showUndated = config?.showUndated ?? true;\n }\n\n /** Scan all nodes in the model for date properties. Call after data ingestion. */\n scan(model: GraphModel): void {\n this._nodeDates.clear();\n\n let earliest: Date | null = null;\n let latest: Date | null = null;\n\n for (const [id, node] of model.nodes) {\n const date = this._extractDate(node);\n if (date) {\n this._nodeDates.set(id, date);\n if (!earliest || date < earliest) earliest = date;\n if (!latest || date > latest) latest = date;\n }\n }\n\n if (earliest && latest) {\n this._dateRange = [earliest, latest];\n // Default cursor: show everything\n this._cursor = latest;\n } else {\n this._dateRange = null;\n this._cursor = null;\n }\n }\n\n /** Get the global date range [earliest, latest], or null if no dates found. */\n get dateRange(): [Date, Date] | null {\n return this._dateRange;\n }\n\n /** Get the current cursor position. */\n get cursor(): Date | null {\n return this._cursor;\n }\n\n /** Number of nodes that have a date assigned. */\n get datedNodeCount(): number {\n return this._nodeDates.size;\n }\n\n /** Whether to show nodes without date properties. */\n get showUndated(): boolean {\n return this._showUndated;\n }\n\n set showUndated(value: boolean) {\n this._showUndated = value;\n }\n\n /** Set the cursor (cutoff date). Nodes with date <= cursor are visible. */\n setCursor(date: Date): void {\n this._cursor = date;\n }\n\n /** Get the date for a specific node, or null if undated. */\n getNodeDate(nodeId: string): Date | null {\n return this._nodeDates.get(nodeId) ?? null;\n }\n\n /**\n * Get the set of node IDs visible at the current cursor position.\n * - Dated nodes: visible if date <= cursor\n * - Undated nodes: visible if showUndated is true\n *\n * If no date range was found (no dated nodes), returns null\n * (meaning: don't filter, show everything).\n */\n getVisibleNodeIds(allNodeIds: Iterable<string>): Set<string> | null {\n if (!this._dateRange || !this._cursor) return null;\n\n const visible = new Set<string>();\n const cursor = this._cursor.getTime();\n\n for (const id of allNodeIds) {\n const date = this._nodeDates.get(id);\n if (date) {\n if (date.getTime() <= cursor) {\n visible.add(id);\n }\n } else if (this._showUndated) {\n visible.add(id);\n }\n }\n\n return visible;\n }\n\n /**\n * Compute histogram buckets for the date distribution.\n * Returns an array of { date: Date, count: number } entries.\n */\n computeHistogram(bucketCount: number = 50): Array<{ date: Date; count: number }> {\n if (!this._dateRange) return [];\n\n const [earliest, latest] = this._dateRange;\n const range = latest.getTime() - earliest.getTime();\n if (range <= 0) return [{ date: earliest, count: this._nodeDates.size }];\n\n const bucketSize = range / bucketCount;\n const buckets: Array<{ date: Date; count: number }> = [];\n\n for (let i = 0; i < bucketCount; i++) {\n buckets.push({\n date: new Date(earliest.getTime() + i * bucketSize),\n count: 0,\n });\n }\n\n for (const date of this._nodeDates.values()) {\n const idx = Math.min(\n Math.floor((date.getTime() - earliest.getTime()) / bucketSize),\n bucketCount - 1\n );\n buckets[idx].count++;\n }\n\n return buckets;\n }\n\n /** Extract the earliest date from a node's properties across known namespaces. */\n private _extractDate(node: GraphNode): Date | null {\n let earliest: Date | null = null;\n\n for (const shortName of this._dateProperties) {\n for (const ns of KNOWN_DATE_NAMESPACES) {\n const vals = node.properties?.get(ns + shortName);\n if (vals && vals.length > 0) {\n const d = new Date(vals[0].value);\n if (!isNaN(d.getTime())) {\n if (!earliest || d < earliest) earliest = d;\n }\n }\n }\n }\n\n return earliest;\n }\n}\n","import type { TemporalFilter, TemporalConfig } from '../core/temporal-filter.js';\n\n/**\n * DOM-based timeline overlay rendered inside the visualization container.\n *\n * Shows a single progressive slider: drag right to reveal more of the graph\n * chronologically. Includes play/pause, date labels, visible node count,\n * and a mini histogram showing node density over time.\n */\nexport class TimelineOverlay {\n private _container: HTMLElement;\n private _filter: TemporalFilter;\n private _config: TemporalConfig;\n\n // DOM elements\n private _root: HTMLElement | null = null;\n private _slider: HTMLInputElement | null = null;\n private _dateLabel: HTMLElement | null = null;\n private _countLabel: HTMLElement | null = null;\n private _playBtn: HTMLElement | null = null;\n private _histCanvas: HTMLCanvasElement | null = null;\n private _startLabel: HTMLElement | null = null;\n private _endLabel: HTMLElement | null = null;\n\n // State\n private _playInterval: ReturnType<typeof setInterval> | null = null;\n private _isPlaying = false;\n private _onChange: ((cursor: Date, visibleCount: number) => void) | null = null;\n\n constructor(\n container: HTMLElement,\n filter: TemporalFilter,\n config: TemporalConfig,\n ) {\n this._container = container;\n this._filter = filter;\n this._config = config;\n }\n\n /** Set callback for cursor changes */\n onChange(fn: (cursor: Date, visibleCount: number) => void): void {\n this._onChange = fn;\n }\n\n /** Build and attach the overlay DOM. Call after filter.scan(). */\n mount(): void {\n if (this._root) return; // already mounted\n\n const range = this._filter.dateRange;\n if (!range) return; // no dates found\n\n this._root = document.createElement('div');\n this._root.className = 'gv-timeline-overlay';\n this._root.innerHTML = this._buildHTML();\n this._injectStyles();\n this._container.appendChild(this._root);\n\n // Cache element refs\n this._slider = this._root.querySelector('.gv-tl-slider') as HTMLInputElement;\n this._dateLabel = this._root.querySelector('.gv-tl-date') as HTMLElement;\n this._countLabel = this._root.querySelector('.gv-tl-count') as HTMLElement;\n this._playBtn = this._root.querySelector('.gv-tl-play') as HTMLElement;\n this._histCanvas = this._root.querySelector('.gv-tl-hist') as HTMLCanvasElement;\n this._startLabel = this._root.querySelector('.gv-tl-start') as HTMLElement;\n this._endLabel = this._root.querySelector('.gv-tl-end') as HTMLElement;\n\n // Draw histogram\n this._drawHistogram();\n\n // Set initial slider position to max (show everything)\n this._slider.value = this._slider.max;\n this._updateDateLabel();\n\n // Event listeners\n this._slider.addEventListener('input', () => this._onSliderInput());\n this._playBtn?.addEventListener('click', () => this._togglePlay());\n }\n\n /** Remove the overlay from the DOM */\n unmount(): void {\n this.pause();\n if (this._root) {\n this._root.remove();\n this._root = null;\n }\n }\n\n /** Whether the overlay is currently mounted */\n get mounted(): boolean {\n return this._root !== null;\n }\n\n /** Update the overlay after data changes (re-scan, redraw histogram) */\n refresh(): void {\n if (!this._root) return;\n const range = this._filter.dateRange;\n if (!range) {\n this.unmount();\n return;\n }\n\n // Update slider range\n if (this._slider) {\n this._slider.min = String(range[0].getTime());\n this._slider.max = String(range[1].getTime());\n }\n\n // Redraw histogram\n this._drawHistogram();\n\n // Update labels\n if (this._startLabel) this._startLabel.textContent = this._formatDateShort(range[0]);\n if (this._endLabel) this._endLabel.textContent = this._formatDateShort(range[1]);\n\n this._updateDateLabel();\n }\n\n /** Start auto-play from current position */\n play(): void {\n if (this._isPlaying) return;\n this._isPlaying = true;\n if (this._playBtn) this._playBtn.textContent = '⏸';\n\n const range = this._filter.dateRange;\n if (!range || !this._slider) return;\n\n const totalRange = range[1].getTime() - range[0].getTime();\n const stepMs = this._getStepMs();\n const speed = this._config.playSpeed ?? 200;\n\n this._playInterval = setInterval(() => {\n const current = parseInt(this._slider!.value, 10);\n const next = current + stepMs;\n if (next >= range[1].getTime()) {\n this._slider!.value = String(range[1].getTime());\n this._onSliderInput();\n this.pause();\n return;\n }\n this._slider!.value = String(next);\n this._onSliderInput();\n }, speed);\n }\n\n /** Pause auto-play */\n pause(): void {\n this._isPlaying = false;\n if (this._playBtn) this._playBtn.textContent = '▶';\n if (this._playInterval) {\n clearInterval(this._playInterval);\n this._playInterval = null;\n }\n }\n\n /** Set the slider to a specific date programmatically */\n setCursor(date: Date): void {\n if (!this._slider) return;\n this._slider.value = String(date.getTime());\n this._onSliderInput();\n }\n\n // --- Private ---\n\n private _buildHTML(): string {\n const range = this._filter.dateRange!;\n const minMs = range[0].getTime();\n const maxMs = range[1].getTime();\n\n return `\n <div class=\"gv-tl-bar\">\n <button class=\"gv-tl-play\" title=\"Play/Pause timeline\">▶</button>\n <div class=\"gv-tl-track\">\n <div class=\"gv-tl-labels\">\n <span class=\"gv-tl-start\">${this._formatDateShort(range[0])}</span>\n <span class=\"gv-tl-date\"></span>\n <span class=\"gv-tl-end\">${this._formatDateShort(range[1])}</span>\n </div>\n <div class=\"gv-tl-slider-wrap\">\n <canvas class=\"gv-tl-hist\" width=\"600\" height=\"32\"></canvas>\n <input type=\"range\" class=\"gv-tl-slider\" min=\"${minMs}\" max=\"${maxMs}\" value=\"${maxMs}\" step=\"1\" />\n </div>\n </div>\n <div class=\"gv-tl-count\" title=\"Visible nodes\">—</div>\n </div>\n `;\n }\n\n private _onSliderInput(): void {\n if (!this._slider) return;\n const ms = parseInt(this._slider.value, 10);\n const date = new Date(ms);\n this._filter.setCursor(date);\n this._updateDateLabel();\n this._onChange?.(date, 0); // count will be set by the caller after re-render\n }\n\n private _updateDateLabel(): void {\n if (!this._dateLabel || !this._slider) return;\n const ms = parseInt(this._slider.value, 10);\n const date = new Date(ms);\n this._dateLabel.textContent = this._formatDateFull(date);\n }\n\n /** Update the visible count badge (called externally after render) */\n updateCount(count: number): void {\n if (this._countLabel) {\n this._countLabel.textContent = count.toLocaleString();\n }\n }\n\n private _togglePlay(): void {\n if (this._isPlaying) {\n this.pause();\n } else {\n // If at the end, reset to beginning\n if (this._slider && this._filter.dateRange) {\n const current = parseInt(this._slider.value, 10);\n if (current >= this._filter.dateRange[1].getTime() - this._getStepMs()) {\n this._slider.value = String(this._filter.dateRange[0].getTime());\n this._onSliderInput();\n }\n }\n this.play();\n }\n }\n\n private _getStepMs(): number {\n const step = this._config.stepSize ?? 'day';\n const range = this._filter.dateRange;\n if (!range) return 86400000;\n\n // Scale steps so that 100 steps covers the full range\n const totalRange = range[1].getTime() - range[0].getTime();\n const autoStep = totalRange / 200;\n\n switch (step) {\n case 'hour': return Math.max(3600000, autoStep);\n case 'day': return Math.max(86400000, autoStep);\n case 'week': return Math.max(604800000, autoStep);\n case 'month': return Math.max(2592000000, autoStep);\n default: return autoStep;\n }\n }\n\n private _drawHistogram(): void {\n const canvas = this._histCanvas;\n if (!canvas) return;\n const ctx = canvas.getContext('2d');\n if (!ctx) return;\n\n const buckets = this._filter.computeHistogram(canvas.width / 4);\n if (buckets.length === 0) return;\n\n const maxCount = Math.max(...buckets.map(b => b.count));\n if (maxCount === 0) return;\n\n const w = canvas.width;\n const h = canvas.height;\n const barW = w / buckets.length;\n\n ctx.clearRect(0, 0, w, h);\n\n for (let i = 0; i < buckets.length; i++) {\n const ratio = buckets[i].count / maxCount;\n const barH = ratio * (h - 2);\n ctx.fillStyle = 'rgba(99, 102, 241, 0.25)';\n ctx.fillRect(i * barW, h - barH, barW - 1, barH);\n }\n }\n\n private _formatDateShort(date: Date): string {\n return date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });\n }\n\n private _formatDateFull(date: Date): string {\n return date.toLocaleDateString('en-US', {\n month: 'short', day: 'numeric', year: 'numeric',\n });\n }\n\n private _styleId = 'gv-timeline-styles';\n\n private _injectStyles(): void {\n if (document.getElementById(this._styleId)) return;\n\n const style = document.createElement('style');\n style.id = this._styleId;\n style.textContent = `\n .gv-timeline-overlay {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n z-index: 90;\n pointer-events: none;\n }\n .gv-tl-bar {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 10px 16px 12px;\n background: linear-gradient(to top,\n var(--gv-bg, rgba(10, 10, 15, 0.95)) 0%,\n var(--gv-bg, rgba(10, 10, 15, 0.85)) 60%,\n transparent 100%);\n pointer-events: auto;\n }\n .gv-tl-play {\n width: 32px;\n height: 32px;\n border-radius: 50%;\n border: 1px solid var(--gv-surface-border, rgba(100, 100, 140, 0.3));\n background: var(--gv-surface, rgba(20, 20, 30, 0.9));\n color: var(--gv-text, #e0e0e0);\n cursor: pointer;\n font-size: 14px;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n transition: background 0.2s, border-color 0.2s;\n padding: 0;\n line-height: 1;\n }\n .gv-tl-play:hover {\n background: var(--gv-primary-muted, rgba(99, 102, 241, 0.3));\n border-color: var(--gv-primary, rgba(99, 102, 241, 0.5));\n }\n .gv-tl-track {\n flex: 1;\n min-width: 0;\n position: relative;\n }\n .gv-tl-labels {\n display: flex;\n justify-content: space-between;\n font-size: 10px;\n color: var(--gv-text-muted, #666);\n margin-bottom: 2px;\n padding: 0 2px;\n }\n .gv-tl-date {\n font-weight: 600;\n color: var(--gv-text-secondary, #aaa);\n font-size: 11px;\n }\n .gv-tl-slider-wrap {\n position: relative;\n height: 32px;\n }\n .gv-tl-hist {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n pointer-events: none;\n opacity: 0.7;\n }\n .gv-tl-slider {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n margin: 0;\n -webkit-appearance: none;\n appearance: none;\n background: transparent;\n cursor: pointer;\n z-index: 2;\n }\n .gv-tl-slider::-webkit-slider-runnable-track {\n height: 4px;\n border-radius: 2px;\n background: var(--gv-surface-border, rgba(100, 100, 140, 0.3));\n margin-top: 14px;\n }\n .gv-tl-slider::-webkit-slider-thumb {\n -webkit-appearance: none;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n background: var(--gv-primary, #6366f1);\n border: 2px solid var(--gv-bg, #0a0a0f);\n margin-top: -6px;\n box-shadow: 0 0 8px var(--gv-primary-muted, rgba(99, 102, 241, 0.4));\n transition: transform 0.15s ease;\n }\n .gv-tl-slider::-webkit-slider-thumb:hover {\n transform: scale(1.2);\n }\n .gv-tl-slider::-moz-range-track {\n height: 4px;\n border-radius: 2px;\n background: var(--gv-surface-border, rgba(100, 100, 140, 0.3));\n border: none;\n }\n .gv-tl-slider::-moz-range-thumb {\n width: 16px;\n height: 16px;\n border-radius: 50%;\n background: var(--gv-primary, #6366f1);\n border: 2px solid var(--gv-bg, #0a0a0f);\n box-shadow: 0 0 8px var(--gv-primary-muted, rgba(99, 102, 241, 0.4));\n }\n .gv-tl-count {\n min-width: 40px;\n padding: 4px 8px;\n border-radius: 12px;\n background: var(--gv-surface, rgba(20, 20, 30, 0.9));\n border: 1px solid var(--gv-surface-border, rgba(100, 100, 140, 0.3));\n color: var(--gv-primary, #6366f1);\n font-size: 12px;\n font-weight: 600;\n text-align: center;\n flex-shrink: 0;\n white-space: nowrap;\n }\n `;\n document.head.appendChild(style);\n }\n}\n","import type {\n RdfTriple,\n RdfValue,\n GraphNode,\n GraphEdge,\n ChangeSet,\n PrefixMap,\n} from './types.js';\n\nconst RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';\n\n/**\n * Determine if an object value looks like a URI (named node) or blank node.\n * URIs are absolute IRIs (`scheme:`) or are wrapped in <>.\n * Blank nodes start with _: prefix.\n */\nfunction isResourceObject(value: string): boolean {\n const trimmed = value.trim();\n if (!trimmed || trimmed.startsWith('\"')) return false;\n if (trimmed.startsWith('_:')) return true;\n if (trimmed.startsWith('<') && trimmed.endsWith('>')) return true;\n if (/\\s/.test(trimmed)) return false;\n return /^[A-Za-z][A-Za-z0-9+.-]*:/.test(trimmed);\n}\n\n/** Strip angle brackets from a URI if present */\nfunction cleanUri(value: string): string {\n const trimmed = value.trim();\n if (trimmed.startsWith('<') && trimmed.endsWith('>')) {\n return trimmed.slice(1, -1);\n }\n return trimmed;\n}\n\n/** Generate a deterministic edge ID */\nfunction edgeId(subject: string, predicate: string, object: string): string {\n return `${subject}\\0${predicate}\\0${object}`;\n}\n\n/**\n * Subject-centric RDF graph model for visualization.\n *\n * - Literal objects → stored as properties on the subject node\n * - URI/blank node objects → stored as edges between nodes\n * - rdf:type → stored in node.types, not as an edge\n *\n * Supports incremental add/remove with stable node and edge IDs.\n *\n * Maintains adjacency indexes (_outEdges, _inEdges) for O(degree) edge\n * lookups instead of O(E) full scans. These are used by FocusFilter BFS,\n * ReificationCollapser, and ViewConfig application on the render hot path.\n */\nexport class GraphModel {\n readonly nodes = new Map<string, GraphNode>();\n readonly edges = new Map<string, GraphEdge>();\n prefixes: PrefixMap = {};\n\n /**\n * Stores triples in canonical form — subject, predicate, and (when the\n * object is a resource) object are `cleanUri`-normalised at ingress so\n * downstream consumers and lookup paths (e.g. `removeTriples`,\n * reification mirrors, diff exporters) all operate on a single\n * representation. Literal objects are kept verbatim so quoting,\n * datatypes, and language tags survive the round-trip.\n */\n private _triples: RdfTriple[] = [];\n\n /** Set of predicate URIs treated as metadata (not rendered as edges) */\n private _metadataPredicates = new Set<string>();\n\n /** Adjacency index: nodeId → Set of edge IDs originating from this node */\n private _outEdges = new Map<string, Set<string>>();\n\n /** Adjacency index: nodeId → Set of edge IDs pointing to this node */\n private _inEdges = new Map<string, Set<string>>();\n\n /** Knowledge Asset groups: KA URI → Set of member node IDs */\n readonly kaGroups = new Map<string, Set<string>>();\n\n /** Predicate used for KA membership (set via setKaMembershipPredicate) */\n private _kaMembershipPredicate: string | null = null;\n\n constructor(metadataPredicates?: string[]) {\n if (metadataPredicates) {\n for (const p of metadataPredicates) {\n this._metadataPredicates.add(p);\n }\n }\n }\n\n /** Get all stored triples */\n get triples(): readonly RdfTriple[] {\n return this._triples;\n }\n\n /** Set the predicate used for Knowledge Asset membership grouping */\n setKaMembershipPredicate(predicate: string | null): void {\n this._kaMembershipPredicate = predicate;\n }\n\n /** Get or create a node */\n private ensureNode(id: string): GraphNode {\n let node = this.nodes.get(id);\n if (!node) {\n node = {\n id,\n types: [],\n label: id,\n properties: new Map(),\n imageUrl: null,\n metadata: new Map(),\n degree: 0,\n isBoundary: false,\n };\n this.nodes.set(id, node);\n }\n return node;\n }\n\n /** Ensure adjacency sets exist for a node */\n private ensureAdjacency(id: string): void {\n if (!this._outEdges.has(id)) this._outEdges.set(id, new Set());\n if (!this._inEdges.has(id)) this._inEdges.set(id, new Set());\n }\n\n /** Add a single triple to the model */\n addTriple(triple: RdfTriple): void {\n const subj = cleanUri(triple.subject);\n const pred = cleanUri(triple.predicate);\n const objectIsResource = isResourceObject(triple.object);\n const obj = objectIsResource ? cleanUri(triple.object) : triple.object;\n\n this._triples.push({\n ...triple,\n subject: subj,\n predicate: pred,\n object: obj,\n });\n\n const subjectNode = this.ensureNode(subj);\n\n // rdf:type → add to node types, don't create edge\n if (pred === RDF_TYPE) {\n if (!subjectNode.types.includes(obj)) {\n subjectNode.types.push(obj);\n }\n return;\n }\n\n if (objectIsResource) {\n // Metadata predicates → store as metadata properties, not edges\n if (this._metadataPredicates.has(pred)) {\n const val: RdfValue = {\n value: obj,\n datatype: triple.datatype,\n language: triple.language,\n };\n const existing = subjectNode.metadata.get(pred);\n if (existing) {\n existing.push(val);\n } else {\n subjectNode.metadata.set(pred, [val]);\n }\n return;\n }\n\n // Resource object → create edge + ensure target node\n const targetNode = this.ensureNode(obj);\n const eid = edgeId(subj, pred, obj);\n\n if (!this.edges.has(eid)) {\n this.edges.set(eid, {\n id: eid,\n source: subj,\n target: obj,\n predicate: pred,\n label: pred,\n });\n subjectNode.degree++;\n targetNode.degree++;\n\n // Maintain adjacency indexes\n this.ensureAdjacency(subj);\n this.ensureAdjacency(obj);\n this._outEdges.get(subj)!.add(eid);\n this._inEdges.get(obj)!.add(eid);\n }\n\n // Track KA membership\n if (this._kaMembershipPredicate && pred === this._kaMembershipPredicate) {\n let members = this.kaGroups.get(obj);\n if (!members) {\n members = new Set();\n this.kaGroups.set(obj, members);\n }\n members.add(subj);\n }\n } else {\n // Literal object → store as property or metadata\n const val: RdfValue = {\n value: triple.object,\n datatype: triple.datatype,\n language: triple.language,\n };\n\n if (this._metadataPredicates.has(pred)) {\n const existing = subjectNode.metadata.get(pred);\n if (existing) {\n existing.push(val);\n } else {\n subjectNode.metadata.set(pred, [val]);\n }\n } else {\n const existing = subjectNode.properties.get(pred);\n if (existing) {\n existing.push(val);\n } else {\n subjectNode.properties.set(pred, [val]);\n }\n }\n }\n }\n\n /** Add multiple triples and return the changeset */\n addTriples(triples: RdfTriple[]): ChangeSet {\n const nodesBefore = new Set(this.nodes.keys());\n const edgesBefore = new Set(this.edges.keys());\n\n for (const t of triples) {\n this.addTriple(t);\n }\n\n const addedNodes: string[] = [];\n const addedEdges: string[] = [];\n const modifiedNodes = new Set<string>();\n\n for (const id of this.nodes.keys()) {\n if (!nodesBefore.has(id)) {\n addedNodes.push(id);\n } else {\n modifiedNodes.add(id);\n }\n }\n\n for (const id of this.edges.keys()) {\n if (!edgesBefore.has(id)) {\n addedEdges.push(id);\n }\n }\n\n return {\n addedNodes,\n removedNodes: [],\n addedEdges,\n removedEdges: [],\n modifiedNodes: [...modifiedNodes],\n };\n }\n\n /** Remove triples matching the given criteria and return changeset */\n removeTriples(triples: RdfTriple[]): ChangeSet {\n const removedEdges: string[] = [];\n const modifiedNodes = new Set<string>();\n\n for (const triple of triples) {\n const subj = cleanUri(triple.subject);\n const pred = cleanUri(triple.predicate);\n const objectIsResource = isResourceObject(triple.object);\n const obj = objectIsResource ? cleanUri(triple.object) : triple.object;\n\n // Match against the canonical form `_triples` stores (see field doc).\n const idx = this._triples.findIndex(\n (t) => t.subject === subj && t.predicate === pred && t.object === obj\n );\n if (idx !== -1) this._triples.splice(idx, 1);\n\n if (pred === RDF_TYPE) {\n const node = this.nodes.get(subj);\n if (node) {\n node.types = node.types.filter((t) => t !== obj);\n modifiedNodes.add(subj);\n }\n continue;\n }\n\n if (objectIsResource && !this._metadataPredicates.has(pred)) {\n const eid = edgeId(subj, pred, obj);\n if (this.edges.has(eid)) {\n this.edges.delete(eid);\n removedEdges.push(eid);\n\n // Maintain adjacency indexes\n this._outEdges.get(subj)?.delete(eid);\n this._inEdges.get(obj)?.delete(eid);\n\n const srcNode = this.nodes.get(subj);\n const tgtNode = this.nodes.get(obj);\n if (srcNode) srcNode.degree--;\n if (tgtNode) tgtNode.degree--;\n\n modifiedNodes.add(subj);\n modifiedNodes.add(obj);\n }\n } else {\n const node = this.nodes.get(subj);\n if (node) {\n const store = this._metadataPredicates.has(pred) ? node.metadata : node.properties;\n const vals = store.get(pred);\n if (vals) {\n const filtered = vals.filter((v) => v.value !== obj);\n if (filtered.length === 0) {\n store.delete(pred);\n } else {\n store.set(pred, filtered);\n }\n modifiedNodes.add(subj);\n }\n }\n }\n }\n\n // Remove orphan nodes (no edges, no properties, no types)\n const removedNodes: string[] = [];\n for (const [id, node] of this.nodes) {\n if (\n node.degree === 0 &&\n node.types.length === 0 &&\n node.properties.size === 0 &&\n node.metadata.size === 0\n ) {\n this.nodes.delete(id);\n this._outEdges.delete(id);\n this._inEdges.delete(id);\n removedNodes.push(id);\n }\n }\n\n return {\n addedNodes: [],\n removedNodes,\n addedEdges: [],\n removedEdges,\n modifiedNodes: [...modifiedNodes],\n };\n }\n\n /** Get a node by ID */\n getNode(id: string): GraphNode | undefined {\n return this.nodes.get(id);\n }\n\n /** Get all edges originating from a node — O(degree) via adjacency index */\n getEdgesFrom(id: string): GraphEdge[] {\n const edgeIds = this._outEdges.get(id);\n if (!edgeIds) return [];\n const result: GraphEdge[] = [];\n for (const eid of edgeIds) {\n const edge = this.edges.get(eid);\n if (edge) result.push(edge);\n }\n return result;\n }\n\n /** Get all edges pointing to a node — O(degree) via adjacency index */\n getEdgesTo(id: string): GraphEdge[] {\n const edgeIds = this._inEdges.get(id);\n if (!edgeIds) return [];\n const result: GraphEdge[] = [];\n for (const eid of edgeIds) {\n const edge = this.edges.get(eid);\n if (edge) result.push(edge);\n }\n return result;\n }\n\n /** Get all neighbor node IDs (both directions) — O(degree) via adjacency index */\n getNeighborIds(id: string): Set<string> {\n const neighbors = new Set<string>();\n const outIds = this._outEdges.get(id);\n if (outIds) {\n for (const eid of outIds) {\n const edge = this.edges.get(eid);\n if (edge) neighbors.add(edge.target);\n }\n }\n const inIds = this._inEdges.get(id);\n if (inIds) {\n for (const eid of inIds) {\n const edge = this.edges.get(eid);\n if (edge) neighbors.add(edge.source);\n }\n }\n return neighbors;\n }\n\n /** Get the node with the highest degree */\n getHighestDegreeNode(): GraphNode | null {\n let best: GraphNode | null = null;\n for (const node of this.nodes.values()) {\n if (!best || node.degree > best.degree) {\n best = node;\n }\n }\n return best;\n }\n\n /** Clear all data */\n clear(): void {\n this.nodes.clear();\n this.edges.clear();\n this._triples = [];\n this._outEdges.clear();\n this._inEdges.clear();\n }\n\n /** Total number of triples */\n get tripleCount(): number {\n return this._triples.length;\n }\n}\n","import type { PrefixMap } from './types.js';\n\n/** Well-known namespace prefixes shipped as defaults */\nconst WELL_KNOWN_PREFIXES: PrefixMap = {\n rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',\n rdfs: 'http://www.w3.org/2000/01/rdf-schema#',\n owl: 'http://www.w3.org/2002/07/owl#',\n xsd: 'http://www.w3.org/2001/XMLSchema#',\n schema: 'https://schema.org/',\n foaf: 'http://xmlns.com/foaf/0.1/',\n dcterms: 'http://purl.org/dc/terms/',\n dc: 'http://purl.org/dc/elements/1.1/',\n prov: 'http://www.w3.org/ns/prov#',\n skos: 'http://www.w3.org/2004/02/skos/core#',\n};\n\n/**\n * Manages namespace prefix ↔ URI mappings for compacting and expanding URIs.\n */\nexport class PrefixManager {\n private _prefixes: Map<string, string>;\n /** Reverse map: namespace URI → prefix */\n private _reverse: Map<string, string>;\n\n constructor(userPrefixes?: PrefixMap) {\n this._prefixes = new Map();\n this._reverse = new Map();\n\n // Load well-known first, then user overrides\n for (const [prefix, ns] of Object.entries(WELL_KNOWN_PREFIXES)) {\n this._prefixes.set(prefix, ns);\n this._reverse.set(ns, prefix);\n }\n\n if (userPrefixes) {\n for (const [prefix, ns] of Object.entries(userPrefixes)) {\n this._prefixes.set(prefix, ns);\n this._reverse.set(ns, prefix);\n }\n }\n }\n\n /** Register a new prefix */\n addPrefix(prefix: string, namespace: string): void {\n this._prefixes.set(prefix, namespace);\n this._reverse.set(namespace, prefix);\n }\n\n /** Merge in multiple prefixes */\n addPrefixes(prefixes: PrefixMap): void {\n for (const [prefix, ns] of Object.entries(prefixes)) {\n this.addPrefix(prefix, ns);\n }\n }\n\n /** Get the namespace URI for a prefix */\n getNamespace(prefix: string): string | undefined {\n return this._prefixes.get(prefix);\n }\n\n /** Get the prefix for a namespace URI */\n getPrefix(namespace: string): string | undefined {\n return this._reverse.get(namespace);\n }\n\n /**\n * Compact a full URI to prefixed form (e.g., \"https://schema.org/name\" → \"schema:name\").\n * Returns null if no matching prefix is found.\n */\n compact(uri: string): string | null {\n for (const [ns, prefix] of this._reverse) {\n if (uri.startsWith(ns)) {\n const localName = uri.slice(ns.length);\n if (localName.length > 0) {\n return `${prefix}:${localName}`;\n }\n }\n }\n return null;\n }\n\n /**\n * Expand a prefixed term to a full URI (e.g., \"schema:name\" → \"https://schema.org/name\").\n * Returns the input unchanged if it's already a full URI or no prefix matches.\n */\n expand(term: string): string {\n const colonIdx = term.indexOf(':');\n if (colonIdx === -1) return term;\n\n // Don't expand things that look like full URIs\n if (term.startsWith('http://') || term.startsWith('https://') || term.startsWith('urn:')) {\n return term;\n }\n\n const prefix = term.slice(0, colonIdx);\n const localName = term.slice(colonIdx + 1);\n const ns = this._prefixes.get(prefix);\n\n return ns ? ns + localName : term;\n }\n\n /**\n * Extract a short, human-readable local name from a URI.\n *\n * Strategy, in priority order:\n * 1. Part after the last `#` (RDF fragment: http://ex.org/ns#Foo → Foo)\n * 2. Part after the last `/` (HTTP path: http://ex.org/foo/Bar → Bar)\n * 3. If the URI contains URL-encoded slashes (%2F), decode them and\n * take the basename. This handles URIs like\n * `urn:dkg:code:file:packages%2Fnode-ui%2Fsrc%2Fui%2FApp.tsx`,\n * which previously returned the entire URI.\n * 4. Part after the last `:` (URN tail: urn:dkg:task:my-slug → my-slug)\n * 5. The URI itself (last resort).\n */\n static localName(uri: string): string {\n const hashIdx = uri.lastIndexOf('#');\n if (hashIdx !== -1) return uri.slice(hashIdx + 1);\n\n const slashIdx = uri.lastIndexOf('/');\n if (slashIdx !== -1) return uri.slice(slashIdx + 1);\n\n // URL-encoded path: decode and take the last segment.\n if (uri.includes('%2F') || uri.includes('%2f')) {\n try {\n const decoded = decodeURIComponent(uri);\n const decodedSlash = decoded.lastIndexOf('/');\n if (decodedSlash !== -1) return decoded.slice(decodedSlash + 1);\n } catch {\n // Malformed encoding — fall through to colon split.\n }\n }\n\n const colonIdx = uri.lastIndexOf(':');\n if (colonIdx !== -1 && colonIdx < uri.length - 1) {\n return uri.slice(colonIdx + 1);\n }\n\n return uri;\n }\n\n /** Get all registered prefixes */\n get prefixes(): PrefixMap {\n const result: PrefixMap = {};\n for (const [prefix, ns] of this._prefixes) {\n result[prefix] = ns;\n }\n return result;\n }\n}\n","import type { GraphNode, LabelMode, LabelConfig, GraphEdge } from './types.js';\nimport { PrefixManager } from './prefix-manager.js';\n\n/** Default label predicates in priority order */\nconst DEFAULT_LABEL_PREDICATES = [\n 'http://www.w3.org/2000/01/rdf-schema#label',\n 'https://schema.org/name',\n 'http://xmlns.com/foaf/0.1/name',\n 'http://purl.org/dc/terms/title',\n 'http://purl.org/dc/elements/1.1/title',\n 'https://schema.org/alternateName',\n];\n\n/**\n * Resolves display labels for nodes and edges based on the current label mode.\n *\n * - \"strict\": Shows compact prefixed URI (e.g., \"schema:Person\", \"prov:wasGeneratedBy\")\n * - \"humanized\": Shows friendly label (e.g., \"Person\", \"was generated by\")\n */\nexport class LabelResolver {\n private _mode: LabelMode;\n private _prefixManager: PrefixManager;\n private _labelPredicates: string[];\n\n constructor(mode: LabelMode, prefixManager: PrefixManager, config?: LabelConfig) {\n this._mode = mode;\n this._prefixManager = prefixManager;\n this._labelPredicates = config?.predicates ?? DEFAULT_LABEL_PREDICATES;\n }\n\n get mode(): LabelMode {\n return this._mode;\n }\n\n set mode(value: LabelMode) {\n this._mode = value;\n }\n\n /** Resolve a display label for a node */\n resolveNodeLabel(node: GraphNode): string {\n // First: check if the node has an explicit label property\n for (const pred of this._labelPredicates) {\n const values = node.properties.get(pred);\n if (values && values.length > 0) {\n return values[0].value;\n }\n }\n\n // Fall back to the URI-derived label\n return this.resolveUri(node.id);\n }\n\n /** Resolve a display label for an edge */\n resolveEdgeLabel(edge: GraphEdge): string {\n return this.resolveUri(edge.predicate);\n }\n\n /** Resolve a display label for a URI */\n resolveUri(uri: string): string {\n if (this._mode === 'strict') {\n return this._prefixManager.compact(uri) ?? PrefixManager.localName(uri);\n }\n\n // Humanized mode\n return humanize(PrefixManager.localName(uri));\n }\n\n /** Resolve a type label (used for type badges) */\n resolveTypeLabel(typeUri: string): string {\n if (this._mode === 'strict') {\n return this._prefixManager.compact(typeUri) ?? PrefixManager.localName(typeUri);\n }\n return humanize(PrefixManager.localName(typeUri));\n }\n\n /** Resolve label for a predicate (used in property lists) */\n resolvePredicateLabel(predicateUri: string): string {\n return this.resolveUri(predicateUri);\n }\n\n /**\n * Update all labels on nodes and edges in the graph.\n * Call this after changing label mode or adding new data.\n */\n updateLabels(nodes: Map<string, GraphNode>, edges: Map<string, GraphEdge>): void {\n for (const node of nodes.values()) {\n node.label = this.resolveNodeLabel(node);\n }\n for (const edge of edges.values()) {\n edge.label = this.resolveEdgeLabel(edge);\n }\n }\n}\n\n/**\n * Convert a camelCase or PascalCase local name into a human-friendly label.\n * e.g., \"wasGeneratedBy\" → \"was generated by\"\n * e.g., \"SocialMediaPosting\" → \"Social Media Posting\"\n */\nfunction humanize(localName: string): string {\n if (!localName) return '';\n\n // Handle snake_case\n let result = localName.replace(/_/g, ' ');\n\n // Insert spaces before capitals in camelCase/PascalCase\n result = result.replace(/([a-z])([A-Z])/g, '$1 $2');\n result = result.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');\n\n // Lowercase everything for humanized display (except if it's a short acronym)\n if (result.length > 4) {\n result = result.toLowerCase();\n }\n\n return result.trim();\n}\n","import type { GraphNode, GraphEdge, StyleConfig } from './types.js';\nimport type { ColorPalette } from './palette.js';\nimport { PALETTE_DARK } from './palette.js';\nimport { PrefixManager } from './prefix-manager.js';\n\n/** Auto-assigned namespace colors for common vocabularies */\nconst NAMESPACE_COLORS: Record<string, string> = {\n 'https://schema.org/': '#8b5cf6', // Purple for schema.org\n 'http://www.w3.org/ns/prov#': '#3b82f6', // Blue for PROV-O\n 'http://xmlns.com/foaf/0.1/': '#10b981', // Emerald for FOAF\n 'http://purl.org/dc/terms/': '#f59e0b', // Amber for Dublin Core\n};\n\n/**\n * Resolves colors for nodes and edges based on configurable rules.\n *\n * When a ColorPalette is provided, it drives default colors:\n * - defaultNodeColor → palette.primary\n * - defaultEdgeColor → palette.edgeColor\n * - Label/text colors → palette.textPrimary/textSecondary\n *\n * Priority (highest first):\n * 1. Per-class color (matching rdf:type)\n * 2. Per-namespace color (matching namespace of first rdf:type)\n * 3. Default color (from palette or config)\n */\nexport class StyleEngine {\n private _config: Required<StyleConfig>;\n private _prefixManager: PrefixManager;\n private _palette: ColorPalette;\n private _hasExplicitDefaultNodeColor: boolean;\n private _hasExplicitDefaultEdgeColor: boolean;\n\n constructor(config: StyleConfig | undefined, prefixManager: PrefixManager, palette?: ColorPalette) {\n this._prefixManager = prefixManager;\n this._palette = palette ?? PALETTE_DARK;\n this._hasExplicitDefaultNodeColor = config?.defaultNodeColor !== undefined;\n this._hasExplicitDefaultEdgeColor = config?.defaultEdgeColor !== undefined;\n this._config = {\n nodeColors: config?.nodeColors ?? {},\n classColors: config?.classColors ?? {},\n namespaceColors: config?.namespaceColors ?? {},\n predicateColors: config?.predicateColors ?? {},\n gradient: config?.gradient ?? true,\n gradientIntensity: config?.gradientIntensity ?? 0.3,\n defaultNodeColor: config?.defaultNodeColor ?? this._palette.primary,\n defaultEdgeColor: config?.defaultEdgeColor ?? this._palette.edgeColor,\n edgeWidth: config?.edgeWidth ?? 1.5,\n edgeArrowSize: config?.edgeArrowSize ?? 4,\n borderWidth: config?.borderWidth ?? 1.5,\n fontFamily: config?.fontFamily ?? 'system-ui, -apple-system, sans-serif',\n fontSize: config?.fontSize ?? 11,\n };\n }\n\n /** The active color palette */\n get palette(): ColorPalette {\n return this._palette;\n }\n\n /** Update the active palette (recalculates defaults not explicitly set in config) */\n setPalette(palette: ColorPalette): void {\n this._palette = palette;\n // Only override defaults that weren't explicitly set by the user\n this._config = { ...this._config };\n if (!this._hasExplicitDefaultNodeColor) this._config.defaultNodeColor = palette.primary;\n if (!this._hasExplicitDefaultEdgeColor) this._config.defaultEdgeColor = palette.edgeColor;\n }\n\n get config(): Readonly<Required<StyleConfig>> {\n return this._config;\n }\n\n /** Update per-node color overrides (merges with existing) */\n setNodeColors(colors: Record<string, string>): void {\n this._config = { ...this._config, nodeColors: { ...this._config.nodeColors, ...colors } };\n }\n\n /** Get the fill color for a node */\n getNodeColor(node: GraphNode): string {\n // 0. Per-node color override (highest priority — used for trust-level coloring)\n const nodeColor = this._config.nodeColors[node.id];\n if (nodeColor) return nodeColor;\n\n // 1. Check per-class colors (user-supplied, using compact or full URIs)\n for (const type of node.types) {\n const compact = this._prefixManager.compact(type);\n if (compact && this._config.classColors[compact]) {\n return this._config.classColors[compact];\n }\n if (this._config.classColors[type]) {\n return this._config.classColors[type];\n }\n }\n\n // 2. Check per-namespace colors (user-supplied first, then built-in)\n for (const type of node.types) {\n for (const [ns, color] of Object.entries(this._config.namespaceColors)) {\n const fullNs = this._prefixManager.getNamespace(ns) ?? ns;\n if (type.startsWith(fullNs)) return color;\n }\n // Built-in namespace colors\n for (const [ns, color] of Object.entries(NAMESPACE_COLORS)) {\n if (type.startsWith(ns)) return color;\n }\n }\n\n // 3. Default\n return this._config.defaultNodeColor;\n }\n\n /** Get the color for an edge */\n getEdgeColor(edge: GraphEdge): string {\n const compact = this._prefixManager.compact(edge.predicate);\n if (compact && this._config.predicateColors[compact]) {\n return this._config.predicateColors[compact];\n }\n if (this._config.predicateColors[edge.predicate]) {\n return this._config.predicateColors[edge.predicate];\n }\n return this._config.defaultEdgeColor;\n }\n\n /** Get border color (slightly lighter than fill) */\n getBorderColor(fillColor: string): string {\n return lightenHex(fillColor, 0.3);\n }\n\n /** Get gradient center color (lighter) */\n getGradientCenter(fillColor: string): string {\n return lightenHex(fillColor, this._config.gradientIntensity);\n }\n}\n\n/** Lighten a hex color by a factor (0.0 = no change, 1.0 = white) */\nfunction lightenHex(hex: string, factor: number): string {\n const rgb = hexToRgb(hex);\n if (!rgb) return hex;\n\n const r = Math.round(rgb.r + (255 - rgb.r) * factor);\n const g = Math.round(rgb.g + (255 - rgb.g) * factor);\n const b = Math.round(rgb.b + (255 - rgb.b) * factor);\n\n return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;\n}\n\nfunction hexToRgb(hex: string): { r: number; g: number; b: number } | null {\n const match = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n if (!match) return null;\n return {\n r: parseInt(match[1], 16),\n g: parseInt(match[2], 16),\n b: parseInt(match[3], 16),\n };\n}\n","import type { GraphNode, GraphEdge, HexagonConfig } from './types.js';\nimport { StyleEngine } from './style-engine.js';\nimport { PrefixManager } from './prefix-manager.js';\n\nconst DEFAULT_HEXAGON: Required<HexagonConfig> = {\n baseSize: 36,\n minSize: 24,\n maxSize: 72,\n scaleWithDegree: true,\n imagePredicates: [\n 'https://schema.org/image',\n 'http://xmlns.com/foaf/0.1/img',\n 'http://xmlns.com/foaf/0.1/depiction',\n ],\n imageUrlPredicate: 'https://schema.org/url',\n circleTypes: [],\n};\n\n/** Cache for loaded images */\nconst imageCache = new Map<string, HTMLImageElement | null>();\n\n/**\n * Draws hexagonal nodes on a Canvas 2D context.\n * Called by force-graph's nodeCanvasObject callback.\n *\n * Nodes whose rdf:type matches a `circleTypes` entry are drawn as simple\n * filled circles (much cheaper — no gradient, no border, no image, no label).\n */\nexport class HexagonPainter {\n private _config: Required<HexagonConfig>;\n private _styleEngine: StyleEngine;\n private _maxDegree = 1;\n /** Expanded circleTypes — full URIs resolved from compact forms */\n private _circleTypeSet: Set<string>;\n /** Callback to trigger graph repaint when an image finishes loading */\n private _onImageLoad: (() => void) | null = null;\n /** Trust visualization style */\n private _trustStyle: 'opacity' | 'border' | 'glow' | 'none' = 'none';\n /** Whether to show provenance badges */\n private _showBadges = true;\n\n constructor(\n config: HexagonConfig | undefined,\n styleEngine: StyleEngine,\n prefixManager?: PrefixManager\n ) {\n this._config = { ...DEFAULT_HEXAGON, ...config };\n this._styleEngine = styleEngine;\n\n // Build a set of full URIs for circle types (expand compact forms)\n this._circleTypeSet = new Set<string>();\n for (const t of this._config.circleTypes) {\n this._circleTypeSet.add(t);\n if (prefixManager) {\n this._circleTypeSet.add(prefixManager.expand(t));\n }\n }\n }\n\n /** Check if a node should render as a simple circle */\n isCircleNode(node: GraphNode): boolean {\n if (this._circleTypeSet.size === 0) return false;\n return node.types.some((t) => this._circleTypeSet.has(t));\n }\n\n /** Set callback to trigger repaint when images load */\n setImageLoadCallback(cb: () => void): void {\n this._onImageLoad = cb;\n }\n\n /** Update the max degree for scaling calculations */\n setMaxDegree(maxDegree: number): void {\n this._maxDegree = Math.max(1, maxDegree);\n }\n\n /** Set the trust visualization style */\n setTrustStyle(style: 'opacity' | 'border' | 'glow' | 'none'): void {\n this._trustStyle = style;\n }\n\n /** Enable/disable provenance badges */\n setShowBadges(show: boolean): void {\n this._showBadges = show;\n }\n\n /** Get the radius for a node (optionally scaled by degree, boosted if has image) */\n getRadius(node: GraphNode): number {\n // Circle nodes get a slightly smaller base\n if (this.isCircleNode(node)) {\n if (!this._config.scaleWithDegree) return this._config.minSize * 0.7;\n const ratio = node.degree / this._maxDegree;\n const range = this._config.maxSize * 0.6 - this._config.minSize * 0.7;\n return this._config.minSize * 0.7 + ratio * range;\n }\n\n // Size multipliers: explicit sizeMultiplier > isHighRisk > default\n const multiplier = node.sizeMultiplier ?? (node.isHighRisk ? 2.0 : 1.0);\n\n // Nodes with images get a minimum size so the image is visible\n const imageMinSize = node.imageUrl ? Math.max(this._config.maxSize * 0.6, 8) : 0;\n\n if (!this._config.scaleWithDegree) return Math.max(this._config.baseSize, imageMinSize) * multiplier;\n\n const ratio = node.degree / this._maxDegree;\n const range = this._config.maxSize - this._config.minSize;\n const degreeSize = this._config.minSize + ratio * range;\n return Math.max(degreeSize, imageMinSize) * multiplier;\n }\n\n /**\n * Paint a node onto the canvas.\n * Circle-type nodes get a minimal filled circle (fast path).\n * All others get the full hexagon treatment.\n *\n * @param pulseScale - Optional multiplier for risk pulse animation (1.0 = no pulse)\n * @returns the bounding radius for hit detection.\n */\n paint(\n ctx: CanvasRenderingContext2D,\n node: GraphNode,\n x: number,\n y: number,\n globalScale: number,\n pulseScale = 1.0\n ): number {\n // Fast path: simple circle for low-priority node types\n if (this.isCircleNode(node)) {\n return this.paintCircle(ctx, node, x, y, globalScale);\n }\n\n const baseRadius = this.getRadius(node);\n const radius = baseRadius * pulseScale;\n const color = this._styleEngine.getNodeColor(node);\n const palette = this._styleEngine.palette;\n const styleConfig = this._styleEngine.config;\n\n ctx.save();\n\n // Risk pulse glow (larger translucent hexagon behind)\n if (pulseScale > 1.03 && node.isHighRisk) {\n const glowRadius = radius * 1.25;\n const glowAlpha = (pulseScale - 1) * 2.5; // 0–0.45\n ctx.beginPath();\n for (let i = 0; i < 6; i++) {\n const angle = (Math.PI / 3) * i;\n const hx = x + glowRadius * Math.cos(angle);\n const hy = y + glowRadius * Math.sin(angle);\n if (i === 0) ctx.moveTo(hx, hy);\n else ctx.lineTo(hx, hy);\n }\n ctx.closePath();\n ctx.fillStyle = `rgba(220, 50, 50, ${Math.min(0.4, glowAlpha)})`;\n ctx.fill();\n }\n\n // Draw hexagon path (flat-top orientation)\n ctx.beginPath();\n for (let i = 0; i < 6; i++) {\n const angle = (Math.PI / 3) * i;\n const hx = x + radius * Math.cos(angle);\n const hy = y + radius * Math.sin(angle);\n if (i === 0) ctx.moveTo(hx, hy);\n else ctx.lineTo(hx, hy);\n }\n ctx.closePath();\n\n // Fill with gradient or flat color\n if (styleConfig.gradient) {\n const gradient = ctx.createRadialGradient(x, y, 0, x, y, radius);\n gradient.addColorStop(0, this._styleEngine.getGradientCenter(color));\n gradient.addColorStop(1, color);\n ctx.fillStyle = gradient;\n } else {\n ctx.fillStyle = color;\n }\n ctx.fill();\n\n // Trust glow (multi-source nodes get a colored glow behind)\n const sourceCount = node.provenance?.sources?.length ?? 0;\n if (sourceCount >= 2 && this._trustStyle === 'glow') {\n ctx.shadowColor = palette.info;\n ctx.shadowBlur = Math.min(12, sourceCount * 3) / globalScale;\n ctx.fill(); // re-fill with shadow\n ctx.shadowBlur = 0;\n ctx.shadowColor = 'transparent';\n }\n\n // Border — risk, trust, or normal\n if (node.riskScore && node.riskScore > 0) {\n const riskIntensity = Math.min(1, node.riskScore);\n ctx.strokeStyle = `rgba(220, 50, 50, ${0.5 + riskIntensity * 0.5})`;\n ctx.lineWidth = (styleConfig.borderWidth + (node.isHighRisk ? 1.5 : 0.5)) / globalScale;\n } else if (sourceCount >= 2 && this._trustStyle === 'border') {\n // Multi-source trust: thicker, brighter border\n ctx.strokeStyle = palette.info;\n ctx.lineWidth = (styleConfig.borderWidth + Math.min(3, sourceCount)) / globalScale;\n } else {\n ctx.strokeStyle = this._styleEngine.getBorderColor(color);\n ctx.lineWidth = styleConfig.borderWidth / globalScale;\n }\n\n // Boundary node indicator (subtle lighter border, no dashes)\n if (node.isBoundary && !(node.riskScore && node.riskScore > 0)) {\n ctx.strokeStyle = lightenColor(color, 0.5);\n ctx.lineWidth = (styleConfig.borderWidth + 1) / globalScale;\n }\n\n ctx.stroke();\n ctx.setLineDash([]);\n\n // Trust opacity adjustment\n if (sourceCount < 2 && this._trustStyle === 'opacity') {\n ctx.globalAlpha = 0.7;\n }\n\n // Avatar image (clipped to hexagon)\n if (node.imageUrl) {\n this.paintAvatar(ctx, node.imageUrl, x, y, radius);\n }\n\n ctx.globalAlpha = 1;\n\n // Badges (source count + verified indicator)\n if (globalScale > 3.0) {\n this._paintBadges(ctx, node, x, y, radius, globalScale);\n }\n\n // Label below hexagon — fade in/out with zoom\n const fadeStart = 0.3;\n const fadeFull = 0.8;\n if (globalScale > fadeStart) {\n const opacity = Math.min(1, (globalScale - fadeStart) / (fadeFull - fadeStart));\n const fontSize = styleConfig.fontSize / globalScale;\n ctx.font = `${fontSize}px ${styleConfig.fontFamily}`;\n ctx.textAlign = 'center';\n ctx.textBaseline = 'top';\n ctx.globalAlpha = opacity;\n ctx.fillStyle = palette.textPrimary;\n ctx.fillText(\n truncateLabel(node.label, 24),\n x,\n y + radius + 2 / globalScale\n );\n ctx.globalAlpha = 1;\n }\n\n ctx.restore();\n return radius;\n }\n\n /** Paint provenance badges on a node */\n private _paintBadges(\n ctx: CanvasRenderingContext2D,\n node: GraphNode,\n x: number,\n y: number,\n radius: number,\n globalScale: number\n ): void {\n if (!this._showBadges) return;\n\n const prov = node.provenance;\n if (!prov) return;\n\n const palette = this._styleEngine.palette;\n const badgeSize = Math.max(4, 8 / globalScale);\n\n // Source count badge (bottom-right)\n if (prov.sources.length > 0) {\n const bx = x + radius * 0.7;\n const by = y + radius * 0.6;\n const count = prov.sources.length;\n\n // Badge circle\n ctx.beginPath();\n ctx.arc(bx, by, badgeSize, 0, 2 * Math.PI);\n const intensity = Math.min(1, count / 3);\n ctx.fillStyle = count >= 2 ? palette.info : palette.textMuted;\n ctx.globalAlpha = 0.4 + intensity * 0.6;\n ctx.fill();\n\n // Badge number\n if (badgeSize > 3) {\n ctx.fillStyle = '#fff';\n ctx.globalAlpha = 1;\n ctx.font = `bold ${badgeSize * 1.2}px system-ui`;\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n ctx.fillText(String(count), bx, by);\n }\n }\n\n // Verified badge (top-right) — checkmark for content hash\n if (prov.contentHash || prov.blockchainAnchor) {\n const bx = x + radius * 0.7;\n const by = y - radius * 0.6;\n\n ctx.beginPath();\n ctx.arc(bx, by, badgeSize, 0, 2 * Math.PI);\n ctx.fillStyle = prov.blockchainAnchor ? palette.safe : palette.info;\n ctx.globalAlpha = 0.85;\n ctx.fill();\n\n // Checkmark\n ctx.globalAlpha = 1;\n ctx.strokeStyle = '#fff';\n ctx.lineWidth = badgeSize * 0.3;\n ctx.lineCap = 'round';\n ctx.lineJoin = 'round';\n ctx.beginPath();\n const s = badgeSize * 0.5;\n ctx.moveTo(bx - s * 0.5, by);\n ctx.lineTo(bx - s * 0.1, by + s * 0.4);\n ctx.lineTo(bx + s * 0.5, by - s * 0.3);\n ctx.stroke();\n }\n\n ctx.globalAlpha = 1;\n }\n\n /**\n * Fast path: paint a minimal filled circle.\n * No gradient, no border, no image, no label — just a flat arc.\n */\n private paintCircle(\n ctx: CanvasRenderingContext2D,\n node: GraphNode,\n x: number,\n y: number,\n _globalScale: number\n ): number {\n const radius = this.getRadius(node);\n const color = this._styleEngine.getNodeColor(node);\n\n ctx.beginPath();\n ctx.arc(x, y, radius, 0, 2 * Math.PI);\n ctx.fillStyle = color;\n ctx.fill();\n\n return radius;\n }\n\n /** Paint an avatar image clipped to the hexagon shape */\n private paintAvatar(\n ctx: CanvasRenderingContext2D,\n url: string,\n x: number,\n y: number,\n radius: number\n ): void {\n let img = imageCache.get(url);\n\n if (img === undefined) {\n // Start loading\n const newImg = new Image();\n // Only set crossOrigin for remote URLs — data URIs break with CORS attributes\n if (!url.startsWith('data:')) {\n newImg.crossOrigin = 'anonymous';\n }\n imageCache.set(url, null); // Mark as loading\n newImg.src = url;\n\n newImg.onload = () => {\n imageCache.set(url, newImg);\n // Trigger graph repaint so the image appears\n this._onImageLoad?.();\n };\n newImg.onerror = () => {\n // Don't permanently cache failures — allow retry\n imageCache.delete(url);\n };\n return;\n }\n\n if (!img) return; // Still loading\n\n ctx.save();\n\n // Clip to hexagon\n ctx.beginPath();\n for (let i = 0; i < 6; i++) {\n const angle = (Math.PI / 3) * i;\n const hx = x + radius * Math.cos(angle);\n const hy = y + radius * Math.sin(angle);\n if (i === 0) ctx.moveTo(hx, hy);\n else ctx.lineTo(hx, hy);\n }\n ctx.closePath();\n ctx.clip();\n\n // Draw image centered in the hexagon, cropped by hexagon clip path\n const imgAspect = img.naturalWidth / img.naturalHeight;\n // Full photos (http/https URLs) fill the hexagon; small SVG icons stay compact\n const isIcon = url.startsWith('data:');\n const scale = isIcon ? 0.5 : 1.0;\n let drawW: number, drawH: number;\n const size = radius * 2 * scale;\n if (imgAspect > 1) {\n drawH = size;\n drawW = size * imgAspect;\n } else {\n drawW = size;\n drawH = size / imgAspect;\n }\n ctx.globalAlpha = 0.95;\n ctx.drawImage(img, x - drawW / 2, y - drawH / 2, drawW, drawH);\n ctx.globalAlpha = 1;\n\n ctx.restore();\n }\n\n /**\n * Resolve image URL for a node from its properties,\n * following indirect links if needed.\n *\n * Two resolution strategies:\n * 1. Direct: node has a literal property (schema:image → \"https://...jpg\")\n * 2. Indirect: node links to an image node which has the URL\n * (node → schema:image → ImageObject → schema:url → \"https://...jpg\")\n */\n resolveImageUrl(\n node: GraphNode,\n getNode: (id: string) => GraphNode | undefined,\n getEdgesFrom?: (id: string) => GraphEdge[]\n ): string | null {\n // Strategy 1: Direct literal image URL on the node\n for (const pred of this._config.imagePredicates) {\n const literalVals = node.properties.get(pred);\n if (literalVals && literalVals.length > 0) {\n const url = literalVals[0].value;\n if (url.startsWith('http://') || url.startsWith('https://')) {\n return url;\n }\n }\n }\n\n // Strategy 2: Indirect — follow edge to an intermediate image node\n if (getEdgesFrom) {\n const edges = getEdgesFrom(node.id);\n for (const pred of this._config.imagePredicates) {\n for (const edge of edges) {\n if (edge.predicate !== pred) continue;\n\n const imageNode = getNode(edge.target);\n if (!imageNode) continue;\n\n // Check the intermediate node for the actual URL\n const urlVals = imageNode.properties.get(this._config.imageUrlPredicate);\n if (urlVals && urlVals.length > 0) {\n const url = urlVals[0].value;\n if (url.startsWith('http://') || url.startsWith('https://')) {\n return url;\n }\n }\n\n // Also check if the intermediate node has a direct image literal\n for (const imgPred of this._config.imagePredicates) {\n const vals = imageNode.properties.get(imgPred);\n if (vals && vals.length > 0) {\n const url = vals[0].value;\n if (url.startsWith('http://') || url.startsWith('https://')) {\n return url;\n }\n }\n }\n }\n }\n }\n\n return null;\n }\n}\n\n/**\n * Shorten a node label to fit under a hexagon.\n *\n * For path-like labels (\"packages/node-ui/src/ui/views/ProjectView.tsx\")\n * the informative bit is usually the *basename*, not the prefix — so we\n * middle-elide to preserve a hint of the parent and the full tail:\n * packages/node-ui/src/ui/views/ProjectView.tsx\n * → packages/…/ProjectView.tsx\n *\n * For `#123 Some Pull Request Title`-style labels we also preserve the\n * leading identifier (`#123 …`) because the number is more identifying\n * than the first words of the title.\n *\n * Everything else falls back to a tail ellipsis.\n */\nfunction truncateLabel(label: string, maxLen: number): string {\n if (label.length <= maxLen) return label;\n\n // Path-like: keep first segment + basename, elide the middle.\n if (label.includes('/')) {\n const parts = label.split('/');\n const head = parts[0];\n const tail = parts[parts.length - 1];\n const candidate = `${head}/\\u2026/${tail}`;\n if (parts.length >= 3 && candidate.length <= maxLen) return candidate;\n // Still too long — just keep the basename.\n if (tail.length <= maxLen - 2) return `\\u2026/${tail}`;\n }\n\n // \"#123 Title Of The Thing\" — keep the number, elide the title tail.\n const numMatch = /^(#\\d+)\\s+(.+)$/.exec(label);\n if (numMatch) {\n const prefix = numMatch[1];\n const rest = numMatch[2];\n const room = maxLen - prefix.length - 2;\n if (room > 4) return `${prefix} ${rest.slice(0, room)}\\u2026`;\n }\n\n return label.slice(0, maxLen - 1) + '\\u2026';\n}\n\nfunction lightenColor(hex: string, factor: number): string {\n const match = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n if (!match) return hex;\n const r = Math.round(parseInt(match[1], 16) + (255 - parseInt(match[1], 16)) * factor);\n const g = Math.round(parseInt(match[2], 16) + (255 - parseInt(match[2], 16)) * factor);\n const b = Math.round(parseInt(match[3], 16) + (255 - parseInt(match[3], 16)) * factor);\n return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;\n}\n","import type { GraphNode, GraphEdge, FocusConfig } from './types.js';\nimport type { GraphModel } from './graph-model.js';\n\nconst DEFAULT_FOCUS: Required<FocusConfig> = {\n focalNode: null,\n hops: 2,\n maxNodes: 200,\n expandOnClick: true,\n};\n\n/**\n * Filters a large GraphModel down to a renderable subgraph\n * centered around a focal node with N-hop expansion.\n */\nexport class FocusFilter {\n private _config: Required<FocusConfig>;\n private _visibleNodes = new Set<string>();\n private _focalNode: string | null = null;\n private _enabled = false;\n\n constructor(config: FocusConfig | undefined) {\n this._config = { ...DEFAULT_FOCUS, ...config };\n this._focalNode = this._config.focalNode ?? null;\n }\n\n get focalNode(): string | null {\n return this._focalNode;\n }\n\n get enabled(): boolean {\n return this._enabled;\n }\n\n get visibleNodeIds(): ReadonlySet<string> {\n return this._visibleNodes;\n }\n\n /**\n * Compute the visible subgraph. Call this after data changes.\n * Automatically enables focus mode if the graph exceeds maxNodes.\n */\n compute(model: GraphModel): {\n nodes: Map<string, GraphNode>;\n edges: Map<string, GraphEdge>;\n } {\n // If graph is small enough, show everything\n if (model.nodes.size <= this._config.maxNodes) {\n this._enabled = false;\n // Mark all as non-boundary\n for (const node of model.nodes.values()) {\n node.isBoundary = false;\n }\n return { nodes: model.nodes, edges: model.edges };\n }\n\n this._enabled = true;\n\n // Pick focal node if not set\n if (!this._focalNode) {\n const best = model.getHighestDegreeNode();\n this._focalNode = best?.id ?? null;\n }\n\n if (!this._focalNode) {\n return { nodes: new Map(), edges: new Map() };\n }\n\n // BFS from focal node up to N hops\n this._visibleNodes.clear();\n const queue: Array<{ id: string; depth: number }> = [{ id: this._focalNode, depth: 0 }];\n this._visibleNodes.add(this._focalNode);\n\n while (queue.length > 0 && this._visibleNodes.size < this._config.maxNodes) {\n const current = queue.shift()!;\n\n if (current.depth >= this._config.hops) continue;\n\n const neighbors = model.getNeighborIds(current.id);\n for (const neighborId of neighbors) {\n if (this._visibleNodes.size >= this._config.maxNodes) break;\n\n if (!this._visibleNodes.has(neighborId)) {\n this._visibleNodes.add(neighborId);\n queue.push({ id: neighborId, depth: current.depth + 1 });\n }\n }\n }\n\n // Build filtered node/edge maps and mark boundary nodes\n const filteredNodes = new Map<string, GraphNode>();\n const filteredEdges = new Map<string, GraphEdge>();\n\n for (const id of this._visibleNodes) {\n const node = model.getNode(id);\n if (node) {\n // Check if this node has hidden neighbors (= boundary)\n const allNeighbors = model.getNeighborIds(id);\n node.isBoundary = false;\n for (const nid of allNeighbors) {\n if (!this._visibleNodes.has(nid)) {\n node.isBoundary = true;\n break;\n }\n }\n filteredNodes.set(id, node);\n }\n }\n\n for (const edge of model.edges.values()) {\n if (this._visibleNodes.has(edge.source) && this._visibleNodes.has(edge.target)) {\n filteredEdges.set(edge.id, edge);\n }\n }\n\n return { nodes: filteredNodes, edges: filteredEdges };\n }\n\n /** Change the focal node and recompute */\n setFocus(nodeId: string | null): void {\n this._focalNode = nodeId;\n }\n\n /** Expand a boundary node: add it as a secondary focus point */\n expandNode(nodeId: string, model: GraphModel): void {\n // Add the node's immediate neighbors to visible set\n const neighbors = model.getNeighborIds(nodeId);\n for (const nid of neighbors) {\n this._visibleNodes.add(nid);\n }\n }\n\n /** Disable focus filtering (show all) */\n disable(): void {\n this._enabled = false;\n this._visibleNodes.clear();\n }\n\n /** Re-enable with current or new config */\n enable(focalNode?: string): void {\n this._enabled = true;\n if (focalNode) this._focalNode = focalNode;\n }\n}\n","import type { ReificationConfig, ReificationPattern, PropertyAnnotation } from './types.js';\nimport { GraphModel } from './graph-model.js';\n\n/** Standard RDF reification pattern */\nconst STANDARD_RDF_REIFICATION: ReificationPattern = {\n statementType: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#Statement',\n subjectPredicate: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#subject',\n predicatePredicate: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#predicate',\n objectPredicate: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#object',\n};\n\nconst DEFAULT_CONFIG: Required<ReificationConfig> = {\n enabled: false,\n patterns: [STANDARD_RDF_REIFICATION],\n};\n\ninterface DetectedStatement {\n statementNodeId: string;\n reifiedSubject: string | null;\n reifiedPredicate: string | null;\n reifiedObject: string | null;\n /** All other properties on the statement node (provenance, timestamps, etc.) */\n annotations: Array<{ predicate: string; value: string; datatype?: string }>;\n}\n\n/**\n * Detects and collapses reified statement nodes.\n *\n * When a node matches a reification pattern (has the right rdf:type + subject/predicate/object links),\n * it is removed from the visual graph. Its non-structural properties (provenance, timestamps, etc.)\n * are attached as annotations to the corresponding property on the reified subject node.\n */\nexport class ReificationCollapser {\n private _config: Required<ReificationConfig>;\n\n constructor(config: ReificationConfig | undefined) {\n this._config = {\n enabled: config?.enabled ?? DEFAULT_CONFIG.enabled,\n patterns: config?.patterns ?? DEFAULT_CONFIG.patterns,\n };\n }\n\n get enabled(): boolean {\n return this._config.enabled;\n }\n\n /**\n * Process the graph model: detect reified statements, collapse them,\n * and attach their metadata as annotations on the original properties.\n *\n * Returns the set of node IDs that were collapsed (removed from visual rendering).\n */\n collapse(model: GraphModel): Set<string> {\n if (!this._config.enabled) return new Set();\n\n const collapsedNodeIds = new Set<string>();\n const detected = this.detectStatements(model);\n\n for (const stmt of detected) {\n if (!stmt.reifiedSubject) continue;\n\n const subjectNode = model.getNode(stmt.reifiedSubject);\n if (!subjectNode) continue;\n\n // Attach annotations to the correct property on the subject node\n if (stmt.reifiedPredicate && stmt.annotations.length > 0) {\n const annotations: PropertyAnnotation[] = stmt.annotations.map((a) => ({\n predicate: a.predicate,\n value: a.value,\n datatype: a.datatype,\n }));\n\n // Find the property values for the reified predicate\n const propValues = subjectNode.properties.get(stmt.reifiedPredicate);\n if (propValues) {\n // Try to match the specific value and attach annotations\n for (const pv of propValues) {\n if (stmt.reifiedObject && pv.value === stmt.reifiedObject) {\n pv.annotations = [...(pv.annotations ?? []), ...annotations];\n break;\n }\n }\n // If no specific match, attach to the first value\n if (propValues.length > 0 && !propValues.some((pv) => pv.annotations?.length)) {\n propValues[0].annotations = [...(propValues[0].annotations ?? []), ...annotations];\n }\n }\n\n // Also check metadata map\n const metaValues = subjectNode.metadata.get(stmt.reifiedPredicate);\n if (metaValues && metaValues.length > 0) {\n metaValues[0].annotations = [...(metaValues[0].annotations ?? []), ...annotations];\n }\n }\n\n // Mark the statement node for removal from visual graph\n collapsedNodeIds.add(stmt.statementNodeId);\n }\n\n return collapsedNodeIds;\n }\n\n /** Detect reified statements in the model */\n private detectStatements(model: GraphModel): DetectedStatement[] {\n const results: DetectedStatement[] = [];\n\n for (const pattern of this._config.patterns) {\n // Find all nodes with the statement type\n for (const node of model.nodes.values()) {\n if (!node.types.includes(pattern.statementType)) continue;\n\n const stmt: DetectedStatement = {\n statementNodeId: node.id,\n reifiedSubject: null,\n reifiedPredicate: null,\n reifiedObject: null,\n annotations: [],\n };\n\n // Extract subject/predicate/object links from edges\n const edgesFrom = model.getEdgesFrom(node.id);\n for (const edge of edgesFrom) {\n if (edge.predicate === pattern.subjectPredicate) {\n stmt.reifiedSubject = edge.target;\n } else if (edge.predicate === pattern.predicatePredicate) {\n stmt.reifiedPredicate = edge.target;\n } else if (edge.predicate === pattern.objectPredicate) {\n stmt.reifiedObject = edge.target;\n }\n }\n\n // Also check properties for the object (might be a literal)\n for (const [pred, values] of node.properties) {\n if (pred === pattern.objectPredicate && values.length > 0) {\n stmt.reifiedObject = values[0].value;\n }\n }\n\n // Also check metadata map — needed when the reification predicates\n // (aboutSubject, forPredicate, hasValue) are configured as metadata\n for (const [pred, values] of node.metadata) {\n if (values.length === 0) continue;\n if (!stmt.reifiedSubject && pred === pattern.subjectPredicate) {\n stmt.reifiedSubject = values[0].value;\n } else if (!stmt.reifiedPredicate && pred === pattern.predicatePredicate) {\n stmt.reifiedPredicate = values[0].value;\n } else if (!stmt.reifiedObject && pred === pattern.objectPredicate) {\n stmt.reifiedObject = values[0].value;\n }\n }\n\n // Everything else on the statement node becomes an annotation\n const structuralPreds = new Set([\n pattern.subjectPredicate,\n pattern.predicatePredicate,\n pattern.objectPredicate,\n ]);\n\n for (const [pred, values] of node.properties) {\n if (!structuralPreds.has(pred)) {\n for (const v of values) {\n stmt.annotations.push({ predicate: pred, value: v.value, datatype: v.datatype });\n }\n }\n }\n for (const [pred, values] of node.metadata) {\n for (const v of values) {\n stmt.annotations.push({ predicate: pred, value: v.value, datatype: v.datatype });\n }\n }\n\n results.push(stmt);\n }\n }\n\n return results;\n }\n}\n","import type { MetadataConfig } from './types.js';\n\n/** Default metadata predicates — properties shown in the metadata panel, not as edges */\nconst DEFAULT_METADATA_PREDICATES = [\n // PROV-O\n 'http://www.w3.org/ns/prov#wasGeneratedBy',\n 'http://www.w3.org/ns/prov#wasAttributedTo',\n 'http://www.w3.org/ns/prov#wasDerivedFrom',\n 'http://www.w3.org/ns/prov#startedAtTime',\n 'http://www.w3.org/ns/prov#endedAtTime',\n 'http://www.w3.org/ns/prov#generatedAtTime',\n 'http://www.w3.org/ns/prov#wasStartedBy',\n 'http://www.w3.org/ns/prov#wasEndedBy',\n // Dublin Core\n 'http://purl.org/dc/terms/created',\n 'http://purl.org/dc/terms/modified',\n 'http://purl.org/dc/terms/source',\n 'http://purl.org/dc/terms/creator',\n 'http://purl.org/dc/terms/publisher',\n // Common admin\n 'http://www.w3.org/2002/07/owl#versionInfo',\n];\n\n/**\n * Provides the list of predicates that should be treated as metadata\n * (shown in the detail panel, not rendered as graph edges).\n *\n * This is a simple config resolver — the actual separation happens in GraphModel.\n */\nexport class MetadataExtractor {\n private _predicates: Set<string>;\n\n constructor(config?: MetadataConfig) {\n this._predicates = new Set([\n ...DEFAULT_METADATA_PREDICATES,\n ...(config?.predicates ?? []),\n ]);\n }\n\n /** Get all metadata predicate URIs */\n get predicates(): string[] {\n return [...this._predicates];\n }\n\n /** Check if a predicate is a metadata predicate */\n isMetadata(predicateUri: string): boolean {\n return this._predicates.has(predicateUri);\n }\n\n /** Add additional metadata predicates at runtime */\n addPredicates(predicates: string[]): void {\n for (const p of predicates) {\n this._predicates.add(p);\n }\n }\n}\n","import type { GraphEventType, GraphEventMap, GraphEventHandler } from './types.js';\n\n/**\n * Typed event emitter for graph visualization events.\n */\nexport class GraphEventEmitter {\n private _handlers = new Map<GraphEventType, Set<GraphEventHandler<GraphEventType>>>();\n\n /** Subscribe to an event */\n on<T extends GraphEventType>(event: T, handler: GraphEventHandler<T>): () => void {\n let set = this._handlers.get(event);\n if (!set) {\n set = new Set();\n this._handlers.set(event, set);\n }\n set.add(handler as GraphEventHandler<GraphEventType>);\n\n // Return unsubscribe function\n return () => {\n set!.delete(handler as GraphEventHandler<GraphEventType>);\n };\n }\n\n /** Unsubscribe from an event */\n off<T extends GraphEventType>(event: T, handler: GraphEventHandler<T>): void {\n const set = this._handlers.get(event);\n if (set) {\n set.delete(handler as GraphEventHandler<GraphEventType>);\n }\n }\n\n /** Emit an event to all registered handlers */\n emit<T extends GraphEventType>(event: T, data: GraphEventMap[T]): void {\n const set = this._handlers.get(event);\n if (set) {\n for (const handler of set) {\n try {\n (handler as GraphEventHandler<T>)(data);\n } catch (err) {\n console.error(`[dkg-graph-viz] Error in ${event} handler:`, err);\n }\n }\n }\n }\n\n /** Remove all handlers for an event, or all handlers if no event specified */\n removeAll(event?: GraphEventType): void {\n if (event) {\n this._handlers.delete(event);\n } else {\n this._handlers.clear();\n }\n }\n}\n","/* eslint-disable @typescript-eslint/no-explicit-any */\nimport ForceGraph from 'force-graph';\nimport type { GraphNode, GraphEdge, ForceNode, ForceLink } from './types.js';\nimport type { RendererBackend, RendererBackendConfig } from './renderer-backend.js';\nimport type { AnimationConfig } from './view-config.js';\nimport type { ColorPalette } from './palette.js';\nimport { HexagonPainter } from './hexagon-painter.js';\nimport { StyleEngine } from './style-engine.js';\nimport { GraphEventEmitter } from './events.js';\n\n/**\n * 2D Canvas renderer backend using force-graph.\n *\n * Renders hexagonal nodes via HexagonPainter on an HTML5 Canvas 2D context.\n * Implements RendererBackend so it can be swapped with a 3D renderer.\n */\nexport class Canvas2DRenderer implements RendererBackend {\n private _container: HTMLElement;\n private _graph: any = null;\n private _hexPainter: HexagonPainter;\n private _styleEngine: StyleEngine;\n private _events: GraphEventEmitter;\n private _currentNodes = new Map<string, ForceNode>();\n private _positionCache = new Map<string, {\n x?: number;\n y?: number;\n vx?: number;\n vy?: number;\n fx?: number | null;\n fy?: number | null;\n }>();\n private _currentLinks: ForceLink[] = [];\n private _repaintPending = false;\n\n // Animation state\n private _riskPulseEnabled = false;\n private _hoverTraceEnabled = false;\n private _hoveredNodeId: string | null = null;\n private _fadeInEnabled = false;\n private _loadTime = 0;\n private _fadeInDuration = 1500;\n private _animFrameId: number | null = null;\n private _initialFitDone = false;\n private _autoFitDisabled = false;\n private _lastTopologyKey = '';\n private _continuousSimulation = false;\n private _fitTimeoutId: ReturnType<typeof setTimeout> | null = null;\n\n /** force-graph version compatibility guard */\n private _setAlphaTarget(value: number): void {\n const maybeFn = this._graph?.d3AlphaTarget;\n if (typeof maybeFn === 'function') {\n maybeFn.call(this._graph, value);\n }\n }\n\n // KA boundary state\n private _kaGroups: Map<string, Set<string>> = new Map();\n private _kaEnabled = false;\n private _kaBoundaryOpacity = 0.06;\n\n constructor(config: RendererBackendConfig) {\n this._container = config.container;\n this._hexPainter = config.hexPainter;\n this._styleEngine = config.styleEngine;\n this._events = config.events;\n }\n\n /** Initialize the force-graph instance */\n init(): void {\n if (this._graph) return;\n\n const styleConfig = this._styleEngine.config;\n\n const width = this._container.clientWidth || this._container.offsetWidth || 800;\n const height = this._container.clientHeight || this._container.offsetHeight || 600;\n\n // When an image finishes loading, nudge the graph to repaint\n this._hexPainter.setImageLoadCallback(() => {\n this._scheduleRepaint();\n });\n\n this._graph = (ForceGraph as any)()(this._container)\n .width(width)\n .height(height)\n .backgroundColor('transparent')\n .nodeCanvasObjectMode((node: any) => {\n // Featured nodes (sizeMultiplier) render in 'after' pass — always on top\n const fn = node as ForceNode;\n if (fn._graphNode?.sizeMultiplier && fn._graphNode.sizeMultiplier > 1) {\n return 'after';\n }\n return 'replace';\n })\n .nodeCanvasObject((node: any, ctx: CanvasRenderingContext2D, globalScale: number) => {\n const fn = node as ForceNode;\n if (!fn._graphNode) return;\n const x = fn.x ?? 0;\n const y = fn.y ?? 0;\n\n // Fade-in on initial load\n if (this._fadeInEnabled && this._loadTime > 0) {\n const elapsed = performance.now() - this._loadTime;\n if (elapsed < this._fadeInDuration) {\n const t = elapsed / this._fadeInDuration;\n const ease = 1 - Math.pow(1 - t, 3); // easeOutCubic\n ctx.globalAlpha = ease;\n }\n }\n\n // Risk pulse: sin-modulated scale for high-risk nodes (wall-clock time)\n let pulseScale = 1.0;\n if (this._riskPulseEnabled && fn._graphNode.isHighRisk) {\n const t = performance.now() / 1000; // seconds\n const hash = simpleHash(fn._graphNode.id);\n pulseScale = 1 + 0.15 * Math.sin(t * 1.25 + hash);\n }\n\n this._hexPainter.paint(ctx, fn._graphNode, x, y, globalScale, pulseScale);\n ctx.globalAlpha = 1;\n })\n .nodePointerAreaPaint((node: any, color: string, ctx: CanvasRenderingContext2D) => {\n const fn = node as ForceNode;\n if (!fn._graphNode) return;\n const radius = this._hexPainter.getRadius(fn._graphNode);\n const hitRadius = radius + 6;\n const x = fn.x ?? 0;\n const y = fn.y ?? 0;\n\n ctx.beginPath();\n ctx.arc(x, y, hitRadius, 0, Math.PI * 2);\n ctx.fillStyle = color;\n ctx.fill();\n })\n .linkColor((link: any) => {\n const fl = link as ForceLink;\n if (fl._graphEdge) {\n return this._styleEngine.getEdgeColor(fl._graphEdge);\n }\n return styleConfig.defaultEdgeColor;\n })\n .linkWidth(styleConfig.edgeWidth)\n .linkDirectionalArrowLength(styleConfig.edgeArrowSize)\n .linkDirectionalArrowRelPos(1)\n .linkCanvasObjectMode(() => 'after')\n .linkCanvasObject((link: any, ctx: CanvasRenderingContext2D, globalScale: number) => {\n if (globalScale < 0.3) return;\n\n const fl = link as ForceLink;\n if (!fl._graphEdge) return;\n\n const label = fl._graphEdge.label;\n if (!label) return;\n\n const fadeStart = 0.3;\n const fadeFull = 0.8;\n const opacity = Math.min(1, (globalScale - fadeStart) / (fadeFull - fadeStart));\n\n const fontSize = 12 / globalScale;\n\n const src = fl.source as any;\n const tgt = fl.target as any;\n if (!src.x || !tgt.x) return;\n\n const midX = (src.x + tgt.x) / 2;\n const midY = (src.y + tgt.y) / 2;\n\n ctx.save();\n ctx.globalAlpha = opacity;\n ctx.font = `${fontSize}px ${styleConfig.fontFamily}`;\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n\n // Background pill for readability\n const textWidth = ctx.measureText(label).width;\n const pad = 2 / globalScale;\n ctx.fillStyle = 'rgba(15, 23, 42, 0.75)';\n ctx.beginPath();\n ctx.roundRect(\n midX - textWidth / 2 - pad,\n midY - fontSize / 2 - pad,\n textWidth + pad * 2,\n fontSize + pad * 2,\n 2 / globalScale\n );\n ctx.fill();\n\n ctx.fillStyle = '#94a3b8';\n ctx.fillText(label, midX, midY);\n ctx.restore();\n })\n .onRenderFramePre((ctx: CanvasRenderingContext2D, _globalScale: number) => {\n this._drawKaBoundaries(ctx);\n })\n .onNodeClick((node: any) => {\n const fn = node as ForceNode;\n if (fn._graphNode) {\n this._events.emit('node:click', fn._graphNode);\n }\n })\n .onNodeDrag((_node: any) => {\n try {\n this._graph.d3ReheatSimulation();\n } catch {\n /* noop */\n }\n })\n .onNodeDragEnd((_node: any) => {\n try {\n this._graph.d3ReheatSimulation();\n } catch {\n /* noop */\n }\n })\n .onNodeHover((node: any) => {\n const fn = node as ForceNode | null;\n if (fn?._graphNode) {\n this._hoveredNodeId = fn._graphNode.id;\n this._events.emit('node:hover', fn._graphNode);\n this._container.style.cursor = 'pointer';\n } else {\n this._hoveredNodeId = null;\n this._events.emit('node:unhover', null as unknown as GraphNode);\n this._container.style.cursor = 'default';\n }\n })\n .onLinkClick((link: any) => {\n const fl = link as ForceLink;\n if (fl._graphEdge) {\n this._events.emit('edge:click', fl._graphEdge);\n }\n })\n .onBackgroundClick((_event: any) => {\n this._events.emit('background:click', { x: 0, y: 0 });\n })\n .cooldownTicks(200)\n .d3AlphaDecay(0.02)\n .d3VelocityDecay(0.3)\n .warmupTicks(60)\n .onEngineStop(() => {\n if (!this._autoFitDisabled && !this._initialFitDone && this._graph) {\n this._initialFitDone = true;\n this._graph.zoomToFit(400, 40);\n }\n });\n\n // Tune the default d3 forces for better spread with large graphs\n const charge = this._graph.d3Force('charge') as any;\n if (charge?.strength) { charge.strength(-150); charge.distanceMax?.(1000); }\n const link = this._graph.d3Force('link') as any;\n if (link?.distance) { link.distance(80).strength(0.2); }\n const center = this._graph.d3Force('center') as any;\n if (center?.strength) { center.strength(0.03); }\n }\n\n /**\n * Render the graph with the given nodes and edges.\n * Preserves positions of existing nodes for smooth transitions.\n */\n render(\n nodes: Map<string, GraphNode>,\n edges: Map<string, GraphEdge>,\n collapsedNodeIds?: Set<string>\n ): void {\n if (!this._graph) this.init();\n\n // Reset auto-fit when graph topology changes.\n // Sorted node IDs + sorted edge source->target keys for strong collision resistance.\n let topoHash = 0;\n const sortedNodeIds = [...nodes.keys()].sort();\n for (const id of sortedNodeIds) {\n topoHash = ((topoHash * 31) + simpleHash(id)) | 0;\n }\n const sortedEdgeKeys: string[] = [];\n for (const e of edges.values()) {\n sortedEdgeKeys.push(`${e.source}\\0${e.target}`);\n }\n sortedEdgeKeys.sort();\n for (const ek of sortedEdgeKeys) {\n topoHash = ((topoHash * 31) + simpleHash(ek)) | 0;\n }\n const topoKey = String(topoHash);\n if (topoKey !== this._lastTopologyKey) {\n this._lastTopologyKey = topoKey;\n this._initialFitDone = false;\n }\n\n // Record load time for fade-in animation\n if (this._loadTime === 0) {\n this._loadTime = performance.now();\n }\n\n // Update max degree for hexagon scaling\n let maxDegree = 1;\n for (const node of nodes.values()) {\n if (node.degree > maxDegree) maxDegree = node.degree;\n }\n this._hexPainter.setMaxDegree(maxDegree);\n\n // Build force-graph node array, preserving positions for existing nodes\n // and assigning chronological x-positions for dated nodes\n const dateNs = ['http://schema.org/dateCreated', 'https://schema.org/dateCreated'];\n const nodeDates = new Map<string, number>();\n for (const [id, gn] of nodes) {\n for (const ns of dateNs) {\n const vals = gn.properties?.get(ns);\n if (vals && vals.length > 0) {\n const d = new Date(vals[0].value);\n if (!isNaN(d.getTime())) { nodeDates.set(id, d.getTime()); break; }\n }\n }\n }\n let xByDate: Map<string, number> | null = null;\n if (nodeDates.size >= 2) {\n const times = [...nodeDates.values()];\n const minT = Math.min(...times);\n const maxT = Math.max(...times);\n const range = maxT - minT || 1;\n const spread = Math.max(400, nodeDates.size * 120);\n xByDate = new Map();\n for (const [id, t] of nodeDates) {\n xByDate.set(id, ((t - minT) / range - 0.5) * spread);\n }\n }\n\n const newNodes = new Map<string, ForceNode>();\n for (const [id, gn] of nodes) {\n if (collapsedNodeIds?.has(id)) continue;\n\n const existing = this._currentNodes.get(id);\n const cached = this._positionCache.get(id);\n const chronoX = xByDate?.get(id);\n newNodes.set(id, {\n id,\n x: existing?.x ?? cached?.x ?? chronoX ?? (Math.random() - 0.5) * 800,\n y: existing?.y ?? cached?.y ?? (Math.random() - 0.5) * 400,\n vx: existing?.vx ?? cached?.vx,\n vy: existing?.vy ?? cached?.vy,\n fx: typeof existing?.fx === 'number'\n ? existing.fx\n : (typeof cached?.fx === 'number' ? cached.fx : undefined),\n fy: typeof existing?.fy === 'number'\n ? existing.fy\n : (typeof cached?.fy === 'number' ? cached.fy : undefined),\n _graphNode: gn,\n });\n }\n\n // Build force-graph link array\n const validNodeIds = new Set(newNodes.keys());\n const newLinks: ForceLink[] = [];\n for (const edge of edges.values()) {\n if (collapsedNodeIds?.has(edge.source) || collapsedNodeIds?.has(edge.target)) continue;\n if (!validNodeIds.has(edge.source) || !validNodeIds.has(edge.target)) continue;\n\n newLinks.push({\n source: edge.source,\n target: edge.target,\n _graphEdge: edge,\n });\n }\n\n this._currentNodes = newNodes;\n this._currentLinks = newLinks;\n const freshCache = new Map<string, { x?: number; y?: number; vx?: number; vy?: number; fx?: number | null; fy?: number | null }>();\n for (const [id, node] of newNodes) {\n freshCache.set(id, {\n x: node.x,\n y: node.y,\n vx: node.vx,\n vy: node.vy,\n fx: typeof node.fx === 'number' ? node.fx : undefined,\n fy: typeof node.fy === 'number' ? node.fy : undefined,\n });\n }\n this._positionCache = freshCache;\n\n // Sort nodes so featured/high-risk nodes are last (painted on top)\n const sortedNodes = [...newNodes.values()].sort((a, b) => {\n const aOrder = (a._graphNode?.sizeMultiplier && a._graphNode.sizeMultiplier > 1) ? 100\n : a._graphNode?.isHighRisk ? 10 : 0;\n const bOrder = (b._graphNode?.sizeMultiplier && b._graphNode.sizeMultiplier > 1) ? 100\n : b._graphNode?.isHighRisk ? 10 : 0;\n return aOrder - bOrder;\n });\n\n this._graph.graphData({\n nodes: sortedNodes,\n links: newLinks,\n });\n\n // Schedule initial fit independently of engine stop (handles continuous mode too)\n if (!this._autoFitDisabled && !this._initialFitDone) {\n if (this._fitTimeoutId !== null) clearTimeout(this._fitTimeoutId);\n this._fitTimeoutId = setTimeout(() => {\n if (this._graph && !this._autoFitDisabled && !this._initialFitDone) {\n this._initialFitDone = true;\n this._graph.zoomToFit(400, 40);\n }\n this._fitTimeoutId = null;\n }, 300);\n }\n\n // Add a gentle x-force to maintain chronological left→right ordering\n if (xByDate && xByDate.size >= 2) {\n this._graph.d3Force('chronoX', (alpha: number) => {\n for (const fn of sortedNodes) {\n const targetX = xByDate!.get(fn.id);\n if (targetX !== undefined && fn.fx == null) {\n fn.vx = ((fn.vx ?? 0) + (targetX - (fn.x ?? 0)) * 0.05 * alpha);\n }\n }\n });\n } else {\n this._graph.d3Force('chronoX', null);\n }\n }\n\n /** Focus the camera on a specific node */\n centerOnNode(nodeId: string, durationMs = 350, zoomLevel?: number): void {\n const fn = this._currentNodes.get(nodeId);\n if (fn && this._graph) {\n this._graph.centerAt(fn.x, fn.y, durationMs);\n if (zoomLevel != null) {\n this._graph.zoom(zoomLevel, durationMs);\n }\n }\n }\n\n /** Fit all nodes in view */\n zoomToFit(padding = 40, durationMs = 500): void {\n this._graph?.zoomToFit(durationMs, padding);\n }\n\n /** Prevent the renderer from automatically calling zoomToFit when the simulation settles. */\n set autoFitDisabled(value: boolean) {\n this._autoFitDisabled = value;\n }\n\n /** Get the underlying canvas element for export */\n getCanvas(): HTMLCanvasElement | null {\n if (!this._graph) return null;\n const canvas = this._container.querySelector('canvas');\n return canvas ?? null;\n }\n\n /** Repaint node/edge visuals without rebuilding graph data. */\n refresh(): void {\n if (!this._graph) return;\n const maybeRefresh = this._graph.refresh;\n if (typeof maybeRefresh === 'function') {\n maybeRefresh.call(this._graph);\n return;\n }\n this._scheduleRepaint();\n }\n\n /**\n * Schedule a debounced repaint — used when images finish loading\n * after the simulation has cooled down. Limits the reheat to 3\n * ticks so the charge force can't displace settled nodes noticeably.\n */\n private _scheduleRepaint(): void {\n if (this._repaintPending) return;\n this._repaintPending = true;\n requestAnimationFrame(() => {\n this._repaintPending = false;\n if (!this._graph) return;\n\n if (this._continuousSimulation || this._riskPulseEnabled) {\n try { this._graph.d3ReheatSimulation(); } catch { /* noop */ }\n } else {\n this._graph.cooldownTicks(3);\n try { this._graph.d3ReheatSimulation(); } catch { /* noop */ }\n this._setAlphaTarget(0);\n setTimeout(() => {\n if (this._graph && !this._continuousSimulation && !this._riskPulseEnabled) {\n this._graph.cooldownTicks(0);\n this._setAlphaTarget(0);\n }\n }, 60);\n }\n });\n }\n\n /** Stop the animation loop */\n private _stopAnimLoop(): void {\n if (this._animFrameId !== null) {\n cancelAnimationFrame(this._animFrameId);\n this._animFrameId = null;\n }\n }\n\n /** Configure Knowledge Asset boundary rendering */\n setKaGroups(groups: Map<string, Set<string>>, enabled: boolean, opacity = 0.06): void {\n this._kaGroups = groups;\n this._kaEnabled = enabled;\n this._kaBoundaryOpacity = opacity;\n }\n\n /** Draw convex hulls around KA groups */\n private _drawKaBoundaries(ctx: CanvasRenderingContext2D): void {\n if (!this._kaEnabled || this._kaGroups.size === 0) return;\n\n const palette = this._styleEngine.palette;\n let colorIdx = 0;\n\n for (const [_kaUri, memberIds] of this._kaGroups) {\n const points: [number, number][] = [];\n for (const nodeId of memberIds) {\n const fn = this._currentNodes.get(nodeId);\n if (fn && fn.x !== undefined && fn.y !== undefined) {\n points.push([fn.x, fn.y]);\n }\n }\n if (points.length < 3) continue;\n\n const hull = convexHull(points);\n if (hull.length < 3) continue;\n\n const color = palette.nodeColors[colorIdx % palette.nodeColors.length];\n colorIdx++;\n\n ctx.save();\n ctx.beginPath();\n // Expand hull slightly with padding\n const cx = hull.reduce((s, p) => s + p[0], 0) / hull.length;\n const cy = hull.reduce((s, p) => s + p[1], 0) / hull.length;\n for (let i = 0; i < hull.length; i++) {\n const dx = hull[i][0] - cx;\n const dy = hull[i][1] - cy;\n const len = Math.sqrt(dx * dx + dy * dy);\n const pad = 30;\n const px = hull[i][0] + (len > 0 ? (dx / len) * pad : 0);\n const py = hull[i][1] + (len > 0 ? (dy / len) * pad : 0);\n if (i === 0) ctx.moveTo(px, py);\n else ctx.lineTo(px, py);\n }\n ctx.closePath();\n ctx.fillStyle = color;\n ctx.globalAlpha = this._kaBoundaryOpacity;\n ctx.fill();\n ctx.strokeStyle = color;\n ctx.globalAlpha = this._kaBoundaryOpacity * 2.5;\n ctx.lineWidth = 1;\n ctx.stroke();\n ctx.restore();\n }\n }\n\n /** Resize to fit container */\n resize(): void {\n if (this._graph) {\n this._graph.width(this._container.clientWidth);\n this._graph.height(this._container.clientHeight);\n }\n }\n\n /** Clean up */\n destroy(): void {\n this._stopAnimLoop();\n if (this._fitTimeoutId !== null) {\n clearTimeout(this._fitTimeoutId);\n this._fitTimeoutId = null;\n }\n if (this._graph) {\n this._graph._destructor();\n this._graph = null;\n }\n this._currentNodes.clear();\n this._positionCache.clear();\n this._currentLinks = [];\n }\n\n /** Apply animation configuration: link particles, drift, pulse, hover trace, fade-in */\n applyAnimation(config: AnimationConfig): void {\n if (!this._graph) return;\n\n // Risk pulse\n this._riskPulseEnabled = config.riskPulse ?? false;\n // Fade-in\n this._fadeInEnabled = config.fadeIn ?? false;\n // Hover trace\n this._hoverTraceEnabled = config.hoverTrace ?? false;\n\n const baseSpeed = config.linkParticleSpeed ?? 0.005;\n const baseColor = config.linkParticleColor ?? 'rgba(100,150,255,0.6)';\n const baseWidth = config.linkParticleWidth ?? 1.5;\n\n if (config.linkParticles || config.hoverTrace) {\n const particleCount = config.linkParticleCount ?? 1;\n\n if (this._hoverTraceEnabled) {\n // Per-link callbacks for hover trace\n this._graph\n .linkDirectionalParticles((link: any) => {\n if (!this._hoveredNodeId) return config.linkParticles ? particleCount : 0;\n const fl = link as ForceLink;\n const src = typeof fl.source === 'object' ? (fl.source as any).id : fl.source;\n const tgt = typeof fl.target === 'object' ? (fl.target as any).id : fl.target;\n const isHovered = src === this._hoveredNodeId || tgt === this._hoveredNodeId;\n return isHovered ? 4 : (config.linkParticles ? particleCount : 0);\n })\n .linkDirectionalParticleSpeed((link: any) => {\n if (!this._hoveredNodeId) return baseSpeed;\n const fl = link as ForceLink;\n const src = typeof fl.source === 'object' ? (fl.source as any).id : fl.source;\n const tgt = typeof fl.target === 'object' ? (fl.target as any).id : fl.target;\n return (src === this._hoveredNodeId || tgt === this._hoveredNodeId) ? 0.02 : baseSpeed;\n })\n .linkDirectionalParticleWidth((link: any) => {\n if (!this._hoveredNodeId) return baseWidth;\n const fl = link as ForceLink;\n const src = typeof fl.source === 'object' ? (fl.source as any).id : fl.source;\n const tgt = typeof fl.target === 'object' ? (fl.target as any).id : fl.target;\n return (src === this._hoveredNodeId || tgt === this._hoveredNodeId) ? 3 : baseWidth;\n })\n .linkDirectionalParticleColor((link: any) => {\n if (!this._hoveredNodeId) return baseColor;\n const fl = link as ForceLink;\n const src = typeof fl.source === 'object' ? (fl.source as any).id : fl.source;\n const tgt = typeof fl.target === 'object' ? (fl.target as any).id : fl.target;\n return (src === this._hoveredNodeId || tgt === this._hoveredNodeId) ? '#60a0ff' : baseColor;\n });\n } else {\n this._graph\n .linkDirectionalParticles(particleCount)\n .linkDirectionalParticleSpeed(baseSpeed)\n .linkDirectionalParticleWidth(baseWidth)\n .linkDirectionalParticleColor(() => baseColor);\n }\n } else {\n this._graph.linkDirectionalParticles(0);\n }\n\n // When drift or pulse is active, the simulation must run indefinitely\n const needsContinuousRender = config.drift || this._riskPulseEnabled;\n this._continuousSimulation = needsContinuousRender;\n\n if (needsContinuousRender) {\n this._graph.cooldownTicks(Infinity);\n this._setAlphaTarget(config.driftAlpha ?? 0.008);\n\n // render() runs before applyAnimation(), so the fallback in render() may\n // not fire when _continuousSimulation wasn't set yet. Trigger here too.\n if (!this._autoFitDisabled && !this._initialFitDone) {\n if (this._fitTimeoutId !== null) clearTimeout(this._fitTimeoutId);\n this._fitTimeoutId = setTimeout(() => {\n if (this._graph && !this._autoFitDisabled && !this._initialFitDone) {\n this._initialFitDone = true;\n this._graph.zoomToFit(400, 40);\n }\n this._fitTimeoutId = null;\n }, 300);\n }\n } else {\n this._graph.cooldownTicks(120);\n this._setAlphaTarget(0);\n }\n\n // Stop any previous animation loop before potentially starting a new one\n this._stopAnimLoop();\n\n // For risk pulse, we need force-graph to keep calling nodeCanvasObject.\n // Drift keeps the simulation warm, which triggers continuous redraws.\n // If pulse is on but drift is off, reheat periodically to force redraws.\n if (this._riskPulseEnabled && !config.drift) {\n const loop = () => {\n if (!this._riskPulseEnabled || !this._graph) {\n this._animFrameId = null;\n return;\n }\n try { this._graph.d3ReheatSimulation(); } catch { /* noop */ }\n this._animFrameId = requestAnimationFrame(loop);\n };\n this._animFrameId = requestAnimationFrame(loop);\n }\n }\n\n /** Apply palette: update background color */\n applyPalette(palette: ColorPalette): void {\n if (!this._graph) return;\n this._graph.backgroundColor(palette.background === '#0a0a0f' ? 'transparent' : palette.background);\n }\n}\n\n/** Simple numeric hash of a string (for phase-offsetting pulse animations) */\nfunction simpleHash(s: string): number {\n let h = 0;\n for (let i = 0; i < s.length; i++) {\n h = ((h << 5) - h + s.charCodeAt(i)) | 0;\n }\n return h;\n}\n\n/** Graham scan convex hull algorithm */\nfunction convexHull(points: [number, number][]): [number, number][] {\n if (points.length < 3) return points;\n const pts = [...points].sort((a, b) => a[0] - b[0] || a[1] - b[1]);\n\n const cross = (o: [number, number], a: [number, number], b: [number, number]) =>\n (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);\n\n const lower: [number, number][] = [];\n for (const p of pts) {\n while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0)\n lower.pop();\n lower.push(p);\n }\n\n const upper: [number, number][] = [];\n for (let i = pts.length - 1; i >= 0; i--) {\n const p = pts[i];\n while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0)\n upper.pop();\n upper.push(p);\n }\n\n lower.pop();\n upper.pop();\n return lower.concat(upper);\n}\n\n/**\n * @deprecated Use Canvas2DRenderer instead. This alias exists for backward compatibility.\n */\nexport const ForceGraphRenderer = Canvas2DRenderer;\n","import type {\n RdfGraphVizConfig,\n RdfTriple,\n GraphNode,\n GraphEdge,\n GraphEventType,\n GraphEventHandler,\n LabelMode,\n ChangeSet,\n} from './types.js';\nimport type { GraphDataSource } from '../data-sources/types.js';\nimport type { ViewConfig } from './view-config.js';\nimport type { RendererBackend } from './renderer-backend.js';\nimport { applyViewConfig } from './view-config.js';\nimport { resolvePalette, injectPaletteCssVars } from './palette.js';\nimport { ProvenanceResolver } from './provenance-resolver.js';\nimport { TemporalFilter } from './temporal-filter.js';\nimport { TimelineOverlay } from '../overlays/timeline-overlay.js';\nimport { GraphModel } from './graph-model.js';\nimport { PrefixManager } from './prefix-manager.js';\nimport { LabelResolver } from './label-resolver.js';\nimport { StyleEngine } from './style-engine.js';\nimport { HexagonPainter } from './hexagon-painter.js';\nimport { FocusFilter } from './focus-filter.js';\nimport { ReificationCollapser } from './reification-collapser.js';\nimport { MetadataExtractor } from './metadata-extractor.js';\nimport { GraphEventEmitter } from './events.js';\nimport { Canvas2DRenderer } from './renderer.js';\n\n/**\n * Main entry point for the RDF Graph Visualization library.\n *\n * @example\n * ```typescript\n * const viz = new RdfGraphViz(document.getElementById('graph')!, {\n * labelMode: 'humanized',\n * style: { classColors: { 'schema:Person': '#7C3AED' } },\n * focus: { hops: 2, maxNodes: 200 },\n * });\n *\n * await viz.loadNTriples(ntriplesString);\n * viz.on('node:click', (node) => console.log(node));\n * ```\n */\nexport class RdfGraphViz {\n private _model: GraphModel;\n private _prefixManager: PrefixManager;\n private _labelResolver: LabelResolver;\n private _styleEngine: StyleEngine;\n private _hexPainter: HexagonPainter;\n private _focusFilter: FocusFilter;\n private _reificationCollapser: ReificationCollapser;\n private _metadataExtractor: MetadataExtractor;\n private _events: GraphEventEmitter;\n private _renderer: RendererBackend;\n private _config: RdfGraphVizConfig;\n private _collapsedNodeIds = new Set<string>();\n private _rendererReady: Promise<void>;\n private _container: HTMLElement;\n private _temporalFilter: TemporalFilter;\n private _timelineOverlay: TimelineOverlay | null = null;\n private _renderQueued = false;\n private _autoFitDisabled = false;\n\n constructor(container: HTMLElement, config: RdfGraphVizConfig = {}) {\n this._config = config;\n this._container = container;\n\n // Initialize subsystems\n this._metadataExtractor = new MetadataExtractor(config.metadata);\n this._model = new GraphModel(this._metadataExtractor.predicates);\n this._prefixManager = new PrefixManager(config.prefixes);\n this._labelResolver = new LabelResolver(\n config.labelMode ?? 'humanized',\n this._prefixManager,\n config.labels\n );\n this._styleEngine = new StyleEngine(config.style, this._prefixManager);\n this._hexPainter = new HexagonPainter(config.hexagon, this._styleEngine, this._prefixManager);\n this._focusFilter = new FocusFilter(config.focus);\n this._reificationCollapser = new ReificationCollapser(config.reification);\n this._temporalFilter = new TemporalFilter();\n this._events = new GraphEventEmitter();\n\n const backendConfig = {\n container,\n hexPainter: this._hexPainter,\n styleEngine: this._styleEngine,\n events: this._events,\n };\n\n // Create renderer backend based on config (default: 2D canvas)\n if (config.renderer === '3d') {\n // Dynamic import for tree-shaking: 3d-force-graph only loaded when needed\n let resolveReady: () => void;\n this._rendererReady = new Promise((r) => { resolveReady = r; });\n\n // Temporary placeholder until async import resolves\n this._renderer = null as unknown as RendererBackend;\n\n import('./renderer-3d.js').then(({ WebGL3DRenderer }) => {\n this._renderer = new WebGL3DRenderer(backendConfig);\n resolveReady();\n }).catch((err) => {\n console.warn('[dkg-graph-viz] 3D renderer not available, falling back to 2D:', err.message);\n this._renderer = new Canvas2DRenderer(backendConfig);\n resolveReady();\n });\n } else {\n this._renderer = new Canvas2DRenderer(backendConfig);\n this._rendererReady = Promise.resolve();\n }\n\n if (config.autoFitDisabled) {\n this._autoFitDisabled = true;\n if (this._renderer) {\n this._renderer.autoFitDisabled = true;\n } else {\n this._rendererReady.then(() => {\n if (this._renderer) this._renderer.autoFitDisabled = true;\n });\n }\n }\n\n // Auto-resize on container resize\n if (typeof ResizeObserver !== 'undefined') {\n const observer = new ResizeObserver(() => this._renderer?.resize());\n observer.observe(container);\n }\n }\n\n // --- Data Loading ---\n\n /** Load N-Triples or N-Quads string */\n async loadNTriples(input: string): Promise<void> {\n const { parseNTriples } = await import('../parsers/ntriples.js');\n const triples = parseNTriples(input);\n this._ingest(triples);\n }\n\n /** Load N-Quads string */\n async loadNQuads(input: string): Promise<void> {\n const { parseNQuads } = await import('../parsers/ntriples.js');\n const triples = parseNQuads(input);\n this._ingest(triples);\n }\n\n /** Load Turtle string */\n async loadTurtle(input: string): Promise<void> {\n const { parseTurtle } = await import('../parsers/turtle.js');\n const { triples, prefixes } = parseTurtle(input);\n this._prefixManager.addPrefixes(prefixes);\n this._ingest(triples);\n }\n\n /** Load JSON-LD (object or string) */\n async loadJsonLd(input: Record<string, unknown> | string): Promise<void> {\n const { parseJsonLd } = await import('../parsers/jsonld.js');\n const triples = await parseJsonLd(input);\n this._ingest(triples);\n }\n\n /** Load raw triple array (synchronous — no parsing needed) */\n loadTriples(\n input: Array<RdfTriple | { s: string; p: string; o: string } | [string, string, string]>\n ): void {\n const triples: RdfTriple[] = input.map((item) => {\n if (Array.isArray(item)) {\n return { subject: item[0], predicate: item[1], object: item[2] };\n }\n if ('s' in item && 'p' in item && 'o' in item) {\n const spo = item as { s: string; p: string; o: string };\n return { subject: spo.s, predicate: spo.p, object: spo.o };\n }\n return item as RdfTriple;\n });\n this._ingest(triples);\n }\n\n // --- Data Source Loading ---\n\n /**\n * Load data from a GraphDataSource using a SPARQL CONSTRUCT query.\n *\n * This is the recommended way to load data from a knowledge graph.\n * The source can be an in-browser Oxigraph store or a remote SPARQL endpoint.\n *\n * @param source - A GraphDataSource (OxigraphSource, RemoteSparqlSource, etc.)\n * @param sparql - SPARQL CONSTRUCT query that returns the triples to visualize\n *\n * @example\n * ```typescript\n * const source = new OxigraphSource();\n * await source.init();\n * await source.loadNTriples(ntData);\n *\n * await viz.loadFromSource(source, `\n * CONSTRUCT { ?s ?p ?o }\n * WHERE {\n * ?s a <https://schema.org/Person> ;\n * ?p ?o .\n * } LIMIT 5000\n * `);\n * ```\n */\n async loadFromSource(source: GraphDataSource, sparql: string): Promise<void> {\n const result = await source.construct(sparql);\n this._ingest(result.triples);\n }\n\n /**\n * Execute a SPARQL SELECT query against a data source.\n * Returns raw bindings — useful for analytics, counts, and lookups\n * without loading triples into the graph model.\n */\n async querySource(source: GraphDataSource, sparql: string) {\n return source.select(sparql);\n }\n\n // --- Incremental Updates ---\n\n /** Add triples to the existing graph */\n addTriples(triples: RdfTriple[]): ChangeSet {\n const changes = this._model.addTriples(triples);\n this._postIngest();\n this._events.emit('data:change', changes);\n return changes;\n }\n\n /** Remove triples from the graph */\n removeTriples(triples: RdfTriple[]): ChangeSet {\n const changes = this._model.removeTriples(triples);\n this._postIngest();\n this._events.emit('data:change', changes);\n return changes;\n }\n\n // --- Navigation ---\n\n /** Focus on a specific node with N-hop expansion */\n focus(nodeId: string, hops?: number): void {\n this._focusFilter.setFocus(nodeId);\n if (hops !== undefined) {\n this._focusFilter = new FocusFilter({\n ...this._config.focus,\n focalNode: nodeId,\n hops,\n });\n }\n this._renderCurrent();\n this._renderer?.centerOnNode(nodeId, 500, 2);\n this._events.emit('focus:change', {\n focalNode: nodeId,\n visibleNodes: this._focusFilter.visibleNodeIds.size,\n });\n }\n\n /** Recenter camera on a node without changing focus filter state. */\n centerOnNode(nodeId: string, opts?: { durationMs?: number; zoomLevel?: number }): void {\n this._renderer?.centerOnNode(nodeId, opts?.durationMs, opts?.zoomLevel);\n }\n\n /** Expand a boundary node's neighborhood */\n expandNode(nodeId: string): void {\n this._focusFilter.expandNode(nodeId, this._model);\n this._renderCurrent();\n }\n\n /** Show all nodes (disable focus filter) */\n showAll(): void {\n this._focusFilter.disable();\n this._renderCurrent();\n }\n\n // --- Timeline ---\n\n /** Get the temporal filter (for advanced usage) */\n get temporalFilter(): TemporalFilter {\n return this._temporalFilter;\n }\n\n /** Get the timeline overlay (for advanced usage, null if not enabled) */\n get timelineOverlay(): TimelineOverlay | null {\n return this._timelineOverlay;\n }\n\n /** Set the temporal cursor: only show nodes with dates up to this point */\n setTimeCursor(date: Date): void {\n this._temporalFilter.setCursor(date);\n this._renderCurrent();\n this._events.emit('temporal:change', {\n cursor: date,\n visibleCount: this._lastVisibleCount(),\n totalDated: this._temporalFilter.datedNodeCount,\n });\n }\n\n /** Get the date range of nodes in the graph */\n getDateRange(): [Date, Date] | null {\n return this._temporalFilter.dateRange;\n }\n\n /** Start timeline playback */\n playTimeline(): void {\n this._timelineOverlay?.play();\n }\n\n /** Pause timeline playback */\n pauseTimeline(): void {\n this._timelineOverlay?.pause();\n }\n\n /** Count visible nodes after last render (for event payloads) */\n private _lastVisibleCount(): number {\n const { nodes } = this._focusFilter.compute(this._model);\n const temporalVisible = this._temporalFilter.getVisibleNodeIds(nodes.keys());\n return temporalVisible ? temporalVisible.size : nodes.size;\n }\n\n /** Fit all visible nodes in view */\n zoomToFit(padding?: number): void {\n this._renderer?.zoomToFit(padding);\n }\n\n /** Prevent the renderer from automatically calling zoomToFit when the simulation settles. */\n set autoFitDisabled(value: boolean) {\n this._autoFitDisabled = value;\n if (this._renderer) {\n this._renderer.autoFitDisabled = value;\n } else {\n this._rendererReady.then(() => {\n if (this._renderer) this._renderer.autoFitDisabled = value;\n });\n }\n }\n\n // --- Label Mode ---\n\n /** Get current label mode */\n get labelMode(): LabelMode {\n return this._labelResolver.mode;\n }\n\n /** Set label mode and re-render */\n setLabelMode(mode: LabelMode): void {\n this._labelResolver.mode = mode;\n this._labelResolver.updateLabels(this._model.nodes, this._model.edges);\n this._renderCurrent();\n }\n\n // --- Events ---\n\n /** Subscribe to a graph event */\n on<T extends GraphEventType>(event: T, handler: GraphEventHandler<T>): () => void {\n return this._events.on(event, handler);\n }\n\n /** Unsubscribe from a graph event */\n off<T extends GraphEventType>(event: T, handler: GraphEventHandler<T>): void {\n this._events.off(event, handler);\n }\n\n // --- Accessors ---\n\n /** Get a node by ID */\n getNode(id: string): GraphNode | undefined {\n return this._model.getNode(id);\n }\n\n /** Get all edges from a node */\n getEdgesFrom(id: string): GraphEdge[] {\n return this._model.getEdgesFrom(id);\n }\n\n /** Get all edges to a node */\n getEdgesTo(id: string): GraphEdge[] {\n return this._model.getEdgesTo(id);\n }\n\n /** Get the underlying graph model (for advanced usage) */\n get model(): GraphModel {\n return this._model;\n }\n\n /** Get the style engine (for dynamic per-node color overrides) */\n get styleEngine(): StyleEngine {\n return this._styleEngine;\n }\n\n /** Get the prefix manager (for compact/expand operations) */\n get prefixes(): PrefixManager {\n return this._prefixManager;\n }\n\n // --- Highlight ---\n\n /** Saved visual state for highlighted nodes, used by clearHighlight() to revert */\n private _highlightState = new Map<string, {\n riskScore?: number;\n isHighRisk?: boolean;\n sizeMultiplier?: number;\n }>();\n\n /**\n * Visually emphasize a set of nodes without modifying underlying data.\n * Typically used to highlight SPARQL query results in the current view.\n *\n * @example\n * ```typescript\n * const result = await viz.querySource(kg, `\n * SELECT ?post WHERE {\n * ?post <https://schema.org/interactionCount> ?risk .\n * FILTER(?risk > 50)\n * }\n * `);\n * viz.highlightNodes(result.bindings.map(b => b.post));\n * ```\n */\n highlightNodes(nodeIds: string[]): void {\n // Revert any previous highlight first\n this.clearHighlight();\n\n const idSet = new Set(nodeIds);\n\n for (const [id, node] of this._model.nodes) {\n if (idSet.has(id)) {\n // Save current state for revert\n this._highlightState.set(id, {\n riskScore: node.riskScore,\n isHighRisk: node.isHighRisk,\n sizeMultiplier: node.sizeMultiplier,\n });\n\n // Apply highlight emphasis\n node.isHighRisk = true;\n if (!node.sizeMultiplier || node.sizeMultiplier < 2.0) {\n node.sizeMultiplier = 2.0;\n }\n }\n }\n\n if (this._renderer?.refresh) this._renderer.refresh();\n else this._renderCurrent();\n }\n\n /** Revert all highlighted nodes to their original visual state */\n clearHighlight(): void {\n if (this._highlightState.size === 0) return;\n\n for (const [id, saved] of this._highlightState) {\n const node = this._model.nodes.get(id);\n if (node) {\n node.riskScore = saved.riskScore;\n node.isHighRisk = saved.isHighRisk;\n node.sizeMultiplier = saved.sizeMultiplier;\n }\n }\n\n this._highlightState.clear();\n if (this._renderer?.refresh) this._renderer.refresh();\n else this._renderCurrent();\n }\n\n // --- Export ---\n\n /**\n * Export the current visualization as a PNG image.\n * Captures the canvas content from the active renderer backend.\n */\n async exportImage(): Promise<Blob> {\n const canvas = this._renderer.getCanvas();\n if (!canvas) {\n throw new Error('Cannot export: renderer not initialized or no canvas available');\n }\n\n return new Promise<Blob>((resolve, reject) => {\n canvas.toBlob((blob) => {\n if (blob) resolve(blob);\n else reject(new Error('Canvas toBlob returned null'));\n }, 'image/png');\n });\n }\n\n /**\n * Export the current graph model as N-Triples string.\n * Serializes all triples currently loaded in the visualization.\n */\n exportTriples(): string {\n const lines: string[] = [];\n\n for (const triple of this._model.triples) {\n const subj = formatNTerm(triple.subject);\n const pred = formatNTerm(triple.predicate);\n\n if (triple.datatype || triple.language || !isUri(triple.object)) {\n // Literal\n let obj = `\"${escapeNTriples(triple.object)}\"`;\n if (triple.language) {\n obj += `@${triple.language}`;\n } else if (triple.datatype) {\n obj += `^^<${triple.datatype}>`;\n }\n lines.push(`${subj} ${pred} ${obj} .`);\n } else {\n lines.push(`${subj} ${pred} ${formatNTerm(triple.object)} .`);\n }\n }\n\n return lines.join('\\n');\n }\n\n // --- Lifecycle ---\n\n /**\n * Apply a ViewConfig to the current graph.\n *\n * Sets focal entity styling, platform icons, risk/highlight scores,\n * and node type icons based on the declarative config.\n * Automatically calls refresh() to re-render.\n *\n * @example\n * ```typescript\n * await viz.loadFromSource(kg, sparql);\n * viz.applyViewConfig(config);\n * ```\n */\n applyView(config: ViewConfig): void {\n // Resolve and apply palette\n const palette = resolvePalette(config.palette, config.paletteOverrides);\n this._styleEngine.setPalette(palette);\n\n // Inject CSS custom properties for demo/app UIs\n injectPaletteCssVars(this._container, palette);\n\n // Configure trust visualization\n if (config.trust?.enabled) {\n this._hexPainter.setTrustStyle(config.trust.style ?? 'border');\n this._hexPainter.setShowBadges(config.trust.showBadge !== false);\n } else {\n this._hexPainter.setTrustStyle('none');\n }\n\n applyViewConfig(config, this._model);\n\n // Resolve provenance for all nodes\n const provenanceResolver = new ProvenanceResolver();\n provenanceResolver.resolve(this._model);\n\n // Configure temporal filter\n if (config.temporal?.enabled) {\n this._temporalFilter = new TemporalFilter(config.temporal);\n this._temporalFilter.scan(this._model);\n\n // Mount timeline overlay if dates were found\n if (this._temporalFilter.dateRange) {\n // Unmount previous overlay if any\n this._timelineOverlay?.unmount();\n\n this._timelineOverlay = new TimelineOverlay(\n this._container,\n this._temporalFilter,\n config.temporal,\n );\n this._timelineOverlay.onChange((cursor) => {\n this._renderCurrent();\n this._events.emit('temporal:change', {\n cursor,\n visibleCount: this._lastVisibleCount(),\n totalDated: this._temporalFilter.datedNodeCount,\n });\n });\n this._timelineOverlay.mount();\n }\n } else {\n // Disable timeline if previously enabled\n this._timelineOverlay?.unmount();\n this._timelineOverlay = null;\n }\n\n this._renderCurrent();\n\n // Configure KA membership grouping\n if (config.knowledgeAssets?.enabled) {\n const kaPred = config.knowledgeAssets.membershipPredicate ?? 'dkg:partOfAsset';\n this._model.setKaMembershipPredicate(kaPred);\n }\n\n // Apply animation after renderer is fully ready (3D init is async)\n this._rendererReady.then(() => {\n if (config.animation && this._renderer?.applyAnimation) {\n this._renderer.applyAnimation(config.animation);\n }\n // Apply palette to renderer (background, etc.)\n if (this._renderer?.applyPalette) {\n this._renderer.applyPalette(palette);\n }\n // Apply KA boundaries\n if (config.knowledgeAssets?.enabled && this._renderer?.setKaGroups) {\n this._renderer.setKaGroups(\n this._model.kaGroups,\n config.knowledgeAssets.showBoundaries !== false,\n config.knowledgeAssets.boundaryOpacity ?? 0.06\n );\n }\n });\n }\n\n /** Force a visual re-render (call after modifying node properties externally) */\n refresh(): void {\n this._renderCurrent();\n }\n\n /** Destroy the visualization and clean up resources */\n destroy(): void {\n this._timelineOverlay?.unmount();\n this._timelineOverlay = null;\n this._renderer?.destroy();\n this._events.removeAll();\n this._model.clear();\n this._highlightState.clear();\n }\n\n // --- Internal ---\n\n /** Ingest triples, resolve labels/images, and render */\n private _ingest(triples: RdfTriple[]): void {\n this._model.addTriples(triples);\n this._postIngest();\n }\n\n /** Post-ingestion processing: resolve images, labels, collapse, render */\n private _postIngest(): void {\n // Resolve image URLs for all nodes (direct and indirect)\n for (const node of this._model.nodes.values()) {\n if (!node.imageUrl) {\n node.imageUrl = this._hexPainter.resolveImageUrl(\n node,\n (id) => this._model.getNode(id),\n (id) => this._model.getEdgesFrom(id)\n );\n }\n }\n\n // Resolve labels\n this._labelResolver.updateLabels(this._model.nodes, this._model.edges);\n\n // Collapse reified statements\n this._collapsedNodeIds = this._reificationCollapser.collapse(this._model);\n\n this._renderCurrent();\n }\n\n /** Render with current focus filter + temporal filter state */\n private _renderCurrent(): void {\n // In 3D mode, renderer module loads asynchronously.\n // Queue one deferred render instead of throwing on early data loads.\n if (!this._renderer) {\n if (!this._renderQueued) {\n this._renderQueued = true;\n this._rendererReady.then(() => {\n this._renderQueued = false;\n this._renderCurrent();\n });\n }\n return;\n }\n\n let { nodes, edges } = this._focusFilter.compute(this._model);\n\n // Apply temporal filter: hide nodes outside the current time window\n const temporalVisible = this._temporalFilter.getVisibleNodeIds(nodes.keys());\n if (temporalVisible) {\n const filteredNodes = new Map<string, GraphNode>();\n const filteredEdges = new Map<string, GraphEdge>();\n\n for (const [id, node] of nodes) {\n if (temporalVisible.has(id)) {\n filteredNodes.set(id, node);\n }\n }\n for (const [id, edge] of edges) {\n if (filteredNodes.has(edge.source) && filteredNodes.has(edge.target)) {\n filteredEdges.set(id, edge);\n }\n }\n\n nodes = filteredNodes;\n edges = filteredEdges;\n }\n\n this._renderer.render(nodes, edges, this._collapsedNodeIds);\n\n // Update timeline overlay count badge\n if (this._timelineOverlay?.mounted) {\n this._timelineOverlay.updateCount(nodes.size);\n }\n }\n}\n\n// --- N-Triples serialization helpers ---\n\nfunction isUri(value: string): boolean {\n return value.startsWith('http://') || value.startsWith('https://') ||\n value.startsWith('urn:') || value.startsWith('_:');\n}\n\nfunction formatNTerm(uri: string): string {\n if (uri.startsWith('_:')) return uri;\n return `<${uri}>`;\n}\n\nfunction escapeNTriples(str: string): string {\n return str\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, '\\\\n')\n .replace(/\\r/g, '\\\\r')\n .replace(/\\t/g, '\\\\t');\n}\n","import type { RdfTriple } from '../core/types.js';\nimport type {\n GraphDataSource,\n SparqlSelectResult,\n SparqlConstructResult,\n SparqlBinding,\n} from './types.js';\n\n/**\n * In-browser knowledge graph powered by Oxigraph WASM.\n *\n * Provides a full SPARQL 1.1 store that runs entirely in the browser —\n * perfect for development, demos, and offline usage.\n *\n * @example\n * ```typescript\n * const source = new OxigraphSource();\n * await source.init();\n * await source.loadNTriples(ntriplesString);\n *\n * const result = await source.construct(`\n * CONSTRUCT { ?s ?p ?o }\n * WHERE { ?s a <https://schema.org/SocialMediaPosting> ; ?p ?o }\n * LIMIT 1000\n * `);\n *\n * viz.loadTriples(result.triples);\n * ```\n */\nexport class OxigraphSource implements GraphDataSource {\n readonly name = 'Oxigraph (in-browser)';\n private _store: any = null;\n private _oxigraph: any = null;\n\n /**\n * Initialize the Oxigraph WASM module.\n * Must be called before any other method.\n *\n * @param wasmUrl - Optional URL to the `web_bg.wasm` file.\n * In Vite/browser environments, pass this explicitly to avoid\n * bundler issues with WASM file resolution:\n * ```\n * await source.init('/data/oxigraph.wasm');\n * ```\n * In Node.js, this can be omitted.\n */\n async init(wasmUrl?: string): Promise<void> {\n if (this._store) return;\n\n // Dynamic import — uses the web entry point for browser\n const oxigraph = await import('oxigraph/web');\n\n // Initialize WASM — pass explicit URL if provided (needed for Vite/bundlers)\n if (wasmUrl) {\n await oxigraph.default(wasmUrl);\n } else if (typeof oxigraph.default === 'function') {\n await oxigraph.default();\n }\n\n this._oxigraph = oxigraph;\n this._store = new oxigraph.Store();\n }\n\n private ensureReady(): void {\n if (!this._store) {\n throw new Error('OxigraphSource not initialized. Call init() first.');\n }\n }\n\n async loadNTriples(data: string): Promise<void> {\n this.ensureReady();\n this._store.load(data, { format: 'application/n-triples' });\n }\n\n async loadTurtle(data: string): Promise<void> {\n this.ensureReady();\n this._store.load(data, { format: 'text/turtle' });\n }\n\n async select(sparql: string): Promise<SparqlSelectResult> {\n this.ensureReady();\n\n const results = this._store.query(sparql);\n\n // Oxigraph returns an array of Map<Variable, Term> for SELECT\n const bindings: SparqlBinding[] = [];\n const variableSet = new Set<string>();\n\n for (const row of results) {\n const binding: SparqlBinding = {};\n for (const [variable, term] of row) {\n const varName = variable.value;\n variableSet.add(varName);\n binding[varName] = this.termToString(term);\n }\n bindings.push(binding);\n }\n\n return {\n variables: [...variableSet],\n bindings,\n };\n }\n\n async construct(sparql: string): Promise<SparqlConstructResult> {\n this.ensureReady();\n\n const quads = this._store.query(sparql);\n const triples: RdfTriple[] = [];\n\n for (const quad of quads) {\n triples.push({\n subject: this.termToString(quad.subject),\n predicate: this.termToString(quad.predicate),\n object: this.termToString(quad.object),\n datatype: quad.object.datatype?.value,\n language: quad.object.language || undefined,\n });\n }\n\n return { triples };\n }\n\n async tripleCount(): Promise<number> {\n this.ensureReady();\n return this._store.size;\n }\n\n async isReady(): Promise<boolean> {\n return this._store !== null;\n }\n\n /** Get the raw Oxigraph store for advanced usage */\n get store(): any {\n return this._store;\n }\n\n /** Convert an Oxigraph RDF term to a string */\n private termToString(term: any): string {\n if (!term) return '';\n\n switch (term.termType) {\n case 'NamedNode':\n return term.value;\n case 'BlankNode':\n return `_:${term.value}`;\n case 'Literal': {\n // Return just the value string — datatype/language handled separately\n return term.value;\n }\n default:\n return term.value ?? String(term);\n }\n }\n}\n","import type { RdfTriple } from '../core/types.js';\nimport type {\n GraphDataSource,\n SparqlSelectResult,\n SparqlConstructResult,\n SparqlBinding,\n} from './types.js';\n\nexport interface RemoteSparqlConfig {\n /** SPARQL endpoint URL (e.g., http://localhost:9999/blazegraph/sparql) */\n endpoint: string;\n /** Optional namespace/repository path appended to endpoint */\n namespace?: string;\n /** Additional HTTP headers (e.g., for auth) */\n headers?: Record<string, string>;\n /** Request timeout in milliseconds (default: 30000) */\n timeoutMs?: number;\n}\n\n/**\n * Remote SPARQL endpoint data source.\n *\n * Connects to any SPARQL 1.1 compatible endpoint (Blazegraph, GraphDB,\n * OriginTrail DKG node, Virtuoso, etc.) via HTTP.\n *\n * @example\n * ```typescript\n * const source = new RemoteSparqlSource({\n * endpoint: 'http://localhost:9999/blazegraph/sparql',\n * namespace: 'kb',\n * });\n *\n * const result = await source.construct(`\n * CONSTRUCT { ?s ?p ?o }\n * WHERE { ?s a <https://schema.org/SocialMediaPosting> ; ?p ?o }\n * LIMIT 1000\n * `);\n *\n * viz.loadTriples(result.triples);\n * ```\n */\nexport class RemoteSparqlSource implements GraphDataSource {\n readonly name: string;\n private _config: Required<RemoteSparqlConfig>;\n\n constructor(config: RemoteSparqlConfig) {\n this._config = {\n endpoint: config.endpoint,\n namespace: config.namespace ?? '',\n headers: config.headers ?? {},\n timeoutMs: config.timeoutMs ?? 30000,\n };\n this.name = `SPARQL: ${this.endpointUrl}`;\n }\n\n private get endpointUrl(): string {\n const base = this._config.endpoint.replace(/\\/$/, '');\n return this._config.namespace ? `${base}/namespace/${this._config.namespace}/sparql` : base;\n }\n\n async loadNTriples(_data: string): Promise<void> {\n // For remote sources, data is already in the store.\n // Could implement SPARQL UPDATE INSERT DATA if needed.\n console.warn('RemoteSparqlSource.loadNTriples: remote stores are read-only from the viz. Load data via your KG admin tools.');\n }\n\n async select(sparql: string): Promise<SparqlSelectResult> {\n const response = await this.executeQuery(sparql, 'application/sparql-results+json');\n const json = await response.json();\n\n const variables = json.head?.vars ?? [];\n const bindings: SparqlBinding[] = (json.results?.bindings ?? []).map((row: any) => {\n const binding: SparqlBinding = {};\n for (const v of variables) {\n if (row[v]) {\n binding[v] = row[v].value;\n }\n }\n return binding;\n });\n\n return { variables, bindings };\n }\n\n async construct(sparql: string): Promise<SparqlConstructResult> {\n const response = await this.executeQuery(sparql, 'application/n-triples');\n const ntText = await response.text();\n const triples = this.parseNTriples(ntText);\n return { triples };\n }\n\n async tripleCount(): Promise<number> {\n const result = await this.select('SELECT (COUNT(*) AS ?count) WHERE { ?s ?p ?o }');\n return parseInt(result.bindings[0]?.count ?? '0', 10);\n }\n\n async isReady(): Promise<boolean> {\n try {\n const controller = new AbortController();\n setTimeout(() => controller.abort(), 5000);\n const response = await fetch(this.endpointUrl, {\n method: 'GET',\n signal: controller.signal,\n });\n return response.ok;\n } catch {\n return false;\n }\n }\n\n private async executeQuery(sparql: string, accept: string): Promise<Response> {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), this._config.timeoutMs);\n\n try {\n // W3C SPARQL 1.1 Protocol \"query via POST directly\" (§2.1.3): send the\n // raw query as the request body with `Content-Type:\n // application/sparql-query`, NOT URL-encoded form data.\n //\n // The old `application/x-www-form-urlencoded` + `query=<encoded>` form\n // hits Jetty's default 200 KB form-content limit on Blazegraph\n // (`UTFDataFormatException` / HTTP 400 \"Unable to parse form content\")\n // for large queries — e.g. a CONSTRUCT/SELECT carrying a big `VALUES`\n // block — which is the same defect fixed in the Blazegraph/SPARQL-HTTP\n // storage adapters. Direct POST has no such form-size cap and is\n // supported by every SPARQL 1.1 endpoint (Blazegraph, GraphDB,\n // Virtuoso, OriginTrail DKG node, …).\n const response = await fetch(this.endpointUrl, {\n method: 'POST',\n headers: {\n // Caller-supplied headers (e.g. auth) go FIRST so the\n // protocol-required headers below cannot be overridden. The body is\n // always a raw SPARQL query, so a caller default like\n // `Content-Type: application/x-www-form-urlencoded` would corrupt the\n // request (and recreate the large-query form-size regression) or make\n // a compliant endpoint reject it.\n ...this._config.headers,\n 'Content-Type': 'application/sparql-query',\n 'Accept': accept,\n },\n body: sparql,\n signal: controller.signal,\n });\n\n if (!response.ok) {\n throw new Error(`SPARQL query failed: ${response.status} ${response.statusText}`);\n }\n\n return response;\n } finally {\n clearTimeout(timeout);\n }\n }\n\n /** Minimal N-Triples parser for CONSTRUCT results */\n private parseNTriples(text: string): RdfTriple[] {\n const triples: RdfTriple[] = [];\n const lines = text.split('\\n');\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith('#')) continue;\n\n // Match: <subject> <predicate> <object> . OR <s> <p> \"literal\"^^<dt> .\n const uriMatch = trimmed.match(/^<([^>]+)>\\s+<([^>]+)>\\s+<([^>]+)>\\s*\\.$/);\n if (uriMatch) {\n triples.push({\n subject: uriMatch[1],\n predicate: uriMatch[2],\n object: uriMatch[3],\n });\n continue;\n }\n\n const litMatch = trimmed.match(/^<([^>]+)>\\s+<([^>]+)>\\s+\"(.+)\"(?:\\^\\^<([^>]+)>|@(\\S+))?\\s*\\.$/);\n if (litMatch) {\n triples.push({\n subject: litMatch[1],\n predicate: litMatch[2],\n object: litMatch[3],\n datatype: litMatch[4],\n language: litMatch[5],\n });\n }\n }\n\n return triples;\n }\n}\n"]}